@event4u/agent-switch 1.6.1 → 2.0.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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The limit-dialog model (Phase 4) — the PURE, testable core behind the CLI's
3
+ * interactive `agent-switch rebind` picker. It computes the switch-target rows
4
+ * and the pre-selected best-headroom suggestion; it performs NO switch and does
5
+ * NO I/O. The switch itself is always a user interaction (§ Out of scope,
6
+ * road-to-live-rebind.md): this module only shapes what the user is shown and
7
+ * maps their keystroke to a target. The credential write lives in rebind.ts.
8
+ *
9
+ * The suggestion reuses the usage helper `maxUtilization` — the same ranking
10
+ * `pickSwitchTarget` uses (lowest max-utilization = most headroom); no new
11
+ * ranking is invented. Unlike `pickSwitchTarget` it is NOT threshold-gated: the
12
+ * user opened the dialog on demand, so a best-headroom target is always
13
+ * pre-selected as a convenience (the pick, not the suggestion, is the switch).
14
+ */
15
+ import { maxUtilization } from "./usage.js";
16
+ /**
17
+ * Build the dialog model. `runningProfile` is the profile the live session
18
+ * serves; it is excluded from the suggestion (and, marked `isRunning`, from the
19
+ * selectable rows). Pure — takes plain data, returns plain data.
20
+ */
21
+ export function buildRebindDialog(candidates, runningProfile) {
22
+ const rows = candidates.map((c) => ({
23
+ profile: c.name,
24
+ label: c.label ?? null,
25
+ maxUtil: c.snapshot ? maxUtilization(c.snapshot) : null,
26
+ isRunning: c.name === runningProfile,
27
+ }));
28
+ // Suggestion = lowest known max-utilization among the non-running rows (most
29
+ // headroom). Same ranking as pickSwitchTarget's `m < best.max`, reusing the
30
+ // maxUtilization helper — no new ranking invented. Ties keep the first row.
31
+ let best = null;
32
+ for (const r of rows) {
33
+ if (r.isRunning || r.maxUtil === null)
34
+ continue;
35
+ if (best === null || r.maxUtil < best.maxUtil)
36
+ best = r;
37
+ }
38
+ return { rows, suggestedProfile: best ? best.profile : null };
39
+ }
40
+ /** The selectable rows (non-running), in dialog order — the list the CLI
41
+ * numbers 1..N and that {@link resolveRebindChoice} indexes against. */
42
+ export function selectableRows(dialog) {
43
+ return dialog.rows.filter((r) => !r.isRunning);
44
+ }
45
+ /**
46
+ * Map a raw picker keystroke to a target profile or a control action — the pure
47
+ * selection→target mapping the readline loop delegates to. `q`/`c` (any case) =
48
+ * cancel; empty = the pre-selected suggestion (or invalid when none); a 1-based
49
+ * index into the selectable rows = that profile; anything else = invalid. Never
50
+ * switches — it only resolves intent.
51
+ */
52
+ export function resolveRebindChoice(input, dialog) {
53
+ const trimmed = input.trim().toLowerCase();
54
+ if (trimmed === "q" || trimmed === "c")
55
+ return { kind: "cancel" };
56
+ const selectable = selectableRows(dialog);
57
+ if (trimmed === "") {
58
+ return dialog.suggestedProfile ? { kind: "target", profile: dialog.suggestedProfile } : { kind: "invalid" };
59
+ }
60
+ const n = Number(trimmed);
61
+ if (Number.isInteger(n) && n >= 1 && n <= selectable.length) {
62
+ return { kind: "target", profile: selectable[n - 1].profile };
63
+ }
64
+ return { kind: "invalid" };
65
+ }