tickmarkr 1.90.8 → 1.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { PassThrough } from "node:stream";
4
4
  import { render, Text, useApp, useInput } from "ink";
5
5
  import { useRef, useState } from "react";
6
6
  import { MODEL_ID_RE } from "../../adapters/types.js";
7
+ import { retiredModelReason } from "../../adapters/model-lints.js";
7
8
  import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
8
9
  import { TIERS } from "../../graph/schema.js";
9
10
  import { FleetListScreen, FleetReviewScreen, ToggleMark } from "./components.js";
@@ -108,7 +109,7 @@ function splitSeat(seat) {
108
109
  const at = seat.indexOf(":");
109
110
  return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
110
111
  }
111
- export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], }) {
112
+ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, }) {
112
113
  const { exit } = useApp();
113
114
  const [screen, setScreen] = useState(entry === "presets" ? "modes" : "probe");
114
115
  const screenRef = useRef(screen);
@@ -134,6 +135,10 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
134
135
  const tierCursorRef = useRef(0);
135
136
  const [tierCursor, setTierCursor] = useState(0);
136
137
  const pendingClassificationRef = useRef(null);
138
+ // OBS-508: the suggestion riding the in-flight classification (prefills tier + note), and the
139
+ // rows a bulk `s` staged while waiting on the one first-touch channel answer.
140
+ const pendingSuggestionRef = useRef(null);
141
+ const bulkRef = useRef(null);
137
142
  const noteRef = useRef("");
138
143
  const [note, setNote] = useState("");
139
144
  const [notice, setNotice] = useState("");
@@ -178,6 +183,9 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
178
183
  // type-to-search on the long lists — reset on every screen change (per-screen-visit filter)
179
184
  const filterRef = useRef("");
180
185
  const [filter, setFilter] = useState("");
186
+ // `a` on the models screen reveals retired/preview/non-worker shapes; sticky for the session.
187
+ const showAllModelsRef = useRef(false);
188
+ const [, setShowAllModels] = useState(false);
181
189
  const enabledModelGroups = () => modelGroups.filter((group) => !denyRef.current?.has(group.adapter));
182
190
  const stagedMap = () => mapRef.current;
183
191
  const currentModelRows = () => {
@@ -199,7 +207,12 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
199
207
  // type-to-search over the long lists: plain case-insensitive substring on the row's visible
200
208
  // label. Each filterable screen derives its rows through these so input and render agree.
201
209
  const matches = (label, f = filterRef.current) => f === "" || label.toLowerCase().includes(f.toLowerCase());
202
- const filteredModelRows = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f));
210
+ // Operator directive 2026-08-13: retired shapes (dated snapshots, previews, non-worker SKUs,
211
+ // legacy families) hide by DEFAULT — omp reports 218 ids and most can never carry a worker.
212
+ // `a` reveals them; a CLASSIFIED row is never hidden (a tiered dated snapshot was meant).
213
+ const filteredModelRows = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f)
214
+ && (showAllModelsRef.current || row.tier !== undefined || retiredModelReason(row.model) === null));
215
+ const hiddenModelCount = (f = filterRef.current) => currentModelRows().filter((row) => matches(row.model, f)).length - filteredModelRows(f).length;
203
216
  const filteredCandidates = (f = filterRef.current) => candidatesRef.current.filter((candidate) => matches(candidate.label, f));
204
217
  const filteredSteeringRows = (f = filterRef.current) => steeringRowsRef.current.filter((option) => matches(option, f));
205
218
  const judgeKeepRow = initialJudge ? `(keep default) ${initialJudge}` : "(keep default)";
@@ -266,9 +279,26 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
266
279
  setNotice("");
267
280
  showScreen("review");
268
281
  };
