tickmarkr 1.90.8 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/dist/adapters/catalog-remote.d.ts +11 -1
- package/dist/adapters/catalog-remote.js +169 -19
- package/dist/adapters/catalog.js +59 -0
- package/dist/adapters/model-lints.d.ts +37 -2
- package/dist/adapters/model-lints.js +180 -36
- package/dist/adapters/registry.js +32 -6
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +13 -0
- package/dist/cli/commands/doctor.d.ts +12 -0
- package/dist/cli/commands/doctor.js +90 -20
- package/dist/cli/commands/fleet-picker.js +1 -1
- package/dist/cli/commands/fleet.d.ts +4 -4
- package/dist/cli/commands/fleet.js +198 -37
- package/dist/cli/commands/init.js +6 -4
- package/dist/compile/native.js +5 -4
- package/dist/config/config.d.ts +34 -1
- package/dist/config/config.js +101 -5
- package/dist/config/fleet-overlay.d.ts +9 -2
- package/dist/config/fleet-overlay.js +159 -14
- package/dist/gates/acceptance.js +13 -4
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +12 -2
- package/dist/route/router.js +31 -2
- package/dist/run/daemon.js +1 -1
- package/dist/tui/ink/components.d.ts +16 -1
- package/dist/tui/ink/components.js +16 -2
- package/dist/tui/ink/fleet-app.d.ts +57 -8
- package/dist/tui/ink/fleet-app.js +1228 -727
- 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,99 +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";
|
|
5
|
+
import { retiredModelReason } from "../../adapters/model-lints.js";
|
|
7
6
|
import { fleetFirstTouchProvenance } from "../../config/fleet-overlay.js";
|
|
8
7
|
import { TIERS } from "../../graph/schema.js";
|
|
9
|
-
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";
|
|
10
10
|
const STEERING_KEYS = ["review", "consult"];
|
|
11
|
-
const escape = String.fromCharCode(27);
|
|
12
11
|
const CHANNELS = ["sub", "api"];
|
|
13
|
-
const inkBookkeepingWrites = new Set(["", `${escape}[?25l`, `${escape}[?25h`, `${escape}[?2026h`, `${escape}[?2026l`]);
|
|
14
|
-
function inkOutput(output) {
|
|
15
|
-
if (typeof output.on === "function" && typeof output.off === "function")
|
|
16
|
-
return output;
|
|
17
|
-
const facade = Object.create(output);
|
|
18
|
-
const write = output.write.bind(output);
|
|
19
|
-
facade.write = ((chunk, ...args) => {
|
|
20
|
-
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
21
|
-
if (inkBookkeepingWrites.has(text))
|
|
22
|
-
return true;
|
|
23
|
-
return Reflect.apply(write, output, [chunk, ...args]);
|
|
24
|
-
});
|
|
25
|
-
facade.on = () => facade;
|
|
26
|
-
facade.off = () => facade;
|
|
27
|
-
return facade;
|
|
28
|
-
}
|
|
29
|
-
function inkInput(input, initialInput) {
|
|
30
|
-
const productionInput = typeof input.ref === "function" && typeof input.unref === "function";
|
|
31
|
-
// Isolate Ink's listeners on a bridge so every editor exit can detach the
|
|
32
|
-
// one listener it owns from the operator's terminal. Older injected FleetIO
|
|
33
|
-
// streams can arrive as decoded keypress events, while real TTYs forward raw
|
|
34
|
-
// data and leave decoding entirely to Ink.
|
|
35
|
-
const stream = new PassThrough();
|
|
36
|
-
stream.isTTY = input.isTTY;
|
|
37
|
-
stream.setRawMode = input.setRawMode?.bind(input);
|
|
38
|
-
stream.ref = () => {
|
|
39
|
-
if (productionInput)
|
|
40
|
-
input.ref();
|
|
41
|
-
return stream;
|
|
42
|
-
};
|
|
43
|
-
stream.unref = () => {
|
|
44
|
-
if (productionInput)
|
|
45
|
-
input.unref();
|
|
46
|
-
return stream;
|
|
47
|
-
};
|
|
48
|
-
const queued = [...initialInput];
|
|
49
|
-
let active = true;
|
|
50
|
-
let scheduled;
|
|
51
|
-
const pump = () => {
|
|
52
|
-
scheduled = undefined;
|
|
53
|
-
if (!active)
|
|
54
|
-
return;
|
|
55
|
-
const next = queued.shift();
|
|
56
|
-
if (next === undefined)
|
|
57
|
-
return;
|
|
58
|
-
stream.write(next);
|
|
59
|
-
scheduled = setTimeout(pump, 0);
|
|
60
|
-
};
|
|
61
|
-
const onData = (chunk) => {
|
|
62
|
-
queued.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
63
|
-
scheduled ??= setTimeout(pump, 0);
|
|
64
|
-
};
|
|
65
|
-
const onKeypress = (sequence, key) => {
|
|
66
|
-
const token = key?.sequence ?? sequence;
|
|
67
|
-
if (token === undefined)
|
|
68
|
-
return;
|
|
69
|
-
queued.push(token);
|
|
70
|
-
scheduled ??= setTimeout(pump, 0);
|
|
71
|
-
};
|
|
72
|
-
if (productionInput) {
|
|
73
|
-
input.on("data", onData);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
emitKeypressEvents(input);
|
|
77
|
-
input.on("keypress", onKeypress);
|
|
78
|
-
}
|
|
79
|
-
input.resume();
|
|
80
|
-
if (queued.length > 0)
|
|
81
|
-
scheduled = setTimeout(pump, 0);
|
|
82
|
-
return {
|
|
83
|
-
stream: stream,
|
|
84
|
-
stop() {
|
|
85
|
-
active = false;
|
|
86
|
-
if (scheduled)
|
|
87
|
-
clearTimeout(scheduled);
|
|
88
|
-
if (productionInput)
|
|
89
|
-
input.off("data", onData);
|
|
90
|
-
else
|
|
91
|
-
input.off("keypress", onKeypress);
|
|
92
|
-
input.pause();
|
|
93
|
-
stream.end();
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
12
|
export function formatDoctorAge(ageMs) {
|
|
98
13
|
if (ageMs === null)
|
|
99
14
|
return "no probe data";
|
|
@@ -108,123 +23,147 @@ function splitSeat(seat) {
|
|
|
108
23
|
const at = seat.indexOf(":");
|
|
109
24
|
return { adapter: seat.slice(0, at), model: seat.slice(at + 1) };
|
|
110
25
|
}
|
|
111
|
-
export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewOverlay, reloadGuard, entry = "probe", initialJudge = "", judgeSeats = [], }) {
|
|
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, }) {
|
|
112
27
|
const { exit } = useApp();
|
|
113
|
-
const [
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const currentModelRows = () => {
|
|
184
|
-
const group = enabledModelGroups()[modelGroupRef.current];
|
|
185
|
-
if (!group)
|
|
186
|
-
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) => {
|
|
187
98
|
const rows = group.rows.map((row) => {
|
|
188
|
-
const staged =
|
|
189
|
-
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
|
+
};
|
|
190
110
|
});
|
|
191
111
|
const known = new Set(rows.map((row) => row.model));
|
|
192
|
-
for (const staged of
|
|
112
|
+
for (const staged of ui.classifications) {
|
|
193
113
|
if (staged.adapter === group.adapter && !known.has(staged.model)) {
|
|
194
|
-
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
|
+
});
|
|
195
121
|
}
|
|
196
122
|
}
|
|
197
123
|
return rows;
|
|
198
124
|
};
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const
|
|
125
|
+
const matches = (label, f) => f === "" || label.toLowerCase().includes(f.toLowerCase());
|
|
126
|
+
// Operator directive 2026-08-13: retired shapes (dated snapshots, previews, non-worker SKUs,
|
|
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
|
+
];
|
|
205
148
|
const judgeKeepRow = initialJudge ? `(keep default) ${initialJudge}` : "(keep default)";
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const next = key.backspace || key.delete
|
|
213
|
-
? filterRef.current.slice(0, -1)
|
|
214
|
-
: (printable ? filterRef.current + input : null);
|
|
215
|
-
if (next === null)
|
|
216
|
-
return false;
|
|
217
|
-
filterRef.current = next;
|
|
218
|
-
setFilter(next);
|
|
219
|
-
const clamped = Math.min(cursorRef.current, Math.max(countFor(next) - 1, 0));
|
|
220
|
-
cursorRef.current = clamped;
|
|
221
|
-
setCursorState(clamped);
|
|
222
|
-
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;
|
|
223
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 ──────────────────────────────────────────────────────
|
|
224
163
|
const editorState = () => ({
|
|
225
|
-
denyAdapters: [...
|
|
226
|
-
denyModels: [...
|
|
227
|
-
classifications:
|
|
164
|
+
denyAdapters: [...ui.deny].sort(),
|
|
165
|
+
denyModels: [...ui.denyModels].sort(),
|
|
166
|
+
classifications: ui.classifications.map((classification) => classification.vendor && classification.channel
|
|
228
167
|
? {
|
|
229
168
|
...classification,
|
|
230
169
|
note: fleetFirstTouchProvenance(classification.note, {
|
|
@@ -233,665 +172,1209 @@ export function FleetApp({ ageMs, agents, initialDenyAdapters, initialDenyModels
|
|
|
233
172
|
}),
|
|
234
173
|
}
|
|
235
174
|
: classification),
|
|
236
|
-
selectedMode:
|
|
237
|
-
map:
|
|
238
|
-
...(
|
|
239
|
-
steering: structuredClone(
|
|
175
|
+
selectedMode: ui.selectedMode,
|
|
176
|
+
map: ui.map,
|
|
177
|
+
...(ui.judgeSeat ? { judgeSeat: splitSeat(ui.judgeSeat) } : {}),
|
|
178
|
+
steering: structuredClone(ui.steering),
|
|
240
179
|
});
|
|
241
180
|
const finish = (result) => {
|
|
242
|
-
if (
|
|
181
|
+
if (ui.done)
|
|
243
182
|
return;
|
|
244
|
-
|
|
183
|
+
ui.done = true;
|
|
245
184
|
exit(result);
|
|
246
185
|
};
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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();
|
|
255
203
|
};
|
|
256
204
|
// The one review funnel: an empty staged overlay finishes as no-changes, a real diff shows the
|
|
257
|
-
// confirm
|
|
205
|
+
// confirm overlay. Reached from `w` and, in preset entry, from a preset pick.
|
|
258
206
|
const goReview = () => {
|
|
259
207
|
const nextReview = reviewOverlay(editorState());
|
|
260
208
|
if (nextReview.kind === "empty") {
|
|
261
209
|
finish({ kind: "no-changes" });
|
|
262
210
|
return;
|
|
263
211
|
}
|
|
264
|
-
|
|
265
|
-
setReview(reviewRef.current);
|
|
266
|
-
setNotice("");
|
|
267
|
-
showScreen("review");
|
|
212
|
+
setOverlay({ kind: "review", review: nextReview, scroll: 0 });
|
|
268
213
|
};
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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();
|
|
227
|
+
};
|
|
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
|
|
234
|
+
// (when the operator keeps that band) the provenance note arrives pre-typed. Free to override.
|
|
235
|
+
const suggestion = group.rows.find((row) => row.model === model)?.suggestion ?? null;
|
|
236
|
+
const tierAt = suggestion ? Math.max(TIERS.indexOf(suggestion.tier), 0) : 0;
|
|
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,
|
|
275
242
|
model,
|
|
276
|
-
|
|
243
|
+
channelAt: 0,
|
|
244
|
+
tierAt,
|
|
245
|
+
note: "",
|
|
246
|
+
suggestion,
|
|
277
247
|
};
|
|
278
248
|
if (!firstTouch) {
|
|
279
|
-
|
|
280
|
-
showScreen("tiers");
|
|
249
|
+
setOverlay({ ...base, stage: "tier" });
|
|
281
250
|
return;
|
|
282
251
|
}
|
|
283
252
|
if (!group.vendor) {
|
|
284
|
-
|
|
253
|
+
ui.notice = `${adapter} has no vendor declaration — classification cannot be saved`;
|
|
254
|
+
bump();
|
|
285
255
|
return;
|
|
286
256
|
}
|
|
287
|
-
|
|
288
|
-
const answered = channelByAdapterRef.current[group.adapter];
|
|
257
|
+
const answered = ui.channelByAdapter[adapter];
|
|
289
258
|
if (answered) {
|
|
290
|
-
|
|
291
|
-
pendingClassificationRef.current = pending;
|
|
292
|
-
showScreen("tiers");
|
|
259
|
+
setOverlay({ ...base, stage: "tier", vendor: group.vendor });
|
|
293
260
|
return;
|
|
294
261
|
}
|
|
295
|
-
|
|
296
|
-
channelCursorRef.current = 0;
|
|
297
|
-
setChannelCursor(0);
|
|
298
|
-
setNotice("");
|
|
299
|
-
showScreen("channel");
|
|
262
|
+
setOverlay({ ...base, stage: "channel", vendor: group.vendor });
|
|
300
263
|
};
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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) {
|
|
328
303
|
finish({ kind: "quit" });
|
|
329
|
-
return;
|
|
304
|
+
return true;
|
|
330
305
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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)
|
|
338
314
|
return;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (key.downArrow || input === "j") {
|
|
342
|
-
const next = Math.min(cursorRef.current + 1, Math.max(agents.length - 1, 0));
|
|
343
|
-
cursorRef.current = next;
|
|
344
|
-
setCursor(next);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (key.upArrow || input === "k") {
|
|
348
|
-
const next = Math.max(cursorRef.current - 1, 0);
|
|
349
|
-
cursorRef.current = next;
|
|
350
|
-
setCursor(next);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (input === " " && agents.length > 0) {
|
|
354
|
-
const next = new Set(denyRef.current);
|
|
355
|
-
const id = agents[cursorRef.current].id;
|
|
356
|
-
if (next.has(id))
|
|
357
|
-
next.delete(id);
|
|
358
|
-
else
|
|
359
|
-
next.add(id);
|
|
360
|
-
denyRef.current = next;
|
|
361
|
-
setDenyAdapters(next);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (key.return) {
|
|
365
|
-
if (enabledModelGroups().length === 0)
|
|
366
|
-
showScreen("modes");
|
|
367
|
-
else
|
|
368
|
-
showScreen("models");
|
|
369
|
-
}
|
|
315
|
+
if (key.ctrl && input === "c") {
|
|
316
|
+
finish({ kind: "quit" });
|
|
370
317
|
return;
|
|
371
318
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (hotkey === "q") {
|
|
372
|
+
guardedQuit(armed);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
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
|
+
}
|
|
379
400
|
return;
|
|
380
401
|
}
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
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();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (key.upArrow || input === "k") {
|
|
417
|
+
overlay.scroll = Math.max(window.scroll - 1, 0);
|
|
418
|
+
bump();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (key.pageDown) {
|
|
422
|
+
overlay.scroll = Math.min(window.scroll + window.cap, window.maxScroll);
|
|
423
|
+
bump();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (key.pageUp) {
|
|
427
|
+
overlay.scroll = Math.max(window.scroll - window.cap, 0);
|
|
428
|
+
bump();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
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 });
|
|
448
|
+
}
|
|
386
449
|
return;
|
|
387
450
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
451
|
+
if (overlay.kind === "classify") {
|
|
452
|
+
if (key.escape) {
|
|
453
|
+
setOverlay(null);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
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
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
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
|
+
}
|
|
399
525
|
return;
|
|
400
526
|
}
|
|
401
|
-
if (
|
|
402
|
-
if (
|
|
403
|
-
|
|
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();
|
|
404
553
|
return;
|
|
405
554
|
}
|
|
406
|
-
|
|
555
|
+
if (input && !key.ctrl && !key.meta) {
|
|
556
|
+
overlay.text += input;
|
|
557
|
+
ui.notice = "";
|
|
558
|
+
bump();
|
|
559
|
+
}
|
|
407
560
|
return;
|
|
408
561
|
}
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
}
|
|
414
585
|
return;
|
|
415
586
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
modelCursorRef.current = 0;
|
|
422
|
-
setModelCursor(0);
|
|
423
|
-
filterRef.current = "";
|
|
424
|
-
setFilter("");
|
|
587
|
+
if (overlay.kind === "candidates") {
|
|
588
|
+
const rows = overlay.rows.filter((candidate) => matches(candidate.label, ui.filter));
|
|
589
|
+
if (key.escape) {
|
|
590
|
+
setOverlay(null);
|
|
591
|
+
return;
|
|
425
592
|
}
|
|
426
|
-
|
|
427
|
-
|
|
593
|
+
if (key.downArrow) {
|
|
594
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
595
|
+
bump();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
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;
|
|
428
627
|
}
|
|
628
|
+
editFilter((f) => overlay.rows.filter((candidate) => matches(candidate.label, f)).length, (max) => {
|
|
629
|
+
overlay.at = Math.min(overlay.at, max);
|
|
630
|
+
});
|
|
429
631
|
return;
|
|
430
632
|
}
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (screenRef.current === "add-model") {
|
|
437
|
-
if (key.return) {
|
|
438
|
-
const group = enabledModelGroups()[modelGroupRef.current];
|
|
439
|
-
const candidate = modelIdRef.current;
|
|
440
|
-
if (!MODEL_ID_RE.test(candidate)) {
|
|
441
|
-
setNotice(`fleet: model id must match ${MODEL_ID_RE.source}`);
|
|
633
|
+
if (overlay.kind === "poolmode") {
|
|
634
|
+
if (key.escape) {
|
|
635
|
+
setOverlay(overlay.picker); // back to the picker — chain and cursor intact
|
|
442
636
|
return;
|
|
443
637
|
}
|
|
444
|
-
if (
|
|
445
|
-
|
|
638
|
+
if (key.downArrow || input === "j") {
|
|
639
|
+
overlay.at = 1;
|
|
640
|
+
bump();
|
|
446
641
|
return;
|
|
447
642
|
}
|
|
448
|
-
if (
|
|
449
|
-
|
|
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
|
+
}
|
|
450
659
|
return;
|
|
451
660
|
}
|
|
452
|
-
if (
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
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();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
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;
|
|
686
|
+
}
|
|
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
|
+
});
|
|
456
709
|
return;
|
|
457
710
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
setNotice("");
|
|
463
|
-
}
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (screenRef.current === "channel") {
|
|
467
|
-
if (key.downArrow || input === "j") {
|
|
468
|
-
const next = Math.min(channelCursorRef.current + 1, CHANNELS.length - 1);
|
|
469
|
-
channelCursorRef.current = next;
|
|
470
|
-
setChannelCursor(next);
|
|
711
|
+
// judge overlay
|
|
712
|
+
const rows = [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, ui.filter));
|
|
713
|
+
if (key.escape) {
|
|
714
|
+
setOverlay(null);
|
|
471
715
|
return;
|
|
472
716
|
}
|
|
473
|
-
if (key.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
setChannelCursor(next);
|
|
717
|
+
if (key.downArrow) {
|
|
718
|
+
overlay.at = Math.min(overlay.at + 1, Math.max(rows.length - 1, 0));
|
|
719
|
+
bump();
|
|
477
720
|
return;
|
|
478
721
|
}
|
|
479
|
-
if (key.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
pendingClassificationRef.current.channel = channel;
|
|
483
|
-
showScreen("tiers");
|
|
484
|
-
}
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (screenRef.current === "tiers") {
|
|
488
|
-
if (key.downArrow || input === "j") {
|
|
489
|
-
const next = Math.min(tierCursorRef.current + 1, TIERS.length - 1);
|
|
490
|
-
tierCursorRef.current = next;
|
|
491
|
-
setTierCursor(next);
|
|
722
|
+
if (key.upArrow) {
|
|
723
|
+
overlay.at = Math.max(overlay.at - 1, 0);
|
|
724
|
+
bump();
|
|
492
725
|
return;
|
|
493
726
|
}
|
|
494
|
-
if (key.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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);
|
|
498
732
|
return;
|
|
499
733
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
setNote("");
|
|
504
|
-
setNotice("");
|
|
505
|
-
showScreen("provenance");
|
|
506
|
-
}
|
|
734
|
+
editFilter((f) => [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, f)).length, (max) => {
|
|
735
|
+
overlay.at = Math.min(overlay.at, max);
|
|
736
|
+
});
|
|
507
737
|
return;
|
|
508
738
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const pending = pendingClassificationRef.current;
|
|
516
|
-
if (pending) {
|
|
517
|
-
const next = [...classificationsRef.current, { ...pending, note: noteRef.current.trim() }];
|
|
518
|
-
classificationsRef.current = next;
|
|
519
|
-
setClassificationRevision((revision) => revision + 1);
|
|
520
|
-
}
|
|
521
|
-
pendingClassificationRef.current = null;
|
|
522
|
-
setNotice("");
|
|
523
|
-
showScreen("models");
|
|
739
|
+
// ── browser (no overlay) ─────────────────────────────────────────────────
|
|
740
|
+
if (ui.searching) {
|
|
741
|
+
if (key.escape) {
|
|
742
|
+
ui.searching = false;
|
|
743
|
+
ui.filter = "";
|
|
744
|
+
bump();
|
|
524
745
|
return;
|
|
525
746
|
}
|
|
526
|
-
if (key.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
setNote(next);
|
|
747
|
+
if (key.return) {
|
|
748
|
+
ui.searching = false;
|
|
749
|
+
bump();
|
|
530
750
|
return;
|
|
531
751
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
setNote(next);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (screenRef.current === "modes") {
|
|
539
|
-
// preset entry adds one final `custom` row after the modes — the full-walk escape hatch
|
|
540
|
-
const rowCount = modeOptions.length + (presetPickRef.current ? 1 : 0);
|
|
541
|
-
const currentModeCursor = modeCursorRef.current ?? 0;
|
|
542
|
-
if (key.downArrow || input === "j") {
|
|
543
|
-
const next = Math.min(currentModeCursor + 1, Math.max(rowCount - 1, 0));
|
|
544
|
-
modeCursorRef.current = next;
|
|
545
|
-
setModeCursor(next);
|
|
752
|
+
if (key.downArrow) {
|
|
753
|
+
ui.listAt = Math.min(ui.listAt + 1, Math.max(listLength() - 1, 0));
|
|
754
|
+
bump();
|
|
546
755
|
return;
|
|
547
756
|
}
|
|
548
|
-
if (key.upArrow
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
setModeCursor(next);
|
|
757
|
+
if (key.upArrow) {
|
|
758
|
+
ui.listAt = Math.max(ui.listAt - 1, 0);
|
|
759
|
+
bump();
|
|
552
760
|
return;
|
|
553
761
|
}
|
|
554
|
-
|
|
555
|
-
if (presetPickRef.current && currentModeCursor === modeOptions.length) {
|
|
556
|
-
presetPickRef.current = false;
|
|
557
|
-
showScreen("agents");
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (!modeOptions[currentModeCursor])
|
|
561
|
-
return;
|
|
562
|
-
selectedModeRef.current = modeOptions[currentModeCursor].id;
|
|
563
|
-
if (presetPickRef.current) {
|
|
564
|
-
// entry context: a preset pick goes straight to the diff confirm
|
|
565
|
-
goReview();
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
showScreen("shapes");
|
|
569
|
-
}
|
|
762
|
+
editFilter((f) => modelRows(f).length);
|
|
570
763
|
return;
|
|
571
764
|
}
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
shapeCursorRef.current = next;
|
|
577
|
-
setShapeCursor(next);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (key.upArrow || input === "k") {
|
|
581
|
-
const next = Math.max(shapeCursorRef.current - 1, 0);
|
|
582
|
-
shapeCursorRef.current = next;
|
|
583
|
-
setShapeCursor(next);
|
|
765
|
+
if (key.escape) {
|
|
766
|
+
if (ui.filter !== "") {
|
|
767
|
+
ui.filter = "";
|
|
768
|
+
bump();
|
|
584
769
|
return;
|
|
585
770
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const nextMap = { ...stagedMap(), [shape]: nextEntry };
|
|
591
|
-
mapRef.current = nextMap;
|
|
592
|
-
setMap(nextMap);
|
|
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 });
|
|
593
775
|
return;
|
|
594
776
|
}
|
|
595
|
-
|
|
596
|
-
candidatesRef.current = candidatesForShape(shape, selectedModeRef.current, stagedMap());
|
|
597
|
-
candidateCursorRef.current = 0;
|
|
598
|
-
setCandidateCursor(0);
|
|
599
|
-
showScreen("candidates");
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (input === "f" && shape) {
|
|
603
|
-
const current = stagedMap()[shape]?.prefer ?? [];
|
|
604
|
-
preferRowsRef.current = preferOptionsForShape(shape, current);
|
|
605
|
-
preferChainRef.current = current.slice();
|
|
606
|
-
preferCursorRef.current = 0;
|
|
607
|
-
setPreferCursor(0);
|
|
608
|
-
showScreen("shape-prefer");
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
if (key.return)
|
|
612
|
-
finishEditor();
|
|
777
|
+
guardedQuit(armed);
|
|
613
778
|
return;
|
|
614
779
|
}
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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();
|
|
627
794
|
return;
|
|
628
795
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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();
|
|
638
809
|
return;
|
|
639
810
|
}
|
|
640
|
-
|
|
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();
|
|
641
825
|
return;
|
|
642
826
|
}
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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();
|
|
648
837
|
return;
|
|
649
838
|
}
|
|
650
|
-
if (key.upArrow ||
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
setPreferCursor(next);
|
|
839
|
+
if (key.upArrow || hotkey === "k") {
|
|
840
|
+
ui.railAt = Math.max(ui.railAt - 1, 0);
|
|
841
|
+
bump();
|
|
654
842
|
return;
|
|
655
843
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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);
|
|
661
857
|
else
|
|
662
|
-
|
|
663
|
-
|
|
858
|
+
next.add(id);
|
|
859
|
+
ui.deny = next;
|
|
860
|
+
clampList();
|
|
861
|
+
bump();
|
|
664
862
|
return;
|
|
665
863
|
}
|
|
666
864
|
if (key.return) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
mapRef.current = nextMap;
|
|
676
|
-
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;
|
|
677
873
|
}
|
|
678
|
-
|
|
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();
|
|
679
889
|
}
|
|
680
890
|
return;
|
|
681
891
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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();
|
|
688
923
|
return;
|
|
689
924
|
}
|
|
690
|
-
if (key.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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 });
|
|
694
937
|
return;
|
|
695
938
|
}
|
|
696
|
-
if (
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
setNotice("");
|
|
701
|
-
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();
|
|
702
943
|
return;
|
|
703
944
|
}
|
|
704
|
-
|
|
705
|
-
const current = steeringRef.current?.[which] ?? [];
|
|
706
|
-
steeringPickerRef.current = which;
|
|
707
|
-
steeringRowsRef.current = steeringOptionsFor(which, current);
|
|
708
|
-
steeringChainRef.current = current.slice();
|
|
709
|
-
steeringPickerCursorRef.current = 0;
|
|
710
|
-
setSteeringPickerCursor(0);
|
|
711
|
-
setNotice("");
|
|
712
|
-
showScreen("steering-prefer");
|
|
945
|
+
beginClassification(row.adapter, row.model);
|
|
713
946
|
return;
|
|
714
947
|
}
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const next = Math.min(judgeCursorRef.current + 1, Math.max(rows.length - 1, 0));
|
|
723
|
-
judgeCursorRef.current = next;
|
|
724
|
-
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: "" });
|
|
725
955
|
return;
|
|
726
956
|
}
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
+
});
|
|
731
1003
|
return;
|
|
732
1004
|
}
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
judgeSeatRef.current = seat;
|
|
738
|
-
setJudgeSeat(seat);
|
|
739
|
-
showScreen("steering");
|
|
1005
|
+
if (hotkey === "a") {
|
|
1006
|
+
ui.showAll = !ui.showAll;
|
|
1007
|
+
clampList();
|
|
1008
|
+
bump();
|
|
740
1009
|
return;
|
|
741
1010
|
}
|
|
742
|
-
filterInput((f) => filteredJudgeRows(f).length, judgeCursorRef, setJudgeCursor, input, key);
|
|
743
1011
|
return;
|
|
744
1012
|
}
|
|
745
|
-
if (
|
|
746
|
-
const rows =
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
steeringPickerCursorRef.current = next;
|
|
750
|
-
setSteeringPickerCursor(next);
|
|
1013
|
+
if (ui.view === "shapes") {
|
|
1014
|
+
const rows = shapeList();
|
|
1015
|
+
const shape = rows[ui.listAt]?.id;
|
|
1016
|
+
if (!shape)
|
|
751
1017
|
return;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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();
|
|
757
1026
|
return;
|
|
758
1027
|
}
|
|
759
|
-
if (
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
});
|
|
767
1040
|
return;
|
|
768
1041
|
}
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
+
});
|
|
1058
|
+
}
|
|
782
1059
|
return;
|
|
783
1060
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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 });
|
|
787
1069
|
return;
|
|
788
1070
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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");
|
|
797
1267
|
}
|
|
798
|
-
finish({ kind: "write", review: reviewRef.current });
|
|
799
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;
|
|
800
1275
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
id: "doctor",
|
|
805
|
-
content: _jsx(Text, { children: `probe data: ${formatDoctorAge(ageMs)} (.tickmarkr/doctor.json)` }),
|
|
806
|
-
}], cursor: 0 }));
|
|
807
|
-
}
|
|
808
|
-
if (screen === "agents") {
|
|
809
|
-
const rows = agents.map((agent) => {
|
|
810
|
-
const active = !denyAdapters.has(agent.id);
|
|
811
|
-
return {
|
|
812
|
-
id: agent.id,
|
|
813
|
-
content: (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: active }), _jsx(Text, { children: ` ${agent.id} ${agent.version} ${agent.authed ? "authed" : "unauthed"}` })] })),
|
|
814
|
-
};
|
|
815
|
-
});
|
|
816
|
-
return (_jsx(FleetListScreen, { title: "step 2/6 \u00B7 agent CLIs", legend: "\u2191\u2193/jk move \u00B7 space toggle \u00B7 enter next \u00B7 esc/q quit", rows: rows, cursor: cursor }));
|
|
817
|
-
}
|
|
818
|
-
const group = enabledModelGroups()[modelGroup];
|
|
819
|
-
if (screen === "models") {
|
|
820
|
-
const rows = filteredModelRows().map((row) => {
|
|
821
|
-
const denied = group ? denyModels.has(`${group.adapter}:${row.model}`) : false;
|
|
822
|
-
return {
|
|
823
|
-
id: row.model,
|
|
824
|
-
content: row.tier ? (_jsxs(_Fragment, { children: [_jsx(ToggleMark, { active: !denied }), _jsx(Text, { children: ` ${row.model} ${row.tier} ${denied ? "denied" : "allowed"}` })] })) : _jsx(Text, { children: `( ) ${row.model} ??? unclassified${row.detectedAt ? ` (${row.detectedAt})` : ""}` }),
|
|
825
|
-
};
|
|
826
|
-
});
|
|
827
|
-
return (_jsx(FleetListScreen, { title: `step 3/6 · models · ${group?.adapter ?? ""}`, legend: "\u2191\u2193 to move \u00B7 Space to toggle \u00B7 t to classify tier \u00B7 n to add model \u00B7 Enter for next \u00B7 Type to search \u00B7 Esc to quit", rows: rows, cursor: modelCursor, details: notice ? [notice] : [], filter: filter }));
|
|
828
|
-
}
|
|
829
|
-
if (screen === "add-model")
|
|
830
|
-
return (_jsx(FleetListScreen, { title: `add model · ${group?.adapter ?? ""}`, legend: "type model id \u00B7 enter classify \u00B7 esc cancel", rows: [{ id: "model", content: _jsx(Text, { children: modelId || "model id (required):" }) }], cursor: 0, details: notice ? [notice] : [] }));
|
|
831
|
-
if (screen === "channel")
|
|
832
|
-
return (_jsx(FleetListScreen, { title: `channel · ${pendingClassificationRef.current?.adapter ?? ""}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: CHANNELS.map((channel) => ({ id: channel, content: _jsx(Text, { children: channel }) })), cursor: channelCursor }));
|
|
833
|
-
if (screen === "tiers") {
|
|
834
|
-
return (_jsx(FleetListScreen, { title: `pick · tier · ${pendingClassificationRef.current?.adapter}:${pendingClassificationRef.current?.model}`, legend: "\u2191\u2193/jk move \u00B7 enter select \u00B7 esc/q quit", rows: TIERS.map((tier) => ({ id: tier, content: _jsx(Text, { children: tier }) })), cursor: tierCursor }));
|
|
835
|
-
}
|
|
836
|
-
if (screen === "provenance")
|
|
837
|
-
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] : [] }));
|
|
838
|
-
if (screen === "modes") {
|
|
839
|
-
const presetPick = presetPickRef.current;
|
|
840
|
-
const rows = modeOptions.map((option) => ({
|
|
841
|
-
id: option.id,
|
|
842
|
-
content: (_jsxs(_Fragment, { children: [option.id === initialMode ? _jsx(ToggleMark, { active: true }) : _jsx(Text, { children: " " }), _jsx(Text, { children: ` ${option.id.padEnd(11)} ${option.gloss}` })] })),
|
|
843
|
-
}));
|
|
844
|
-
if (presetPick) {
|
|
845
|
-
rows.push({
|
|
846
|
-
id: "custom",
|
|
847
|
-
content: (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: ` ${"custom".padEnd(11)} walk the full editor (agents → models → shapes → seats)` })] })),
|
|
848
|
-
});
|
|
1276
|
+
if (ui.view === "shapes") {
|
|
1277
|
+
const row = shapeList()[ui.listAt];
|
|
1278
|
+
return row ? [row.label] : [];
|
|
849
1279
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
{
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
},
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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() })] }));
|
|
895
1378
|
}
|
|
896
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, }) {
|
|
897
1380
|
const agents = adapters.flatMap((adapter) => {
|
|
@@ -911,7 +1394,19 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
|
|
|
911
1394
|
}));
|
|
912
1395
|
const bridgedInput = inkInput(input, initialInput);
|
|
913
1396
|
const legacyOutput = typeof output.on !== "function" || typeof output.off !== "function";
|
|
914
|
-
const
|
|
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(), {
|
|
915
1410
|
// FleetIO's injected stream predates Ink and did not require ref/unref.
|
|
916
1411
|
// Real terminal streams pass through unchanged; the compatibility facade
|
|
917
1412
|
// adds only those lifecycle methods and leaves input decoding to Ink.
|
|
@@ -924,13 +1419,19 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialDenyAd
|
|
|
924
1419
|
// cold-start byte sequence cannot collapse intermediate compatibility frames.
|
|
925
1420
|
debug: debug || legacyOutput,
|
|
926
1421
|
});
|
|
1422
|
+
const onResize = () => app.rerender(editorElement());
|
|
1423
|
+
if (!legacyOutput)
|
|
1424
|
+
output.on("resize", onResize);
|
|
927
1425
|
let result;
|
|
928
1426
|
try {
|
|
929
1427
|
result = await app.waitUntilExit();
|
|
930
1428
|
return result;
|
|
931
1429
|
}
|
|
932
1430
|
finally {
|
|
1431
|
+
if (!legacyOutput)
|
|
1432
|
+
output.off("resize", onResize);
|
|
933
1433
|
app.unmount();
|
|
934
1434
|
bridgedInput.stop();
|
|
1435
|
+
leaveAltScreen();
|
|
935
1436
|
}
|
|
936
1437
|
}
|