@openeryc/pi-coding-agent 0.75.55 → 0.75.57
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 +21 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/fetch-openai-models.d.ts +25 -0
- package/dist/core/fetch-openai-models.d.ts.map +1 -0
- package/dist/core/fetch-openai-models.js +107 -0
- package/dist/core/fetch-openai-models.js.map +1 -0
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +8 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts +118 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +137 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-hub.d.ts +117 -0
- package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-hub.js +775 -0
- package/dist/modes/interactive/components/model-hub.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +2 -44
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -275
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +310 -132
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/models.md +20 -0
- package/docs/providers.md +13 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified model hub: Select current model, manage cycle whitelist, manage custom providers.
|
|
3
|
+
*/
|
|
4
|
+
import { modelsAreEqual } from "@openeryc/pi-ai";
|
|
5
|
+
import { Container, fuzzyFilter, getKeybindings, Input, Key, matchesKey, Spacer, Text, } from "@openeryc/pi-tui";
|
|
6
|
+
import { theme } from "../theme/theme.js";
|
|
7
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
|
+
import { keyHint, keyText } from "./keybinding-hints.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Pure helpers (exported for tests)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function formatTokenCount(count) {
|
|
13
|
+
if (count >= 1_000_000) {
|
|
14
|
+
const millions = count / 1_000_000;
|
|
15
|
+
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
|
16
|
+
}
|
|
17
|
+
if (count >= 1_000) {
|
|
18
|
+
const thousands = count / 1_000;
|
|
19
|
+
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
|
20
|
+
}
|
|
21
|
+
return count.toString();
|
|
22
|
+
}
|
|
23
|
+
function modelBadges(model) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
if (model.reasoning)
|
|
26
|
+
parts.push("R");
|
|
27
|
+
parts.push(formatTokenCount(model.contextWindow));
|
|
28
|
+
return parts.join(" ");
|
|
29
|
+
}
|
|
30
|
+
function isEnabled(enabledIds, id) {
|
|
31
|
+
return enabledIds === null || enabledIds.includes(id);
|
|
32
|
+
}
|
|
33
|
+
function toggle(enabledIds, id) {
|
|
34
|
+
if (enabledIds === null)
|
|
35
|
+
return [id];
|
|
36
|
+
const index = enabledIds.indexOf(id);
|
|
37
|
+
if (index >= 0)
|
|
38
|
+
return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)];
|
|
39
|
+
return [...enabledIds, id];
|
|
40
|
+
}
|
|
41
|
+
function enableAll(enabledIds, allIds, targetIds) {
|
|
42
|
+
if (enabledIds === null)
|
|
43
|
+
return null;
|
|
44
|
+
const targets = targetIds ?? allIds;
|
|
45
|
+
const result = [...enabledIds];
|
|
46
|
+
for (const id of targets) {
|
|
47
|
+
if (!result.includes(id))
|
|
48
|
+
result.push(id);
|
|
49
|
+
}
|
|
50
|
+
return result.length === allIds.length ? null : result;
|
|
51
|
+
}
|
|
52
|
+
function clearAll(enabledIds, allIds, targetIds) {
|
|
53
|
+
if (enabledIds === null) {
|
|
54
|
+
return targetIds ? allIds.filter((id) => !targetIds.includes(id)) : [];
|
|
55
|
+
}
|
|
56
|
+
const targets = new Set(targetIds ?? enabledIds);
|
|
57
|
+
return enabledIds.filter((id) => !targets.has(id));
|
|
58
|
+
}
|
|
59
|
+
function move(enabledIds, id, delta) {
|
|
60
|
+
if (enabledIds === null)
|
|
61
|
+
return null;
|
|
62
|
+
const list = [...enabledIds];
|
|
63
|
+
const index = list.indexOf(id);
|
|
64
|
+
if (index < 0)
|
|
65
|
+
return list;
|
|
66
|
+
const newIndex = index + delta;
|
|
67
|
+
if (newIndex < 0 || newIndex >= list.length)
|
|
68
|
+
return list;
|
|
69
|
+
const result = [...list];
|
|
70
|
+
[result[index], result[newIndex]] = [result[newIndex], result[index]];
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function getSortedIds(enabledIds, allIds) {
|
|
74
|
+
if (enabledIds === null)
|
|
75
|
+
return allIds;
|
|
76
|
+
const enabledSet = new Set(enabledIds);
|
|
77
|
+
return [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))];
|
|
78
|
+
}
|
|
79
|
+
export function buildSelectRows(options) {
|
|
80
|
+
const { models, currentModel, recentKeys, query, expandedProviders, collapsedLimit = 12 } = options;
|
|
81
|
+
const rows = [];
|
|
82
|
+
if (query) {
|
|
83
|
+
const filtered = fuzzyFilter(models, query, ({ id, provider, model }) => `${id} ${provider} ${provider}/${id} ${model.name ?? ""} ${provider} ${id}`);
|
|
84
|
+
for (const item of filtered) {
|
|
85
|
+
rows.push({ kind: "model", item, badges: modelBadges(item.model) });
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
const byKey = new Map(models.map((m) => [`${m.provider}/${m.id}`, m]));
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
// Current first (if available)
|
|
92
|
+
if (currentModel) {
|
|
93
|
+
const key = `${currentModel.provider}/${currentModel.id}`;
|
|
94
|
+
const item = byKey.get(key);
|
|
95
|
+
if (item) {
|
|
96
|
+
rows.push({ kind: "header", label: "Current" });
|
|
97
|
+
rows.push({ kind: "model", item, badges: modelBadges(item.model) });
|
|
98
|
+
seen.add(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Recent
|
|
102
|
+
const recentItems = [];
|
|
103
|
+
for (const key of recentKeys) {
|
|
104
|
+
if (seen.has(key))
|
|
105
|
+
continue;
|
|
106
|
+
const item = byKey.get(key);
|
|
107
|
+
if (item) {
|
|
108
|
+
recentItems.push(item);
|
|
109
|
+
seen.add(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (recentItems.length > 0) {
|
|
113
|
+
rows.push({ kind: "header", label: "Recent" });
|
|
114
|
+
for (const item of recentItems.slice(0, 8)) {
|
|
115
|
+
rows.push({ kind: "model", item, badges: modelBadges(item.model) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Group remaining by provider
|
|
119
|
+
const byProvider = new Map();
|
|
120
|
+
for (const item of models) {
|
|
121
|
+
const key = `${item.provider}/${item.id}`;
|
|
122
|
+
if (seen.has(key))
|
|
123
|
+
continue;
|
|
124
|
+
const list = byProvider.get(item.provider) ?? [];
|
|
125
|
+
list.push(item);
|
|
126
|
+
byProvider.set(item.provider, list);
|
|
127
|
+
}
|
|
128
|
+
const providers = [...byProvider.keys()].sort((a, b) => a.localeCompare(b));
|
|
129
|
+
const currentProvider = currentModel?.provider;
|
|
130
|
+
for (const provider of providers) {
|
|
131
|
+
const list = byProvider.get(provider) ?? [];
|
|
132
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
133
|
+
const expand = expandedProviders.has(provider) || provider === currentProvider || list.length <= collapsedLimit;
|
|
134
|
+
rows.push({ kind: "header", label: provider });
|
|
135
|
+
if (!expand) {
|
|
136
|
+
rows.push({ kind: "collapsed", provider, count: list.length });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const item of list) {
|
|
140
|
+
rows.push({ kind: "model", item, badges: modelBadges(item.model) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Component
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
export class ModelHubComponent extends Container {
|
|
149
|
+
searchInput;
|
|
150
|
+
listContainer;
|
|
151
|
+
headerText;
|
|
152
|
+
footerText;
|
|
153
|
+
hintText;
|
|
154
|
+
_focused = false;
|
|
155
|
+
get focused() {
|
|
156
|
+
return this._focused;
|
|
157
|
+
}
|
|
158
|
+
set focused(value) {
|
|
159
|
+
this._focused = value;
|
|
160
|
+
this.searchInput.focused = value;
|
|
161
|
+
}
|
|
162
|
+
tui;
|
|
163
|
+
currentModel;
|
|
164
|
+
settingsManager;
|
|
165
|
+
modelRegistry;
|
|
166
|
+
callbacks;
|
|
167
|
+
tab;
|
|
168
|
+
allModels = [];
|
|
169
|
+
scopedModels;
|
|
170
|
+
// Select state
|
|
171
|
+
selectRows = [];
|
|
172
|
+
selectIndex = 0;
|
|
173
|
+
expandedProviders = new Set();
|
|
174
|
+
// Cycle state
|
|
175
|
+
cycleAllIds = [];
|
|
176
|
+
cycleModelsById = new Map();
|
|
177
|
+
enabledIds = null;
|
|
178
|
+
cycleItems = [];
|
|
179
|
+
cycleIndex = 0;
|
|
180
|
+
cycleDirty = false;
|
|
181
|
+
// Manage state
|
|
182
|
+
manageRows = [];
|
|
183
|
+
manageIndex = 0;
|
|
184
|
+
errorMessage;
|
|
185
|
+
maxVisible = 10;
|
|
186
|
+
loadGeneration = 0;
|
|
187
|
+
constructor(tui, currentModel, settingsManager, modelRegistry, scopedModels, enabledModelIds, callbacks, options) {
|
|
188
|
+
super();
|
|
189
|
+
this.tui = tui;
|
|
190
|
+
this.currentModel = currentModel;
|
|
191
|
+
this.settingsManager = settingsManager;
|
|
192
|
+
this.modelRegistry = modelRegistry;
|
|
193
|
+
this.scopedModels = scopedModels;
|
|
194
|
+
this.callbacks = callbacks;
|
|
195
|
+
this.tab = options?.initialTab ?? "select";
|
|
196
|
+
this.enabledIds = enabledModelIds === null ? null : [...enabledModelIds];
|
|
197
|
+
this.addChild(new DynamicBorder());
|
|
198
|
+
this.addChild(new Spacer(1));
|
|
199
|
+
this.headerText = new Text(this.getHeaderText(), 0, 0);
|
|
200
|
+
this.addChild(this.headerText);
|
|
201
|
+
this.hintText = new Text(this.getHintText(), 0, 0);
|
|
202
|
+
this.addChild(this.hintText);
|
|
203
|
+
this.addChild(new Spacer(1));
|
|
204
|
+
this.searchInput = new Input();
|
|
205
|
+
if (options?.initialSearchInput) {
|
|
206
|
+
this.searchInput.setValue(options.initialSearchInput);
|
|
207
|
+
}
|
|
208
|
+
this.searchInput.onSubmit = () => this.handleConfirm();
|
|
209
|
+
this.addChild(this.searchInput);
|
|
210
|
+
this.addChild(new Spacer(1));
|
|
211
|
+
this.listContainer = new Container();
|
|
212
|
+
this.addChild(this.listContainer);
|
|
213
|
+
this.addChild(new Spacer(1));
|
|
214
|
+
this.footerText = new Text(this.getFooterText(), 0, 0);
|
|
215
|
+
this.addChild(this.footerText);
|
|
216
|
+
this.addChild(new DynamicBorder());
|
|
217
|
+
void this.reload().then(() => {
|
|
218
|
+
if (options?.initialSearchInput) {
|
|
219
|
+
this.applySearch();
|
|
220
|
+
}
|
|
221
|
+
this.tui.requestRender();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ---- public helpers for tests ----
|
|
225
|
+
getTab() {
|
|
226
|
+
return this.tab;
|
|
227
|
+
}
|
|
228
|
+
getSearchInput() {
|
|
229
|
+
return this.searchInput;
|
|
230
|
+
}
|
|
231
|
+
/** Test helper: force reload completion */
|
|
232
|
+
async waitForLoad() {
|
|
233
|
+
await this.reload();
|
|
234
|
+
}
|
|
235
|
+
// ---- load ----
|
|
236
|
+
async reload() {
|
|
237
|
+
const gen = ++this.loadGeneration;
|
|
238
|
+
this.modelRegistry.refresh();
|
|
239
|
+
const loadError = this.modelRegistry.getError();
|
|
240
|
+
if (loadError) {
|
|
241
|
+
this.errorMessage = loadError;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.errorMessage = undefined;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const available = this.modelRegistry.getAvailable();
|
|
248
|
+
if (gen !== this.loadGeneration)
|
|
249
|
+
return;
|
|
250
|
+
this.allModels = available.map((model) => ({
|
|
251
|
+
provider: model.provider,
|
|
252
|
+
id: model.id,
|
|
253
|
+
model,
|
|
254
|
+
}));
|
|
255
|
+
// Refresh scoped models against registry
|
|
256
|
+
this.scopedModels = this.scopedModels.map((scoped) => {
|
|
257
|
+
const refreshed = this.modelRegistry.find(scoped.model.provider, scoped.model.id);
|
|
258
|
+
return refreshed ? { ...scoped, model: refreshed } : scoped;
|
|
259
|
+
});
|
|
260
|
+
this.cycleModelsById.clear();
|
|
261
|
+
this.cycleAllIds = [];
|
|
262
|
+
for (const item of this.allModels) {
|
|
263
|
+
const fullId = `${item.provider}/${item.id}`;
|
|
264
|
+
this.cycleModelsById.set(fullId, item.model);
|
|
265
|
+
this.cycleAllIds.push(fullId);
|
|
266
|
+
}
|
|
267
|
+
this.rebuildManageRows();
|
|
268
|
+
this.applySearch();
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
this.allModels = [];
|
|
272
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
273
|
+
this.applySearch();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
rebuildManageRows() {
|
|
277
|
+
const rows = [];
|
|
278
|
+
rows.push({ kind: "action", action: "add", label: "+ Add custom provider (models.json)" });
|
|
279
|
+
for (const p of this.modelRegistry.listModelsJsonProviders()) {
|
|
280
|
+
const modelLabel = p.modelIds.length === 0
|
|
281
|
+
? "override only"
|
|
282
|
+
: `${p.modelIds.length} model${p.modelIds.length === 1 ? "" : "s"}`;
|
|
283
|
+
rows.push({
|
|
284
|
+
kind: "provider",
|
|
285
|
+
source: "models_json",
|
|
286
|
+
id: p.name,
|
|
287
|
+
label: p.name,
|
|
288
|
+
detail: `${p.isBuiltIn ? "built-in override" : "custom"} · ${modelLabel}${p.baseUrl ? ` · ${p.baseUrl}` : ""}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const openaiCompat = this.modelRegistry.authStorage.getOpenAICompatibleProviders();
|
|
292
|
+
for (const [id, info] of Object.entries(openaiCompat)) {
|
|
293
|
+
rows.push({
|
|
294
|
+
kind: "provider",
|
|
295
|
+
source: "openai_compatible",
|
|
296
|
+
id,
|
|
297
|
+
label: id,
|
|
298
|
+
detail: `openai-compatible · ${info.models.length} model${info.models.length === 1 ? "" : "s"} · ${info.baseUrl}`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
this.manageRows = rows;
|
|
302
|
+
this.manageIndex = Math.min(this.manageIndex, Math.max(0, rows.length - 1));
|
|
303
|
+
}
|
|
304
|
+
// ---- header / footer ----
|
|
305
|
+
getHeaderText() {
|
|
306
|
+
const tabs = [
|
|
307
|
+
{ id: "select", label: "Select" },
|
|
308
|
+
{ id: "cycle", label: "Cycle" },
|
|
309
|
+
{ id: "manage", label: "Manage" },
|
|
310
|
+
];
|
|
311
|
+
const parts = tabs.map((t) => (t.id === this.tab ? theme.fg("accent", t.label) : theme.fg("muted", t.label)));
|
|
312
|
+
return `${theme.fg("muted", "Models: ")}${parts.join(theme.fg("muted", " | "))}`;
|
|
313
|
+
}
|
|
314
|
+
getHintText() {
|
|
315
|
+
const tabHint = keyHint("tui.input.tab", "tabs") +
|
|
316
|
+
theme.fg("muted", "/") +
|
|
317
|
+
keyHint("app.model.hubTabPrev", "←") +
|
|
318
|
+
theme.fg("muted", "/") +
|
|
319
|
+
keyHint("app.model.hubTabNext", "→");
|
|
320
|
+
if (this.tab === "select") {
|
|
321
|
+
return tabHint + theme.fg("muted", " · type to search · Enter select · collapsed providers: Enter expand");
|
|
322
|
+
}
|
|
323
|
+
if (this.tab === "cycle") {
|
|
324
|
+
return tabHint + theme.fg("muted", ` · session-only until ${keyText("app.models.save")}`);
|
|
325
|
+
}
|
|
326
|
+
return tabHint + theme.fg("muted", " · Enter edit name/url/key/models · Backspace/Delete remove");
|
|
327
|
+
}
|
|
328
|
+
getFooterText() {
|
|
329
|
+
if (this.tab === "select") {
|
|
330
|
+
const modelCount = this.selectRows.filter((r) => r.kind === "model").length;
|
|
331
|
+
return theme.fg("dim", ` ${modelCount} model${modelCount === 1 ? "" : "s"} · Esc cancel`);
|
|
332
|
+
}
|
|
333
|
+
if (this.tab === "cycle") {
|
|
334
|
+
const enabledCount = this.enabledIds?.length ?? this.cycleAllIds.length;
|
|
335
|
+
const allEnabled = this.enabledIds === null;
|
|
336
|
+
const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.cycleAllIds.length} enabled`;
|
|
337
|
+
const parts = [
|
|
338
|
+
`${keyText("tui.select.confirm")} toggle`,
|
|
339
|
+
`${keyText("app.models.enableAll")} all`,
|
|
340
|
+
`${keyText("app.models.clearAll")} clear`,
|
|
341
|
+
`${keyText("app.models.toggleProvider")} provider`,
|
|
342
|
+
`${keyText("app.models.reorderUp")}/${keyText("app.models.reorderDown")} reorder`,
|
|
343
|
+
`${keyText("app.models.save")} save`,
|
|
344
|
+
countText,
|
|
345
|
+
];
|
|
346
|
+
return this.cycleDirty
|
|
347
|
+
? theme.fg("dim", ` ${parts.join(" · ")} `) + theme.fg("warning", "(unsaved)")
|
|
348
|
+
: theme.fg("dim", ` ${parts.join(" · ")}`);
|
|
349
|
+
}
|
|
350
|
+
return theme.fg("dim", " Enter edit name/url/key/models · Backspace delete · writing models.json drops comments/trailing commas");
|
|
351
|
+
}
|
|
352
|
+
setTab(tab) {
|
|
353
|
+
if (this.tab === tab)
|
|
354
|
+
return;
|
|
355
|
+
this.tab = tab;
|
|
356
|
+
this.headerText.setText(this.getHeaderText());
|
|
357
|
+
this.hintText.setText(this.getHintText());
|
|
358
|
+
this.applySearch();
|
|
359
|
+
}
|
|
360
|
+
cycleTab(delta = 1) {
|
|
361
|
+
const order = ["select", "cycle", "manage"];
|
|
362
|
+
const idx = order.indexOf(this.tab);
|
|
363
|
+
const next = (idx + delta + order.length * 10) % order.length;
|
|
364
|
+
this.setTab(order[next]);
|
|
365
|
+
}
|
|
366
|
+
/** Left/right switch tabs only when the search box is empty, so typing is unaffected. */
|
|
367
|
+
trySwitchTabWithArrows(keyData, kb) {
|
|
368
|
+
if (this.searchInput.getValue())
|
|
369
|
+
return false;
|
|
370
|
+
if (kb.matches(keyData, "app.model.hubTabNext") || matchesKey(keyData, Key.right)) {
|
|
371
|
+
this.cycleTab(1);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (kb.matches(keyData, "app.model.hubTabPrev") || matchesKey(keyData, Key.left)) {
|
|
375
|
+
this.cycleTab(-1);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
// ---- list building ----
|
|
381
|
+
applySearch() {
|
|
382
|
+
const query = this.searchInput.getValue();
|
|
383
|
+
if (this.tab === "select") {
|
|
384
|
+
this.selectRows = buildSelectRows({
|
|
385
|
+
models: this.allModels,
|
|
386
|
+
currentModel: this.currentModel,
|
|
387
|
+
recentKeys: this.settingsManager.getRecentModels(),
|
|
388
|
+
query,
|
|
389
|
+
expandedProviders: this.expandedProviders,
|
|
390
|
+
});
|
|
391
|
+
// Prefer current model selection
|
|
392
|
+
const currentIdx = this.selectRows.findIndex((r) => r.kind === "model" && modelsAreEqual(this.currentModel, r.item.model));
|
|
393
|
+
this.selectIndex =
|
|
394
|
+
currentIdx >= 0 ? currentIdx : Math.min(this.selectIndex, Math.max(0, this.selectRows.length - 1));
|
|
395
|
+
// Snap to first selectable if landing on header
|
|
396
|
+
this.ensureSelectableIndex("select");
|
|
397
|
+
}
|
|
398
|
+
else if (this.tab === "cycle") {
|
|
399
|
+
const items = getSortedIds(this.enabledIds, this.cycleAllIds)
|
|
400
|
+
.filter((id) => this.cycleModelsById.has(id))
|
|
401
|
+
.map((id) => ({
|
|
402
|
+
fullId: id,
|
|
403
|
+
model: this.cycleModelsById.get(id),
|
|
404
|
+
enabled: isEnabled(this.enabledIds, id),
|
|
405
|
+
}));
|
|
406
|
+
this.cycleItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items;
|
|
407
|
+
this.cycleIndex = Math.min(this.cycleIndex, Math.max(0, this.cycleItems.length - 1));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
this.rebuildManageRows();
|
|
411
|
+
if (query) {
|
|
412
|
+
this.manageRows = fuzzyFilter(this.manageRows, query, (r) => r.kind === "action" ? r.label : `${r.label} ${r.detail} ${r.id}`);
|
|
413
|
+
}
|
|
414
|
+
this.manageIndex = Math.min(this.manageIndex, Math.max(0, this.manageRows.length - 1));
|
|
415
|
+
}
|
|
416
|
+
this.updateList();
|
|
417
|
+
this.footerText.setText(this.getFooterText());
|
|
418
|
+
}
|
|
419
|
+
ensureSelectableIndex(which) {
|
|
420
|
+
if (which !== "select" || this.selectRows.length === 0)
|
|
421
|
+
return;
|
|
422
|
+
if (this.selectRows[this.selectIndex]?.kind === "header") {
|
|
423
|
+
const next = this.selectRows.findIndex((r, i) => i >= this.selectIndex && r.kind !== "header");
|
|
424
|
+
if (next >= 0) {
|
|
425
|
+
this.selectIndex = next;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const prev = [...this.selectRows]
|
|
429
|
+
.map((r, i) => ({ r, i }))
|
|
430
|
+
.reverse()
|
|
431
|
+
.find(({ r }) => r.kind !== "header");
|
|
432
|
+
if (prev)
|
|
433
|
+
this.selectIndex = prev.i;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
updateList() {
|
|
437
|
+
this.listContainer.clear();
|
|
438
|
+
if (this.errorMessage && this.tab !== "manage") {
|
|
439
|
+
for (const line of this.errorMessage.split("\n")) {
|
|
440
|
+
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (this.tab === "select") {
|
|
444
|
+
this.renderSelectList();
|
|
445
|
+
}
|
|
446
|
+
else if (this.tab === "cycle") {
|
|
447
|
+
this.renderCycleList();
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
this.renderManageList();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
renderSelectList() {
|
|
454
|
+
if (this.selectRows.length === 0) {
|
|
455
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const startIndex = Math.max(0, Math.min(this.selectIndex - Math.floor(this.maxVisible / 2), this.selectRows.length - this.maxVisible));
|
|
459
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.selectRows.length);
|
|
460
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
461
|
+
const row = this.selectRows[i];
|
|
462
|
+
const isSelected = i === this.selectIndex;
|
|
463
|
+
if (row.kind === "header") {
|
|
464
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` ── ${row.label} ──`), 0, 0));
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (row.kind === "collapsed") {
|
|
468
|
+
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
469
|
+
const text = isSelected
|
|
470
|
+
? theme.fg("accent", `▸ ${row.provider} (${row.count})`)
|
|
471
|
+
: theme.fg("muted", `▸ ${row.provider} (${row.count})`);
|
|
472
|
+
this.listContainer.addChild(new Text(`${prefix}${text}`, 0, 0));
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const isCurrent = modelsAreEqual(this.currentModel, row.item.model);
|
|
476
|
+
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
477
|
+
const modelText = isSelected ? theme.fg("accent", row.item.id) : row.item.id;
|
|
478
|
+
const providerBadge = theme.fg("muted", ` [${row.item.provider}]`);
|
|
479
|
+
const badges = theme.fg("dim", ` ${row.badges}`);
|
|
480
|
+
const check = isCurrent ? theme.fg("success", " ✓") : "";
|
|
481
|
+
this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${badges}${check}`, 0, 0));
|
|
482
|
+
}
|
|
483
|
+
if (startIndex > 0 || endIndex < this.selectRows.length) {
|
|
484
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` (${this.selectIndex + 1}/${this.selectRows.length})`), 0, 0));
|
|
485
|
+
}
|
|
486
|
+
const selected = this.selectRows[this.selectIndex];
|
|
487
|
+
if (selected?.kind === "model") {
|
|
488
|
+
this.listContainer.addChild(new Spacer(1));
|
|
489
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` ${selected.item.model.name}`), 0, 0));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
renderCycleList() {
|
|
493
|
+
if (this.cycleItems.length === 0) {
|
|
494
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const startIndex = Math.max(0, Math.min(this.cycleIndex - Math.floor(this.maxVisible / 2), this.cycleItems.length - this.maxVisible));
|
|
498
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.cycleItems.length);
|
|
499
|
+
const allEnabled = this.enabledIds === null;
|
|
500
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
501
|
+
const item = this.cycleItems[i];
|
|
502
|
+
const isSelected = i === this.cycleIndex;
|
|
503
|
+
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
504
|
+
const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id;
|
|
505
|
+
const providerBadge = theme.fg("muted", ` [${item.model.provider}]`);
|
|
506
|
+
const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗");
|
|
507
|
+
this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));
|
|
508
|
+
}
|
|
509
|
+
if (startIndex > 0 || endIndex < this.cycleItems.length) {
|
|
510
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` (${this.cycleIndex + 1}/${this.cycleItems.length})`), 0, 0));
|
|
511
|
+
}
|
|
512
|
+
if (this.cycleItems.length > 0) {
|
|
513
|
+
const selected = this.cycleItems[this.cycleIndex];
|
|
514
|
+
this.listContainer.addChild(new Spacer(1));
|
|
515
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` ${selected?.model.name}`), 0, 0));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
renderManageList() {
|
|
519
|
+
if (this.manageRows.length === 0) {
|
|
520
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No custom providers"), 0, 0));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const startIndex = Math.max(0, Math.min(this.manageIndex - Math.floor(this.maxVisible / 2), this.manageRows.length - this.maxVisible));
|
|
524
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.manageRows.length);
|
|
525
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
526
|
+
const row = this.manageRows[i];
|
|
527
|
+
const isSelected = i === this.manageIndex;
|
|
528
|
+
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
529
|
+
if (row.kind === "action") {
|
|
530
|
+
const text = isSelected ? theme.fg("accent", row.label) : theme.fg("success", row.label);
|
|
531
|
+
this.listContainer.addChild(new Text(`${prefix}${text}`, 0, 0));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const text = isSelected ? theme.fg("accent", row.label) : row.label;
|
|
535
|
+
const detail = theme.fg("muted", ` ${row.detail}`);
|
|
536
|
+
this.listContainer.addChild(new Text(`${prefix}${text}`, 0, 0));
|
|
537
|
+
this.listContainer.addChild(new Text(`${detail}`, 0, 0));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// ---- input ----
|
|
542
|
+
handleInput(keyData) {
|
|
543
|
+
const kb = getKeybindings();
|
|
544
|
+
if (kb.matches(keyData, "tui.input.tab")) {
|
|
545
|
+
this.cycleTab(1);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (this.trySwitchTabWithArrows(keyData, kb)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (kb.matches(keyData, "tui.select.cancel") || matchesKey(keyData, Key.escape)) {
|
|
552
|
+
this.callbacks.onCancel();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (matchesKey(keyData, Key.ctrl("c"))) {
|
|
556
|
+
if (this.searchInput.getValue()) {
|
|
557
|
+
this.searchInput.setValue("");
|
|
558
|
+
this.applySearch();
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
this.callbacks.onCancel();
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (this.tab === "select") {
|
|
566
|
+
this.handleSelectInput(keyData, kb);
|
|
567
|
+
}
|
|
568
|
+
else if (this.tab === "cycle") {
|
|
569
|
+
this.handleCycleInput(keyData, kb);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
this.handleManageInput(keyData, kb);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
handleSelectInput(keyData, kb) {
|
|
576
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
577
|
+
if (this.selectRows.length === 0)
|
|
578
|
+
return;
|
|
579
|
+
let next = this.selectIndex === 0 ? this.selectRows.length - 1 : this.selectIndex - 1;
|
|
580
|
+
// Skip headers when moving
|
|
581
|
+
for (let guard = 0; guard < this.selectRows.length; guard++) {
|
|
582
|
+
if (this.selectRows[next]?.kind !== "header")
|
|
583
|
+
break;
|
|
584
|
+
next = next === 0 ? this.selectRows.length - 1 : next - 1;
|
|
585
|
+
}
|
|
586
|
+
this.selectIndex = next;
|
|
587
|
+
this.updateList();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
591
|
+
if (this.selectRows.length === 0)
|
|
592
|
+
return;
|
|
593
|
+
let next = this.selectIndex === this.selectRows.length - 1 ? 0 : this.selectIndex + 1;
|
|
594
|
+
for (let guard = 0; guard < this.selectRows.length; guard++) {
|
|
595
|
+
if (this.selectRows[next]?.kind !== "header")
|
|
596
|
+
break;
|
|
597
|
+
next = next === this.selectRows.length - 1 ? 0 : next + 1;
|
|
598
|
+
}
|
|
599
|
+
this.selectIndex = next;
|
|
600
|
+
this.updateList();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
604
|
+
this.handleConfirm();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
this.searchInput.handleInput(keyData);
|
|
608
|
+
this.applySearch();
|
|
609
|
+
}
|
|
610
|
+
handleCycleInput(keyData, kb) {
|
|
611
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
612
|
+
if (this.cycleItems.length === 0)
|
|
613
|
+
return;
|
|
614
|
+
this.cycleIndex = this.cycleIndex === 0 ? this.cycleItems.length - 1 : this.cycleIndex - 1;
|
|
615
|
+
this.updateList();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
619
|
+
if (this.cycleItems.length === 0)
|
|
620
|
+
return;
|
|
621
|
+
this.cycleIndex = this.cycleIndex === this.cycleItems.length - 1 ? 0 : this.cycleIndex + 1;
|
|
622
|
+
this.updateList();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const reorderUp = kb.matches(keyData, "app.models.reorderUp");
|
|
626
|
+
const reorderDown = kb.matches(keyData, "app.models.reorderDown");
|
|
627
|
+
if (reorderUp || reorderDown) {
|
|
628
|
+
if (this.enabledIds === null)
|
|
629
|
+
return;
|
|
630
|
+
const item = this.cycleItems[this.cycleIndex];
|
|
631
|
+
if (item && isEnabled(this.enabledIds, item.fullId)) {
|
|
632
|
+
const delta = reorderUp ? -1 : 1;
|
|
633
|
+
const currentIndex = this.enabledIds.indexOf(item.fullId);
|
|
634
|
+
const newIndex = currentIndex + delta;
|
|
635
|
+
if (newIndex >= 0 && newIndex < this.enabledIds.length) {
|
|
636
|
+
this.enabledIds = move(this.enabledIds, item.fullId, delta);
|
|
637
|
+
this.cycleDirty = true;
|
|
638
|
+
this.cycleIndex += delta;
|
|
639
|
+
this.applySearch();
|
|
640
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
646
|
+
const item = this.cycleItems[this.cycleIndex];
|
|
647
|
+
if (item) {
|
|
648
|
+
this.enabledIds = toggle(this.enabledIds, item.fullId);
|
|
649
|
+
this.cycleDirty = true;
|
|
650
|
+
this.applySearch();
|
|
651
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (kb.matches(keyData, "app.models.enableAll")) {
|
|
656
|
+
const targetIds = this.searchInput.getValue() ? this.cycleItems.map((i) => i.fullId) : undefined;
|
|
657
|
+
this.enabledIds = enableAll(this.enabledIds, this.cycleAllIds, targetIds);
|
|
658
|
+
this.cycleDirty = true;
|
|
659
|
+
this.applySearch();
|
|
660
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (kb.matches(keyData, "app.models.clearAll")) {
|
|
664
|
+
const targetIds = this.searchInput.getValue() ? this.cycleItems.map((i) => i.fullId) : undefined;
|
|
665
|
+
this.enabledIds = clearAll(this.enabledIds, this.cycleAllIds, targetIds);
|
|
666
|
+
this.cycleDirty = true;
|
|
667
|
+
this.applySearch();
|
|
668
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (kb.matches(keyData, "app.models.toggleProvider")) {
|
|
672
|
+
const item = this.cycleItems[this.cycleIndex];
|
|
673
|
+
if (item) {
|
|
674
|
+
const provider = item.model.provider;
|
|
675
|
+
const providerIds = this.cycleAllIds.filter((id) => this.cycleModelsById.get(id).provider === provider);
|
|
676
|
+
const allOn = providerIds.every((id) => isEnabled(this.enabledIds, id));
|
|
677
|
+
this.enabledIds = allOn
|
|
678
|
+
? clearAll(this.enabledIds, this.cycleAllIds, providerIds)
|
|
679
|
+
: enableAll(this.enabledIds, this.cycleAllIds, providerIds);
|
|
680
|
+
this.cycleDirty = true;
|
|
681
|
+
this.applySearch();
|
|
682
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (kb.matches(keyData, "app.models.save")) {
|
|
687
|
+
void this.callbacks.onCyclePersist(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
688
|
+
this.cycleDirty = false;
|
|
689
|
+
this.footerText.setText(this.getFooterText());
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
this.searchInput.handleInput(keyData);
|
|
693
|
+
this.applySearch();
|
|
694
|
+
}
|
|
695
|
+
handleManageInput(keyData, kb) {
|
|
696
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
697
|
+
if (this.manageRows.length === 0)
|
|
698
|
+
return;
|
|
699
|
+
this.manageIndex = this.manageIndex === 0 ? this.manageRows.length - 1 : this.manageIndex - 1;
|
|
700
|
+
this.updateList();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
704
|
+
if (this.manageRows.length === 0)
|
|
705
|
+
return;
|
|
706
|
+
this.manageIndex = this.manageIndex === this.manageRows.length - 1 ? 0 : this.manageIndex + 1;
|
|
707
|
+
this.updateList();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
711
|
+
this.handleConfirm();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
// Backspace / Delete remove selected provider
|
|
715
|
+
if (matchesKey(keyData, Key.backspace) || matchesKey(keyData, Key.delete)) {
|
|
716
|
+
const row = this.manageRows[this.manageIndex];
|
|
717
|
+
if (row?.kind === "provider") {
|
|
718
|
+
void this.callbacks.onManageRemove(row.source, row.id);
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
this.searchInput.handleInput(keyData);
|
|
723
|
+
this.applySearch();
|
|
724
|
+
}
|
|
725
|
+
handleConfirm() {
|
|
726
|
+
if (this.tab === "select") {
|
|
727
|
+
const row = this.selectRows[this.selectIndex];
|
|
728
|
+
if (!row)
|
|
729
|
+
return;
|
|
730
|
+
if (row.kind === "collapsed") {
|
|
731
|
+
this.expandedProviders.add(row.provider);
|
|
732
|
+
this.applySearch();
|
|
733
|
+
// Move selection to first model under that provider
|
|
734
|
+
const idx = this.selectRows.findIndex((r) => r.kind === "model" && r.item.provider === row.provider);
|
|
735
|
+
if (idx >= 0)
|
|
736
|
+
this.selectIndex = idx;
|
|
737
|
+
this.updateList();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (row.kind === "model") {
|
|
741
|
+
this.settingsManager.setDefaultModelAndProvider(row.item.model.provider, row.item.model.id);
|
|
742
|
+
void this.callbacks.onSelectModel(row.item.model);
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (this.tab === "cycle") {
|
|
747
|
+
const item = this.cycleItems[this.cycleIndex];
|
|
748
|
+
if (item) {
|
|
749
|
+
this.enabledIds = toggle(this.enabledIds, item.fullId);
|
|
750
|
+
this.cycleDirty = true;
|
|
751
|
+
this.applySearch();
|
|
752
|
+
void this.callbacks.onCycleChange(this.enabledIds === null ? null : [...this.enabledIds]);
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const row = this.manageRows[this.manageIndex];
|
|
757
|
+
if (!row)
|
|
758
|
+
return;
|
|
759
|
+
if (row.kind === "action" && row.action === "add") {
|
|
760
|
+
void this.callbacks.onManageAdd();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (row.kind === "provider") {
|
|
764
|
+
void this.callbacks.onManageEdit(row.source, row.id);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/** Called by parent after manage mutation so list refreshes. */
|
|
768
|
+
async refreshAfterManage() {
|
|
769
|
+
await this.reload();
|
|
770
|
+
this.tui.requestRender();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Back-compat alias used by older tests/imports. */
|
|
774
|
+
export { ModelHubComponent as ModelSelectorComponent };
|
|
775
|
+
//# sourceMappingURL=model-hub.js.map
|