@north-light/crouter 0.3.262 → 0.3.269

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.
Files changed (150) hide show
  1. package/dist/api/client.d.ts +5 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/canvas.d.ts +53 -2
  4. package/dist/api/dto/health.d.ts +2 -0
  5. package/dist/api/dto/nodes.d.ts +6 -2
  6. package/dist/api/routes.d.ts +1 -0
  7. package/dist/api/routes.js +1 -0
  8. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
  9. package/dist/builtin-memory/internal/memory-loading.md +3 -3
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +10 -5
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
  14. package/dist/clients/attach/input/controller.d.ts +4 -0
  15. package/dist/clients/attach/input/controller.js +8 -0
  16. package/dist/clients/attach/session/bindings.d.ts +3 -0
  17. package/dist/clients/attach/session/bindings.js +7 -0
  18. package/dist/clients/attach/session/keys.d.ts +2 -0
  19. package/dist/clients/attach/session/keys.js +3 -3
  20. package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
  21. package/dist/clients/attach/session/whip-achievements.js +145 -0
  22. package/dist/clients/attach/slash/dispatch.js +5 -3
  23. package/dist/clients/attach/viewer.js +668 -667
  24. package/dist/commands/canvas-history/grep.js +3 -22
  25. package/dist/commands/canvas-history/read.js +5 -3
  26. package/dist/commands/canvas-history/search.js +3 -22
  27. package/dist/commands/canvas-history/shared.d.ts +8 -0
  28. package/dist/commands/canvas-history/shared.js +64 -1
  29. package/dist/commands/canvas-history/stats.d.ts +1 -0
  30. package/dist/commands/canvas-history/stats.js +68 -0
  31. package/dist/commands/canvas-history.js +4 -3
  32. package/dist/commands/memory/read.js +15 -38
  33. package/dist/commands/memory/shared.d.ts +1 -1
  34. package/dist/commands/memory/shared.js +1 -1
  35. package/dist/commands/node/create.js +2 -2
  36. package/dist/commands/sys/config.js +19 -2
  37. package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
  38. package/dist/commands/sys/context/admin/model.d.ts +7 -2
  39. package/dist/commands/sys/context/admin/model.js +7 -1
  40. package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
  41. package/dist/commands/sys/context/admin/read-view.js +9 -4
  42. package/dist/commands/sys/context/resolve.d.ts +32 -3
  43. package/dist/commands/sys/context/resolve.js +74 -25
  44. package/dist/commands/sys/panels/models-panel.d.ts +28 -5
  45. package/dist/commands/sys/panels/models-panel.js +560 -75
  46. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
  47. package/dist/core/__tests__/integration/revive.test.js +4 -2
  48. package/dist/core/__tests__/model-pin-durability.test.js +25 -1
  49. package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
  50. package/dist/core/__tests__/model-routes-config.test.js +142 -0
  51. package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
  52. package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
  53. package/dist/core/__tests__/relaunch-root.test.js +17 -0
  54. package/dist/core/__tests__/seam/broker-provider-retry.test.js +21 -6
  55. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
  56. package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
  57. package/dist/core/broker-client/transport-relay.js +5 -1
  58. package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
  59. package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
  60. package/dist/core/canvas/browse/app.d.ts +1 -1
  61. package/dist/core/canvas/browse/app.js +13 -10
  62. package/dist/core/canvas/browse/model.js +16 -1
  63. package/dist/core/canvas/browse/render.d.ts +23 -3
  64. package/dist/core/canvas/browse/render.js +429 -64
  65. package/dist/core/canvas/canvas.js +12 -8
  66. package/dist/core/canvas/extensions.d.ts +1 -1
  67. package/dist/core/canvas/extensions.js +7 -1
  68. package/dist/core/canvas/history.d.ts +36 -1
  69. package/dist/core/canvas/history.js +328 -7
  70. package/dist/core/canvas/labels.d.ts +2 -2
  71. package/dist/core/canvas/migrations.js +32 -0
  72. package/dist/core/canvas/node-recap.d.ts +23 -0
  73. package/dist/core/canvas/node-recap.js +67 -0
  74. package/dist/core/canvas/render-source.d.ts +34 -2
  75. package/dist/core/canvas/render-source.js +83 -24
  76. package/dist/core/canvas/types.d.ts +9 -0
  77. package/dist/core/config.d.ts +19 -2
  78. package/dist/core/config.js +174 -51
  79. package/dist/core/keybindings/catalog.d.ts +2 -2
  80. package/dist/core/keybindings/catalog.js +11 -8
  81. package/dist/core/model-routes.js +11 -4
  82. package/dist/core/runtime/broker/auth-reload.js +4 -1
  83. package/dist/core/runtime/broker/fault-retry.js +7 -2
  84. package/dist/core/runtime/broker/frame-client.js +4 -0
  85. package/dist/core/runtime/broker/frame-dispatch.js +7 -0
  86. package/dist/core/runtime/broker/inbox.d.ts +12 -2
  87. package/dist/core/runtime/broker/inbox.js +68 -38
  88. package/dist/core/runtime/broker-protocol.d.ts +6 -0
  89. package/dist/core/runtime/broker.js +14 -0
  90. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  91. package/dist/core/runtime/canvas-extensions.js +2 -0
  92. package/dist/core/runtime/interactive-deliver.d.ts +8 -2
  93. package/dist/core/runtime/interactive-deliver.js +12 -3
  94. package/dist/core/runtime/launch-target.js +1 -1
  95. package/dist/core/runtime/launch.d.ts +12 -6
  96. package/dist/core/runtime/launch.js +19 -17
  97. package/dist/core/runtime/model-swap.d.ts +5 -4
  98. package/dist/core/runtime/model-swap.js +7 -5
  99. package/dist/core/runtime/promote.js +2 -1
  100. package/dist/core/runtime/reset.js +4 -2
  101. package/dist/core/runtime/revive.js +22 -0
  102. package/dist/core/runtime/spawn.d.ts +3 -3
  103. package/dist/core/runtime/spawn.js +20 -3
  104. package/dist/core/runtime/stamp/channel.d.ts +18 -0
  105. package/dist/core/runtime/stamp/channel.js +74 -0
  106. package/dist/core/runtime/stamp/protocol.d.ts +86 -0
  107. package/dist/core/runtime/stamp/protocol.js +113 -0
  108. package/dist/core/runtime/tmux-driver.d.ts +11 -1
  109. package/dist/core/runtime/tmux-driver.js +27 -11
  110. package/dist/core/self-update.js +2 -5
  111. package/dist/core/substrate/listings.d.ts +1 -1
  112. package/dist/core/substrate/listings.js +6 -2
  113. package/dist/core/substrate/on-read.d.ts +61 -5
  114. package/dist/core/substrate/on-read.js +129 -22
  115. package/dist/core/substrate/render.d.ts +3 -2
  116. package/dist/core/substrate/render.js +133 -38
  117. package/dist/core/tui/draw.d.ts +1 -0
  118. package/dist/core/tui/draw.js +5 -1
  119. package/dist/core/tui/markdown.d.ts +17 -0
  120. package/dist/core/tui/markdown.js +407 -0
  121. package/dist/core/user-settings.d.ts +4 -0
  122. package/dist/core/user-settings.js +1 -0
  123. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
  124. package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
  125. package/dist/daemon/api/bridge.d.ts +3 -2
  126. package/dist/daemon/api/bridge.js +34 -9
  127. package/dist/daemon/api/handlers/canvas.js +80 -18
  128. package/dist/daemon/api/handlers/health.js +17 -12
  129. package/dist/daemon/api/handlers/messages.js +12 -1
  130. package/dist/daemon/api/handlers/nodes.js +10 -3
  131. package/dist/daemon/api/map.js +2 -0
  132. package/dist/daemon/messaging/node-message.js +15 -1
  133. package/dist/daemon/reconcilers/broker-supervision.js +26 -9
  134. package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
  135. package/dist/daemon/reconcilers/live-obligation.js +17 -0
  136. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
  137. package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
  138. package/dist/pi-extensions/canvas-recap.js +4 -0
  139. package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
  140. package/dist/pi-extensions/canvas-stamp.js +77 -0
  141. package/dist/pi-extensions/canvas-stophook.js +3 -0
  142. package/dist/shared/__tests__/env-boundary.test.js +4 -2
  143. package/dist/shared/crtr-version.d.ts +2 -0
  144. package/dist/shared/crtr-version.js +18 -0
  145. package/dist/shared/env.d.ts +10 -0
  146. package/dist/shared/env.js +19 -4
  147. package/dist/types.d.ts +14 -2
  148. package/dist/types.js +4 -0
  149. package/package.json +1 -1
  150. 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: "provider" },
