@jwilger/pi-development-system 0.10.6 → 0.12.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/README.md +9 -1
- package/agents/advisor.md +40 -0
- package/agents/architect.md +66 -0
- package/agents/coder.md +57 -0
- package/agents/implementer.md +47 -0
- package/agents/lens-cagan.md +54 -0
- package/agents/lens-perri.md +54 -0
- package/agents/lens-pichler.md +54 -0
- package/agents/lens-rumelt.md +54 -0
- package/agents/lens-torres.md +54 -0
- package/agents/researcher.md +70 -0
- package/agents/reviewer.md +57 -0
- package/agents/tasker.md +58 -0
- package/agents/writer.md +39 -0
- package/extensions/development-system.ts +4 -0
- package/package.json +5 -3
- package/skills/delegation/SKILL.md +66 -0
- package/src/core/models.ts +18 -0
- package/src/core/routing.ts +67 -0
- package/src/core/spawn-overrides.ts +59 -0
- package/src/jev/questions/route.ts +93 -0
- package/src/state/config.ts +2 -4
- package/src/state/route-task-tool.ts +74 -0
- package/src/subagents/VENDORED.md +40 -0
- package/src/subagents/index.ts +544 -0
- package/src/subagents/orch/inherited-tools.ts +85 -0
- package/src/subagents/orch/mailbox.ts +262 -0
- package/src/subagents/orch/manager.ts +778 -0
- package/src/subagents/orch/paths.ts +74 -0
- package/src/subagents/orch/prompt.ts +25 -0
- package/src/subagents/orch/runtime.ts +791 -0
- package/src/subagents/orch/tools.ts +177 -0
- package/src/subagents/orch/transcript.ts +187 -0
- package/src/subagents/prefs/agent-import.ts +246 -0
- package/src/subagents/prefs/config.ts +1029 -0
- package/src/subagents/prefs/import-discovery.ts +644 -0
- package/src/subagents/prefs/import-instructions.ts +157 -0
- package/src/subagents/prefs/models.ts +85 -0
- package/src/subagents/prefs/settings.ts +371 -0
- package/src/subagents/types.ts +209 -0
- package/src/subagents/ui/agent-navigation-editor.ts +210 -0
- package/src/subagents/ui/dialog.ts +651 -0
- package/src/subagents/ui/import-picker.ts +232 -0
- package/src/subagents/ui/live-agent-view.ts +572 -0
- package/src/subagents/ui/model-picker.ts +452 -0
- package/src/subagents/ui/settings-ui.ts +329 -0
- package/src/subagents/ui/status-ui.ts +545 -0
- package/src/subagents/ui/thread-tree.ts +135 -0
- package/src/subagents/ui/ui.ts +1507 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
+
import fuzzysort, { type SnapshotKeys } from "fuzzysort";
|
|
3
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
getSelectListTheme,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
type ScopedModel,
|
|
8
|
+
type Theme,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
Container,
|
|
12
|
+
getKeybindings,
|
|
13
|
+
Input,
|
|
14
|
+
Key,
|
|
15
|
+
matchesKey,
|
|
16
|
+
SelectList,
|
|
17
|
+
stripTerminalSequences,
|
|
18
|
+
truncateToWidth,
|
|
19
|
+
Text,
|
|
20
|
+
type SelectItem,
|
|
21
|
+
} from "@earendil-works/pi-tui";
|
|
22
|
+
import { modelIdentity } from "../prefs/models.ts";
|
|
23
|
+
import { dialogHeight, frameDialog, DIALOG_OPTIONS } from "./dialog.ts";
|
|
24
|
+
|
|
25
|
+
const MODEL_EDITOR_LAYOUT = {
|
|
26
|
+
minPrimaryColumnWidth: 24,
|
|
27
|
+
maxPrimaryColumnWidth: 52,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
const PICKER_TITLE = "Preferred models";
|
|
31
|
+
const SELECTED_GROUP = "Selected models · ordered fallback";
|
|
32
|
+
const ACTIONS_GROUP = "Actions";
|
|
33
|
+
|
|
34
|
+
export const MODEL_EDITOR_CANCEL = Symbol("model-editor-cancel");
|
|
35
|
+
|
|
36
|
+
type AvailableModel = Pick<Model<Api>, "provider" | "id" | "name">;
|
|
37
|
+
|
|
38
|
+
interface ModelSelectItem extends SelectItem {
|
|
39
|
+
group: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OrderedModelEditorComponentOptions {
|
|
43
|
+
tui: { requestRender(force?: boolean): void; terminal?: { rows: number } };
|
|
44
|
+
theme: Theme;
|
|
45
|
+
availableModels: readonly AvailableModel[];
|
|
46
|
+
scopedModels: readonly ScopedModel[];
|
|
47
|
+
initialModels?: readonly string[];
|
|
48
|
+
modelSuggestions?: readonly string[];
|
|
49
|
+
onDone(models: string[]): void;
|
|
50
|
+
onCancel(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sanitizeModelText(text: string): string {
|
|
54
|
+
return stripTerminalSequences(text).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderModelIdentity(identity: string): string {
|
|
58
|
+
return sanitizeModelText(identity);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function modelSearchText(text: string): string {
|
|
62
|
+
// Providers spell the same version differently (sonnet-5.5 vs sonnet-5-5).
|
|
63
|
+
// Strip separators rather than splitting them into repeated numeric terms;
|
|
64
|
+
// raw identities and labels remain untouched.
|
|
65
|
+
return sanitizeModelText(text).replace(/[._:/-]+/g, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sortModels(models: readonly AvailableModel[]): AvailableModel[] {
|
|
69
|
+
const unique = new Map<string, AvailableModel>();
|
|
70
|
+
for (const model of models) unique.set(modelIdentity(model), model);
|
|
71
|
+
return [...unique.values()].sort(
|
|
72
|
+
(a, b) =>
|
|
73
|
+
a.provider.localeCompare(b.provider) ||
|
|
74
|
+
a.id.localeCompare(b.id) ||
|
|
75
|
+
sanitizeModelText(a.name).localeCompare(sanitizeModelText(b.name)),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function describeModel(model: AvailableModel, scopedIdentities: ReadonlySet<string>): string {
|
|
80
|
+
const annotation = scopedIdentities.has(modelIdentity(model))
|
|
81
|
+
? "scoped in this session"
|
|
82
|
+
: "portable preference · not scoped in this session";
|
|
83
|
+
const name = sanitizeModelText(model.name);
|
|
84
|
+
return [name, annotation].filter(Boolean).join(" · ");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class OrderedModelEditorComponent extends Container {
|
|
88
|
+
private readonly tui: {
|
|
89
|
+
requestRender(force?: boolean): void;
|
|
90
|
+
terminal?: { rows: number };
|
|
91
|
+
};
|
|
92
|
+
private readonly theme: Theme;
|
|
93
|
+
private readonly allModels: readonly AvailableModel[];
|
|
94
|
+
private readonly modelSearch: SnapshotKeys<AvailableModel>;
|
|
95
|
+
private readonly suggestedScores: ReadonlyMap<string, number>;
|
|
96
|
+
private readonly modelsByIdentity = new Map<string, AvailableModel>();
|
|
97
|
+
private readonly scopedIdentities: ReadonlySet<string>;
|
|
98
|
+
private readonly onDone: (models: string[]) => void;
|
|
99
|
+
private readonly onCancel: () => void;
|
|
100
|
+
private readonly titleText = new Text();
|
|
101
|
+
private readonly subtitleText = new Text();
|
|
102
|
+
private readonly footerText = new Text();
|
|
103
|
+
private readonly searchInput = new Input({ placeholder: "Search models" });
|
|
104
|
+
private draft: string[];
|
|
105
|
+
private pickerQuery = "";
|
|
106
|
+
private currentItems: ModelSelectItem[] = [];
|
|
107
|
+
private selectList = new SelectList([], 1, getSelectListTheme(), MODEL_EDITOR_LAYOUT);
|
|
108
|
+
private _focused = false;
|
|
109
|
+
|
|
110
|
+
constructor(options: OrderedModelEditorComponentOptions) {
|
|
111
|
+
super();
|
|
112
|
+
this.tui = options.tui;
|
|
113
|
+
this.theme = options.theme;
|
|
114
|
+
this.allModels = sortModels(options.availableModels);
|
|
115
|
+
// The registry is fixed for this dialog; prepare searchable fields once.
|
|
116
|
+
// Scope annotations deliberately aren't searchable model metadata.
|
|
117
|
+
this.modelSearch = fuzzysort.snapshot(this.allModels, {
|
|
118
|
+
keys: [
|
|
119
|
+
(model) => modelSearchText(modelIdentity(model)),
|
|
120
|
+
(model) => modelSearchText(model.name),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
this.suggestedScores = this.searchScores(options.modelSuggestions ?? []);
|
|
124
|
+
this.onDone = options.onDone;
|
|
125
|
+
this.onCancel = options.onCancel;
|
|
126
|
+
for (const model of this.allModels) this.modelsByIdentity.set(modelIdentity(model), model);
|
|
127
|
+
this.scopedIdentities = new Set(
|
|
128
|
+
options.scopedModels.map((entry) => modelIdentity(entry.model)),
|
|
129
|
+
);
|
|
130
|
+
this.draft = [...new Set(options.initialModels ?? [])];
|
|
131
|
+
this.titleText.setText(this.theme.fg("accent", PICKER_TITLE));
|
|
132
|
+
this.subtitleText.setText(
|
|
133
|
+
this.theme.fg(
|
|
134
|
+
"muted",
|
|
135
|
+
this.withScopeWarning(
|
|
136
|
+
this.suggestedScores.size
|
|
137
|
+
? "Selected first; suggested matches scoped first, then other models; the rest A–Z. Type to search."
|
|
138
|
+
: "Selected first. Search by provider/id or model name; other models A–Z.",
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
this.footerText.setText(
|
|
143
|
+
this.theme.fg("dim", "↑↓ select · Enter toggle · Ctrl+↑↓ reorder · Ctrl+S done · Esc cancel"),
|
|
144
|
+
);
|
|
145
|
+
this.addChild(this.searchInput);
|
|
146
|
+
this.refreshPickerList();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get focused(): boolean {
|
|
150
|
+
return this._focused;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
set focused(value: boolean) {
|
|
154
|
+
this._focused = value;
|
|
155
|
+
this.searchInput.focused = value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getMode(): "picker" {
|
|
159
|
+
return "picker";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getDraftModels(): readonly string[] {
|
|
163
|
+
return this.draft;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getCurrentItems(): readonly SelectItem[] {
|
|
167
|
+
return this.currentItems;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getSelectList(): SelectList {
|
|
171
|
+
return this.selectList;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getSearchInput(): Input {
|
|
175
|
+
return this.searchInput;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
render(width: number): string[] {
|
|
179
|
+
const height = dialogHeight(this.tui);
|
|
180
|
+
const inner = Math.max(1, width - 4);
|
|
181
|
+
const header = [
|
|
182
|
+
truncateToWidth(
|
|
183
|
+
this.subtitleText
|
|
184
|
+
.render(1000)
|
|
185
|
+
.map((line) => line.trim())
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join(" "),
|
|
188
|
+
inner,
|
|
189
|
+
"",
|
|
190
|
+
),
|
|
191
|
+
...this.searchInput.render(inner),
|
|
192
|
+
];
|
|
193
|
+
const budget = Math.max(1, height - 4 - header.length);
|
|
194
|
+
const selected = this.selectList.getSelectedItem()?.value;
|
|
195
|
+
// Headings are visual rows only: keyboard navigation selects models, not groups.
|
|
196
|
+
const displayRows: (ModelSelectItem | string)[] = [];
|
|
197
|
+
let previousGroup: string | undefined;
|
|
198
|
+
const hasResults = this.currentItems.some(
|
|
199
|
+
(item) => item.group !== SELECTED_GROUP && item.group !== ACTIONS_GROUP,
|
|
200
|
+
);
|
|
201
|
+
for (const item of this.currentItems) {
|
|
202
|
+
if (item.group !== previousGroup) {
|
|
203
|
+
if (item.group === ACTIONS_GROUP && !hasResults) displayRows.push("No matching models");
|
|
204
|
+
displayRows.push(item.group);
|
|
205
|
+
}
|
|
206
|
+
previousGroup = item.group;
|
|
207
|
+
displayRows.push(item);
|
|
208
|
+
}
|
|
209
|
+
const index = Math.max(
|
|
210
|
+
0,
|
|
211
|
+
displayRows.findIndex((row) => typeof row !== "string" && row.value === selected),
|
|
212
|
+
);
|
|
213
|
+
const start = Math.max(0, Math.min(index - budget + 1, displayRows.length - budget));
|
|
214
|
+
// Only format visible rows; a registry can contain thousands of models.
|
|
215
|
+
const rows = displayRows.slice(start, start + budget).map((row) => {
|
|
216
|
+
if (typeof row === "string") return this.theme.fg("muted", row);
|
|
217
|
+
const text = `${row.value === selected ? "›" : " "} ${row.label} ${this.theme.fg("muted", row.description ?? "")}`;
|
|
218
|
+
return row.value === selected ? this.theme.fg("accent", text) : text;
|
|
219
|
+
});
|
|
220
|
+
return frameDialog(
|
|
221
|
+
this.theme,
|
|
222
|
+
width,
|
|
223
|
+
height,
|
|
224
|
+
this.titleText.render(1000).join(" ").trim(),
|
|
225
|
+
[...header, ...rows].map((line) => ` ${line}`),
|
|
226
|
+
this.footerText
|
|
227
|
+
.render(1000)
|
|
228
|
+
.map((line) => line.trim())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.join(" "),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
handleInput(keyData: string): void {
|
|
235
|
+
if (matchesKey(keyData, Key.ctrl("s"))) {
|
|
236
|
+
this.onDone([...this.draft]);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const earlier = matchesKey(keyData, Key.ctrl("up"));
|
|
240
|
+
const later = matchesKey(keyData, Key.ctrl("down"));
|
|
241
|
+
if (earlier || later) {
|
|
242
|
+
const selected = this.selectList.getSelectedItem()?.value;
|
|
243
|
+
const index = selected === undefined ? -1 : this.draft.indexOf(selected);
|
|
244
|
+
const next = index + (earlier ? -1 : 1);
|
|
245
|
+
// Consume reorder keys even on unselected models or at a list boundary.
|
|
246
|
+
if (index >= 0 && next >= 0 && next < this.draft.length) {
|
|
247
|
+
[this.draft[index], this.draft[next]] = [this.draft[next], this.draft[index]];
|
|
248
|
+
this.refreshPickerList(selected);
|
|
249
|
+
this.tui.requestRender();
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const kb = getKeybindings();
|
|
254
|
+
const isNav =
|
|
255
|
+
kb.matches(keyData, "tui.select.up") ||
|
|
256
|
+
kb.matches(keyData, "tui.select.down") ||
|
|
257
|
+
kb.matches(keyData, "tui.select.confirm") ||
|
|
258
|
+
kb.matches(keyData, "tui.select.cancel");
|
|
259
|
+
if (isNav) {
|
|
260
|
+
this.selectList.handleInput(keyData);
|
|
261
|
+
} else {
|
|
262
|
+
this.searchInput.handleInput(keyData);
|
|
263
|
+
const nextQuery = this.searchInput.getValue();
|
|
264
|
+
if (nextQuery !== this.pickerQuery) {
|
|
265
|
+
this.pickerQuery = nextQuery;
|
|
266
|
+
// Keep selected preferences visible, but focus the first search result
|
|
267
|
+
// so typing a query and pressing Enter still adds the matching model.
|
|
268
|
+
this.refreshPickerList(undefined, true);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
this.tui.requestRender();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private refreshPickerList(selectedValue?: string, focusSearchResult = false): void {
|
|
275
|
+
const previousIndex = this.currentItems.findIndex(
|
|
276
|
+
(item) => item.value === this.selectList.getSelectedItem()?.value,
|
|
277
|
+
);
|
|
278
|
+
this.currentItems = [
|
|
279
|
+
...this.draft.map((identity, index) => ({
|
|
280
|
+
value: identity,
|
|
281
|
+
label: `[x] ${index + 1}. ${renderModelIdentity(identity)}`,
|
|
282
|
+
description: this.describeDraftIdentity(identity),
|
|
283
|
+
group: SELECTED_GROUP,
|
|
284
|
+
})),
|
|
285
|
+
...this.getPickerItems(),
|
|
286
|
+
{
|
|
287
|
+
value: "action:done",
|
|
288
|
+
label: "Done",
|
|
289
|
+
description: "Commit this ordered model preference list",
|
|
290
|
+
group: ACTIONS_GROUP,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
value: "action:clear",
|
|
294
|
+
label: "Use inherited default",
|
|
295
|
+
description: "Clear explicit preferences and inherit the default model",
|
|
296
|
+
group: ACTIONS_GROUP,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
value: "action:cancel",
|
|
300
|
+
label: "Cancel",
|
|
301
|
+
description: "Discard unsaved model preference changes",
|
|
302
|
+
group: ACTIONS_GROUP,
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
const list = new SelectList(
|
|
306
|
+
this.currentItems,
|
|
307
|
+
Math.min(this.currentItems.length, 10),
|
|
308
|
+
getSelectListTheme(),
|
|
309
|
+
MODEL_EDITOR_LAYOUT,
|
|
310
|
+
);
|
|
311
|
+
const preferredIndex = focusSearchResult
|
|
312
|
+
? this.currentItems.findIndex(
|
|
313
|
+
(item) => item.group !== SELECTED_GROUP && item.group !== ACTIONS_GROUP,
|
|
314
|
+
)
|
|
315
|
+
: this.currentItems.findIndex((item) => item.value === selectedValue);
|
|
316
|
+
list.setSelectedIndex(
|
|
317
|
+
preferredIndex >= 0
|
|
318
|
+
? preferredIndex
|
|
319
|
+
: focusSearchResult
|
|
320
|
+
? 0
|
|
321
|
+
: Math.max(0, Math.min(previousIndex, this.currentItems.length - 1)),
|
|
322
|
+
);
|
|
323
|
+
list.onSelect = (item) => {
|
|
324
|
+
if (this.currentItems.find((row) => row.value === item.value)?.group === ACTIONS_GROUP) {
|
|
325
|
+
if (item.value === "action:done") this.onDone([...this.draft]);
|
|
326
|
+
else if (item.value === "action:cancel") this.onCancel();
|
|
327
|
+
else {
|
|
328
|
+
this.draft = [];
|
|
329
|
+
this.refreshPickerList("action:done");
|
|
330
|
+
this.tui.requestRender();
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const index = this.draft.indexOf(item.value);
|
|
335
|
+
if (index >= 0) this.draft.splice(index, 1);
|
|
336
|
+
else this.draft.push(item.value);
|
|
337
|
+
this.refreshPickerList(item.value);
|
|
338
|
+
this.tui.requestRender();
|
|
339
|
+
};
|
|
340
|
+
list.onSelectionChange = () => this.tui.requestRender();
|
|
341
|
+
list.onCancel = () => this.onCancel();
|
|
342
|
+
this.selectList = list;
|
|
343
|
+
if (this.children[1]) this.children[1] = list;
|
|
344
|
+
else this.addChild(list);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private searchScores(queries: readonly string[]): Map<string, number> {
|
|
348
|
+
const scores = new Map<string, number>();
|
|
349
|
+
const uniqueQueries = new Set(
|
|
350
|
+
queries.map((query) => modelSearchText(query).trim()).filter(Boolean),
|
|
351
|
+
);
|
|
352
|
+
for (const query of uniqueQueries) {
|
|
353
|
+
// Both defaults are restrictive (10 results, score >= .5); collect every
|
|
354
|
+
// fuzzy match so suggestions aren't silently capped or omitted.
|
|
355
|
+
for (const result of fuzzysort.go(query, this.modelSearch, {
|
|
356
|
+
limit: 0,
|
|
357
|
+
threshold: 0,
|
|
358
|
+
})) {
|
|
359
|
+
const identity = modelIdentity(result.obj);
|
|
360
|
+
scores.set(identity, Math.max(scores.get(identity) ?? 0, result.score));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return scores;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private getPickerItems(): ModelSelectItem[] {
|
|
367
|
+
const excluded = new Set(this.draft);
|
|
368
|
+
const query = this.pickerQuery.trim();
|
|
369
|
+
const queryScores = query ? this.searchScores([query]) : undefined;
|
|
370
|
+
const scoped: AvailableModel[] = [];
|
|
371
|
+
const other: AvailableModel[] = [];
|
|
372
|
+
const remaining: AvailableModel[] = [];
|
|
373
|
+
for (const model of this.allModels) {
|
|
374
|
+
const identity = modelIdentity(model);
|
|
375
|
+
if (excluded.has(identity) || (queryScores && !queryScores.has(identity))) continue;
|
|
376
|
+
if (this.suggestedScores.has(identity)) {
|
|
377
|
+
(this.scopedIdentities.has(identity) ? scoped : other).push(model);
|
|
378
|
+
} else {
|
|
379
|
+
remaining.push(model);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const bySuggestionScore = (a: AvailableModel, b: AvailableModel) =>
|
|
383
|
+
this.suggestedScores.get(modelIdentity(b))! - this.suggestedScores.get(modelIdentity(a))!;
|
|
384
|
+
// Stable score sorting preserves alphabetical order for ties. The last
|
|
385
|
+
// tier retains the registry's alphabetical order, irrespective of scope.
|
|
386
|
+
scoped.sort(bySuggestionScore);
|
|
387
|
+
other.sort(bySuggestionScore);
|
|
388
|
+
const items = (models: AvailableModel[], group: string): ModelSelectItem[] =>
|
|
389
|
+
models.map((model) => ({
|
|
390
|
+
value: modelIdentity(model),
|
|
391
|
+
label: `[ ] ${renderModelIdentity(modelIdentity(model))}`,
|
|
392
|
+
description: describeModel(model, this.scopedIdentities),
|
|
393
|
+
group,
|
|
394
|
+
}));
|
|
395
|
+
return [
|
|
396
|
+
...items(scoped, "Suggested matches · scoped models"),
|
|
397
|
+
...items(other, "Suggested matches · other models"),
|
|
398
|
+
...items(
|
|
399
|
+
remaining,
|
|
400
|
+
scoped.length || other.length ? "All other models · A–Z" : "All models · A–Z",
|
|
401
|
+
),
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private describeDraftIdentity(identity: string): string {
|
|
406
|
+
const model = this.modelsByIdentity.get(identity);
|
|
407
|
+
if (!model) return "unavailable in current model registry";
|
|
408
|
+
return describeModel(model, this.scopedIdentities);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private withScopeWarning(text: string): string {
|
|
412
|
+
if (this.scopedIdentities.size === 0) {
|
|
413
|
+
return `No scoped models: Pick First (scoped) cannot run explicit preferences. Configure /scoped-models or change Model Picking. ${text}`;
|
|
414
|
+
}
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function editModelPreferences(
|
|
420
|
+
ctx: ExtensionCommandContext,
|
|
421
|
+
currentModels?: readonly string[],
|
|
422
|
+
modelSuggestions?: readonly string[],
|
|
423
|
+
): Promise<readonly string[] | typeof MODEL_EDITOR_CANCEL> {
|
|
424
|
+
if (ctx.mode !== "tui") {
|
|
425
|
+
if (ctx.hasUI) {
|
|
426
|
+
ctx.ui.notify(
|
|
427
|
+
"Ordered model preferences require interactive TUI mode; draft unchanged.",
|
|
428
|
+
"warning",
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return MODEL_EDITOR_CANCEL;
|
|
432
|
+
}
|
|
433
|
+
const availableModels = ctx.modelRegistry.getAvailable().map((model) => ({
|
|
434
|
+
provider: model.provider,
|
|
435
|
+
id: model.id,
|
|
436
|
+
name: model.name,
|
|
437
|
+
}));
|
|
438
|
+
return ctx.ui.custom<readonly string[] | typeof MODEL_EDITOR_CANCEL>(
|
|
439
|
+
(tui, theme, _keys, done) =>
|
|
440
|
+
new OrderedModelEditorComponent({
|
|
441
|
+
tui,
|
|
442
|
+
theme,
|
|
443
|
+
availableModels,
|
|
444
|
+
scopedModels: ctx.scopedModels,
|
|
445
|
+
initialModels: currentModels,
|
|
446
|
+
modelSuggestions,
|
|
447
|
+
onDone: (models) => done(models),
|
|
448
|
+
onCancel: () => done(MODEL_EDITOR_CANCEL),
|
|
449
|
+
}),
|
|
450
|
+
DIALOG_OPTIONS,
|
|
451
|
+
);
|
|
452
|
+
}
|