@north-light/crouter 0.3.262 → 0.3.263
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/dist/api/dto/health.d.ts +2 -0
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +5 -3
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/viewer.js +616 -616
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +558 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +2 -2
- package/dist/core/canvas/render-source.d.ts +1 -1
- package/dist/core/canvas/render-source.js +3 -2
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +162 -47
- package/dist/core/keybindings/catalog.js +9 -7
- package/dist/core/model-routes.js +10 -3
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/handlers/canvas.js +18 -2
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { envProfileId } from '../../../shared/env.js';
|
|
2
2
|
import { ModelSelectorComponent, SettingsManager, ThinkingSelectorComponent, } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
3
|
+
import { Input, truncateToWidth } from '@earendil-works/pi-tui';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, isManagedProvider, readSubscriptionPool, } from '../../../core/subscription-state.js';
|
|
6
|
-
import { readRawConfigAtRoot, persistDefaultKindModel, persistModelLadders, readMergedLaunchConfig, } from '../../../core/config.js';
|
|
6
|
+
import { readRawConfigAtRoot, persistDefaultKindModel, persistModelLadders, persistModelRoute, readMergedLaunchConfig, } from '../../../core/config.js';
|
|
7
7
|
import { findProjectScopeRoots, userScopeRoot } from '../../../core/scope.js';
|
|
8
8
|
import { loadProfileManifest, profileRoot, } from '../../../core/profiles/manifest.js';
|
|
9
9
|
import { configuredLadderProviders, inferPortableModelSelection, MODEL_PROVIDER_KEYS, MODEL_STRENGTHS, THINKING_SUFFIXES, } from '../../../core/runtime/model-selection.js';
|
|
10
|
-
import { fitTail, padAnsi, renderRows, theme, } from '../../../core/tui/panel.js';
|
|
10
|
+
import { fitTail, padAnsi, renderRows, theme, wrapText, } from '../../../core/tui/panel.js';
|
|
11
11
|
const MODELS_CHROME_LINES = 3;
|
|
12
12
|
const RUNTIME_PROVIDER_ID = {
|
|
13
13
|
anthropic: ANTHROPIC_PROVIDER_ID,
|
|
@@ -18,9 +18,9 @@ const PROVIDER_LABEL = {
|
|
|
18
18
|
openai: "OpenAI",
|
|
19
19
|
};
|
|
20
20
|
const KIND_COLUMNS = [
|
|
21
|
-
{ mode: "base", dimension: "
|
|
21
|
+
{ mode: "base", dimension: "family" },
|
|
22
22
|
{ mode: "base", dimension: "strength" },
|
|
23
|
-
{ mode: "orchestrator", dimension: "
|
|
23
|
+
{ mode: "orchestrator", dimension: "family" },
|
|
24
24
|
{ mode: "orchestrator", dimension: "strength" },
|
|
25
25
|
];
|
|
26
26
|
function modelConfigTargets() {
|
|
@@ -88,6 +88,8 @@ export class ModelsPanel {
|
|
|
88
88
|
label;
|
|
89
89
|
mode = "list";
|
|
90
90
|
ladders;
|
|
91
|
+
modelRoutes;
|
|
92
|
+
legacyModelRoutes;
|
|
91
93
|
kinds;
|
|
92
94
|
/** The ladder providers this panel offers — only columns whose runtime
|
|
93
95
|
* provider is signed in (all of them when nobody is, so config stays
|
|
@@ -105,10 +107,11 @@ export class ModelsPanel {
|
|
|
105
107
|
modelSelector;
|
|
106
108
|
thinkingSelector;
|
|
107
109
|
pending;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
choice;
|
|
111
|
+
choiceIndex = 0;
|
|
112
|
+
routeForm;
|
|
113
|
+
routeInput = new Input();
|
|
114
|
+
pendingRouteCell;
|
|
112
115
|
onChange;
|
|
113
116
|
constructor(tui, runtime, registry, matches, label) {
|
|
114
117
|
this.tui = tui;
|
|
@@ -122,9 +125,27 @@ export class ModelsPanel {
|
|
|
122
125
|
this.rawTarget = readRawConfigAtRoot(this.currentTarget().root);
|
|
123
126
|
const merged = readMergedLaunchConfig();
|
|
124
127
|
this.ladders = merged.modelLadders;
|
|
128
|
+
this.modelRoutes = merged.modelRoutes ?? {};
|
|
129
|
+
this.legacyModelRoutes = merged.legacyModelRoutes ?? {};
|
|
125
130
|
this.kinds = merged.kinds;
|
|
126
131
|
this.providerKeys = this.offeredProviders();
|
|
127
132
|
this.rows = this.buildRows();
|
|
133
|
+
this.routeInput.onSubmit = (value) => {
|
|
134
|
+
const form = this.routeForm;
|
|
135
|
+
if (!form || form.editing === null)
|
|
136
|
+
return;
|
|
137
|
+
form.text[form.editing] = value.trim();
|
|
138
|
+
form.editing = null;
|
|
139
|
+
this.routeInput.focused = false;
|
|
140
|
+
this.notify();
|
|
141
|
+
};
|
|
142
|
+
this.routeInput.onEscape = () => {
|
|
143
|
+
if (!this.routeForm)
|
|
144
|
+
return;
|
|
145
|
+
this.routeForm.editing = null;
|
|
146
|
+
this.routeInput.focused = false;
|
|
147
|
+
this.notify();
|
|
148
|
+
};
|
|
128
149
|
}
|
|
129
150
|
/** Same signed-in rule as the broker's Alt+M cycle: pool occupancy for a
|
|
130
151
|
* managed provider (read live), else the registry's auth status. */
|
|
@@ -160,10 +181,27 @@ export class ModelsPanel {
|
|
|
160
181
|
savedButShadowed(path) {
|
|
161
182
|
return `Saved to ${this.currentTarget().label}; overridden by ${path ?? "a more specific config"}`;
|
|
162
183
|
}
|
|
184
|
+
routeIds() {
|
|
185
|
+
return [
|
|
186
|
+
...new Set([
|
|
187
|
+
...Object.keys(this.modelRoutes),
|
|
188
|
+
...Object.keys(this.legacyModelRoutes),
|
|
189
|
+
]),
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
routeFamilies() {
|
|
193
|
+
return [
|
|
194
|
+
...new Set(this.routeIds().map((routeId) => (this.modelRoutes[routeId] ?? this.legacyModelRoutes[routeId])
|
|
195
|
+
.family)),
|
|
196
|
+
];
|
|
197
|
+
}
|
|
163
198
|
buildRows() {
|
|
164
199
|
const rows = [{ section: "ladder", kind: "defaultProvider" }];
|
|
165
200
|
for (const strength of MODEL_STRENGTHS)
|
|
166
201
|
rows.push({ section: "ladder", kind: "strength", strength });
|
|
202
|
+
for (const routeId of this.routeIds())
|
|
203
|
+
rows.push({ section: "route", routeId });
|
|
204
|
+
rows.push({ section: "route", routeId: null });
|
|
167
205
|
for (const name of Object.keys(this.kinds)) {
|
|
168
206
|
if (name.includes("/"))
|
|
169
207
|
continue;
|
|
@@ -174,6 +212,8 @@ export class ModelsPanel {
|
|
|
174
212
|
reload() {
|
|
175
213
|
const merged = readMergedLaunchConfig();
|
|
176
214
|
this.ladders = merged.modelLadders;
|
|
215
|
+
this.modelRoutes = merged.modelRoutes ?? {};
|
|
216
|
+
this.legacyModelRoutes = merged.legacyModelRoutes ?? {};
|
|
177
217
|
this.kinds = merged.kinds;
|
|
178
218
|
this.rawTarget = readRawConfigAtRoot(this.currentTarget().root);
|
|
179
219
|
this.providerKeys = this.offeredProviders();
|
|
@@ -200,15 +240,60 @@ export class ModelsPanel {
|
|
|
200
240
|
columnsFor(row) {
|
|
201
241
|
if (row.section === "ladder")
|
|
202
242
|
return row.kind === "strength" ? this.providerKeys.length : 1;
|
|
243
|
+
if (row.section === "route")
|
|
244
|
+
return 1;
|
|
203
245
|
return KIND_COLUMNS.length;
|
|
204
246
|
}
|
|
205
247
|
clampColumn() {
|
|
206
248
|
this.column = Math.min(this.column, this.columnsFor(this.currentRow()) - 1);
|
|
207
249
|
}
|
|
208
250
|
handleInput(data) {
|
|
251
|
+
if (this.mode === "route-form") {
|
|
252
|
+
const form = this.routeForm;
|
|
253
|
+
if (!form) {
|
|
254
|
+
this.mode = "list";
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
if (form.editing !== null) {
|
|
258
|
+
this.routeInput.handleInput(data);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (this.matches("crtr.setup.models.cancel", data)) {
|
|
262
|
+
this.closeRouteForm();
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (this.matches("crtr.setup.list-down", data)) {
|
|
266
|
+
form.index = Math.min(form.fields.length - 1, form.index + 1);
|
|
267
|
+
this.notice = "";
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (this.matches("crtr.setup.list-up", data)) {
|
|
271
|
+
form.index = Math.max(0, form.index - 1);
|
|
272
|
+
this.notice = "";
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (this.matches("crtr.setup.models.open", data) ||
|
|
276
|
+
this.matches("crtr.setup.models.commit", data)) {
|
|
277
|
+
this.activateRouteField();
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (this.mode === "route-delete") {
|
|
283
|
+
if (this.matches("crtr.setup.confirm.accept", data)) {
|
|
284
|
+
this.deleteRoute();
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (this.matches("crtr.setup.confirm.back", data)) {
|
|
288
|
+
this.mode = "route-form";
|
|
289
|
+
this.notify();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
209
294
|
if (this.mode === "dimension-picker") {
|
|
210
295
|
if (this.matches("crtr.setup.models.commit", data)) {
|
|
211
|
-
this.
|
|
296
|
+
this.commitChoice();
|
|
212
297
|
return true;
|
|
213
298
|
}
|
|
214
299
|
if (this.matches("crtr.setup.models.cancel", data)) {
|
|
@@ -216,11 +301,11 @@ export class ModelsPanel {
|
|
|
216
301
|
return true;
|
|
217
302
|
}
|
|
218
303
|
if (this.matches("crtr.setup.list-down", data)) {
|
|
219
|
-
this.
|
|
304
|
+
this.choiceIndex = Math.min((this.choice?.options.length ?? 1) - 1, this.choiceIndex + 1);
|
|
220
305
|
return true;
|
|
221
306
|
}
|
|
222
307
|
if (this.matches("crtr.setup.list-up", data)) {
|
|
223
|
-
this.
|
|
308
|
+
this.choiceIndex = Math.max(0, this.choiceIndex - 1);
|
|
224
309
|
return true;
|
|
225
310
|
}
|
|
226
311
|
return true;
|
|
@@ -306,6 +391,10 @@ export class ModelsPanel {
|
|
|
306
391
|
this.openModelPicker(this.providerKeys[this.column], row.strength);
|
|
307
392
|
return;
|
|
308
393
|
}
|
|
394
|
+
if (row.section === "route") {
|
|
395
|
+
this.openRouteForm(row.routeId);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
309
398
|
if (row.section === "kind") {
|
|
310
399
|
const col = KIND_COLUMNS[this.column];
|
|
311
400
|
if (!col)
|
|
@@ -313,7 +402,28 @@ export class ModelsPanel {
|
|
|
313
402
|
this.openDimensionPicker(row.name, col.mode, col.dimension);
|
|
314
403
|
}
|
|
315
404
|
}
|
|
316
|
-
|
|
405
|
+
portableSelection(value) {
|
|
406
|
+
if (value !== undefined) {
|
|
407
|
+
const slash = value.lastIndexOf("/");
|
|
408
|
+
const family = slash === -1 ? "" : value.slice(0, slash);
|
|
409
|
+
const strength = slash === -1 ? "" : value.slice(slash + 1);
|
|
410
|
+
if (this.routeFamilies().includes(family) &&
|
|
411
|
+
MODEL_STRENGTHS.includes(strength)) {
|
|
412
|
+
return { family, strength: strength };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const inferred = inferPortableModelSelection(value, this.ladders);
|
|
416
|
+
return { family: inferred.provider, strength: inferred.strength };
|
|
417
|
+
}
|
|
418
|
+
familyLabel(family) {
|
|
419
|
+
return family === "anthropic" || family === "openai"
|
|
420
|
+
? PROVIDER_LABEL[family]
|
|
421
|
+
: family;
|
|
422
|
+
}
|
|
423
|
+
familyOptions() {
|
|
424
|
+
return [...new Set([...this.providerKeys, ...this.routeFamilies()])];
|
|
425
|
+
}
|
|
426
|
+
/** Snapshot of the mode's current family+strength. For orchestrator this
|
|
317
427
|
* falls back to the base `model` when no explicit override exists, so the
|
|
318
428
|
* inherited pair is what an edit preserves. */
|
|
319
429
|
kindSelection(name, mode) {
|
|
@@ -321,59 +431,322 @@ export class ModelsPanel {
|
|
|
321
431
|
const value = mode === "orchestrator"
|
|
322
432
|
? (cfg?.orchestratorModel ?? cfg?.model)
|
|
323
433
|
: cfg?.model;
|
|
324
|
-
return
|
|
434
|
+
return this.portableSelection(value);
|
|
325
435
|
}
|
|
326
436
|
openDimensionPicker(name, mode, dimension) {
|
|
327
437
|
const current = this.kindSelection(name, mode);
|
|
328
|
-
const options = dimension === "
|
|
329
|
-
const currentToken = dimension === "
|
|
330
|
-
|
|
331
|
-
|
|
438
|
+
const options = dimension === "family" ? this.familyOptions() : MODEL_STRENGTHS;
|
|
439
|
+
const currentToken = dimension === "family" ? current.family : current.strength;
|
|
440
|
+
const modeLabel = mode === "orchestrator" ? "orchestrator" : "base";
|
|
441
|
+
const paired = dimension === "family"
|
|
442
|
+
? `strength ${current.strength}`
|
|
443
|
+
: `family ${this.familyLabel(current.family)}`;
|
|
444
|
+
this.choice = {
|
|
445
|
+
title: `Choose ${modeLabel} ${dimension}`,
|
|
446
|
+
subtitle: `${name} · keeps ${modeLabel} ${paired}`,
|
|
447
|
+
options,
|
|
448
|
+
label: (option) => dimension === "family" ? this.familyLabel(option) : option,
|
|
449
|
+
commit: (chosen) => {
|
|
450
|
+
const family = dimension === "family" ? chosen : current.family;
|
|
451
|
+
const strength = dimension === "strength"
|
|
452
|
+
? chosen
|
|
453
|
+
: current.strength;
|
|
454
|
+
this.writeKindSelection(name, mode, family, strength);
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
this.choiceIndex = Math.max(0, options.indexOf(currentToken));
|
|
332
458
|
this.mode = "dimension-picker";
|
|
333
459
|
this.notify();
|
|
334
460
|
}
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const chosen = pending?.options[this.dimPickIndex];
|
|
338
|
-
if (!pending || chosen === undefined) {
|
|
339
|
-
this.closePicker();
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const provider = pending.dimension === "provider"
|
|
343
|
-
? chosen
|
|
344
|
-
: pending.current.provider;
|
|
345
|
-
const strength = pending.dimension === "strength"
|
|
346
|
-
? chosen
|
|
347
|
-
: pending.current.strength;
|
|
348
|
-
const token = `${provider}/${strength}`;
|
|
461
|
+
writeKindSelection(name, mode, family, strength) {
|
|
462
|
+
const token = `${family}/${strength}`;
|
|
349
463
|
try {
|
|
350
464
|
persistDefaultKindModel({
|
|
351
465
|
root: this.currentTarget().root,
|
|
352
|
-
kind:
|
|
466
|
+
kind: name,
|
|
353
467
|
model: token,
|
|
354
|
-
orchestrator:
|
|
468
|
+
orchestrator: mode === "orchestrator",
|
|
355
469
|
});
|
|
356
470
|
}
|
|
357
471
|
catch (error) {
|
|
358
472
|
this.notice = `Could not save: ${error instanceof Error ? error.message : String(error)}`;
|
|
359
|
-
this.mode = "list";
|
|
360
|
-
this.pendingDim = undefined;
|
|
361
|
-
this.notify();
|
|
362
473
|
return;
|
|
363
474
|
}
|
|
364
|
-
this.mode = "list";
|
|
365
|
-
this.pendingDim = undefined;
|
|
366
475
|
this.reload();
|
|
367
|
-
const after = this.kinds[
|
|
368
|
-
const effective =
|
|
476
|
+
const after = this.kinds[name];
|
|
477
|
+
const effective = mode === "orchestrator" ? after?.orchestratorModel : after?.model;
|
|
369
478
|
if (effective !== token) {
|
|
370
|
-
const field =
|
|
479
|
+
const field = mode === "orchestrator" ? "orchestratorModel" : "model";
|
|
371
480
|
const path = this.shadowingPath((raw) => {
|
|
372
|
-
const entry = raw.kinds?.[
|
|
481
|
+
const entry = raw.kinds?.[name];
|
|
373
482
|
return entry?.[field] !== undefined;
|
|
374
483
|
});
|
|
375
484
|
this.notice = this.savedButShadowed(path);
|
|
376
485
|
}
|
|
486
|
+
}
|
|
487
|
+
commitChoice() {
|
|
488
|
+
const choice = this.choice;
|
|
489
|
+
const chosen = choice?.options[this.choiceIndex];
|
|
490
|
+
if (choice && chosen !== undefined)
|
|
491
|
+
choice.commit(chosen);
|
|
492
|
+
this.closePicker();
|
|
493
|
+
}
|
|
494
|
+
openRouteForm(routeId) {
|
|
495
|
+
const route = routeId === null
|
|
496
|
+
? undefined
|
|
497
|
+
: (this.modelRoutes[routeId] ?? this.legacyModelRoutes[routeId]);
|
|
498
|
+
const fields = [
|
|
499
|
+
...(routeId === null
|
|
500
|
+
? [{ kind: "text", key: "routeId", label: "Route id" }]
|
|
501
|
+
: []),
|
|
502
|
+
{ kind: "text", key: "family", label: "Family" },
|
|
503
|
+
{
|
|
504
|
+
kind: "text",
|
|
505
|
+
key: "credentialSource",
|
|
506
|
+
label: "Credential source",
|
|
507
|
+
},
|
|
508
|
+
{ kind: "provider", label: "Provider id" },
|
|
509
|
+
...MODEL_STRENGTHS.map((strength) => ({ kind: "cell", strength })),
|
|
510
|
+
{ kind: "save" },
|
|
511
|
+
...(routeId === null ? [] : [{ kind: "delete" }]),
|
|
512
|
+
];
|
|
513
|
+
this.routeForm = {
|
|
514
|
+
existing: routeId,
|
|
515
|
+
text: {
|
|
516
|
+
routeId: routeId ?? "",
|
|
517
|
+
family: route?.family ?? "",
|
|
518
|
+
credentialSource: route?.credentialSource ?? "",
|
|
519
|
+
},
|
|
520
|
+
providerId: route?.providerId ?? "",
|
|
521
|
+
models: { ...route?.models },
|
|
522
|
+
fields,
|
|
523
|
+
index: 0,
|
|
524
|
+
editing: null,
|
|
525
|
+
};
|
|
526
|
+
this.mode = "route-form";
|
|
527
|
+
this.notice = "";
|
|
528
|
+
this.notify();
|
|
529
|
+
}
|
|
530
|
+
closeRouteForm() {
|
|
531
|
+
this.routeInput.focused = false;
|
|
532
|
+
this.routeForm = undefined;
|
|
533
|
+
this.mode = "list";
|
|
534
|
+
this.notice = "";
|
|
535
|
+
this.notify();
|
|
536
|
+
}
|
|
537
|
+
activateRouteField() {
|
|
538
|
+
const form = this.routeForm;
|
|
539
|
+
const field = form?.fields[form.index];
|
|
540
|
+
if (!form || !field)
|
|
541
|
+
return;
|
|
542
|
+
if (field.kind === "text") {
|
|
543
|
+
form.editing = field.key;
|
|
544
|
+
this.routeInput.setValue(form.text[field.key]);
|
|
545
|
+
this.routeInput.handleInput("\x05");
|
|
546
|
+
this.routeInput.focused = true;
|
|
547
|
+
this.notify();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (field.kind === "provider") {
|
|
551
|
+
const options = [
|
|
552
|
+
...new Set([
|
|
553
|
+
...this.registry.getAll().map((model) => model.provider),
|
|
554
|
+
...(form.providerId ? [form.providerId] : []),
|
|
555
|
+
]),
|
|
556
|
+
].sort();
|
|
557
|
+
this.choice = {
|
|
558
|
+
title: "Choose provider id",
|
|
559
|
+
subtitle: "Route cells must all belong to this registry provider",
|
|
560
|
+
options,
|
|
561
|
+
label: (option) => option,
|
|
562
|
+
commit: (chosen) => this.setRouteProvider(chosen),
|
|
563
|
+
};
|
|
564
|
+
this.choiceIndex = Math.max(0, options.indexOf(form.providerId));
|
|
565
|
+
this.mode = "dimension-picker";
|
|
566
|
+
this.notify();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (field.kind === "cell") {
|
|
570
|
+
this.openRouteCell(field.strength);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (field.kind === "save") {
|
|
574
|
+
this.saveRoute();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.mode = "route-delete";
|
|
578
|
+
this.notify();
|
|
579
|
+
}
|
|
580
|
+
setRouteProvider(providerId) {
|
|
581
|
+
const form = this.routeForm;
|
|
582
|
+
if (!form)
|
|
583
|
+
return;
|
|
584
|
+
form.providerId = providerId;
|
|
585
|
+
const cleared = [];
|
|
586
|
+
for (const strength of MODEL_STRENGTHS) {
|
|
587
|
+
const ref = form.models[strength];
|
|
588
|
+
if (ref !== undefined && !ref.startsWith(`${providerId}/`)) {
|
|
589
|
+
delete form.models[strength];
|
|
590
|
+
cleared.push(strength);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
this.notice =
|
|
594
|
+
cleared.length === 0
|
|
595
|
+
? ""
|
|
596
|
+
: `Cleared ${cleared.join(", ")} because those models belong to another provider.`;
|
|
597
|
+
}
|
|
598
|
+
openRouteCell(strength) {
|
|
599
|
+
const form = this.routeForm;
|
|
600
|
+
if (!form)
|
|
601
|
+
return;
|
|
602
|
+
if (!form.providerId) {
|
|
603
|
+
this.notice = "Choose the provider id first";
|
|
604
|
+
this.notify();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const scoped = this.registry
|
|
608
|
+
.getAll()
|
|
609
|
+
.filter((model) => model.provider === form.providerId);
|
|
610
|
+
if (scoped.length === 0) {
|
|
611
|
+
this.notice = `No models registered for provider "${form.providerId}" — this route cannot be edited here.`;
|
|
612
|
+
this.notify();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const parsed = parseModelRef(form.models[strength]);
|
|
616
|
+
const current = parsed.provider && parsed.id
|
|
617
|
+
? this.registry.find(parsed.provider, parsed.id)
|
|
618
|
+
: undefined;
|
|
619
|
+
this.pendingRouteCell = { strength };
|
|
620
|
+
this.showModelSelector(scoped, current, (model) => this.onRouteModelPicked(model));
|
|
621
|
+
}
|
|
622
|
+
onRouteModelPicked(model) {
|
|
623
|
+
const pending = this.pendingRouteCell;
|
|
624
|
+
const form = this.routeForm;
|
|
625
|
+
if (!pending || !form) {
|
|
626
|
+
this.closePicker();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (model.provider !== form.providerId) {
|
|
630
|
+
this.notice = `That is a ${model.provider} model — pick a ${form.providerId} model for this route.`;
|
|
631
|
+
this.closePicker();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
pending.model = model;
|
|
635
|
+
if (model.reasoning) {
|
|
636
|
+
const currentLevel = parseModelRef(form.models[pending.strength]).level ?? "medium";
|
|
637
|
+
this.thinkingSelector = new ThinkingSelectorComponent(currentLevel, [...THINKING_SUFFIXES], (level) => this.onThinkingPicked(level), () => this.closePicker());
|
|
638
|
+
this.modelSelector = undefined;
|
|
639
|
+
this.mode = "thinking-picker";
|
|
640
|
+
this.notify();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
form.models[pending.strength] = `${model.provider}/${model.id}`;
|
|
644
|
+
this.closePicker();
|
|
645
|
+
}
|
|
646
|
+
saveRoute() {
|
|
647
|
+
const form = this.routeForm;
|
|
648
|
+
if (!form)
|
|
649
|
+
return;
|
|
650
|
+
const routeId = form.text.routeId.trim();
|
|
651
|
+
if (!routeId) {
|
|
652
|
+
this.notice = "Route id is required.";
|
|
653
|
+
this.notify();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (!form.text.family.trim()) {
|
|
657
|
+
this.notice = "Family is required.";
|
|
658
|
+
this.notify();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!form.text.credentialSource.trim()) {
|
|
662
|
+
this.notice = "Credential source is required.";
|
|
663
|
+
this.notify();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!form.providerId.trim()) {
|
|
667
|
+
this.notice = "Provider id is required.";
|
|
668
|
+
this.notify();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const missing = MODEL_STRENGTHS.filter((strength) => form.models[strength] === undefined);
|
|
672
|
+
if (missing.length > 0) {
|
|
673
|
+
this.notice = `Choose models for ${missing.join(", ")} before saving.`;
|
|
674
|
+
this.notify();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const route = {
|
|
678
|
+
family: form.text.family.trim(),
|
|
679
|
+
credentialSource: form.text.credentialSource.trim(),
|
|
680
|
+
providerId: form.providerId.trim(),
|
|
681
|
+
models: {
|
|
682
|
+
ultra: form.models.ultra,
|
|
683
|
+
strong: form.models.strong,
|
|
684
|
+
medium: form.models.medium,
|
|
685
|
+
light: form.models.light,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
try {
|
|
689
|
+
persistModelRoute(this.currentTarget().root, routeId, route);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
this.notice = `Could not save: ${error instanceof Error ? error.message : String(error)}`;
|
|
693
|
+
this.notify();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this.routeForm = undefined;
|
|
697
|
+
this.mode = "list";
|
|
698
|
+
this.reload();
|
|
699
|
+
const routeRow = this.rows.findIndex((row) => row.section === "route" && row.routeId === routeId);
|
|
700
|
+
if (routeRow >= 0)
|
|
701
|
+
this.selected = routeRow;
|
|
702
|
+
if (JSON.stringify(this.modelRoutes[routeId]) !== JSON.stringify(route)) {
|
|
703
|
+
const path = this.shadowingPath((raw) => raw.modelRoutes?.[routeId] !== undefined);
|
|
704
|
+
this.notice = this.savedButShadowed(path);
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
this.notice = `Saved route "${routeId}" to ${this.currentTarget().label}.`;
|
|
708
|
+
}
|
|
709
|
+
this.notify();
|
|
710
|
+
}
|
|
711
|
+
declaringPath(routeId) {
|
|
712
|
+
for (const root of this.precedenceRoots) {
|
|
713
|
+
if (root === this.currentTarget().root)
|
|
714
|
+
continue;
|
|
715
|
+
const raw = readRawConfigAtRoot(root);
|
|
716
|
+
if (raw?.modelRoutes?.[routeId] !== undefined)
|
|
717
|
+
return join(root, "config.json");
|
|
718
|
+
}
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
deleteRoute() {
|
|
722
|
+
const routeId = this.routeForm?.existing;
|
|
723
|
+
if (!routeId) {
|
|
724
|
+
this.mode = "route-form";
|
|
725
|
+
this.notify();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
persistModelRoute(this.currentTarget().root, routeId, undefined);
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
this.mode = "route-form";
|
|
733
|
+
this.notice = `Could not delete: ${error instanceof Error ? error.message : String(error)}`;
|
|
734
|
+
this.notify();
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const inherited = this.declaringPath(routeId);
|
|
738
|
+
this.routeForm = undefined;
|
|
739
|
+
this.mode = "list";
|
|
740
|
+
this.reload();
|
|
741
|
+
this.notice = inherited
|
|
742
|
+
? `Removed route "${routeId}" from ${this.currentTarget().label}; it is still declared by ${inherited}.`
|
|
743
|
+
: `Removed route "${routeId}" from ${this.currentTarget().label}.`;
|
|
744
|
+
this.notify();
|
|
745
|
+
}
|
|
746
|
+
showModelSelector(scoped, current, onPick) {
|
|
747
|
+
this.modelSelector = new ModelSelectorComponent(this.tui, current, SettingsManager.inMemory(), this.runtime, scoped.map((model) => ({ model })), onPick, () => this.closePicker());
|
|
748
|
+
this.modelSelector.focused = true;
|
|
749
|
+
this.mode = "model-picker";
|
|
377
750
|
this.notify();
|
|
378
751
|
}
|
|
379
752
|
openModelPicker(providerKey, strength) {
|
|
@@ -385,18 +758,14 @@ export class ModelsPanel {
|
|
|
385
758
|
: undefined;
|
|
386
759
|
const scoped = registry
|
|
387
760
|
.getAvailable()
|
|
388
|
-
.filter((
|
|
389
|
-
.map((m) => ({ model: m }));
|
|
761
|
+
.filter((model) => model.provider === expectedRuntime);
|
|
390
762
|
if (scoped.length === 0) {
|
|
391
763
|
this.notice = `No ${providerKey} models available \u2014 sign in first.`;
|
|
392
764
|
this.notify();
|
|
393
765
|
return;
|
|
394
766
|
}
|
|
395
767
|
this.pending = { providerKey, strength, expectedRuntime };
|
|
396
|
-
this.
|
|
397
|
-
this.modelSelector.focused = true;
|
|
398
|
-
this.mode = "model-picker";
|
|
399
|
-
this.notify();
|
|
768
|
+
this.showModelSelector(scoped, currentModel, (model) => this.onModelPicked(model));
|
|
400
769
|
}
|
|
401
770
|
onModelPicked(model) {
|
|
402
771
|
if (!this.pending) {
|
|
@@ -421,8 +790,13 @@ export class ModelsPanel {
|
|
|
421
790
|
this.closePicker();
|
|
422
791
|
}
|
|
423
792
|
onThinkingPicked(level) {
|
|
424
|
-
if (this.
|
|
793
|
+
if (this.pendingRouteCell?.model && this.routeForm) {
|
|
794
|
+
const { model, strength } = this.pendingRouteCell;
|
|
795
|
+
this.routeForm.models[strength] = `${model.provider}/${model.id}${level !== "off" ? `:${level}` : ""}`;
|
|
796
|
+
}
|
|
797
|
+
else if (this.pending?.model) {
|
|
425
798
|
this.writeLadder(this.pending.model, level);
|
|
799
|
+
}
|
|
426
800
|
this.closePicker();
|
|
427
801
|
}
|
|
428
802
|
writeLadder(model, level) {
|
|
@@ -446,14 +820,16 @@ export class ModelsPanel {
|
|
|
446
820
|
}
|
|
447
821
|
}
|
|
448
822
|
closePicker() {
|
|
449
|
-
this.mode = "list";
|
|
823
|
+
this.mode = this.routeForm !== undefined ? "route-form" : "list";
|
|
450
824
|
this.modelSelector = undefined;
|
|
451
825
|
this.thinkingSelector = undefined;
|
|
452
826
|
this.pending = undefined;
|
|
453
|
-
this.
|
|
827
|
+
this.pendingRouteCell = undefined;
|
|
828
|
+
this.choice = undefined;
|
|
454
829
|
this.notify();
|
|
455
830
|
}
|
|
456
831
|
dispose() {
|
|
832
|
+
this.routeForm = undefined;
|
|
457
833
|
this.closePicker();
|
|
458
834
|
}
|
|
459
835
|
targetLadderOverride(providerKey, strength) {
|
|
@@ -473,33 +849,98 @@ export class ModelsPanel {
|
|
|
473
849
|
return `type to search \u00b7 ${this.label("crtr.setup.models.commit")} select \u00b7 tab all/scoped \u00b7 ${this.label("crtr.setup.models.cancel")} cancel`;
|
|
474
850
|
if (this.mode === "thinking-picker")
|
|
475
851
|
return `\u2191/\u2193 level \u00b7 ${this.label("crtr.setup.models.commit")} set \u00b7 ${this.label("crtr.setup.models.cancel")} cancel`;
|
|
852
|
+
if (this.mode === "route-delete")
|
|
853
|
+
return `${this.label("crtr.setup.confirm.accept")} delete \u00b7 ${this.label("crtr.setup.confirm.back")} cancel`;
|
|
854
|
+
if (this.mode === "route-form") {
|
|
855
|
+
return typeof this.routeForm?.editing === "string"
|
|
856
|
+
? "type a value · enter save · esc cancel"
|
|
857
|
+
: `${this.label("crtr.setup.list-down")}/${this.label("crtr.setup.list-up")} field · ${this.label("crtr.setup.models.open")} edit · ${this.label("crtr.setup.models.cancel")} back`;
|
|
858
|
+
}
|
|
476
859
|
return "";
|
|
477
860
|
}
|
|
478
861
|
/** Body lines only — the caller frames and windows them. */
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
862
|
+
renderChoicePicker() {
|
|
863
|
+
const choice = this.choice;
|
|
864
|
+
if (!choice)
|
|
482
865
|
return [theme.fg("warning", "No setting selected.")];
|
|
483
|
-
const modeLabel = pending.mode === "orchestrator" ? "orchestrator" : "base";
|
|
484
|
-
const dimLabel = pending.dimension;
|
|
485
|
-
const paired = pending.dimension === "provider"
|
|
486
|
-
? `strength ${pending.current.strength}`
|
|
487
|
-
: `provider ${PROVIDER_LABEL[pending.current.provider]}`;
|
|
488
866
|
const body = [
|
|
489
|
-
theme.fg("accent", theme.bold(
|
|
490
|
-
theme.fg("dim",
|
|
867
|
+
theme.fg("accent", theme.bold(choice.title)),
|
|
868
|
+
theme.fg("dim", choice.subtitle),
|
|
491
869
|
"",
|
|
492
870
|
];
|
|
493
|
-
for (const [index, option] of
|
|
494
|
-
const active = index === this.
|
|
871
|
+
for (const [index, option] of choice.options.entries()) {
|
|
872
|
+
const active = index === this.choiceIndex;
|
|
495
873
|
const marker = active ? theme.fg("accent", "\u25b6") : " ";
|
|
496
|
-
const label =
|
|
497
|
-
? PROVIDER_LABEL[option]
|
|
498
|
-
: option;
|
|
874
|
+
const label = choice.label(option);
|
|
499
875
|
body.push(`${marker} ${active ? theme.fg("accent", `[${label}]`) : theme.fg("dim", ` ${label} `)}`);
|
|
500
876
|
}
|
|
501
877
|
return body;
|
|
502
878
|
}
|
|
879
|
+
renderRouteForm(width) {
|
|
880
|
+
const form = this.routeForm;
|
|
881
|
+
if (!form)
|
|
882
|
+
return [theme.fg("warning", "No route selected.")];
|
|
883
|
+
const target = this.currentTarget();
|
|
884
|
+
const body = [
|
|
885
|
+
theme.fg("accent", theme.bold(form.existing === null
|
|
886
|
+
? "New provider route"
|
|
887
|
+
: `Provider route: ${form.existing}`)),
|
|
888
|
+
theme.fg("dim", `${target.label} · ${join(target.root, "config.json")}`),
|
|
889
|
+
"",
|
|
890
|
+
];
|
|
891
|
+
const labelWidth = 20;
|
|
892
|
+
for (const [index, field] of form.fields.entries()) {
|
|
893
|
+
const active = index === form.index;
|
|
894
|
+
const marker = active ? theme.fg("accent", "▶") : " ";
|
|
895
|
+
let label;
|
|
896
|
+
let value;
|
|
897
|
+
if (field.kind === "text") {
|
|
898
|
+
label = field.label;
|
|
899
|
+
value = form.text[field.key] || "—";
|
|
900
|
+
if (active && form.editing === field.key) {
|
|
901
|
+
const inputWidth = Math.max(8, width - labelWidth - 4);
|
|
902
|
+
value = this.routeInput.render(inputWidth)[0] ?? "";
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else if (field.kind === "provider") {
|
|
906
|
+
label = field.label;
|
|
907
|
+
value = form.providerId || "—";
|
|
908
|
+
}
|
|
909
|
+
else if (field.kind === "cell") {
|
|
910
|
+
label = field.strength;
|
|
911
|
+
value = shortRef(form.models[field.strength]);
|
|
912
|
+
}
|
|
913
|
+
else if (field.kind === "save") {
|
|
914
|
+
label = "";
|
|
915
|
+
value = "Save route";
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
label = "";
|
|
919
|
+
value = "Delete route";
|
|
920
|
+
}
|
|
921
|
+
const missing = value === "—";
|
|
922
|
+
const rendered = active
|
|
923
|
+
? theme.fg("accent", `[${value}]`)
|
|
924
|
+
: theme.fg(missing ? "warning" : "dim", ` ${value} `);
|
|
925
|
+
body.push(`${marker} ${padAnsi(label, labelWidth)}${rendered}`);
|
|
926
|
+
}
|
|
927
|
+
if (this.notice)
|
|
928
|
+
body.push("", theme.fg("warning", this.notice));
|
|
929
|
+
return body;
|
|
930
|
+
}
|
|
931
|
+
renderRouteDelete(width) {
|
|
932
|
+
const routeId = this.routeForm?.existing ?? "this route";
|
|
933
|
+
const target = this.currentTarget();
|
|
934
|
+
const warning = wrapText(`This removes the entry from ${target.label}. Any "${routeId}" declared by a lower-precedence config becomes effective again; this does not guarantee the route is gone.`, width).map((line) => theme.fg("warning", line));
|
|
935
|
+
return [
|
|
936
|
+
theme.fg("accent", theme.bold(`Delete provider route: ${routeId}`)),
|
|
937
|
+
theme.fg("dim", join(target.root, "config.json")),
|
|
938
|
+
"",
|
|
939
|
+
...warning,
|
|
940
|
+
"",
|
|
941
|
+
theme.fg("dim", `${this.label("crtr.setup.confirm.accept")} delete ${this.label("crtr.setup.confirm.back")} cancel`),
|
|
942
|
+
];
|
|
943
|
+
}
|
|
503
944
|
/** Fits the panel body into `budget` rows. The wizard draws header/tabs above
|
|
504
945
|
* and a footer below, so a body taller than its budget pushes the tab strip
|
|
505
946
|
* off the top of the terminal — the panel must window itself instead. Chrome
|
|
@@ -520,11 +961,17 @@ export class ModelsPanel {
|
|
|
520
961
|
const contentBudget = Math.max(1, height);
|
|
521
962
|
const frame = (lines) => renderRows(lines, width);
|
|
522
963
|
if (this.mode === "dimension-picker")
|
|
523
|
-
return frame(fitTail(this.
|
|
964
|
+
return frame(fitTail(this.renderChoicePicker(), contentBudget));
|
|
524
965
|
if (this.mode === "model-picker")
|
|
525
966
|
return frame(fitTail(this.modelSelector?.render(width) ?? [], contentBudget));
|
|
526
967
|
if (this.mode === "thinking-picker")
|
|
527
968
|
return frame(fitTail(this.thinkingSelector?.render(width) ?? [], contentBudget));
|
|
969
|
+
if (this.mode === "route-form") {
|
|
970
|
+
const selectedLine = 3 + (this.routeForm?.index ?? 0);
|
|
971
|
+
return frame(this.fitBody(this.renderRouteForm(width), selectedLine, contentBudget));
|
|
972
|
+
}
|
|
973
|
+
if (this.mode === "route-delete")
|
|
974
|
+
return frame(fitTail(this.renderRouteDelete(width), contentBudget));
|
|
528
975
|
let selectedLine = 0;
|
|
529
976
|
const body = [
|
|
530
977
|
theme.fg("accent", theme.bold("Models")),
|
|
@@ -597,12 +1044,48 @@ export class ModelsPanel {
|
|
|
597
1044
|
});
|
|
598
1045
|
body.push(`${marker} ${padAnsi(row.strength, ladderLabelWidth)}${cells.join(" ")}`);
|
|
599
1046
|
}
|
|
1047
|
+
body.push("", theme.fg("accent", "Provider routes"));
|
|
1048
|
+
body.push(theme.fg("dim", `${theme.fg("warning", "•")} override in ${target.label} · a declared route replaces the ladders above at launch · id, then family · source · provider`));
|
|
1049
|
+
const routeCellWidth = Math.max(10, Math.floor((innerWidth - 8) / 4));
|
|
1050
|
+
body.push(`${" ".repeat(4)}${MODEL_STRENGTHS.map((strength) => padAnsi(theme.fg("dim", strength), routeCellWidth)).join(" ")}`);
|
|
1051
|
+
const routeIds = this.routeIds();
|
|
1052
|
+
if (routeIds.length === 0) {
|
|
1053
|
+
body.push(theme.fg("dim", " No routes declared — the ladders above are used."));
|
|
1054
|
+
}
|
|
1055
|
+
for (const row of this.rows) {
|
|
1056
|
+
if (row.section !== "route" || row.routeId === null)
|
|
1057
|
+
continue;
|
|
1058
|
+
const routeId = row.routeId;
|
|
1059
|
+
const complete = this.modelRoutes[routeId];
|
|
1060
|
+
const route = complete ?? this.legacyModelRoutes[routeId];
|
|
1061
|
+
const isSel = row === selectedRow;
|
|
1062
|
+
if (isSel)
|
|
1063
|
+
selectedLine = body.length;
|
|
1064
|
+
const marker = isSel ? theme.fg("accent", "▶") : " ";
|
|
1065
|
+
const over = this.rawTarget?.modelRoutes?.[routeId] !== undefined
|
|
1066
|
+
? theme.fg("warning", "•")
|
|
1067
|
+
: " ";
|
|
1068
|
+
const incomplete = complete ? "" : theme.fg("warning", " · incomplete");
|
|
1069
|
+
const metadata = theme.fg("dim", `${route.family} · ${route.credentialSource} · ${route.providerId}`);
|
|
1070
|
+
body.push(`${marker} ${over}${theme.fg(isSel ? "accent" : "dim", routeId)} ${metadata}${incomplete}`);
|
|
1071
|
+
const cells = MODEL_STRENGTHS.map((strength) => {
|
|
1072
|
+
const value = route.models[strength];
|
|
1073
|
+
const rendered = padAnsi(truncateToWidth(shortRef(value), routeCellWidth), routeCellWidth);
|
|
1074
|
+
return theme.fg(value === undefined ? "warning" : "dim", rendered);
|
|
1075
|
+
});
|
|
1076
|
+
body.push(`${" ".repeat(4)}${cells.join(" ")}`);
|
|
1077
|
+
}
|
|
1078
|
+
const newRouteRow = this.rows.find((row) => row.section === "route" && row.routeId === null);
|
|
1079
|
+
const newRouteSelected = newRouteRow === selectedRow;
|
|
1080
|
+
if (newRouteSelected)
|
|
1081
|
+
selectedLine = body.length;
|
|
1082
|
+
body.push(`${newRouteSelected ? theme.fg("accent", "▶") : " "} ${theme.fg(newRouteSelected ? "accent" : "dim", "+ New route")}`);
|
|
600
1083
|
body.push("", theme.fg("accent", "Kind defaults"));
|
|
601
1084
|
body.push(theme.fg("dim", `${theme.fg("warning", "\u2022")} override in ${target.label} \u00b7 orchestrator inherits base until you set it`));
|
|
602
1085
|
const nameW = 16;
|
|
603
|
-
const
|
|
1086
|
+
const familyCellW = 12;
|
|
604
1087
|
const strCellW = 9;
|
|
605
|
-
const groupW =
|
|
1088
|
+
const groupW = familyCellW + 1 + strCellW;
|
|
606
1089
|
// Fixed-width cell: `[value]` when the column is selected, faint (muted) for
|
|
607
1090
|
// an inherited orchestrator value, plain dim otherwise. The 2 brackets/spaces
|
|
608
1091
|
// keep every cell exactly `w` wide so the column headers line up.
|
|
@@ -615,7 +1098,7 @@ export class ModelsPanel {
|
|
|
615
1098
|
: theme.fg("dim", ` ${inner} `);
|
|
616
1099
|
};
|
|
617
1100
|
body.push(`${" ".repeat(2 + nameW)}${padAnsi(theme.bold(theme.fg("dim", "Base")), groupW)} ${padAnsi(theme.bold(theme.fg("dim", "Orchestrator")), groupW)}`);
|
|
618
|
-
body.push(`${" ".repeat(2)}${padAnsi(theme.fg("dim", "Kind"), nameW)}${padAnsi(theme.fg("dim", "
|
|
1101
|
+
body.push(`${" ".repeat(2)}${padAnsi(theme.fg("dim", "Kind"), nameW)}${padAnsi(theme.fg("dim", "Family"), familyCellW)} ${padAnsi(theme.fg("dim", "Strength"), strCellW)} ${padAnsi(theme.fg("dim", "Family"), familyCellW)} ${padAnsi(theme.fg("dim", "Strength"), strCellW)}`);
|
|
619
1102
|
for (const row of this.rows) {
|
|
620
1103
|
if (row.section !== "kind")
|
|
621
1104
|
continue;
|
|
@@ -624,9 +1107,9 @@ export class ModelsPanel {
|
|
|
624
1107
|
if (isSel)
|
|
625
1108
|
selectedLine = body.length;
|
|
626
1109
|
const cfg = this.kinds[row.name];
|
|
627
|
-
const baseSel =
|
|
1110
|
+
const baseSel = this.portableSelection(cfg?.model);
|
|
628
1111
|
const orchInherited = cfg?.orchestratorModel === undefined;
|
|
629
|
-
const orchSel =
|
|
1112
|
+
const orchSel = this.portableSelection(cfg?.orchestratorModel ?? cfg?.model);
|
|
630
1113
|
const baseOver = this.targetKindOverride(row.name, false)
|
|
631
1114
|
? theme.fg("warning", "\u2022")
|
|
632
1115
|
: " ";
|
|
@@ -634,9 +1117,9 @@ export class ModelsPanel {
|
|
|
634
1117
|
? theme.fg("warning", "\u2022")
|
|
635
1118
|
: " ";
|
|
636
1119
|
const cells = [
|
|
637
|
-
cell(`${baseOver}${
|
|
1120
|
+
cell(`${baseOver}${this.familyLabel(baseSel.family)}`, familyCellW, isSel && this.column === 0, false),
|
|
638
1121
|
cell(`${baseOver}${baseSel.strength}`, strCellW, isSel && this.column === 1, false),
|
|
639
|
-
cell(`${orchOver}${
|
|
1122
|
+
cell(`${orchOver}${this.familyLabel(orchSel.family)}`, familyCellW, isSel && this.column === 2, orchInherited),
|
|
640
1123
|
cell(`${orchOver}${orchSel.strength}`, strCellW, isSel && this.column === 3, orchInherited),
|
|
641
1124
|
];
|
|
642
1125
|
body.push(`${marker} ${padAnsi(row.name, nameW)}${cells[0]} ${cells[1]} ${cells[2]} ${cells[3]}`);
|