21
+ { mode: "base", dimension: "family" },
22
22
  { mode: "base", dimension: "strength" },
23
- { mode: "orchestrator", dimension: "provider" },
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
- // The kind cell being edited: which mode/dimension, the options to choose
109
- // from, and the current mode selection so the untouched dimension survives.
110
- pendingDim;
111
- dimPickIndex = 0;
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.commitDimensionPick();
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.dimPickIndex = Math.min((this.pendingDim?.options.length ?? 1) - 1, this.dimPickIndex + 1);
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.dimPickIndex = Math.max(0, this.dimPickIndex - 1);
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
- /** Snapshot of the mode's current provider+strength. For orchestrator this
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 inferPortableModelSelection(value, this.ladders);
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 === "provider" ? this.providerKeys : MODEL_STRENGTHS;
329
- const currentToken = dimension === "provider" ? current.provider : current.strength;
330
- this.pendingDim = { name, mode, dimension, options, current };
331
- this.dimPickIndex = Math.max(0, options.indexOf(currentToken));
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
- commitDimensionPick() {
336
- const pending = this.pendingDim;
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: pending.name,
466
+ kind: name,
353
467
  model: token,
354
- orchestrator: pending.mode === "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[pending.name];
368
- const effective = pending.mode === "orchestrator" ? after?.orchestratorModel : after?.model;
476
+ const after = this.kinds[name];
477
+ const effective = mode === "orchestrator" ? after?.orchestratorModel : after?.model;
369
478
  if (effective !== token) {
370
- const field = pending.mode === "orchestrator" ? "orchestratorModel" : "model";
479
+ const field = mode === "orchestrator" ? "orchestratorModel" : "model";
371
480
  const path = this.shadowingPath((raw) => {
372
- const entry = raw.kinds?.[pending.name];
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((m) => m.provider === expectedRuntime)
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.modelSelector = new ModelSelectorComponent(this.tui, currentModel, SettingsManager.inMemory(), this.runtime, scoped, (model) => this.onModelPicked(model), () => this.closePicker());
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.pending?.model)
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.pendingDim = undefined;
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
- renderDimensionPicker(width) {
480
- const pending = this.pendingDim;
481
- if (!pending)
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(`Choose ${modeLabel} ${dimLabel}`)),
490
- theme.fg("dim", `${pending.name} \u00b7 keeps ${modeLabel} ${paired}`),
867
+ theme.fg("accent", theme.bold(choice.title)),
868
+ theme.fg("dim", choice.subtitle),
491
869
  "",
492
870
  ];
493
- for (const [index, option] of pending.options.entries()) {
494
- const active = index === this.dimPickIndex;
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 = pending.dimension === "provider"
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,19 @@ 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.renderDimensionPicker(width), contentBudget));
964
+ // Provider ids run to dozens of options, so the picker windows on its
965
+ // selection like the list does — a tail cut would scroll the cursor off.
966
+ return frame(this.fitBody(this.renderChoicePicker(), MODELS_CHROME_LINES + this.choiceIndex, contentBudget));
524
967
  if (this.mode === "model-picker")
525
968
  return frame(fitTail(this.modelSelector?.render(width) ?? [], contentBudget));
526
969
  if (this.mode === "thinking-picker")
527
970
  return frame(fitTail(this.thinkingSelector?.render(width) ?? [], contentBudget));
971
+ if (this.mode === "route-form") {
972
+ const selectedLine = 3 + (this.routeForm?.index ?? 0);
973
+ return frame(this.fitBody(this.renderRouteForm(width), selectedLine, contentBudget));
974
+ }
975
+ if (this.mode === "route-delete")
976
+ return frame(fitTail(this.renderRouteDelete(width), contentBudget));
528
977
  let selectedLine = 0;
529
978
  const body = [
530
979
  theme.fg("accent", theme.bold("Models")),
@@ -597,12 +1046,48 @@ export class ModelsPanel {
597
1046
  });
598
1047
  body.push(`${marker} ${padAnsi(row.strength, ladderLabelWidth)}${cells.join(" ")}`);
599
1048
  }
1049
+ body.push("", theme.fg("accent", "Provider routes"));
1050
+ 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`));
1051
+ const routeCellWidth = Math.max(10, Math.floor((innerWidth - 8) / 4));
1052
+ body.push(`${" ".repeat(4)}${MODEL_STRENGTHS.map((strength) => padAnsi(theme.fg("dim", strength), routeCellWidth)).join(" ")}`);
1053
+ const routeIds = this.routeIds();
1054
+ if (routeIds.length === 0) {
1055
+ body.push(theme.fg("dim", " No routes declared — the ladders above are used."));
1056
+ }
1057
+ for (const row of this.rows) {
1058
+ if (row.section !== "route" || row.routeId === null)
1059
+ continue;
1060
+ const routeId = row.routeId;
1061
+ const complete = this.modelRoutes[routeId];
1062
+ const route = complete ?? this.legacyModelRoutes[routeId];
1063
+ const isSel = row === selectedRow;
1064
+ if (isSel)
1065
+ selectedLine = body.length;
1066
+ const marker = isSel ? theme.fg("accent", "▶") : " ";
1067
+ const over = this.rawTarget?.modelRoutes?.[routeId] !== undefined
1068
+ ? theme.fg("warning", "•")
1069
+ : " ";
1070
+ const incomplete = complete ? "" : theme.fg("warning", " · incomplete");
1071
+ const metadata = theme.fg("dim", `${route.family} · ${route.credentialSource} · ${route.providerId}`);
1072
+ body.push(`${marker} ${over}${theme.fg(isSel ? "accent" : "dim", routeId)} ${metadata}${incomplete}`);
1073
+ const cells = MODEL_STRENGTHS.map((strength) => {
1074
+ const value = route.models[strength];
1075
+ const rendered = padAnsi(truncateToWidth(shortRef(value), routeCellWidth), routeCellWidth);
1076
+ return theme.fg(value === undefined ? "warning" : "dim", rendered);
1077
+ });
1078
+ body.push(`${" ".repeat(4)}${cells.join(" ")}`);
1079
+ }
1080
+ const newRouteRow = this.rows.find((row) => row.section === "route" && row.routeId === null);
1081
+ const newRouteSelected = newRouteRow === selectedRow;
1082
+ if (newRouteSelected)
1083
+ selectedLine = body.length;
1084
+ body.push(`${newRouteSelected ? theme.fg("accent", "▶") : " "} ${theme.fg(newRouteSelected ? "accent" : "dim", "+ New route")}`);
600
1085
  body.push("", theme.fg("accent", "Kind defaults"));
601
1086
  body.push(theme.fg("dim", `${theme.fg("warning", "\u2022")} override in ${target.label} \u00b7 orchestrator inherits base until you set it`));
602
1087
  const nameW = 16;
603
- const provCellW = 12;
1088
+ const familyCellW = 12;
604
1089
  const strCellW = 9;
605
- const groupW = provCellW + 1 + strCellW;
1090
+ const groupW = familyCellW + 1 + strCellW;
606
1091
  // Fixed-width cell: `[value]` when the column is selected, faint (muted) for
607
1092
  // an inherited orchestrator value, plain dim otherwise. The 2 brackets/spaces
608
1093
  // keep every cell exactly `w` wide so the column headers line up.
@@ -615,7 +1100,7 @@ export class ModelsPanel {
615
1100
  : theme.fg("dim", ` ${inner} `);
616
1101
  };
617
1102
  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", "Provider"), provCellW)} ${padAnsi(theme.fg("dim", "Strength"), strCellW)} ${padAnsi(theme.fg("dim", "Provider"), provCellW)} ${padAnsi(theme.fg("dim", "Strength"), strCellW)}`);
1103
+ 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
1104
  for (const row of this.rows) {
620
1105
  if (row.section !== "kind")
621
1106
  continue;
@@ -624,9 +1109,9 @@ export class ModelsPanel {
624
1109
  if (isSel)
625
1110
  selectedLine = body.length;
626
1111
  const cfg = this.kinds[row.name];
627
- const baseSel = inferPortableModelSelection(cfg?.model, this.ladders);
1112
+ const baseSel = this.portableSelection(cfg?.model);
628
1113
  const orchInherited = cfg?.orchestratorModel === undefined;
629
- const orchSel = inferPortableModelSelection(cfg?.orchestratorModel ?? cfg?.model, this.ladders);
1114
+ const orchSel = this.portableSelection(cfg?.orchestratorModel ?? cfg?.model);
630
1115
  const baseOver = this.targetKindOverride(row.name, false)
631
1116
  ? theme.fg("warning", "\u2022")
632
1117
  : " ";
@@ -634,9 +1119,9 @@ export class ModelsPanel {
634
1119
  ? theme.fg("warning", "\u2022")
635
1120
  : " ";
636
1121
  const cells = [
637
- cell(`${baseOver}${PROVIDER_LABEL[baseSel.provider]}`, provCellW, isSel && this.column === 0, false),
1122
+ cell(`${baseOver}${this.familyLabel(baseSel.family)}`, familyCellW, isSel && this.column === 0, false),
638
1123
  cell(`${baseOver}${baseSel.strength}`, strCellW, isSel && this.column === 1, false),
639
- cell(`${orchOver}${PROVIDER_LABEL[orchSel.provider]}`, provCellW, isSel && this.column === 2, orchInherited),
1124
+ cell(`${orchOver}${this.familyLabel(orchSel.family)}`, familyCellW, isSel && this.column === 2, orchInherited),
640
1125
  cell(`${orchOver}${orchSel.strength}`, strCellW, isSel && this.column === 3, orchInherited),
641
1126
  ];
642
1127
  body.push(`${marker} ${padAnsi(row.name, nameW)}${cells[0]} ${cells[1]} ${cells[2]} ${cells[3]}`);