282
+ const stageSuggested = (group, rows, firstTouch) => {
283
+ const staged = rows.map((row) => ({
284
+ adapter: group.adapter,
285
+ model: row.model,
286
+ tier: row.suggestion.tier,
287
+ note: row.suggestion.note,
288
+ ...firstTouch,
289
+ }));
290
+ classificationsRef.current = [...classificationsRef.current, ...staged];
291
+ setClassificationRevision((revision) => revision + 1);
292
+ setNotice(`fleet: staged ${staged.length} catalog-suggested classification(s) — nothing writes before the review diff`);
293
+ };
269
294
  const beginClassification = (group, model) => {
270
- tierCursorRef.current = 0;
271
- setTierCursor(0);
295
+ // OBS-508: catalog evidence prefills the flow — tier cursor lands on the suggested band and
296
+ // (when the operator keeps that band) the provenance note arrives pre-typed. Free to override.
297
+ const suggestion = group.rows.find((row) => row.model === model)?.suggestion ?? null;
298
+ pendingSuggestionRef.current = suggestion;
299
+ const tierAt = suggestion ? Math.max(TIERS.indexOf(suggestion.tier), 0) : 0;
300
+ tierCursorRef.current = tierAt;
301
+ setTierCursor(tierAt);
272
302
  const firstTouch = !group.rows.some((row) => row.tier !== undefined);
273
303
  const pending = {
274
304
  adapter: group.adapter,
@@ -309,8 +339,15 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
309
339
  showScreen("steering");
310
340
  return;
311
341
  }
312
- if (key.escape && screenRef.current === "add-model") {
342
+ // The classification sub-flow (add-model → channel → tier → provenance) cancels back to the
343
+ // models list — before this, provenance's own "esc cancel" legend lied: Esc fell through to
344
+ // the walk-home branch in init entry and to QUIT in fleet entry, discarding staged work.
345
+ if (key.escape && (screenRef.current === "add-model" || screenRef.current === "channel" || screenRef.current === "tiers" || screenRef.current === "provenance")) {
346
+ noteRef.current = "";
347
+ setNote("");
313
348
  setNotice("");
349
+ pendingSuggestionRef.current = null;
350
+ bulkRef.current = null;
314
351
  showScreen("models");
315
352
  return;
316
353
  }
@@ -323,13 +360,18 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
323
360
  showScreen("modes");
324
361
  return;
325
362
  }
363
+ // While a type-to-search filter is LIVE, every printable char belongs to the search box:
364
+ // the letter aliases (j/k navigation, q quit, models' t/n/a) go dormant so "fake-n" or
365
+ // "qwen" can actually be typed — the k moved the cursor and the q quit the editor before
366
+ // this (operator field session, 2026-08-13). Arrows, Enter, Space, and Esc keep their roles.
367
+ const letterAlias = filterRef.current === "" ? input : "";
326
368
  const typing = screenRef.current === "provenance" || screenRef.current === "add-model";
327
- if (key.escape || (!typing && input === "q") || (key.ctrl && input === "c")) {
369
+ if (key.escape || (!typing && letterAlias === "q") || (key.ctrl && input === "c")) {
328
370
  finish({ kind: "quit" });
329
371
  return;
330
372
  }
331
373
  if (screenRef.current === "probe") {
332
- if (input === "r") {
374
+ if (letterAlias === "r") {
333
375
  finish({ kind: "refresh" });
334
376
  }
335
377
  else if (key.return) {
@@ -338,13 +380,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
338
380
  return;
339
381
  }
340
382
  if (screenRef.current === "agents") {
341
- if (key.downArrow || input === "j") {
383
+ if (key.downArrow || letterAlias === "j") {
342
384
  const next = Math.min(cursorRef.current + 1, Math.max(agents.length - 1, 0));
343
385
  cursorRef.current = next;
344
386
  setCursor(next);
345
387
  return;
346
388
  }
347
- if (key.upArrow || input === "k") {
389
+ if (key.upArrow || letterAlias === "k") {
348
390
  const next = Math.max(cursorRef.current - 1, 0);
349
391
  cursorRef.current = next;
350
392
  setCursor(next);
@@ -371,14 +413,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
371
413
  }
372
414
  if (screenRef.current === "models") {
373
415
  const rows = filteredModelRows();
374
- if (key.downArrow || input === "j") {
416
+ if (key.downArrow || letterAlias === "j") {
375
417
  const next = Math.min(modelCursorRef.current + 1, Math.max(rows.length - 1, 0));
376
418
  modelCursorRef.current = next;
377
419
  setModelCursor(next);
378
420
  setNotice("");
379
421
  return;
380
422
  }
381
- if (key.upArrow || input === "k") {
423
+ if (key.upArrow || letterAlias === "k") {
382
424
  const next = Math.max(modelCursorRef.current - 1, 0);
383
425
  modelCursorRef.current = next;
384
426
  setModelCursor(next);
@@ -387,7 +429,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
387
429
  }
388
430
  const group = enabledModelGroups()[modelGroupRef.current];
389
431
  const row = rows[modelCursorRef.current];
390
- if (input === " " && group && row?.tier) {
432
+ if (input === " " && group && row) {
433
+ if (!row.tier) {
434
+ // v1.90.9 (operator field report: "not selectable"): an unclassified model has no tier
435
+ // to route on — selecting IS classifying, so Space opens the same channel → tier →
436
+ // provenance flow as `t` instead of dying silently on a row drawn like a checkbox.
437
+ beginClassification(group, row.model);
438
+ return;
439
+ }
391
440
  const id = `${group.adapter}:${row.model}`;
392
441
  const next = new Set(denyModelsRef.current);
393
442
  if (next.has(id))
@@ -398,7 +447,10 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
398
447
  setDenyModels(next);
399
448
  return;
400
449
  }
401
- if (input === "t" && group && row) {
450
+ // Letter hotkeys yield to an ACTIVE search (letterAlias above): with a filter live, every
451
+ // printable char belongs to the search box — otherwise typing "anthropic" toggles show-all
452
+ // at the `a` and opens classify at the `t`.
453
+ if (letterAlias === "t" && group && row) {
402
454
  if (row.tier) {
403
455
  setNotice("fleet: tier reassignment on classified models is not supported in v1 — edit config directly");
404
456
  return;
@@ -406,13 +458,53 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
406
458
  beginClassification(group, row.model);
407
459
  return;
408
460
  }
409
- if (input === "n" && group) {
461
+ if (letterAlias === "n" && group) {
410
462
  modelIdRef.current = "";
411
463
  setModelId("");
412
464
  setNotice("");
413
465
  showScreen("add-model");
414
466
  return;
415
467
  }
468
+ if (letterAlias === "s" && group) {
469
+ // OBS-508: bulk-stage every VISIBLE unclassified model carrying a catalog suggestion —
470
+ // visible means the type-to-search filter and the retired-hide both scope the batch. The
471
+ // staging is consent-preserving: it lands in the same classifications list the single
472
+ // flow feeds, and only the review diff (y) ever writes.
473
+ const suggested = filteredModelRows()
474
+ .filter((candidate) => !candidate.tier && candidate.suggestion !== undefined);
475
+ if (!suggested.length) {
476
+ setNotice("fleet: no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)");
477
+ return;
478
+ }
479
+ const firstTouch = !group.rows.some((candidate) => candidate.tier !== undefined);
480
+ if (!firstTouch) {
481
+ stageSuggested(group, suggested);
482
+ return;
483
+ }
484
+ if (!group.vendor) {
485
+ setNotice(`fleet: ${group.adapter} has no vendor declaration — classification cannot be saved`);
486
+ return;
487
+ }
488
+ const answered = channelByAdapterRef.current[group.adapter];
489
+ if (answered) {
490
+ stageSuggested(group, suggested, { vendor: group.vendor, channel: answered });
491
+ return;
492
+ }
493
+ bulkRef.current = { group, rows: suggested };
494
+ channelCursorRef.current = 0;
495
+ setChannelCursor(0);
496
+ setNotice("");
497
+ showScreen("channel");
498
+ return;
499
+ }
500
+ if (letterAlias === "a") {
501
+ showAllModelsRef.current = !showAllModelsRef.current;
502
+ setShowAllModels(showAllModelsRef.current);
503
+ const clamped = Math.min(modelCursorRef.current, Math.max(filteredModelRows().length - 1, 0));
504
+ modelCursorRef.current = clamped;
505
+ setModelCursor(clamped);
506
+ return;
507
+ }
416
508
  if (key.return) {
417
509
  if (modelGroupRef.current + 1 < enabledModelGroups().length) {
418
510
  const next = modelGroupRef.current + 1;
@@ -464,43 +556,60 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
464
556
  return;
465
557
  }
466
558
  if (screenRef.current === "channel") {
467
- if (key.downArrow || input === "j") {
559
+ if (key.downArrow || letterAlias === "j") {
468
560
  const next = Math.min(channelCursorRef.current + 1, CHANNELS.length - 1);
469
561
  channelCursorRef.current = next;
470
562
  setChannelCursor(next);
471
563
  return;
472
564
  }
473
- if (key.upArrow || input === "k") {
565
+ if (key.upArrow || letterAlias === "k") {
474
566
  const next = Math.max(channelCursorRef.current - 1, 0);
475
567
  channelCursorRef.current = next;
476
568
  setChannelCursor(next);
477
569
  return;
478
570
  }
479
- if (key.return && pendingClassificationRef.current) {
571
+ if (key.return) {
480
572
  const channel = CHANNELS[channelCursorRef.current];
481
- channelByAdapterRef.current[pendingClassificationRef.current.adapter] = channel;
482
- pendingClassificationRef.current.channel = channel;
483
- showScreen("tiers");
573
+ // OBS-508: a bulk stage waiting on the one first-touch channel answer resumes here.
574
+ const bulk = bulkRef.current;
575
+ if (bulk?.group.vendor) {
576
+ channelByAdapterRef.current[bulk.group.adapter] = channel;
577
+ stageSuggested(bulk.group, bulk.rows, { vendor: bulk.group.vendor, channel });
578
+ bulkRef.current = null;
579
+ showScreen("models");
580
+ return;
581
+ }
582
+ if (pendingClassificationRef.current) {
583
+ channelByAdapterRef.current[pendingClassificationRef.current.adapter] = channel;
584
+ pendingClassificationRef.current.channel = channel;
585
+ showScreen("tiers");
586
+ }
484
587
  }
485
588
  return;
486
589
  }
487
590
  if (screenRef.current === "tiers") {
488
- if (key.downArrow || input === "j") {
591
+ if (key.downArrow || letterAlias === "j") {
489
592
  const next = Math.min(tierCursorRef.current + 1, TIERS.length - 1);
490
593
  tierCursorRef.current = next;
491
594
  setTierCursor(next);
492
595
  return;
493
596
  }
494
- if (key.upArrow || input === "k") {
597
+ if (key.upArrow || letterAlias === "k") {
495
598
  const next = Math.max(tierCursorRef.current - 1, 0);
496
599
  tierCursorRef.current = next;
497
600
  setTierCursor(next);
498
601
  return;
499
602
  }
500
603
  if (key.return && pendingClassificationRef.current) {
501
- pendingClassificationRef.current.tier = TIERS[tierCursorRef.current];
502
- noteRef.current = "";
503
- setNote("");
604
+ const chosen = TIERS[tierCursorRef.current];
605
+ pendingClassificationRef.current.tier = chosen;
606
+ // OBS-508: keep the suggested band → the evidence note arrives pre-typed (Enter applies
607
+ // it as-is); override the band → the note starts empty, because the suggestion's text
608
+ // argues for a DIFFERENT tier than the one being recorded.
609
+ const suggestion = pendingSuggestionRef.current;
610
+ const prefill = suggestion && suggestion.tier === chosen ? suggestion.note : "";
611
+ noteRef.current = prefill;
612
+ setNote(prefill);
504
613
  setNotice("");
505
614
  showScreen("provenance");
506
615
  }
@@ -519,6 +628,7 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
519
628
  setClassificationRevision((revision) => revision + 1);
520
629
  }
521
630
  pendingClassificationRef.current = null;
631
+ pendingSuggestionRef.current = null;
522
632
  setNotice("");
523
633
  showScreen("models");
524
634
  return;
@@ -539,13 +649,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
539
649
  // preset entry adds one final `custom` row after the modes — the full-walk escape hatch
540
650
  const rowCount = modeOptions.length + (presetPickRef.current ? 1 : 0);
541
651
  const currentModeCursor = modeCursorRef.current ?? 0;
542
- if (key.downArrow || input === "j") {
652
+ if (key.downArrow || letterAlias === "j") {
543
653
  const next = Math.min(currentModeCursor + 1, Math.max(rowCount - 1, 0));
544
654
  modeCursorRef.current = next;
545
655
  setModeCursor(next);
546
656
  return;
547
657
  }
548
- if (key.upArrow || input === "k") {
658
+ if (key.upArrow || letterAlias === "k") {
549
659
  const next = Math.max(currentModeCursor - 1, 0);
550
660
  modeCursorRef.current = next;
551
661
  setModeCursor(next);
@@ -571,20 +681,20 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
571
681
  }
572
682
  if (screenRef.current === "shapes") {
573
683
  const rows = shapeRows(selectedModeRef.current, stagedMap());
574
- if (key.downArrow || input === "j") {
684
+ if (key.downArrow || letterAlias === "j") {
575
685
  const next = Math.min(shapeCursorRef.current + 1, Math.max(rows.length - 1, 0));
576
686
  shapeCursorRef.current = next;
577
687
  setShapeCursor(next);
578
688
  return;
579
689
  }
580
- if (key.upArrow || input === "k") {
690
+ if (key.upArrow || letterAlias === "k") {
581
691
  const next = Math.max(shapeCursorRef.current - 1, 0);
582
692
  shapeCursorRef.current = next;
583
693
  setShapeCursor(next);
584
694
  return;
585
695
  }
586
696
  const shape = rows[shapeCursorRef.current]?.id;
587
- if (input === "a" && shape) {
697
+ if (letterAlias === "a" && shape) {
588
698
  const nextEntry = { ...stagedMap()[shape] };
589
699
  delete nextEntry.pin;
590
700
  const nextMap = { ...stagedMap(), [shape]: nextEntry };
@@ -592,14 +702,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
592
702
  setMap(nextMap);
593
703
  return;
594
704
  }
595
- if (input === "p" && shape) {
705
+ if (letterAlias === "p" && shape) {
596
706
  candidatesRef.current = candidatesForShape(shape, selectedModeRef.current, stagedMap());
597
707
  candidateCursorRef.current = 0;
598
708
  setCandidateCursor(0);
599
709
  showScreen("candidates");
600
710
  return;
601
711
  }
602
- if (input === "f" && shape) {
712
+ if (letterAlias === "f" && shape) {
603
713
  const current = stagedMap()[shape]?.prefer ?? [];
604
714
  preferRowsRef.current = preferOptionsForShape(shape, current);
605
715
  preferChainRef.current = current.slice();
@@ -614,13 +724,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
614
724
  }
615
725
  if (screenRef.current === "candidates") {
616
726
  const rows = filteredCandidates();
617
- if (key.downArrow || input === "j") {
727
+ if (key.downArrow || letterAlias === "j") {
618
728
  const next = Math.min(candidateCursorRef.current + 1, Math.max(rows.length - 1, 0));
619
729
  candidateCursorRef.current = next;
620
730
  setCandidateCursor(next);
621
731
  return;
622
732
  }
623
- if (key.upArrow || input === "k") {
733
+ if (key.upArrow || letterAlias === "k") {
624
734
  const next = Math.max(candidateCursorRef.current - 1, 0);
625
735
  candidateCursorRef.current = next;
626
736
  setCandidateCursor(next);
@@ -641,13 +751,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
641
751
  return;
642
752
  }
643
753
  if (screenRef.current === "shape-prefer") {
644
- if (key.downArrow || input === "j") {
754
+ if (key.downArrow || letterAlias === "j") {
645
755
  const next = Math.min(preferCursorRef.current + 1, Math.max(preferRowsRef.current.length - 1, 0));
646
756
  preferCursorRef.current = next;
647
757
  setPreferCursor(next);
648
758
  return;
649
759
  }
650
- if (key.upArrow || input === "k") {
760
+ if (key.upArrow || letterAlias === "k") {
651
761
  const next = Math.max(preferCursorRef.current - 1, 0);
652
762
  preferCursorRef.current = next;
653
763
  setPreferCursor(next);
@@ -681,19 +791,19 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
681
791
  }
682
792
  if (screenRef.current === "steering") {
683
793
  // review.prefer + consult.prefer rows plus the single judge seat as the final row
684
- if (key.downArrow || input === "j") {
794
+ if (key.downArrow || letterAlias === "j") {
685
795
  const next = Math.min(steeringCursorRef.current + 1, STEERING_KEYS.length);
686
796
  steeringCursorRef.current = next;
687
797
  setSteeringCursor(next);
688
798
  return;
689
799
  }
690
- if (key.upArrow || input === "k") {
800
+ if (key.upArrow || letterAlias === "k") {
691
801
  const next = Math.max(steeringCursorRef.current - 1, 0);
692
802
  steeringCursorRef.current = next;
693
803
  setSteeringCursor(next);
694
804
  return;
695
805
  }
696
- if (input === "f") {
806
+ if (letterAlias === "f") {
697
807
  if (steeringCursorRef.current === STEERING_KEYS.length) {
698
808
  judgeCursorRef.current = 0;
699
809
  setJudgeCursor(0);
@@ -718,13 +828,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
718
828
  }
719
829
  if (screenRef.current === "judge-pick") {
720
830
  const rows = filteredJudgeRows();
721
- if (key.downArrow || input === "j") {
831
+ if (key.downArrow || letterAlias === "j") {
722
832
  const next = Math.min(judgeCursorRef.current + 1, Math.max(rows.length - 1, 0));
723
833
  judgeCursorRef.current = next;
724
834
  setJudgeCursor(next);
725
835
  return;
726
836
  }
727
- if (key.upArrow || input === "k") {
837
+ if (key.upArrow || letterAlias === "k") {
728
838
  const next = Math.max(judgeCursorRef.current - 1, 0);
729
839
  judgeCursorRef.current = next;
730
840
  setJudgeCursor(next);
@@ -744,13 +854,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
744
854
  }
745
855
  if (screenRef.current === "steering-prefer") {
746
856
  const rows = filteredSteeringRows();
747
- if (key.downArrow || input === "j") {
857
+ if (key.downArrow || letterAlias === "j") {
748
858
  const next = Math.min(steeringPickerCursorRef.current + 1, Math.max(rows.length - 1, 0));
749
859
  steeringPickerCursorRef.current = next;
750
860
  setSteeringPickerCursor(next);
751
861
  return;
752
862
  }
753
- if (key.upArrow || input === "k") {
863
+ if (key.upArrow || letterAlias === "k") {
754
864
  const next = Math.max(steeringPickerCursorRef.current - 1, 0);
755
865
  steeringPickerCursorRef.current = next;
756
866
  setSteeringPickerCursor(next);
@@ -813,25 +923,42 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
813
923
  content: (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: active }), _jsx(Text, { children: ` ${agent.id} ${agent.version} ${agent.authed ? "authed" : "unauthed"}` })] })),
814
924
  };
815
925
  });
816
- return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
926
+ return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", viewRows: viewRows, legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
817
927
  }
818
928
  const group = enabledModelGroups()[modelGroup];
819
929
  if (screen === "models") {
820
- const rows = filteredModelRows().map((row) => {
930
+ const filtered = filteredModelRows();
931
+ const rows = filtered.map((row) => {
821
932
  const denied = group ? denyModels.has(`${group.adapter}:${row.model}`) : false;
822
933
  return {
823
934
  id: row.model,
824
- content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `( ) ${row.model} ??? unclassified${row.detectedAt ? ` (${row.detectedAt})` : ""}` }),
935
+ content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `? ${row.model}${row.suggestion ? ` → ${row.suggestion.tier} suggested` : ""}` }),
825
936
  };
826
937
  });
827
- return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193 to move \u00B7 Space to toggle \u00B7 t to classify tier \u00B7 n to add model \u00B7 Enter for next \u00B7 Type to search \u00B7 Esc to quit", rows: rows, cursor: modelCursor, details: notice ? [notice] : [], filter: filter }));
938
+ // The remedy renders ONCE, for the row under the cursor — 218 identical "Space or t to
939
+ // classify" suffixes drowned the model names and wrapped long ids (operator screenshot,
940
+ // 2026-08-13). A notice always wins the line.
941
+ const focused = filtered[modelCursor];
942
+ const hidden = hiddenModelCount();
943
+ const detail = [
944
+ ...(notice
945
+ ? [notice]
946
+ : focused && !focused.tier
947
+ ? [focused.suggestion
948
+ ? `? ${focused.suggestion.tier} suggested from catalog evidence — Space accepts prefilled, s stages every visible suggestion${focused.detectedAt ? ` (detected ${focused.detectedAt})` : ""}`
949
+ : `? unclassified — Space or t to classify${focused.detectedAt ? ` (detected ${focused.detectedAt})` : ""}; unclassified models are never routed`]
950
+ : []),
951
+ ...(hidden > 0 ? [`… ${hidden} retired/preview/non-worker hidden — a shows all`] : []),
952
+ ...(showAllModelsRef.current ? ["showing retired models — a hides them again"] : []),
953
+ ];
954
+ return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193 move \u00B7 Space toggle/classify \u00B7 s stage suggested \u00B7 t tier \u00B7 n add \u00B7 a all \u00B7 Enter next \u00B7 Type to search \u00B7 Esc quit", viewRows: viewRows, rows: rows, cursor: modelCursor, details: detail, filter: filter }));
828
955
  }
829
956
  if (screen === "add-model")
830
957
  return (_jsx(FleetListScreen, { title: `add model · ${group?.adapter ?? ""}`, legend: "type model id \u00B7 enter classify \u00B7 esc cancel", rows: [{ id: "model", content: _jsx(Text, { children: modelId || "model id (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
831
958
  if (screen === "channel")
832
- return (_jsx(FleetListScreen, { title: `channel · ${pendingClassificationRef.current?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: CHANNELS.map((channel) => ({ id: channel, content: _jsx(Text, { children: channel }) })), cursor: channelCursor }));
959
+ return (_jsx(FleetListScreen, { title: `channel · ${pendingClassificationRef.current?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc cancel \u00B7 q quit", rows: CHANNELS.map((channel) => ({ id: channel, content: _jsx(Text, { children: channel }) })), cursor: channelCursor }));
833
960
  if (screen === "tiers") {
834
- return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
961
+ return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc cancel \u00B7 q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
835
962
  }
836
963
  if (screen === "provenance")
837
964
  return (_jsx(FleetListScreen, { title: `benchmark provenance · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "type note \u00B7 enter apply \u00B7 esc cancel", rows: [{ id: "note", content: _jsx(Text, { children: note || "benchmark provenance note (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
@@ -855,13 +982,13 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
855
982
  }
856
983
  const shape = renderedShapes[shapeCursor]?.id;
857
984
  if (screen === "candidates") {
858
- return (_jsx(FleetListScreen, { title: `pick · ${shape}`, legend: "\u2191\u2193 to move \u00B7 Enter to pin \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredCandidates().map((candidate) => ({
985
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}`, viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Enter to pin \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredCandidates().map((candidate) => ({
859
986
  id: candidate.id,
860
987
  content: _jsx(Text, { children: candidate.label }),
861
988
  })), cursor: candidateCursor, filter: filter }));
862
989
  }
863
990
  if (screen === "shape-prefer")
864
- return (_jsx(FleetListScreen, { title: `pick · ${shape}.prefer`, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: preferRowsRef.current.map((option) => {
991
+ return (_jsx(FleetListScreen, { title: `pick · ${shape}.prefer`, viewRows: viewRows, legend: "\u2191\u2193/jk move \u00B7 space add/drop \u00B7 enter apply (empty clears) \u00B7 esc cancel \u00B7 q quit", rows: preferRowsRef.current.map((option) => {
865
992
  const at = preferChainRef.current.indexOf(option);
866
993
  return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
867
994
  }), cursor: preferCursor }));
@@ -879,14 +1006,14 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
879
1006
  }
880
1007
  if (screen === "judge-pick") {
881
1008
  const selected = judgeSeat ?? judgeKeepRow;
882
- return (_jsx(FleetListScreen, { title: "pick \u00B7 judge", legend: "\u2191\u2193 to move \u00B7 Enter to select \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredJudgeRows().map((label) => ({
1009
+ return (_jsx(FleetListScreen, { title: "pick \u00B7 judge", viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Enter to select \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredJudgeRows().map((label) => ({
883
1010
  id: label,
884
1011
  content: (_jsxs(_Fragment, { children: [label === selected ? _jsx(ToggleMark, { active: true }) : _jsx(Text, { children: " " }), _jsx(Text, { children: ` ${label}` })] })),
885
1012
  })), cursor: judgeCursor, filter: filter }));
886
1013
  }
887
1014
  if (screen === "steering-prefer") {
888
1015
  const which = steeringPickerRef.current;
889
- return (_jsx(FleetListScreen, { title: `pick · ${which}.prefer`, legend: "\u2191\u2193 to move \u00B7 Space to add/drop \u00B7 Enter to apply (empty clears) \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredSteeringRows().map((option) => {
1016
+ return (_jsx(FleetListScreen, { title: `pick · ${which}.prefer`, viewRows: viewRows, legend: "\u2191\u2193 to move \u00B7 Space to add/drop \u00B7 Enter to apply (empty clears) \u00B7 Type to search \u00B7 Esc to cancel \u00B7 q to quit", rows: filteredSteeringRows().map((option) => {
890
1017
  const at = steeringChainRef.current.indexOf(option);
891
1018
  return { id: option, content: _jsx(Text, { children: `${at === -1 ? "·" : String(at + 1)} ${option}` }) };
892
1019
  }), cursor: steeringPickerCursor, filter: filter }));
@@ -911,7 +1038,10 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
911
1038
  }));
912
1039
  const bridgedInput = inkInput(input, initialInput);
913
1040
  const legacyOutput = typeof output.on !== "function" || typeof output.off !== "function";
914
- const app = render(_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats }), {
1041
+ const app = render(_jsx(FleetApp, { ageMs: ageMs, agents: agents, initialDenyAdapters: initialDenyAdapters, initialDenyModels: initialDenyModels, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
1042
+ // v1.90.9: omp's 218-model list rendered taller than the terminal and scrolled the cursor
1043
+ // and chrome off-screen — cap every list to the terminal, minus title/legend/markers/details.
1044
+ viewRows: Math.max(8, (output.rows ?? 40) - 10) }), {
915
1045
  // FleetIO's injected stream predates Ink and did not require ref/unref.
916
1046
  // Real terminal streams pass through unchanged; the compatibility facade
917
1047
  // adds only those lifecycle methods and leaves input decoding to Ink.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.90.8",
3
+ "version": "1.91.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",