tickmarkr 1.91.0 → 1.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +29 -1
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +13 -0
- package/dist/cli/commands/doctor.js +34 -6
- package/dist/cli/commands/fleet-picker.js +1 -1
- package/dist/cli/commands/fleet.d.ts +4 -4
- package/dist/cli/commands/fleet.js +168 -35
- package/dist/cli/commands/init.js +6 -4
- package/dist/config/config.d.ts +34 -1
- package/dist/config/config.js +96 -5
- package/dist/config/fleet-overlay.d.ts +9 -2
- package/dist/config/fleet-overlay.js +176 -18
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +12 -2
- package/dist/route/router.js +31 -2
- package/dist/tui/ink/fleet-app.d.ts +50 -8
- package/dist/tui/ink/fleet-app.js +1210 -839
- package/dist/tui/ink/frame.d.ts +62 -0
- package/dist/tui/ink/frame.js +204 -0
- package/dist/tui/ink/init-app.d.ts +2 -1
- package/dist/tui/ink/init-app.js +28 -100
- package/package.json +1 -1
|
@@ -1,100 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import {
|
|
3
|
-
import { PassThrough } from "node:stream";
|
|
4
|
-
import { render, Text, useApp, useInput } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, render, Text, useApp, useInput } from "ink";
|
|
5
3
|
import { useRef, useState } from "react";
|
|
6
4
|
import { MODEL_ID_RE } from "../../adapters/types.js";
|
|
7
5
|
import { retiredModelReason } from "../../adapters/model-lints.js";
|
|
8
6
|
import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
|
|
9
7
|
import { TIERS } from "../../graph/schema.js";
|
|
10
|
-
import {
|
|
8
|
+
import { windowRows } from "./components.js";
|
|
9
|
+
import { clip, clipPathTail, ElisionMark, enterAltScreen, fmtCtx, fmtMs, fmtUsdPair, Glyph, INK, inkInput, inkOutput, KeyBar, OverlayPanel, padCell, padCellStart, Pointer, SearchRow, } from "./frame.js";
|
|
11
10
|
const STEERING_KEYS = ["review", "consult"];
|
|
12
|
-
const escape = String.fromCharCode(27);
|
|
13
11
|
const CHANNELS = ["sub", "api"];
|
|
14
|
-
const inkBookkeepingWrites = new Set(["", `${escape}[?25l`, `${escape}[?25h`, `${escape}[?2026h`, `${escape}[?2026l`]);
|
|
15
|
-
function inkOutput(output) {
|
|
16
|
-
if (typeof output.on === "function" && typeof output.off === "function")
|
|
17
|
-
return output;
|
|
18
|
-
const facade = Object.create(output);
|
|
19
|
-
const write = output.write.bind(output);
|
|
20
|
-
facade.write = ((chunk, ...args) => {
|
|
21
|
-
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
22
|
-
if (inkBookkeepingWrites.has(text))
|
|
23
|
-
return true;
|
|
24
|
-
return Reflect.apply(write, output, [chunk, ...args]);
|
|
25
|
-
});
|
|
26
|
-
facade.on = () => facade;
|
|
27
|
-
facade.off = () => facade;
|
|
28
|
-
return facade;
|
|
29
|
-
}
|
|
30
|
-
function inkInput(input, initialInput) {
|
|
31
|
-
const productionInput = typeof input.ref === "function" && typeof input.unref === "function";
|
|
32
|
-
// Isolate Ink's listeners on a bridge so every editor exit can detach the
|
|
33
|
-
// one listener it owns from the operator's terminal. Older injected FleetIO
|
|
34
|
-
// streams can arrive as decoded keypress events, while real TTYs forward raw
|
|
35
|
-
// data and leave decoding entirely to Ink.
|
|
36
|
-
const stream = new PassThrough();
|
|
37
|
-
stream.isTTY = input.isTTY;
|
|
38
|
-
stream.setRawMode = input.setRawMode?.bind(input);
|
|
39
|
-
stream.ref = () => {
|
|
40
|
-
if (productionInput)
|
|
41
|
-
input.ref();
|
|
42
|
-
return stream;
|
|
43
|
-
};
|
|
44
|
-
stream.unref = () => {
|
|
45
|
-
if (productionInput)
|
|
46
|
-
input.unref();
|
|
47
|
-
return stream;
|
|
48
|
-
};
|
|
49
|
-
const queued = [...initialInput];
|
|
50
|
-
let active = true;
|
|
51
|
-
let scheduled;
|
|
52
|
-
const pump = () => {
|
|
53
|
-
scheduled = undefined;
|
|
54
|
-
if (!active)
|
|
55
|
-
return;
|
|
56
|
-
const next = queued.shift();
|
|
57
|
-
if (next === undefined)
|
|
58
|
-
return;
|
|
59
|
-
stream.write(next);
|
|
60
|
-
scheduled = setTimeout(pump, 0);
|
|
61
|
-
};
|
|
62
|
-
const onData = (chunk) => {
|
|
63
|
-
queued.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
64
|
-
scheduled ??= setTimeout(pump, 0);
|
|
65
|
-
};
|
|
66
|
-
const onKeypress = (sequence, key) => {
|
|
67
|
-
const token = key?.sequence ?? sequence;
|
|
68
|
-
if (token === undefined)
|
|
69
|
-
return;
|
|
70
|
-
queued.push(token);
|
|
71
|
-
scheduled ??= setTimeout(pump, 0);
|
|
72
|
-
};
|
|
73
|
-
if (productionInput) {
|
|
74
|
-
input.on("data", onData);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
emitKeypressEvents(input);
|
|
78
|
-
input.on("keypress", onKeypress);
|
|
79
|
-
}
|
|
80
|
-
input.resume();
|
|
81
|
-
if (queued.length > 0)
|
|
82
|
-
scheduled = setTimeout(pump, 0);
|
|
83
|
-
return {
|
|
84
|
-
stream: stream,
|
|
85
|
-
stop() {
|
|
86
|
-
active = false;
|
|
87
|
-
if (scheduled)
|
|
88
|
-
clearTimeout(scheduled);
|
|
89
|
-
if (productionInput)
|
|
90
|
-
input.off("data", onData);
|
|
91
|
-
else
|
|
92
|
-
input.off("keypress", onKeypress);
|
|
93
|
-
input.pause();
|
|
94
|
-
stream.end();
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
12
|
export function formatDoctorAge(ageMs) {
|
|
99
13
|
if (ageMs === null)
|
|
100
14
|
return "no probe data";
|
|
@@ -109,135 +23,147 @@ function splitSeat(seat) {
|
|
|
109
23
|
const at = seat.indexOf(":");
|
|
110
24
|
return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
|
|
111
25
|
}
|
|
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, }) {
|
|
26
|
+
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, frameColumns = 100, frameRows, }) {
|
|
113
27
|
const { exit } = useApp();
|
|
114
|
-
const [
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const filterRef = useRef("");
|
|
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);
|
|
189
|
-
const enabledModelGroups = () => modelGroups.filter((group) => !denyRef.current?.has(group.adapter));
|
|
190
|
-
const stagedMap = () => mapRef.current;
|
|
191
|
-
const currentModelRows = () => {
|
|
192
|
-
const group = enabledModelGroups()[modelGroupRef.current];
|
|
193
|
-
if (!group)
|
|
194
|
-
return [];
|
|
28
|
+
const [, setRevision] = useState(0);
|
|
29
|
+
const uiRef = useRef(null);
|
|
30
|
+
uiRef.current ??= {
|
|
31
|
+
focus: "list",
|
|
32
|
+
view: "models",
|
|
33
|
+
railAt: 0,
|
|
34
|
+
adapterAt: -1,
|
|
35
|
+
listAt: 0,
|
|
36
|
+
filter: "",
|
|
37
|
+
searching: false,
|
|
38
|
+
stash: null,
|
|
39
|
+
quitArmed: false,
|
|
40
|
+
showAll: false,
|
|
41
|
+
presetsSeen: false,
|
|
42
|
+
notice: "",
|
|
43
|
+
deny: new Set(initialDenyAdapters),
|
|
44
|
+
denyModels: new Set(initialDenyModels),
|
|
45
|
+
classifications: [],
|
|
46
|
+
selectedMode: initialMode,
|
|
47
|
+
map: structuredClone(initialMap),
|
|
48
|
+
steering: structuredClone(initialSteering),
|
|
49
|
+
judgeSeat: null,
|
|
50
|
+
channelByAdapter: {},
|
|
51
|
+
// v1.92: no entry opens an overlay at launch — the browser opens on the models view (fleet
|
|
52
|
+
// scoping first); the presets overlay raises on the first Shapes entry instead (below).
|
|
53
|
+
overlay: null,
|
|
54
|
+
done: false,
|
|
55
|
+
};
|
|
56
|
+
const ui = uiRef.current;
|
|
57
|
+
const bump = () => setRevision((revision) => revision + 1);
|
|
58
|
+
// OBS-521/OBS-522: one truth for "is there staged work" — the Esc/q loss guard, the header
|
|
59
|
+
// chip, and the empty-`w` notice all read it. Counts staged EDITS, not diff hunks.
|
|
60
|
+
const initialDenySet = new Set(initialDenyAdapters);
|
|
61
|
+
const initialDenyModelSet = new Set(initialDenyModels);
|
|
62
|
+
const stagedCount = () => {
|
|
63
|
+
let n = ui.classifications.length
|
|
64
|
+
+ (ui.selectedMode !== initialMode ? 1 : 0)
|
|
65
|
+
+ (ui.judgeSeat !== null ? 1 : 0);
|
|
66
|
+
for (const adapter of ui.deny)
|
|
67
|
+
if (!initialDenySet.has(adapter))
|
|
68
|
+
n += 1;
|
|
69
|
+
for (const adapter of initialDenySet)
|
|
70
|
+
if (!ui.deny.has(adapter))
|
|
71
|
+
n += 1;
|
|
72
|
+
for (const model of ui.denyModels)
|
|
73
|
+
if (!initialDenyModelSet.has(model))
|
|
74
|
+
n += 1;
|
|
75
|
+
for (const model of initialDenyModelSet)
|
|
76
|
+
if (!ui.denyModels.has(model))
|
|
77
|
+
n += 1;
|
|
78
|
+
for (const shape of new Set([...Object.keys(initialMap), ...Object.keys(ui.map)])) {
|
|
79
|
+
// {} ≡ absent: pin-then-auto leaves an empty entry that writes nothing — not staged work
|
|
80
|
+
if (JSON.stringify(initialMap[shape] ?? {}) !== JSON.stringify(ui.map[shape] ?? {}))
|
|
81
|
+
n += 1;
|
|
82
|
+
}
|
|
83
|
+
for (const key of STEERING_KEYS) {
|
|
84
|
+
if (JSON.stringify(ui.steering[key]) !== JSON.stringify(initialSteering[key]))
|
|
85
|
+
n += 1;
|
|
86
|
+
}
|
|
87
|
+
return n;
|
|
88
|
+
};
|
|
89
|
+
// ── derived data ───────────────────────────────────────────────────────────
|
|
90
|
+
const enabledGroups = () => modelGroups.filter((group) => !ui.deny.has(group.adapter));
|
|
91
|
+
const scopedGroups = () => {
|
|
92
|
+
if (ui.adapterAt === -1)
|
|
93
|
+
return enabledGroups();
|
|
94
|
+
const group = modelGroups[ui.adapterAt];
|
|
95
|
+
return group && !ui.deny.has(group.adapter) ? [group] : [];
|
|
96
|
+
};
|
|
97
|
+
const groupRows = (group) => {
|
|
195
98
|
const rows = group.rows.map((row) => {
|
|
196
|
-
const staged =
|
|
197
|
-
return
|
|
99
|
+
const staged = ui.classifications.find((classification) => classification.adapter === group.adapter && classification.model === row.model);
|
|
100
|
+
return {
|
|
101
|
+
adapter: group.adapter,
|
|
102
|
+
model: row.model,
|
|
103
|
+
tier: staged?.tier ?? row.tier,
|
|
104
|
+
detectedAt: row.detectedAt,
|
|
105
|
+
suggestion: row.suggestion,
|
|
106
|
+
evidence: row.evidence,
|
|
107
|
+
channel: group.channel,
|
|
108
|
+
denied: ui.denyModels.has(`${group.adapter}:${row.model}`),
|
|
109
|
+
};
|
|
198
110
|
});
|
|
199
111
|
const known = new Set(rows.map((row) => row.model));
|
|
200
|
-
for (const staged of
|
|
112
|
+
for (const staged of ui.classifications) {
|
|
201
113
|
if (staged.adapter === group.adapter && !known.has(staged.model)) {
|
|
202
|
-
rows.push({
|
|
114
|
+
rows.push({
|
|
115
|
+
adapter: group.adapter,
|
|
116
|
+
model: staged.model,
|
|
117
|
+
tier: staged.tier,
|
|
118
|
+
channel: group.channel,
|
|
119
|
+
denied: ui.denyModels.has(`${group.adapter}:${staged.model}`),
|
|
120
|
+
});
|
|
203
121
|
}
|
|
204
122
|
}
|
|
205
123
|
return rows;
|
|
206
124
|
};
|
|
207
|
-
|
|
208
|
-
// label. Each filterable screen derives its rows through these so input and render agree.
|
|
209
|
-
const matches = (label, f = filterRef.current) => f === "" || label.toLowerCase().includes(f.toLowerCase());
|
|
125
|
+
const matches = (label, f) => f === "" || label.toLowerCase().includes(f.toLowerCase());
|
|
210
126
|
// Operator directive 2026-08-13: retired shapes (dated snapshots, previews, non-worker SKUs,
|
|
211
|
-
// legacy families) hide by DEFAULT
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
127
|
+
// legacy families) hide by DEFAULT. `a` reveals them; a CLASSIFIED row is never hidden.
|
|
128
|
+
const modelRows = (f = ui.filter) => scopedGroups().flatMap(groupRows).filter((row) => matches(`${row.adapter}/${row.model}`, f)
|
|
129
|
+
&& (ui.showAll || row.tier !== undefined || retiredModelReason(row.model) === null));
|
|
130
|
+
const hiddenModelCount = (f = ui.filter) => scopedGroups().flatMap(groupRows).filter((row) => matches(`${row.adapter}/${row.model}`, f)).length
|
|
131
|
+
- modelRows(f).length;
|
|
132
|
+
/** rail counts mirror the list's retired-hide (never the text filter) so numbers agree on screen */
|
|
133
|
+
const visibleCount = (scope) => {
|
|
134
|
+
const groups = scope === -1 ? enabledGroups() : (modelGroups[scope] ? [modelGroups[scope]] : []);
|
|
135
|
+
return groups.flatMap(groupRows)
|
|
136
|
+
.filter((row) => ui.showAll || row.tier !== undefined || retiredModelReason(row.model) === null).length;
|
|
137
|
+
};
|
|
138
|
+
// three preview callbacks share it in lockstep — the staged deny truth every preview ranks under
|
|
139
|
+
const stagedDeny = () => ({ adapters: [...ui.deny].sort(), models: [...ui.denyModels].sort() });
|
|
140
|
+
const shapeList = () => shapeRows(ui.selectedMode, ui.map, stagedDeny());
|
|
141
|
+
const steeringList = () => [
|
|
142
|
+
...STEERING_KEYS.map((key) => ({
|
|
143
|
+
id: key,
|
|
144
|
+
label: `${key}.prefer → ${ui.steering[key]?.join(", ") ?? "(none)"}`,
|
|
145
|
+
})),
|
|
146
|
+
{ id: "judge", label: `judge → ${ui.judgeSeat ?? (initialJudge ? `${initialJudge} (default)` : "(default)")}` },
|
|
147
|
+
];
|
|
218
148
|
const judgeKeepRow = initialJudge ? `(keep default) ${initialJudge}` : "(keep default)";
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const next = key.backspace || key.delete
|
|
226
|
-
? filterRef.current.slice(0, -1)
|
|
227
|
-
: (printable ? filterRef.current + input : null);
|
|
228
|
-
if (next === null)
|
|
229
|
-
return false;
|
|
230
|
-
filterRef.current = next;
|
|
231
|
-
setFilter(next);
|
|
232
|
-
const clamped = Math.min(cursorRef.current, Math.max(countFor(next) - 1, 0));
|
|
233
|
-
cursorRef.current = clamped;
|
|
234
|
-
setCursorState(clamped);
|
|
235
|
-
return true;
|
|
149
|
+
const listLength = () => {
|
|
150
|
+
if (ui.view === "models")
|
|
151
|
+
return modelRows().length;
|
|
152
|
+
if (ui.view === "shapes")
|
|
153
|
+
return shapeList().length;
|
|
154
|
+
return steeringList().length;
|
|
236
155
|
};
|
|
156
|
+
const railRows = () => [
|
|
157
|
+
{ kind: "view", view: "models", label: "All models", count: visibleCount(-1) },
|
|
158
|
+
{ kind: "view", view: "shapes", label: "Shapes", count: shapeList().length },
|
|
159
|
+
{ kind: "view", view: "steering", label: "Steering", count: STEERING_KEYS.length + 1 },
|
|
160
|
+
...modelGroups.map((_, index) => ({ kind: "adapter", index })),
|
|
161
|
+
];
|
|
162
|
+
// ── state transitions ──────────────────────────────────────────────────────
|
|
237
163
|
const editorState = () => ({
|
|
238
|
-
denyAdapters: [...
|
|
239
|
-
denyModels: [...
|
|
240
|
-
classifications:
|
|
164
|
+
denyAdapters: [...ui.deny].sort(),
|
|
165
|
+
denyModels: [...ui.denyModels].sort(),
|
|
166
|
+
classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
|
|
241
167
|
? {
|
|
242
168
|
...classification,
|
|
243
169
|
note: fleetFirstTouchProvenance(classification.note, {
|
|
@@ -246,779 +172,1209 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
246
172
|
}),
|
|
247
173
|
}
|
|
248
174
|
: classification),
|
|
249
|
-
selectedMode:
|
|
250
|
-
map:
|
|
251
|
-
...(
|
|
252
|
-
steering: structuredClone(
|
|
175
|
+
selectedMode: ui.selectedMode,
|
|
176
|
+
map: ui.map,
|
|
177
|
+
...(ui.judgeSeat ? { judgeSeat: splitSeat(ui.judgeSeat) } : {}),
|
|
178
|
+
steering: structuredClone(ui.steering),
|
|
253
179
|
});
|
|
254
180
|
const finish = (result) => {
|
|
255
|
-
if (
|
|
181
|
+
if (ui.done)
|
|
256
182
|
return;
|
|
257
|
-
|
|
183
|
+
ui.done = true;
|
|
258
184
|
exit(result);
|
|
259
185
|
};
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
186
|
+
const setOverlay = (overlay) => {
|
|
187
|
+
// OBS-520: an overlay borrows ui.filter for its own search box — stash the browser's committed
|
|
188
|
+
// filter on open and restore it on close, so finishing an assignment/classification does not
|
|
189
|
+
// dump the operator back at the top of an unfiltered list.
|
|
190
|
+
if (ui.overlay === null && overlay !== null)
|
|
191
|
+
ui.stash = { filter: ui.filter };
|
|
192
|
+
ui.overlay = overlay;
|
|
193
|
+
if (overlay === null) {
|
|
194
|
+
ui.filter = ui.stash?.filter ?? "";
|
|
195
|
+
ui.stash = null;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
ui.filter = "";
|
|
199
|
+
}
|
|
200
|
+
ui.searching = false;
|
|
201
|
+
ui.notice = "";
|
|
202
|
+
bump();
|
|
268
203
|
};
|
|
269
204
|
// The one review funnel: an empty staged overlay finishes as no-changes, a real diff shows the
|
|
270
|
-
// confirm
|
|
205
|
+
// confirm overlay. Reached from `w` and, in preset entry, from a preset pick.
|
|
271
206
|
const goReview = () => {
|
|
272
207
|
const nextReview = reviewOverlay(editorState());
|
|
273
208
|
if (nextReview.kind === "empty") {
|
|
274
209
|
finish({ kind: "no-changes" });
|
|
275
210
|
return;
|
|
276
211
|
}
|
|
277
|
-
|
|
278
|
-
setReview(reviewRef.current);
|
|
279
|
-
setNotice("");
|
|
280
|
-
showScreen("review");
|
|
212
|
+
setOverlay({ kind: "review", review: nextReview, scroll: 0 });
|
|
281
213
|
};
|
|
282
|
-
const stageSuggested = (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
214
|
+
const stageSuggested = (adapter, rows, firstTouch) => {
|
|
215
|
+
ui.classifications = [
|
|
216
|
+
...ui.classifications,
|
|
217
|
+
...rows.map((row) => ({
|
|
218
|
+
adapter,
|
|
219
|
+
model: row.model,
|
|
220
|
+
tier: row.suggestion.tier,
|
|
221
|
+
note: row.suggestion.note,
|
|
222
|
+
...firstTouch,
|
|
223
|
+
})),
|
|
224
|
+
];
|
|
225
|
+
ui.notice = `staged ${rows.length} catalog-suggested classification(s) — nothing writes before the review diff`;
|
|
226
|
+
bump();
|
|
293
227
|
};
|
|
294
|
-
const
|
|
295
|
-
|
|
228
|
+
const groupOf = (adapter) => modelGroups.find((group) => group.adapter === adapter);
|
|
229
|
+
const beginClassification = (adapter, model) => {
|
|
230
|
+
const group = groupOf(adapter);
|
|
231
|
+
if (!group)
|
|
232
|
+
return;
|
|
233
|
+
// OBS-508: catalog evidence prefills the flow — the tier cursor lands on the suggested band and
|
|
296
234
|
// (when the operator keeps that band) the provenance note arrives pre-typed. Free to override.
|
|
297
235
|
const suggestion = group.rows.find((row) => row.model === model)?.suggestion ?? null;
|
|
298
|
-
pendingSuggestionRef.current = suggestion;
|
|
299
236
|
const tierAt = suggestion ? Math.max(TIERS.indexOf(suggestion.tier), 0) : 0;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
adapter
|
|
237
|
+
const firstTouch = !group.rows.some((row) => row.tier !== undefined)
|
|
238
|
+
&& !ui.classifications.some((c) => c.adapter === adapter);
|
|
239
|
+
const base = {
|
|
240
|
+
kind: "classify",
|
|
241
|
+
adapter,
|
|
305
242
|
model,
|
|
306
|
-
|
|
243
|
+
channelAt: 0,
|
|
244
|
+
tierAt,
|
|
245
|
+
note: "",
|
|
246
|
+
suggestion,
|
|
307
247
|
};
|
|
308
248
|
if (!firstTouch) {
|
|
309
|
-
|
|
310
|
-
showScreen("tiers");
|
|
249
|
+
setOverlay({ ...base, stage: "tier" });
|
|
311
250
|
return;
|
|
312
251
|
}
|
|
313
252
|
if (!group.vendor) {
|
|
314
|
-
|
|
253
|
+
ui.notice = `${adapter} has no vendor declaration — classification cannot be saved`;
|
|
254
|
+
bump();
|
|
315
255
|
return;
|
|
316
256
|
}
|
|
317
|
-
|
|
318
|
-
const answered = channelByAdapterRef.current[group.adapter];
|
|
257
|
+
const answered = ui.channelByAdapter[adapter];
|
|
319
258
|
if (answered) {
|
|
320
|
-
|
|
321
|
-
pendingClassificationRef.current = pending;
|
|
322
|
-
showScreen("tiers");
|
|
259
|
+
setOverlay({ ...base, stage: "tier", vendor: group.vendor });
|
|
323
260
|
return;
|
|
324
261
|
}
|
|
325
|
-
|
|
326
|
-
channelCursorRef.current = 0;
|
|
327
|
-
setChannelCursor(0);
|
|
328
|
-
setNotice("");
|
|
329
|
-
showScreen("channel");
|
|
262
|
+
setOverlay({ ...base, stage: "channel", vendor: group.vendor });
|
|
330
263
|
};
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
264
|
+
const applyClassification = (overlay) => {
|
|
265
|
+
const answered = overlay.vendor ? ui.channelByAdapter[overlay.adapter] : undefined;
|
|
266
|
+
ui.classifications = [
|
|
267
|
+
...ui.classifications,
|
|
268
|
+
{
|
|
269
|
+
adapter: overlay.adapter,
|
|
270
|
+
model: overlay.model,
|
|
271
|
+
tier: TIERS[overlay.tierAt],
|
|
272
|
+
note: overlay.note.trim(),
|
|
273
|
+
...(overlay.vendor && answered ? { vendor: overlay.vendor, channel: answered } : {}),
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
setOverlay(null);
|
|
277
|
+
};
|
|
278
|
+
const clampList = () => {
|
|
279
|
+
ui.listAt = Math.min(ui.listAt, Math.max(listLength() - 1, 0));
|
|
280
|
+
};
|
|
281
|
+
// ── sizing (shared by the input handler and the render below) ──────────────
|
|
282
|
+
const width = Math.max(72, Math.min(frameColumns, 160));
|
|
283
|
+
const RAIL_W = 24;
|
|
284
|
+
const bodyW = width - RAIL_W - 5; // borders + padding
|
|
285
|
+
// OBS-523: reserve what the chrome actually uses — header 1, outer borders 2, padding 1,
|
|
286
|
+
// scope+search 2, elision marks 2, detail lines ≤2, keybar 1 — not 16. Six list rows were
|
|
287
|
+
// blank at every terminal size.
|
|
288
|
+
const capacity = Number.isFinite(viewRows) ? Math.max(4, viewRows) : Number.POSITIVE_INFINITY;
|
|
289
|
+
/** review diff rows: overlay chrome is one row shorter than the browser's (no scope header) */
|
|
290
|
+
const reviewCapacity = () => Number.isFinite(capacity) ? Math.min(Math.max(capacity + 1, 4), 400) : 400;
|
|
291
|
+
const reviewWindow = (overlay) => {
|
|
292
|
+
const lines = overlay.review.diff.split("\n");
|
|
293
|
+
const cap = reviewCapacity();
|
|
294
|
+
const maxScroll = Math.max(lines.length - cap, 0);
|
|
295
|
+
const scroll = Math.min(Math.max(overlay.scroll, 0), maxScroll);
|
|
296
|
+
return { lines, cap, maxScroll, scroll, visible: lines.slice(scroll, scroll + cap) };
|
|
297
|
+
};
|
|
298
|
+
// OBS-521: quitting with staged edits needs a second Esc/q — armed by the first press,
|
|
299
|
+
// disarmed by any other key. Ctrl+C stays an immediate exit.
|
|
300
|
+
const guardedQuit = (armed) => {
|
|
301
|
+
const staged = stagedCount();
|
|
302
|
+
if (staged === 0 || armed) {
|
|
370
303
|
finish({ kind: "quit" });
|
|
371
|
-
return;
|
|
304
|
+
return true;
|
|
372
305
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
306
|
+
ui.quitArmed = true;
|
|
307
|
+
ui.notice = `${staged} staged edit(s) not written — w reviews the diff · press again to discard and quit`;
|
|
308
|
+
bump();
|
|
309
|
+
return false;
|
|
310
|
+
};
|
|
311
|
+
// ── input ──────────────────────────────────────────────────────────────────
|
|
312
|
+
useInput((input, key) => {
|
|
313
|
+
if (ui.done)
|
|
380
314
|
return;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (key.downArrow || letterAlias === "j") {
|
|
384
|
-
const next = Math.min(cursorRef.current + 1, Math.max(agents.length - 1, 0));
|
|
385
|
-
cursorRef.current = next;
|
|
386
|
-
setCursor(next);
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (key.upArrow || letterAlias === "k") {
|
|
390
|
-
const next = Math.max(cursorRef.current - 1, 0);
|
|
391
|
-
cursorRef.current = next;
|
|
392
|
-
setCursor(next);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (input === " " && agents.length > 0) {
|
|
396
|
-
const next = new Set(denyRef.current);
|
|
397
|
-
const id = agents[cursorRef.current].id;
|
|
398
|
-
if (next.has(id))
|
|
399
|
-
next.delete(id);
|
|
400
|
-
else
|
|
401
|
-
next.add(id);
|
|
402
|
-
denyRef.current = next;
|
|
403
|
-
setDenyAdapters(next);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
if (key.return) {
|
|
407
|
-
if (enabledModelGroups().length === 0)
|
|
408
|
-
showScreen("modes");
|
|
409
|
-
else
|
|
410
|
-
showScreen("models");
|
|
411
|
-
}
|
|
315
|
+
if (key.ctrl && input === "c") {
|
|
316
|
+
finish({ kind: "quit" });
|
|
412
317
|
return;
|
|
413
318
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
319
|
+
const armed = ui.quitArmed;
|
|
320
|
+
ui.quitArmed = false;
|
|
321
|
+
const overlay = ui.overlay;
|
|
322
|
+
// Search is an EXPLICIT, visible mode: "/" enters it, Enter commits (filter stays applied),
|
|
323
|
+
// Esc cancels. While it is on, every printable char belongs to the search box so "kimi" or
|
|
324
|
+
// "fake-n" can actually be typed; outside it letters are ALWAYS commands — even with a
|
|
325
|
+
// committed filter applied. The old implicit rule (hotkeys dormant whenever the filter was
|
|
326
|
+
// non-empty) confused both directions in the field: the first letter of a query fired
|
|
327
|
+
// commands, and a live filter silently ate m/w/j/k. (operator field sessions, 2026-08-13
|
|
328
|
+
// and 2026-08-15)
|
|
329
|
+
const hotkey = ui.searching ? "" : input;
|
|
330
|
+
// filter editing shared by the searchable surfaces
|
|
331
|
+
const editFilter = (count, clampAt) => {
|
|
332
|
+
// a LEADING "/" is the browser's search affordance, not query text — swallow it so the
|
|
333
|
+
// muscle memory "/kimi" works in every searchable overlay too (no channel id starts with /).
|
|
334
|
+
// A real terminal may deliver fast typing or a paste as ONE keypress chunk ("/kimi"), so the
|
|
335
|
+
// strip handles the chunk form as well as the lone key. A pasted chunk may also carry a
|
|
336
|
+
// trailing newline ("glm-5.3\r") — control bytes are terminal framing, never query text;
|
|
337
|
+
// left in, they made every row "not match" until the operator retyped the query by hand.
|
|
338
|
+
const chunk = ui.filter === "" && input.startsWith("/") ? input.slice(1) : input;
|
|
339
|
+
const typed = chunk.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
340
|
+
if (typed === "" && input.startsWith("/"))
|
|
341
|
+
return true;
|
|
342
|
+
const printable = typed > " " && !key.ctrl && !key.meta;
|
|
343
|
+
const next = key.backspace || key.delete
|
|
344
|
+
? ui.filter.slice(0, -1)
|
|
345
|
+
: (printable ? ui.filter + typed : null);
|
|
346
|
+
if (next === null)
|
|
347
|
+
return false;
|
|
348
|
+
ui.filter = next;
|
|
349
|
+
// OBS-520: the clamp targets the surface being filtered — an overlay clamps ITS cursor,
|
|
350
|
+
// never the browser's listAt (typing in a picker used to drag the shapes cursor to row 0).
|
|
351
|
+
const max = Math.max(count(next) - 1, 0);
|
|
352
|
+
if (clampAt)
|
|
353
|
+
clampAt(max);
|
|
354
|
+
else
|
|
355
|
+
ui.listAt = Math.min(ui.listAt, max);
|
|
356
|
+
ui.notice = "";
|
|
357
|
+
bump();
|
|
358
|
+
return true;
|
|
359
|
+
};
|
|
360
|
+
if (overlay) {
|
|
361
|
+
if (overlay.kind === "presets") {
|
|
362
|
+
const rowCount = modeOptions.length + (overlay.home ? 1 : 0);
|
|
363
|
+
if (key.escape) {
|
|
364
|
+
if (overlay.home) {
|
|
365
|
+
guardedQuit(armed);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
setOverlay(null);
|
|
438
369
|
return;
|
|
439
370
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (next.has(id))
|
|
443
|
-
next.delete(id);
|
|
444
|
-
else
|
|
445
|
-
next.add(id);
|
|
446
|
-
denyModelsRef.current = next;
|
|
447
|
-
setDenyModels(next);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
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) {
|
|
454
|
-
if (row.tier) {
|
|
455
|
-
setNotice("fleet: tier reassignment on classified models is not supported in v1 — edit config directly");
|
|
371
|
+
if (hotkey === "q") {
|
|
372
|
+
guardedQuit(armed);
|
|
456
373
|
return;
|
|
457
374
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
375
|
+
if (key.downArrow || hotkey === "j") {
|
|
376
|
+
overlay.at = Math.min(overlay.at + 1, rowCount - 1);
|
|
377
|
+
bump();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (key.upArrow || hotkey === "k") {
|
|
381
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
382
|
+
bump();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (key.return) {
|
|
386
|
+
if (overlay.home && overlay.at === modeOptions.length) {
|
|
387
|
+
setOverlay(null); // custom → the browser IS the full editor
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const option = modeOptions[overlay.at];
|
|
391
|
+
if (!option)
|
|
392
|
+
return;
|
|
393
|
+
ui.selectedMode = option.id;
|
|
394
|
+
if (overlay.home) {
|
|
395
|
+
goReview(); // entry context: a preset pick goes straight to the diff confirm
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
setOverlay(null);
|
|
399
|
+
}
|
|
466
400
|
return;
|
|
467
401
|
}
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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)");
|
|
402
|
+
if (overlay.kind === "review") {
|
|
403
|
+
if (key.escape) {
|
|
404
|
+
// non-destructive: back to the browser with everything still staged
|
|
405
|
+
setOverlay(entry === "presets" ? { kind: "presets", at: 0, home: true } : null);
|
|
477
406
|
return;
|
|
478
407
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
408
|
+
// OBS-524: the operator must be able to READ the whole diff they are approving —
|
|
409
|
+
// ↑↓/jk scroll a line, PgUp/PgDn a window; the elision rows carry the counts.
|
|
410
|
+
const window = reviewWindow(overlay);
|
|
411
|
+
if (key.downArrow || input === "j") {
|
|
412
|
+
overlay.scroll = Math.min(window.scroll + 1, window.maxScroll);
|
|
413
|
+
bump();
|
|
482
414
|
return;
|
|
483
415
|
}
|
|
484
|
-
if (
|
|
485
|
-
|
|
416
|
+
if (key.upArrow || input === "k") {
|
|
417
|
+
overlay.scroll = Math.max(window.scroll - 1, 0);
|
|
418
|
+
bump();
|
|
486
419
|
return;
|
|
487
420
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
421
|
+
if (key.pageDown) {
|
|
422
|
+
overlay.scroll = Math.min(window.scroll + window.cap, window.maxScroll);
|
|
423
|
+
bump();
|
|
491
424
|
return;
|
|
492
425
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
}
|
|
508
|
-
if (key.return) {
|
|
509
|
-
if (modelGroupRef.current + 1 < enabledModelGroups().length) {
|
|
510
|
-
const next = modelGroupRef.current + 1;
|
|
511
|
-
modelGroupRef.current = next;
|
|
512
|
-
setModelGroup(next);
|
|
513
|
-
modelCursorRef.current = 0;
|
|
514
|
-
setModelCursor(0);
|
|
515
|
-
filterRef.current = "";
|
|
516
|
-
setFilter("");
|
|
426
|
+
if (key.pageUp) {
|
|
427
|
+
overlay.scroll = Math.max(window.scroll - window.cap, 0);
|
|
428
|
+
bump();
|
|
429
|
+
return;
|
|
517
430
|
}
|
|
518
|
-
|
|
519
|
-
|
|
431
|
+
if (input === "n") {
|
|
432
|
+
finish({ kind: "discard" });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (input === "q") {
|
|
436
|
+
finish({ kind: "quit" });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (input === "y") {
|
|
440
|
+
const loadError = reloadGuard(overlay.review.after);
|
|
441
|
+
if (loadError) {
|
|
442
|
+
setOverlay(null);
|
|
443
|
+
ui.notice = `config loader rejects the proposed overlay — ${loadError}`;
|
|
444
|
+
bump();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
finish({ kind: "write", review: overlay.review });
|
|
520
448
|
}
|
|
521
449
|
return;
|
|
522
450
|
}
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
if (screenRef.current === "add-model") {
|
|
529
|
-
if (key.return) {
|
|
530
|
-
const group = enabledModelGroups()[modelGroupRef.current];
|
|
531
|
-
const candidate = modelIdRef.current;
|
|
532
|
-
if (!MODEL_ID_RE.test(candidate)) {
|
|
533
|
-
setNotice(`fleet: model id must match ${MODEL_ID_RE.source}`);
|
|
451
|
+
if (overlay.kind === "classify") {
|
|
452
|
+
if (key.escape) {
|
|
453
|
+
setOverlay(null);
|
|
534
454
|
return;
|
|
535
455
|
}
|
|
536
|
-
if (
|
|
537
|
-
|
|
456
|
+
if (overlay.stage === "channel") {
|
|
457
|
+
if (key.downArrow || input === "j") {
|
|
458
|
+
overlay.channelAt = Math.min(overlay.channelAt + 1, CHANNELS.length - 1);
|
|
459
|
+
bump();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (key.upArrow || input === "k") {
|
|
463
|
+
overlay.channelAt = Math.max(overlay.channelAt - 1, 0);
|
|
464
|
+
bump();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (key.return) {
|
|
468
|
+
const channel = CHANNELS[overlay.channelAt];
|
|
469
|
+
ui.channelByAdapter[overlay.adapter] = channel;
|
|
470
|
+
// OBS-508: a bulk stage waiting on the one first-touch channel answer resumes here.
|
|
471
|
+
if (overlay.bulk) {
|
|
472
|
+
stageSuggested(overlay.bulk.adapter, overlay.bulk.rows, { vendor: overlay.bulk.vendor, channel });
|
|
473
|
+
const notice = ui.notice;
|
|
474
|
+
setOverlay(null);
|
|
475
|
+
ui.notice = notice;
|
|
476
|
+
bump();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
overlay.stage = "tier";
|
|
480
|
+
bump();
|
|
481
|
+
}
|
|
538
482
|
return;
|
|
539
483
|
}
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
484
|
+
if (overlay.stage === "tier") {
|
|
485
|
+
if (key.downArrow || input === "j") {
|
|
486
|
+
overlay.tierAt = Math.min(overlay.tierAt + 1, TIERS.length - 1);
|
|
487
|
+
bump();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (key.upArrow || input === "k") {
|
|
491
|
+
overlay.tierAt = Math.max(overlay.tierAt - 1, 0);
|
|
492
|
+
bump();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (key.return) {
|
|
496
|
+
// OBS-508: keep the suggested band → the evidence note arrives pre-typed; override the
|
|
497
|
+
// band → the note starts empty (the suggestion argues for a DIFFERENT tier).
|
|
498
|
+
const chosen = TIERS[overlay.tierAt];
|
|
499
|
+
overlay.note = overlay.suggestion && overlay.suggestion.tier === chosen ? overlay.suggestion.note : "";
|
|
500
|
+
overlay.stage = "note";
|
|
501
|
+
bump();
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// note stage — free text
|
|
506
|
+
if (key.return) {
|
|
507
|
+
if (!overlay.note.trim()) {
|
|
508
|
+
ui.notice = "a typed benchmark-provenance note is required";
|
|
509
|
+
bump();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
applyClassification(overlay);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (key.backspace || key.delete) {
|
|
516
|
+
overlay.note = overlay.note.slice(0, -1);
|
|
517
|
+
bump();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (input && !key.ctrl && !key.meta) {
|
|
521
|
+
overlay.note += input;
|
|
522
|
+
ui.notice = "";
|
|
523
|
+
bump();
|
|
524
|
+
}
|
|
548
525
|
return;
|
|
549
526
|
}
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
527
|
+
if (overlay.kind === "add-model") {
|
|
528
|
+
if (key.escape) {
|
|
529
|
+
setOverlay(null);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (key.return) {
|
|
533
|
+
const candidate = overlay.text;
|
|
534
|
+
if (!MODEL_ID_RE.test(candidate)) {
|
|
535
|
+
ui.notice = `model id must match ${MODEL_ID_RE.source}`;
|
|
536
|
+
bump();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const group = groupOf(overlay.adapter);
|
|
540
|
+
const exists = group
|
|
541
|
+
&& groupRows(group).some((row) => row.model === candidate);
|
|
542
|
+
if (exists) {
|
|
543
|
+
ui.notice = `${candidate} is already listed for ${overlay.adapter}`;
|
|
544
|
+
bump();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
beginClassification(overlay.adapter, candidate);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (key.backspace || key.delete) {
|
|
551
|
+
overlay.text = overlay.text.slice(0, -1);
|
|
552
|
+
bump();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (input && !key.ctrl && !key.meta) {
|
|
556
|
+
overlay.text += input;
|
|
557
|
+
ui.notice = "";
|
|
558
|
+
bump();
|
|
559
|
+
}
|
|
563
560
|
return;
|
|
564
561
|
}
|
|
565
|
-
if (
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
562
|
+
if (overlay.kind === "assign") {
|
|
563
|
+
const rows = shapeList();
|
|
564
|
+
if (key.escape) {
|
|
565
|
+
setOverlay(null);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (key.downArrow || input === "j") {
|
|
569
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
570
|
+
bump();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (key.upArrow || input === "k") {
|
|
574
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
575
|
+
bump();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (key.return && rows[overlay.at]) {
|
|
579
|
+
const shape = rows[overlay.at].id;
|
|
580
|
+
ui.map = { ...ui.map, [shape]: { pin: { via: overlay.adapter, model: overlay.model } } };
|
|
581
|
+
setOverlay(null);
|
|
582
|
+
ui.notice = `pinned ${shape} → ${overlay.adapter}:${overlay.model}`;
|
|
583
|
+
bump();
|
|
584
|
+
}
|
|
569
585
|
return;
|
|
570
586
|
}
|
|
571
|
-
if (
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
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");
|
|
587
|
+
if (overlay.kind === "candidates") {
|
|
588
|
+
const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
|
|
589
|
+
if (key.escape) {
|
|
590
|
+
setOverlay(null);
|
|
580
591
|
return;
|
|
581
592
|
}
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
if (key.downArrow) {
|
|
594
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
595
|
+
bump();
|
|
596
|
+
return;
|
|
586
597
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
598
|
+
if (key.upArrow) {
|
|
599
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
600
|
+
bump();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (input === " " && rows[overlay.at]) {
|
|
604
|
+
const id = rows[overlay.at].id;
|
|
605
|
+
const at = overlay.chain.indexOf(id);
|
|
606
|
+
if (at === -1)
|
|
607
|
+
overlay.chain.push(id);
|
|
608
|
+
else
|
|
609
|
+
overlay.chain.splice(at, 1);
|
|
610
|
+
bump();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (key.return) {
|
|
614
|
+
if (overlay.chain.length) {
|
|
615
|
+
// a selection is a POOL, and its mode is a decision, not a default — the tiny
|
|
616
|
+
// poolmode overlay asks it; Esc there restores this picker with the chain intact
|
|
617
|
+
// OBS-525: an existing pool's mode seeds the cursor so a round-trip keeps it
|
|
618
|
+
setOverlay({ kind: "poolmode", picker: overlay, at: ui.map[overlay.shape]?.pool?.mode === "ordered" ? 1 : 0 });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (rows[overlay.at]) {
|
|
622
|
+
ui.map = { ...ui.map, [overlay.shape]: { pin: rows[overlay.at].pin } };
|
|
623
|
+
setOverlay(null);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length, (max) => {
|
|
629
|
+
overlay.at = Math.min(overlay.at, max);
|
|
630
|
+
});
|
|
595
631
|
return;
|
|
596
632
|
}
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
633
|
+
if (overlay.kind === "poolmode") {
|
|
634
|
+
if (key.escape) {
|
|
635
|
+
setOverlay(overlay.picker); // back to the picker — chain and cursor intact
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (key.downArrow || input === "j") {
|
|
639
|
+
overlay.at = 1;
|
|
640
|
+
bump();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (key.upArrow || input === "k") {
|
|
644
|
+
overlay.at = 0;
|
|
645
|
+
bump();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (key.return) {
|
|
649
|
+
const { shape, chain } = overlay.picker;
|
|
650
|
+
// a pool is the shape's WHOLE declaration: pin and prefer leave with it (schema
|
|
651
|
+
// exclusivity — pin+pool and pool+prefer both fail config load)
|
|
652
|
+
const { pin: _pin, prefer: _prefer, pool: _pool, ...entry } = ui.map[shape] ?? {};
|
|
653
|
+
ui.map = {
|
|
654
|
+
...ui.map,
|
|
655
|
+
[shape]: { ...entry, pool: { mode: overlay.at === 0 ? "any" : "ordered", channels: chain.slice() } },
|
|
656
|
+
};
|
|
657
|
+
setOverlay(null);
|
|
658
|
+
}
|
|
601
659
|
return;
|
|
602
660
|
}
|
|
603
|
-
if (
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
if (screenRef.current === "provenance") {
|
|
619
|
-
if (key.return) {
|
|
620
|
-
if (!noteRef.current.trim()) {
|
|
621
|
-
setNotice("fleet: a typed benchmark-provenance note is required");
|
|
661
|
+
if (overlay.kind === "prefer") {
|
|
662
|
+
const rows = overlay.rows.filter((option) => matches(option, ui.filter));
|
|
663
|
+
if (key.escape) {
|
|
664
|
+
setOverlay(null);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (key.downArrow) {
|
|
668
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
669
|
+
bump();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (key.upArrow) {
|
|
673
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
674
|
+
bump();
|
|
622
675
|
return;
|
|
623
676
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
677
|
+
if (input === " " && rows[overlay.at]) {
|
|
678
|
+
const option = rows[overlay.at];
|
|
679
|
+
const at = overlay.chain.indexOf(option);
|
|
680
|
+
if (at === -1)
|
|
681
|
+
overlay.chain.push(option);
|
|
682
|
+
else
|
|
683
|
+
overlay.chain.splice(at, 1);
|
|
684
|
+
bump();
|
|
685
|
+
return;
|
|
629
686
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
687
|
+
if (key.return) {
|
|
688
|
+
if ("shape" in overlay.target) {
|
|
689
|
+
const shape = overlay.target.shape;
|
|
690
|
+
const nextEntry = { ...ui.map[shape] };
|
|
691
|
+
if (overlay.chain.length)
|
|
692
|
+
nextEntry.prefer = overlay.chain.slice();
|
|
693
|
+
else
|
|
694
|
+
delete nextEntry.prefer;
|
|
695
|
+
ui.map = { ...ui.map, [shape]: nextEntry };
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
ui.steering = {
|
|
699
|
+
...ui.steering,
|
|
700
|
+
[overlay.target.steering]: overlay.chain.length ? overlay.chain.slice() : undefined,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
setOverlay(null);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
editFilter((f) => overlay.rows.filter((option) => matches(option, f)).length, (max) => {
|
|
707
|
+
overlay.at = Math.min(overlay.at, max);
|
|
708
|
+
});
|
|
634
709
|
return;
|
|
635
710
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
711
|
+
// judge overlay
|
|
712
|
+
const rows = [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, ui.filter));
|
|
713
|
+
if (key.escape) {
|
|
714
|
+
setOverlay(null);
|
|
640
715
|
return;
|
|
641
716
|
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
setNote(next);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (screenRef.current === "modes") {
|
|
649
|
-
// preset entry adds one final `custom` row after the modes — the full-walk escape hatch
|
|
650
|
-
const rowCount = modeOptions.length + (presetPickRef.current ? 1 : 0);
|
|
651
|
-
const currentModeCursor = modeCursorRef.current ?? 0;
|
|
652
|
-
if (key.downArrow || letterAlias === "j") {
|
|
653
|
-
const next = Math.min(currentModeCursor + 1, Math.max(rowCount - 1, 0));
|
|
654
|
-
modeCursorRef.current = next;
|
|
655
|
-
setModeCursor(next);
|
|
717
|
+
if (key.downArrow) {
|
|
718
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
719
|
+
bump();
|
|
656
720
|
return;
|
|
657
721
|
}
|
|
658
|
-
if (key.upArrow
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
setModeCursor(next);
|
|
722
|
+
if (key.upArrow) {
|
|
723
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
724
|
+
bump();
|
|
662
725
|
return;
|
|
663
726
|
}
|
|
664
|
-
if (key.return) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (!modeOptions[currentModeCursor])
|
|
671
|
-
return;
|
|
672
|
-
selectedModeRef.current = modeOptions[currentModeCursor].id;
|
|
673
|
-
if (presetPickRef.current) {
|
|
674
|
-
// entry context: a preset pick goes straight to the diff confirm
|
|
675
|
-
goReview();
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
showScreen("shapes");
|
|
727
|
+
if (key.return && rows[overlay.at]) {
|
|
728
|
+
// single-select: one seat or back to the config default — never a chain
|
|
729
|
+
const picked = rows[overlay.at];
|
|
730
|
+
ui.judgeSeat = picked === judgeKeepRow ? null : picked;
|
|
731
|
+
setOverlay(null);
|
|
732
|
+
return;
|
|
679
733
|
}
|
|
734
|
+
editFilter((f) => [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, f)).length, (max) => {
|
|
735
|
+
overlay.at = Math.min(overlay.at, max);
|
|
736
|
+
});
|
|
680
737
|
return;
|
|
681
738
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (key.
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
739
|
+
// ── browser (no overlay) ─────────────────────────────────────────────────
|
|
740
|
+
if (ui.searching) {
|
|
741
|
+
if (key.escape) {
|
|
742
|
+
ui.searching = false;
|
|
743
|
+
ui.filter = "";
|
|
744
|
+
bump();
|
|
688
745
|
return;
|
|
689
746
|
}
|
|
690
|
-
if (key.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
setShapeCursor(next);
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
const shape = rows[shapeCursorRef.current]?.id;
|
|
697
|
-
if (letterAlias === "a" && shape) {
|
|
698
|
-
const nextEntry = { ...stagedMap()[shape] };
|
|
699
|
-
delete nextEntry.pin;
|
|
700
|
-
const nextMap = { ...stagedMap(), [shape]: nextEntry };
|
|
701
|
-
mapRef.current = nextMap;
|
|
702
|
-
setMap(nextMap);
|
|
747
|
+
if (key.return) {
|
|
748
|
+
ui.searching = false;
|
|
749
|
+
bump();
|
|
703
750
|
return;
|
|
704
751
|
}
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
setCandidateCursor(0);
|
|
709
|
-
showScreen("candidates");
|
|
752
|
+
if (key.downArrow) {
|
|
753
|
+
ui.listAt = Math.min(ui.listAt + 1, Math.max(listLength() - 1, 0));
|
|
754
|
+
bump();
|
|
710
755
|
return;
|
|
711
756
|
}
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
preferChainRef.current = current.slice();
|
|
716
|
-
preferCursorRef.current = 0;
|
|
717
|
-
setPreferCursor(0);
|
|
718
|
-
showScreen("shape-prefer");
|
|
757
|
+
if (key.upArrow) {
|
|
758
|
+
ui.listAt = Math.max(ui.listAt - 1, 0);
|
|
759
|
+
bump();
|
|
719
760
|
return;
|
|
720
761
|
}
|
|
721
|
-
|
|
722
|
-
finishEditor();
|
|
762
|
+
editFilter((f) => modelRows(f).length);
|
|
723
763
|
return;
|
|
724
764
|
}
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
candidateCursorRef.current = next;
|
|
730
|
-
setCandidateCursor(next);
|
|
765
|
+
if (key.escape) {
|
|
766
|
+
if (ui.filter !== "") {
|
|
767
|
+
ui.filter = "";
|
|
768
|
+
bump();
|
|
731
769
|
return;
|
|
732
770
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
771
|
+
// v1.90.8: inside the init journey one Esc must never discard the whole walk — it returns
|
|
772
|
+
// HOME to the presets overlay; Esc there (or q/ctrl+c anywhere) remains the real quit.
|
|
773
|
+
if (entry === "presets") {
|
|
774
|
+
setOverlay({ kind: "presets", at: Math.max(modeOptions.findIndex((option) => option.id === ui.selectedMode), 0), home: true });
|
|
737
775
|
return;
|
|
738
776
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
777
|
+
guardedQuit(armed);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (hotkey === "q") {
|
|
781
|
+
guardedQuit(armed);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (hotkey === "r") {
|
|
785
|
+
finish({ kind: "refresh" });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (hotkey === "w") {
|
|
789
|
+
// OBS-522: an empty `w` used to EXIT the editor as no-changes — a save key must never
|
|
790
|
+
// double as quit. Say there is nothing to write and stay.
|
|
791
|
+
if (stagedCount() === 0) {
|
|
792
|
+
ui.notice = "no staged edits — nothing to write (Esc quits)";
|
|
793
|
+
bump();
|
|
748
794
|
return;
|
|
749
795
|
}
|
|
750
|
-
|
|
796
|
+
goReview();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (hotkey === "m") {
|
|
800
|
+
setOverlay({ kind: "presets", at: Math.max(modeOptions.findIndex((option) => option.id === ui.selectedMode), 0), home: false });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (hotkey === "/" || (hotkey.startsWith("/") && hotkey.length > 1)) {
|
|
804
|
+
if (ui.view !== "models") {
|
|
805
|
+
// the shapes and steering lists are 9/3 fixed rows — nothing to search; say where search
|
|
806
|
+
// lives instead of eating the key silently (operator field report, 2026-08-16)
|
|
807
|
+
ui.notice = "this list is not searchable — / searches the models view (All models or an adapter in the rail)";
|
|
808
|
+
bump();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
ui.searching = true;
|
|
812
|
+
ui.focus = "list";
|
|
813
|
+
ui.notice = "";
|
|
814
|
+
// fast typing or a paste arrives as one chunk ("/kimi") — the tail is the query; control
|
|
815
|
+
// bytes (a pasted trailing "\r") are framing, not query text
|
|
816
|
+
ui.filter = hotkey.slice(1).replace(/[\u0000-\u001f\u007f]/g, "");
|
|
817
|
+
ui.listAt = Math.min(ui.listAt, Math.max(modelRows(ui.filter).length - 1, 0));
|
|
818
|
+
bump();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// focus moves
|
|
822
|
+
if (key.leftArrow || (key.tab && ui.focus === "list")) {
|
|
823
|
+
ui.focus = "rail";
|
|
824
|
+
bump();
|
|
751
825
|
return;
|
|
752
826
|
}
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
827
|
+
if (key.rightArrow || (key.tab && ui.focus === "rail")) {
|
|
828
|
+
ui.focus = "list";
|
|
829
|
+
bump();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (ui.focus === "rail") {
|
|
833
|
+
const rows = railRows();
|
|
834
|
+
if (key.downArrow || hotkey === "j") {
|
|
835
|
+
ui.railAt = Math.min(ui.railAt + 1, rows.length - 1);
|
|
836
|
+
bump();
|
|
758
837
|
return;
|
|
759
838
|
}
|
|
760
|
-
if (key.upArrow ||
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
setPreferCursor(next);
|
|
839
|
+
if (key.upArrow || hotkey === "k") {
|
|
840
|
+
ui.railAt = Math.max(ui.railAt - 1, 0);
|
|
841
|
+
bump();
|
|
764
842
|
return;
|
|
765
843
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
844
|
+
const row = rows[ui.railAt];
|
|
845
|
+
if (!row)
|
|
846
|
+
return;
|
|
847
|
+
if (input === " " && row.kind === "adapter") {
|
|
848
|
+
const id = modelGroups[row.index].adapter;
|
|
849
|
+
// membership can't fix auth — say so, or the ✗ reads as a toggle that refuses to move
|
|
850
|
+
// (operator field report, 2026-08-16: expired kimi token)
|
|
851
|
+
if (agents.find((agent) => agent.id === id)?.authed === false) {
|
|
852
|
+
ui.notice = `${id} is not authed — Space only toggles fleet membership; re-auth the ${id} CLI, then run tickmarkr doctor`;
|
|
853
|
+
}
|
|
854
|
+
const next = new Set(ui.deny);
|
|
855
|
+
if (next.has(id))
|
|
856
|
+
next.delete(id);
|
|
771
857
|
else
|
|
772
|
-
|
|
773
|
-
|
|
858
|
+
next.add(id);
|
|
859
|
+
ui.deny = next;
|
|
860
|
+
clampList();
|
|
861
|
+
bump();
|
|
774
862
|
return;
|
|
775
863
|
}
|
|
776
864
|
if (key.return) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
mapRef.current = nextMap;
|
|
786
|
-
setMap(nextMap);
|
|
865
|
+
if (row.kind === "view") {
|
|
866
|
+
ui.view = row.view;
|
|
867
|
+
if (row.view === "models")
|
|
868
|
+
ui.adapterAt = -1;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
ui.view = "models";
|
|
872
|
+
ui.adapterAt = row.index;
|
|
787
873
|
}
|
|
788
|
-
|
|
874
|
+
ui.focus = "list";
|
|
875
|
+
ui.listAt = 0;
|
|
876
|
+
ui.filter = "";
|
|
877
|
+
ui.notice = "";
|
|
878
|
+
// v1.92: the FIRST Shapes entry per session raises the presets overlay (both entries:
|
|
879
|
+
// fleet and init) — Esc from it lands right here in the shapes list, never quits.
|
|
880
|
+
if (ui.view === "shapes" && !ui.presetsSeen) {
|
|
881
|
+
ui.presetsSeen = true;
|
|
882
|
+
ui.overlay = {
|
|
883
|
+
kind: "presets",
|
|
884
|
+
at: Math.max(modeOptions.findIndex((option) => option.id === ui.selectedMode), 0),
|
|
885
|
+
home: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
bump();
|
|
789
889
|
}
|
|
790
890
|
return;
|
|
791
891
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
892
|
+
// list focus
|
|
893
|
+
if (key.downArrow || hotkey === "j") {
|
|
894
|
+
ui.listAt = Math.min(ui.listAt + 1, Math.max(listLength() - 1, 0));
|
|
895
|
+
ui.notice = "";
|
|
896
|
+
bump();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (key.upArrow || hotkey === "k") {
|
|
900
|
+
ui.listAt = Math.max(ui.listAt - 1, 0);
|
|
901
|
+
ui.notice = "";
|
|
902
|
+
bump();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (ui.view === "models") {
|
|
906
|
+
const rows = modelRows();
|
|
907
|
+
const row = rows[ui.listAt];
|
|
908
|
+
if (input === " " && row) {
|
|
909
|
+
if (!row.tier) {
|
|
910
|
+
// v1.90.9: an unclassified model has no tier to route on — selecting IS classifying,
|
|
911
|
+
// so Space opens the same channel → tier → provenance flow instead of dying silently.
|
|
912
|
+
beginClassification(row.adapter, row.model);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const id = `${row.adapter}:${row.model}`;
|
|
916
|
+
const next = new Set(ui.denyModels);
|
|
917
|
+
if (next.has(id))
|
|
918
|
+
next.delete(id);
|
|
919
|
+
else
|
|
920
|
+
next.add(id);
|
|
921
|
+
ui.denyModels = next;
|
|
922
|
+
bump();
|
|
798
923
|
return;
|
|
799
924
|
}
|
|
800
|
-
if (key.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
925
|
+
if (key.return && row) {
|
|
926
|
+
if (!row.tier) {
|
|
927
|
+
beginClassification(row.adapter, row.model);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (row.denied || ui.deny.has(row.adapter)) {
|
|
931
|
+
ui.notice = `${row.adapter}:${row.model} is out of the fleet — Space adds it before assigning`;
|
|
932
|
+
bump();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// model-centric assignment: Enter pins the model to a shape (omp: Enter assigns roles)
|
|
936
|
+
setOverlay({ kind: "assign", adapter: row.adapter, model: row.model, at: 0 });
|
|
804
937
|
return;
|
|
805
938
|
}
|
|
806
|
-
if (
|
|
807
|
-
if (
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
setNotice("");
|
|
811
|
-
showScreen("judge-pick");
|
|
939
|
+
if (hotkey === "t" && row) {
|
|
940
|
+
if (row.tier) {
|
|
941
|
+
ui.notice = "tier reassignment on classified models is not supported in v1 — edit config directly";
|
|
942
|
+
bump();
|
|
812
943
|
return;
|
|
813
944
|
}
|
|
814
|
-
|
|
815
|
-
const current = steeringRef.current?.[which] ?? [];
|
|
816
|
-
steeringPickerRef.current = which;
|
|
817
|
-
steeringRowsRef.current = steeringOptionsFor(which, current);
|
|
818
|
-
steeringChainRef.current = current.slice();
|
|
819
|
-
steeringPickerCursorRef.current = 0;
|
|
820
|
-
setSteeringPickerCursor(0);
|
|
821
|
-
setNotice("");
|
|
822
|
-
showScreen("steering-prefer");
|
|
945
|
+
beginClassification(row.adapter, row.model);
|
|
823
946
|
return;
|
|
824
947
|
}
|
|
825
|
-
if (
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const next = Math.min(judgeCursorRef.current + 1, Math.max(rows.length - 1, 0));
|
|
833
|
-
judgeCursorRef.current = next;
|
|
834
|
-
setJudgeCursor(next);
|
|
948
|
+
if (hotkey === "n") {
|
|
949
|
+
if (ui.adapterAt === -1) {
|
|
950
|
+
ui.notice = "select an adapter in the rail first — n adds a model to one adapter";
|
|
951
|
+
bump();
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
setOverlay({ kind: "add-model", adapter: modelGroups[ui.adapterAt].adapter, text: "" });
|
|
835
955
|
return;
|
|
836
956
|
}
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
957
|
+
if (hotkey === "s") {
|
|
958
|
+
// OBS-508: bulk-stage every VISIBLE unclassified model carrying a catalog suggestion —
|
|
959
|
+
// visible means the type-to-search filter and the retired-hide both scope the batch.
|
|
960
|
+
// Adapter-scoped: first-touch channel/vendor provenance is a per-adapter answer.
|
|
961
|
+
if (ui.adapterAt === -1) {
|
|
962
|
+
ui.notice = "select an adapter in the rail first — s stages one adapter's suggestions";
|
|
963
|
+
bump();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const group = modelGroups[ui.adapterAt];
|
|
967
|
+
const suggested = rows
|
|
968
|
+
.filter((candidate) => candidate.adapter === group.adapter && !candidate.tier && candidate.suggestion !== undefined)
|
|
969
|
+
.map((candidate) => ({ model: candidate.model, suggestion: candidate.suggestion }));
|
|
970
|
+
if (!suggested.length) {
|
|
971
|
+
ui.notice = "no catalog tier suggestions among the visible unclassified models — evidence comes from the cached catalogs (AA index / API pricing)";
|
|
972
|
+
bump();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const firstTouch = !group.rows.some((candidate) => candidate.tier !== undefined)
|
|
976
|
+
&& !ui.classifications.some((c) => c.adapter === group.adapter);
|
|
977
|
+
if (!firstTouch) {
|
|
978
|
+
stageSuggested(group.adapter, suggested);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (!group.vendor) {
|
|
982
|
+
ui.notice = `${group.adapter} has no vendor declaration — classification cannot be saved`;
|
|
983
|
+
bump();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const answered = ui.channelByAdapter[group.adapter];
|
|
987
|
+
if (answered) {
|
|
988
|
+
stageSuggested(group.adapter, suggested, { vendor: group.vendor, channel: answered });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
setOverlay({
|
|
992
|
+
kind: "classify",
|
|
993
|
+
adapter: group.adapter,
|
|
994
|
+
model: "",
|
|
995
|
+
vendor: group.vendor,
|
|
996
|
+
stage: "channel",
|
|
997
|
+
channelAt: 0,
|
|
998
|
+
tierAt: 0,
|
|
999
|
+
note: "",
|
|
1000
|
+
suggestion: null,
|
|
1001
|
+
bulk: { adapter: group.adapter, vendor: group.vendor, rows: suggested },
|
|
1002
|
+
});
|
|
841
1003
|
return;
|
|
842
1004
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
judgeSeatRef.current = seat;
|
|
848
|
-
setJudgeSeat(seat);
|
|
849
|
-
showScreen("steering");
|
|
1005
|
+
if (hotkey === "a") {
|
|
1006
|
+
ui.showAll = !ui.showAll;
|
|
1007
|
+
clampList();
|
|
1008
|
+
bump();
|
|
850
1009
|
return;
|
|
851
1010
|
}
|
|
852
|
-
filterInput((f) => filteredJudgeRows(f).length, judgeCursorRef, setJudgeCursor, input, key);
|
|
853
1011
|
return;
|
|
854
1012
|
}
|
|
855
|
-
if (
|
|
856
|
-
const rows =
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
steeringPickerCursorRef.current = next;
|
|
860
|
-
setSteeringPickerCursor(next);
|
|
1013
|
+
if (ui.view === "shapes") {
|
|
1014
|
+
const rows = shapeList();
|
|
1015
|
+
const shape = rows[ui.listAt]?.id;
|
|
1016
|
+
if (!shape)
|
|
861
1017
|
return;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1018
|
+
if (hotkey === "a") {
|
|
1019
|
+
// OBS-525: auto reverts the shape's WHOLE routing declaration — pool included, not just
|
|
1020
|
+
// the pin. prefer survives: it biases auto routing and is valid without pin/pool.
|
|
1021
|
+
const nextEntry = { ...ui.map[shape] };
|
|
1022
|
+
delete nextEntry.pin;
|
|
1023
|
+
delete nextEntry.pool;
|
|
1024
|
+
ui.map = { ...ui.map, [shape]: nextEntry };
|
|
1025
|
+
bump();
|
|
867
1026
|
return;
|
|
868
1027
|
}
|
|
869
|
-
if (
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1028
|
+
if (key.return || hotkey === "p") {
|
|
1029
|
+
const picked = candidatesForShape(shape, ui.selectedMode, ui.map, stagedDeny());
|
|
1030
|
+
setOverlay({
|
|
1031
|
+
kind: "candidates",
|
|
1032
|
+
shape,
|
|
1033
|
+
rows: picked.rows,
|
|
1034
|
+
excludedNote: picked.excludedNote,
|
|
1035
|
+
// OBS-525: an existing pool round-trips — reopening the picker starts from the staged
|
|
1036
|
+
// channels instead of an empty selection that would silently replace the pool with a pin.
|
|
1037
|
+
chain: ui.map[shape]?.pool?.channels.slice() ?? [],
|
|
1038
|
+
at: 0,
|
|
1039
|
+
});
|
|
877
1040
|
return;
|
|
878
1041
|
}
|
|
879
|
-
if (
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1042
|
+
if (hotkey === "f") {
|
|
1043
|
+
if (ui.map[shape]?.pool !== undefined) {
|
|
1044
|
+
// OBS-525: pool+prefer fails the config exclusivity refine — blocking here beats
|
|
1045
|
+
// staging an invalid combination the reload guard bounces at the very end.
|
|
1046
|
+
ui.notice = `${shape} routes a pool — prefer applies to auto/pin routing; press a (auto) first or re-pool via Enter`;
|
|
1047
|
+
bump();
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const current = ui.map[shape]?.prefer ?? [];
|
|
1051
|
+
setOverlay({
|
|
1052
|
+
kind: "prefer",
|
|
1053
|
+
target: { shape },
|
|
1054
|
+
rows: preferOptionsForShape(shape, current),
|
|
1055
|
+
chain: current.slice(),
|
|
1056
|
+
at: 0,
|
|
1057
|
+
});
|
|
890
1058
|
}
|
|
891
|
-
filterInput((f) => filteredSteeringRows(f).length, steeringPickerCursorRef, setSteeringPickerCursor, input, key);
|
|
892
1059
|
return;
|
|
893
1060
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1061
|
+
// steering view
|
|
1062
|
+
const rows = steeringList();
|
|
1063
|
+
const row = rows[ui.listAt];
|
|
1064
|
+
if (!row)
|
|
1065
|
+
return;
|
|
1066
|
+
if (key.return || hotkey === "f") {
|
|
1067
|
+
if (row.id === "judge") {
|
|
1068
|
+
setOverlay({ kind: "judge", at: 0 });
|
|
897
1069
|
return;
|
|
898
1070
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1071
|
+
const which = row.id;
|
|
1072
|
+
const current = ui.steering[which] ?? [];
|
|
1073
|
+
setOverlay({
|
|
1074
|
+
kind: "prefer",
|
|
1075
|
+
target: { steering: which },
|
|
1076
|
+
rows: steeringOptionsFor(which, current),
|
|
1077
|
+
chain: current.slice(),
|
|
1078
|
+
at: 0,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
// ── render ─────────────────────────────────────────────────────────────────
|
|
1083
|
+
const tierCell = (row) => {
|
|
1084
|
+
if (row.tier === "frontier")
|
|
1085
|
+
return { text: "frontier", color: INK.brand };
|
|
1086
|
+
if (row.tier === "mid")
|
|
1087
|
+
return { text: "mid" };
|
|
1088
|
+
if (row.tier === "cheap")
|
|
1089
|
+
return { text: "cheap", dim: true };
|
|
1090
|
+
if (row.suggestion)
|
|
1091
|
+
return { text: `→ ${row.suggestion.tier}?`, dim: true };
|
|
1092
|
+
return { text: "", dim: true };
|
|
1093
|
+
};
|
|
1094
|
+
const priceCell = (row) => {
|
|
1095
|
+
const usd = fmtUsdPair(row.evidence?.inputCostPerMtok, row.evidence?.outputCostPerMtok);
|
|
1096
|
+
if (row.channel === "sub")
|
|
1097
|
+
return "sub";
|
|
1098
|
+
return usd;
|
|
1099
|
+
};
|
|
1100
|
+
const modelDetail = (row) => {
|
|
1101
|
+
const parts = [
|
|
1102
|
+
`${row.adapter}:${row.model}`,
|
|
1103
|
+
row.tier ?? "unclassified",
|
|
1104
|
+
row.evidence?.unauthed !== undefined ? `UNAUTHED — ${row.evidence.unauthed}` : "",
|
|
1105
|
+
row.evidence?.contextWindow !== undefined ? `${fmtCtx(row.evidence.contextWindow)} ctx` : "",
|
|
1106
|
+
row.evidence?.outputWindow !== undefined ? `${fmtCtx(row.evidence.outputWindow)} out` : "",
|
|
1107
|
+
row.evidence?.inputCostPerMtok !== undefined || row.evidence?.outputCostPerMtok !== undefined
|
|
1108
|
+
? `${fmtUsdPair(row.evidence?.inputCostPerMtok, row.evidence?.outputCostPerMtok)} per Mtok`
|
|
1109
|
+
: (row.channel === "sub" ? "sub flat-rate quota" : ""),
|
|
1110
|
+
row.evidence?.probeMs !== undefined ? `probed ${fmtMs(row.evidence.probeMs)}` : "",
|
|
1111
|
+
row.detectedAt ? `detected ${row.detectedAt}` : "",
|
|
1112
|
+
].filter(Boolean);
|
|
1113
|
+
return parts.join(" · ");
|
|
1114
|
+
};
|
|
1115
|
+
const renderModelRow = (row, selected) => {
|
|
1116
|
+
const tier = tierCell(row);
|
|
1117
|
+
const showProbe = bodyW >= 76;
|
|
1118
|
+
const showPrice = bodyW >= 60;
|
|
1119
|
+
// every cell accounted for: pointer 2 + glyph 1 + gap 1 + tier 11 + ctx 6 (+ price 12 + probe 7)
|
|
1120
|
+
const nameW = bodyW - 4 - 11 - 6 - (showPrice ? 12 : 0) - (showProbe ? 7 : 0);
|
|
1121
|
+
// OBS-531: deep router ids (omp/prime-agent) clip tail-preserving — the LAST segment is the
|
|
1122
|
+
// distinguishing half; end-clipping rendered ten identical "prime-agent/anthropic/claude-…" rows.
|
|
1123
|
+
const name = clipPathTail(`${row.adapter}/${row.model}`, nameW);
|
|
1124
|
+
const prefixLen = Math.min(name.length, row.adapter.length + 1);
|
|
1125
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), row.denied
|
|
1126
|
+
? _jsx(Glyph, { kind: "off" })
|
|
1127
|
+
: row.evidence?.unauthed !== undefined
|
|
1128
|
+
? _jsx(Glyph, { kind: "fail" })
|
|
1129
|
+
: row.tier
|
|
1130
|
+
? _jsx(Glyph, { kind: "on" })
|
|
1131
|
+
: _jsx(Glyph, { kind: "unknown" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: row.denied, children: [_jsx(Text, { dimColor: true, children: name.slice(0, prefixLen) }), _jsx(Text, { bold: selected, children: name.slice(prefixLen).padEnd(Math.max(nameW - prefixLen, 0)) })] }), _jsx(Text, { color: tier.color, dimColor: tier.dim, children: padCellStart(tier.text, 11) }), _jsx(Text, { dimColor: true, children: padCellStart(fmtCtx(row.evidence?.contextWindow), 6) }), showPrice && _jsx(Text, { dimColor: priceCell(row) === "sub", children: padCellStart(priceCell(row), 12) }), showProbe && _jsx(Text, { dimColor: true, children: padCellStart(fmtMs(row.evidence?.probeMs), 7) })] }, `${row.adapter}:${row.model}`));
|
|
1132
|
+
};
|
|
1133
|
+
const renderPlainRow = (id, label, selected, rowW = bodyW - 2) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), _jsx(Text, { bold: selected, children: clip(label, rowW) })] }, id));
|
|
1134
|
+
const renderShapeRow = (row, selected, rowW = bodyW - 2) => {
|
|
1135
|
+
const prefix = `${row.id} → `;
|
|
1136
|
+
const rest = row.label.startsWith(prefix) ? row.label.slice(prefix.length) : row.label;
|
|
1137
|
+
// The provenance chip is the row's decision token (fleet --why parity) — it right-aligns and
|
|
1138
|
+
// never truncates; the assignment text absorbs the clipping instead.
|
|
1139
|
+
const marker = " · source: ";
|
|
1140
|
+
const markerAt = rest.lastIndexOf(marker);
|
|
1141
|
+
const body = markerAt === -1 ? rest : rest.slice(0, markerAt);
|
|
1142
|
+
const source = markerAt === -1 ? "" : rest.slice(markerAt + marker.length);
|
|
1143
|
+
const sourceCell = source ? `source: ${source}` : "";
|
|
1144
|
+
const bodyW2 = rowW - 12 - (sourceCell ? sourceCell.length + 2 : 0);
|
|
1145
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), _jsx(Text, { bold: selected, color: selected ? INK.brand : undefined, children: padCell(row.id, 10) }), _jsx(Text, { children: padCell(body, Math.max(bodyW2, 8)) }), sourceCell !== "" && _jsx(Text, { dimColor: true, children: ` ${sourceCell}` })] }, row.id));
|
|
1146
|
+
};
|
|
1147
|
+
const railView = (row, index) => {
|
|
1148
|
+
const active = ui.view === row.view && (row.view !== "models" || ui.adapterAt === -1);
|
|
1149
|
+
const selected = ui.focus === "rail" && ui.railAt === index;
|
|
1150
|
+
return (_jsxs(Text, { children: [selected ? _jsx(Text, { color: INK.brand, children: "❯ " }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: active, color: active ? INK.brand : undefined, children: padCell(row.label, 13) }), _jsx(Text, { dimColor: true, children: padCellStart(String(row.count), 5) })] }, row.view));
|
|
1151
|
+
};
|
|
1152
|
+
const railAdapter = (index, railIndex) => {
|
|
1153
|
+
const group = modelGroups[index];
|
|
1154
|
+
const agent = agents.find((candidate) => candidate.id === group.adapter);
|
|
1155
|
+
const denied = ui.deny.has(group.adapter);
|
|
1156
|
+
const active = ui.view === "models" && ui.adapterAt === index;
|
|
1157
|
+
const selected = ui.focus === "rail" && ui.railAt === railIndex;
|
|
1158
|
+
return (_jsxs(Text, { children: [selected ? _jsx(Text, { color: INK.brand, children: "❯ " }) : _jsx(Text, { children: " " }), denied ? _jsx(Glyph, { kind: "off" }) : agent?.authed === false ? _jsx(Glyph, { kind: "fail" }) : _jsx(Glyph, { kind: "on" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: active, color: active ? INK.brand : undefined, dimColor: denied, children: padCell(group.adapter, 12) }), _jsx(Text, { dimColor: true, children: padCellStart(String(visibleCount(index)), 4) })] }, group.adapter));
|
|
1159
|
+
};
|
|
1160
|
+
const rail = railRows();
|
|
1161
|
+
const overlayNode = (() => {
|
|
1162
|
+
const overlay = ui.overlay;
|
|
1163
|
+
if (!overlay)
|
|
1164
|
+
return null;
|
|
1165
|
+
if (overlay.kind === "presets") {
|
|
1166
|
+
const details = modeOptions[overlay.at] ? modePreview(modeOptions[overlay.at].id, ui.map, stagedDeny()) : [];
|
|
1167
|
+
return (_jsxs(OverlayPanel, { title: overlay.home ? "routing preset" : "routing mode", width: bodyW, children: [_jsx(Text, { dimColor: true, children: overlay.home ? "a preset routes every shape — custom opens the browser" : "floors move with the mode; the browser edits the rest" }), _jsx(Text, { children: " " }), modeOptions.map((option, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === index }), option.id === ui.selectedMode ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === index, children: ` ${padCell(option.id, 12)}` }), _jsx(Text, { dimColor: true, children: clip(option.gloss, bodyW - 20) })] }, option.id))), overlay.home && (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === modeOptions.length }), _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.at === modeOptions.length, children: ` ${padCell("custom", 12)}` }), _jsx(Text, { dimColor: true, children: clip("open the fleet browser (models · shapes · steering)", bodyW - 20) })] })), _jsx(Text, { children: " " }), details.map((line) => _jsx(Text, { dimColor: true, children: line }, line))] }));
|
|
1168
|
+
}
|
|
1169
|
+
if (overlay.kind === "review") {
|
|
1170
|
+
const { visible, scroll, cap, lines } = reviewWindow(overlay);
|
|
1171
|
+
const below = lines.length - scroll - visible.length;
|
|
1172
|
+
return (_jsxs(OverlayPanel, { title: `review · ${overlay.review.path}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("everything staged lands in this one diff — y writes, n discards, ↑↓ scroll, Esc keeps editing", bodyW - 4) }), scroll > 0 && _jsx(ElisionMark, { count: scroll, side: "above" }), visible.map((line, index) => (_jsx(Text, { wrap: "truncate", color: line.startsWith("+") ? INK.brand : line.startsWith("-") ? INK.fail : undefined, dimColor: line.startsWith("@") || line.startsWith("---") || line.startsWith("+++"), children: clip(line, bodyW - 6) || " " }, `${scroll + index}:${line}`))), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: `↓ scrolls · PgDn jumps ${cap} lines` })] }));
|
|
1173
|
+
}
|
|
1174
|
+
if (overlay.kind === "classify") {
|
|
1175
|
+
const subject = overlay.bulk
|
|
1176
|
+
? `${overlay.adapter} · ${overlay.bulk.rows.length} suggested models`
|
|
1177
|
+
: `${overlay.adapter}:${overlay.model}`;
|
|
1178
|
+
return (_jsxs(OverlayPanel, { title: `classify · ${subject}`, width: bodyW, children: [overlay.stage === "channel" && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["first touch for ", overlay.adapter, " \u2014 how is this CLI billed?"] }), _jsx(Text, { children: " " }), CHANNELS.map((channel, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.channelAt === index }), _jsx(Text, { bold: overlay.channelAt === index, children: padCell(channel, 5) }), _jsx(Text, { dimColor: true, children: channel === "sub" ? "flat-rate subscription quota" : "metered API billing" })] }, channel)))] })), overlay.stage === "tier" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: overlay.suggestion
|
|
1179
|
+
? `catalog suggests ${overlay.suggestion.tier} — keep it and the provenance note arrives pre-typed`
|
|
1180
|
+
: "pick the capability band this model routes as" }), _jsx(Text, { children: " " }), TIERS.map((tier, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.tierAt === index }), overlay.suggestion?.tier === tier ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: overlay.tierAt === index, children: ` ${padCell(tier, 9)}` }), _jsx(Text, { dimColor: true, children: tier === "frontier" ? "strongest band — integrity shapes" : tier === "mid" ? "capable daily driver" : "fast + cheap" })] }, tier)))] })), overlay.stage === "note" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "benchmark provenance (required) \u2014 where does this tier claim come from?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: INK.brand, children: "> " }), _jsx(Text, { children: clip(overlay.note, bodyW - 6) || "" }), _jsx(Text, { color: INK.brand, children: "\u2588" })] })] }))] }));
|
|
1181
|
+
}
|
|
1182
|
+
if (overlay.kind === "add-model") {
|
|
1183
|
+
return (_jsxs(OverlayPanel, { title: `add model · ${overlay.adapter}`, width: bodyW, children: [_jsx(Text, { dimColor: true, children: "type the model id exactly as the CLI names it" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: INK.brand, children: "> " }), _jsx(Text, { children: clip(overlay.text, bodyW - 6) }), _jsx(Text, { color: INK.brand, children: "\u2588" })] })] }));
|
|
1184
|
+
}
|
|
1185
|
+
if (overlay.kind === "assign") {
|
|
1186
|
+
const rows = shapeList();
|
|
1187
|
+
return (_jsxs(OverlayPanel, { title: `pin ${overlay.adapter}:${overlay.model} to a shape`, width: bodyW, children: [_jsx(Text, { dimColor: true, children: "Enter pins this model as the shape's route \u2014 a on the shape later reverts to auto" }), _jsx(Text, { children: " " }), rows.map((row, index) => renderShapeRow(row, overlay.at === index, bodyW - 8))] }));
|
|
1188
|
+
}
|
|
1189
|
+
if (overlay.kind === "candidates") {
|
|
1190
|
+
const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
|
|
1191
|
+
const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
|
|
1192
|
+
const chained = overlay.chain.length > 0;
|
|
1193
|
+
return (_jsxs(OverlayPanel, { title: chained ? `pool · ${overlay.shape}` : `pin · ${overlay.shape}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("Enter pins one channel · Space selects a pool in order — Enter then asks its mode (pool replaces pin)", bodyW - 4) }), _jsx(SearchRow, { filter: ui.filter, active: true }), overlay.excludedNote !== undefined
|
|
1194
|
+
&& _jsx(Text, { dimColor: true, wrap: "truncate", children: clip(overlay.excludedNote, bodyW - 4) }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((candidate, index) => {
|
|
1195
|
+
const at = overlay.chain.indexOf(candidate.id);
|
|
1196
|
+
const selected = start + index === overlay.at;
|
|
1197
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Pointer, { on: selected }), at === -1 ? _jsx(Text, { dimColor: true, children: "· " }) : _jsx(Text, { color: INK.brand, children: `${at + 1} ` }), _jsx(Text, { bold: selected, children: clip(candidate.label, bodyW - 10) })] }, candidate.id));
|
|
1198
|
+
}), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
|
|
1199
|
+
}
|
|
1200
|
+
if (overlay.kind === "poolmode") {
|
|
1201
|
+
const modes = [
|
|
1202
|
+
{ id: "any", gloss: "economy engine picks within your selection (cost, then tier)" },
|
|
1203
|
+
{ id: "ordered", gloss: "walk your selection in order; first live wins" },
|
|
1204
|
+
];
|
|
1205
|
+
return (_jsxs(OverlayPanel, { title: `pool mode · ${overlay.picker.shape}`, width: bodyW, children: [_jsx(Text, { dimColor: true, children: clip(`pool: ${overlay.picker.chain.join(" → ")}`, bodyW - 4) }), _jsx(Text, { children: " " }), modes.map((mode, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: overlay.at === index }), _jsx(Text, { bold: overlay.at === index, children: padCell(mode.id, 9) }), _jsx(Text, { dimColor: true, children: clip(`— ${mode.gloss}`, bodyW - 14) })] }, mode.id)))] }));
|
|
1206
|
+
}
|
|
1207
|
+
if (overlay.kind === "prefer") {
|
|
1208
|
+
const title = "shape" in overlay.target ? `${overlay.target.shape}.prefer` : `${overlay.target.steering}.prefer`;
|
|
1209
|
+
const rows = overlay.rows.filter((option) => matches(option, ui.filter));
|
|
1210
|
+
const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
|
|
1211
|
+
return (_jsxs(OverlayPanel, { title: `edit · ${title}`, width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("Space adds/drops in order · Enter applies (empty clears) · ordered chain wins routing ties", bodyW - 4) }), _jsx(SearchRow, { filter: ui.filter, active: true }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((option, index) => {
|
|
1212
|
+
const at = overlay.chain.indexOf(option);
|
|
1213
|
+
const selected = start + index === overlay.at;
|
|
1214
|
+
return (_jsxs(Text, { children: [_jsx(Pointer, { on: selected }), at === -1 ? _jsx(Text, { dimColor: true, children: "· " }) : _jsx(Text, { color: INK.brand, children: `${at + 1} ` }), _jsx(Text, { bold: selected, children: clip(option, bodyW - 10) })] }, option));
|
|
1215
|
+
}), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
|
|
1216
|
+
}
|
|
1217
|
+
// judge
|
|
1218
|
+
const rows = [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, ui.filter));
|
|
1219
|
+
const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
|
|
1220
|
+
const selectedSeat = ui.judgeSeat ?? judgeKeepRow;
|
|
1221
|
+
return (_jsxs(OverlayPanel, { title: "pick \u00B7 judge", width: bodyW, children: [_jsx(Text, { dimColor: true, children: "one seat judges acceptance criteria \u2014 failover stays runtime (GATE-09)" }), _jsx(SearchRow, { filter: ui.filter, active: true }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((label, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: start + index === overlay.at }), label === selectedSeat ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: start + index === overlay.at, children: ` ${clip(label, bodyW - 10)}` })] }, label))), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
|
|
1222
|
+
})();
|
|
1223
|
+
const listNode = (() => {
|
|
1224
|
+
if (overlayNode)
|
|
1225
|
+
return overlayNode;
|
|
1226
|
+
if (ui.view === "models") {
|
|
1227
|
+
const rows = modelRows();
|
|
1228
|
+
const { visible, start, above, below } = windowRows(rows, ui.listAt, capacity);
|
|
1229
|
+
const scopeLabel = ui.adapterAt === -1
|
|
1230
|
+
? "All models"
|
|
1231
|
+
: modelGroups[ui.adapterAt]?.adapter ?? "";
|
|
1232
|
+
const scopeDenied = ui.adapterAt !== -1 && ui.deny.has(modelGroups[ui.adapterAt]?.adapter ?? "");
|
|
1233
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: scopeLabel }), _jsx(Text, { dimColor: true, children: ` ${rows.length}` })] }), _jsx(SearchRow, { filter: ui.filter, active: ui.searching, hint: "/ to search" }), scopeDenied && _jsx(Text, { color: INK.warn, children: `${modelGroups[ui.adapterAt]?.adapter} is out of the fleet — Space on its rail row adds it back` }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((row, index) => renderModelRow(row, ui.focus === "list" && start + index === ui.listAt)), below > 0 && _jsx(ElisionMark, { count: below, side: "below", hint: "/ to search" }), rows.length === 0 && !scopeDenied && _jsx(Text, { dimColor: true, children: " no models match" })] }));
|
|
1234
|
+
}
|
|
1235
|
+
if (ui.view === "shapes") {
|
|
1236
|
+
const rows = shapeList();
|
|
1237
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Shapes" }), _jsx(Text, { dimColor: true, children: ` routed under ${ui.selectedMode}` })] }), _jsx(Text, { children: " " }), rows.map((row, index) => renderShapeRow(row, ui.focus === "list" && index === ui.listAt))] }));
|
|
1238
|
+
}
|
|
1239
|
+
const rows = steeringList();
|
|
1240
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Steering" }), _jsx(Text, { dimColor: true, children: " review · consult · judge" })] }), _jsx(Text, { children: " " }), rows.map((row, index) => renderPlainRow(row.id, row.label, ui.focus === "list" && index === ui.listAt))] }));
|
|
1241
|
+
})();
|
|
1242
|
+
// ── footer ─────────────────────────────────────────────────────────────────
|
|
1243
|
+
const detailLines = () => {
|
|
1244
|
+
if (ui.notice)
|
|
1245
|
+
return [ui.notice];
|
|
1246
|
+
const overlay = ui.overlay;
|
|
1247
|
+
if (overlay) {
|
|
1248
|
+
if (overlay.kind === "candidates") {
|
|
1249
|
+
const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
|
|
1250
|
+
return rows[overlay.at] ? [rows[overlay.at].label] : [];
|
|
1251
|
+
}
|
|
1252
|
+
if (overlay.kind === "prefer") {
|
|
1253
|
+
return overlay.chain.length ? [`chain: ${overlay.chain.join(" → ")}`] : [];
|
|
1254
|
+
}
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
if (ui.view === "models") {
|
|
1258
|
+
const rows = modelRows();
|
|
1259
|
+
const row = rows[ui.listAt];
|
|
1260
|
+
const lines = [];
|
|
1261
|
+
if (row) {
|
|
1262
|
+
lines.push(modelDetail(row));
|
|
1263
|
+
if (!row.tier) {
|
|
1264
|
+
lines.push(row.suggestion
|
|
1265
|
+
? `catalog suggests ${row.suggestion.tier} — Space/Enter classifies with the note pre-typed · s stages every visible suggestion`
|
|
1266
|
+
: "unclassified — Space/Enter classifies; unclassified models are never routed");
|
|
907
1267
|
}
|
|
908
|
-
finish({ kind: "write", review: reviewRef.current });
|
|
909
1268
|
}
|
|
1269
|
+
const hidden = hiddenModelCount();
|
|
1270
|
+
if (hidden > 0 && lines.length < 2)
|
|
1271
|
+
lines.push(`${hidden} retired/preview/non-worker hidden — a shows all`);
|
|
1272
|
+
if (ui.showAll && lines.length < 2)
|
|
1273
|
+
lines.push("showing retired models — a hides them again");
|
|
1274
|
+
return lines;
|
|
910
1275
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
id: "doctor",
|
|
915
|
-
content: _jsx(Text, { children: `probe data: ${formatDoctorAge(ageMs)} (.tickmarkr/doctor.json)` }),
|
|
916
|
-
}], cursor: 0 }));
|
|
917
|
-
}
|
|
918
|
-
if (screen === "agents") {
|
|
919
|
-
const rows = agents.map((agent) => {
|
|
920
|
-
const active = !denyAdapters.has(agent.id);
|
|
921
|
-
return {
|
|
922
|
-
id: agent.id,
|
|
923
|
-
content: (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: active }), _jsx(Text, { children: ` ${agent.id} ${agent.version} ${agent.authed ? "authed" : "unauthed"}` })] })),
|
|
924
|
-
};
|
|
925
|
-
});
|
|
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 }));
|
|
927
|
-
}
|
|
928
|
-
const group = enabledModelGroups()[modelGroup];
|
|
929
|
-
if (screen === "models") {
|
|
930
|
-
const filtered = filteredModelRows();
|
|
931
|
-
const rows = filtered.map((row) => {
|
|
932
|
-
const denied = group ? denyModels.has(`${group.adapter}:${row.model}`) : false;
|
|
933
|
-
return {
|
|
934
|
-
id: row.model,
|
|
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` : ""}` }),
|
|
936
|
-
};
|
|
937
|
-
});
|
|
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 }));
|
|
955
|
-
}
|
|
956
|
-
if (screen === "add-model")
|
|
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] : [] }));
|
|
958
|
-
if (screen === "channel")
|
|
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 }));
|
|
960
|
-
if (screen === "tiers") {
|
|
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 }));
|
|
962
|
-
}
|
|
963
|
-
if (screen === "provenance")
|
|
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] : [] }));
|
|
965
|
-
if (screen === "modes") {
|
|
966
|
-
const presetPick = presetPickRef.current;
|
|
967
|
-
const rows = modeOptions.map((option) => ({
|
|
968
|
-
id: option.id,
|
|
969
|
-
content: (_jsxs(_Fragment, { children: [option.id === initialMode ? _jsx(ToggleMark, { active: true }) : _jsx(Text, { children: " " }), _jsx(Text, { children: ` ${option.id.padEnd(11)} ${option.gloss}` })] })),
|
|
970
|
-
}));
|
|
971
|
-
if (presetPick) {
|
|
972
|
-
rows.push({
|
|
973
|
-
id: "custom",
|
|
974
|
-
content: (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: ` ${"custom".padEnd(11)} walk the full editor (agents → models → shapes → seats)` })] })),
|
|
975
|
-
});
|
|
1276
|
+
if (ui.view === "shapes") {
|
|
1277
|
+
const row = shapeList()[ui.listAt];
|
|
1278
|
+
return row ? [row.label] : [];
|
|
976
1279
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
{
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
},
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1280
|
+
const row = steeringList()[ui.listAt];
|
|
1281
|
+
return row ? [row.label] : [];
|
|
1282
|
+
};
|
|
1283
|
+
const keyBinds = () => {
|
|
1284
|
+
const overlay = ui.overlay;
|
|
1285
|
+
if (overlay) {
|
|
1286
|
+
if (overlay.kind === "presets") {
|
|
1287
|
+
return overlay.home
|
|
1288
|
+
? [{ key: "Enter", label: "apply preset" }, { key: "↑↓", label: "move" }, { key: "Esc", label: "quit" }]
|
|
1289
|
+
: [{ key: "Enter", label: "set mode" }, { key: "↑↓", label: "move" }, { key: "Esc", label: "back" }];
|
|
1290
|
+
}
|
|
1291
|
+
if (overlay.kind === "review") {
|
|
1292
|
+
return [{ key: "y", label: "write" }, { key: "n", label: "discard" }, { key: "↑↓", label: "scroll" }, { key: "Esc", label: "keep editing" }];
|
|
1293
|
+
}
|
|
1294
|
+
if (overlay.kind === "classify") {
|
|
1295
|
+
return overlay.stage === "note"
|
|
1296
|
+
? [{ key: "type", label: "note" }, { key: "Enter", label: "stage" }, { key: "Esc", label: "cancel" }]
|
|
1297
|
+
: [{ key: "↑↓", label: "move" }, { key: "Enter", label: "next" }, { key: "Esc", label: "cancel" }];
|
|
1298
|
+
}
|
|
1299
|
+
if (overlay.kind === "assign") {
|
|
1300
|
+
// OBS-526: assign has no search box — advertising "type search" here promised a dead key
|
|
1301
|
+
return [{ key: "Enter", label: "pin to shape" }, { key: "↑↓", label: "move" }, { key: "Esc", label: "cancel" }];
|
|
1302
|
+
}
|
|
1303
|
+
if (overlay.kind === "add-model") {
|
|
1304
|
+
return [{ key: "type", label: "model id" }, { key: "Enter", label: "classify" }, { key: "Esc", label: "cancel" }];
|
|
1305
|
+
}
|
|
1306
|
+
if (overlay.kind === "prefer") {
|
|
1307
|
+
return [{ key: "Space", label: "add/drop" }, { key: "Enter", label: "apply" }, { key: "type", label: "search" }, { key: "Esc", label: "cancel" }];
|
|
1308
|
+
}
|
|
1309
|
+
if (overlay.kind === "candidates") {
|
|
1310
|
+
return [
|
|
1311
|
+
{ key: "Enter", label: overlay.chain.length ? "apply pool" : "pin" },
|
|
1312
|
+
{ key: "Space", label: "pool in order" },
|
|
1313
|
+
{ key: "type", label: "search" },
|
|
1314
|
+
{ key: "Esc", label: "cancel" },
|
|
1315
|
+
];
|
|
1316
|
+
}
|
|
1317
|
+
if (overlay.kind === "poolmode") {
|
|
1318
|
+
return [
|
|
1319
|
+
{ key: "Enter", label: "apply pool" },
|
|
1320
|
+
{ key: "↑↓", label: "move" },
|
|
1321
|
+
{ key: "Esc", label: "back to picker" },
|
|
1322
|
+
];
|
|
1323
|
+
}
|
|
1324
|
+
return [{ key: "Enter", label: "select" }, { key: "↑↓", label: "move" }, { key: "type", label: "search" }, { key: "Esc", label: "cancel" }];
|
|
1325
|
+
}
|
|
1326
|
+
if (ui.searching) {
|
|
1327
|
+
return [
|
|
1328
|
+
{ key: "type", label: "filter" },
|
|
1329
|
+
{ key: "Enter", label: "apply" },
|
|
1330
|
+
{ key: "↑↓", label: "move" },
|
|
1331
|
+
{ key: "Esc", label: "cancel" },
|
|
1332
|
+
];
|
|
1333
|
+
}
|
|
1334
|
+
const escLabel = ui.filter !== "" ? "clear search" : entry === "presets" ? "presets" : "quit";
|
|
1335
|
+
if (ui.focus === "rail") {
|
|
1336
|
+
return [
|
|
1337
|
+
{ key: "Enter", label: "open" },
|
|
1338
|
+
{ key: "↑↓", label: "move" },
|
|
1339
|
+
{ key: "Space", label: "fleet in/out CLI" },
|
|
1340
|
+
{ key: "→", label: "list" },
|
|
1341
|
+
{ key: "w", label: "review + write" },
|
|
1342
|
+
{ key: "Esc", label: escLabel },
|
|
1343
|
+
];
|
|
1344
|
+
}
|
|
1345
|
+
if (ui.view === "models") {
|
|
1346
|
+
return [
|
|
1347
|
+
{ key: "Enter", label: "assign/classify" },
|
|
1348
|
+
{ key: "Space", label: "fleet in/out" },
|
|
1349
|
+
{ key: "m", label: "presets" },
|
|
1350
|
+
{ key: "w", label: "review + write" },
|
|
1351
|
+
{ key: "←", label: "rail" },
|
|
1352
|
+
{ key: "/", label: "search" },
|
|
1353
|
+
{ key: "Esc", label: escLabel },
|
|
1354
|
+
];
|
|
1355
|
+
}
|
|
1356
|
+
if (ui.view === "shapes") {
|
|
1357
|
+
return [
|
|
1358
|
+
{ key: "Enter", label: "pin from candidates" },
|
|
1359
|
+
{ key: "f", label: "prefer chain" },
|
|
1360
|
+
{ key: "a", label: "auto" },
|
|
1361
|
+
{ key: "w", label: "review + write" },
|
|
1362
|
+
{ key: "←", label: "rail" },
|
|
1363
|
+
{ key: "Esc", label: escLabel },
|
|
1364
|
+
];
|
|
1365
|
+
}
|
|
1366
|
+
return [
|
|
1367
|
+
{ key: "Enter", label: "edit" },
|
|
1368
|
+
{ key: "w", label: "review + write" },
|
|
1369
|
+
{ key: "←", label: "rail" },
|
|
1370
|
+
{ key: "Esc", label: escLabel },
|
|
1371
|
+
];
|
|
1372
|
+
};
|
|
1373
|
+
const details = detailLines();
|
|
1374
|
+
const staged = stagedCount();
|
|
1375
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: frameRows, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: INK.brand, children: " tickmarkr fleet" }), _jsx(Text, { dimColor: true, children: entry === "presets" ? " · init act 3 of 3" : "" }), _jsx(Text, { dimColor: true, children: ` · probe ${formatDoctorAge(ageMs)} · mode ` }), _jsx(Text, { children: ui.selectedMode }), staged > 0 && _jsx(Text, { color: INK.warn, children: ` · ${staged} staged` })] }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderDimColor: true, children: [_jsxs(Box, { width: RAIL_W, flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [rail.slice(0, 3).map((row, index) => railView(row, index)), _jsx(Text, { dimColor: true, children: "─".repeat(RAIL_W - 2) }), rail.slice(3).map((row, index) => railAdapter(row.index, index + 3))] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, paddingTop: 1, borderStyle: "single", borderDimColor: true, borderTop: false, borderBottom: false, borderRight: false, children: listNode })] }), ui.notice
|
|
1376
|
+
? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
|
|
1377
|
+
: details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
|
|
1022
1378
|
}
|
|
1023
1379
|
export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, }) {
|
|
1024
1380
|
const agents = adapters.flatMap((adapter) => {
|
|
@@ -1038,10 +1394,19 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
|
|
|
1038
1394
|
}));
|
|
1039
1395
|
const bridgedInput = inkInput(input, initialInput);
|
|
1040
1396
|
const legacyOutput = typeof output.on !== "function" || typeof output.off !== "function";
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1397
|
+
const realTty = output === process.stdout && output.isTTY === true;
|
|
1398
|
+
const leaveAltScreen = enterAltScreen(output);
|
|
1399
|
+
// OBS-527: the terminal geometry is a LIVE input, not a launch constant — the element is
|
|
1400
|
+
// rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
|
|
1401
|
+
// element type on every rerender ⇒ React keeps the component instance, so staged edits,
|
|
1402
|
+
// cursor, and overlays all survive the resize.
|
|
1403
|
+
const editorElement = () => (_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,
|
|
1404
|
+
// v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
|
|
1405
|
+
// never scroll off-screen (omp's 218-model list did exactly that pre-cap).
|
|
1406
|
+
// OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
|
|
1407
|
+
// elisions, two detail lines, keybar) — the old -16 blanked six list rows at every size.
|
|
1408
|
+
viewRows: Math.max(8, (output.rows ?? 40) - 12), frameColumns: output.columns ?? 100, frameRows: realTty ? output.rows : undefined }));
|
|
1409
|
+
const app = render(editorElement(), {
|
|
1045
1410
|
// FleetIO's injected stream predates Ink and did not require ref/unref.
|
|
1046
1411
|
// Real terminal streams pass through unchanged; the compatibility facade
|
|
1047
1412
|
// adds only those lifecycle methods and leaves input decoding to Ink.
|
|
@@ -1054,13 +1419,19 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
|
|
|
1054
1419
|
// cold-start byte sequence cannot collapse intermediate compatibility frames.
|
|
1055
1420
|
debug: debug || legacyOutput,
|
|
1056
1421
|
});
|
|
1422
|
+
const onResize = () => app.rerender(editorElement());
|
|
1423
|
+
if (!legacyOutput)
|
|
1424
|
+
output.on("resize", onResize);
|
|
1057
1425
|
let result;
|
|
1058
1426
|
try {
|
|
1059
1427
|
result = await app.waitUntilExit();
|
|
1060
1428
|
return result;
|
|
1061
1429
|
}
|
|
1062
1430
|
finally {
|
|
1431
|
+
if (!legacyOutput)
|
|
1432
|
+
output.off("resize", onResize);
|
|
1063
1433
|
app.unmount();
|
|
1064
1434
|
bridgedInput.stop();
|
|
1435
|
+
leaveAltScreen();
|
|
1065
1436
|
}
|
|
1066
1437
|
}
|