@h-rig/omp-extension-plugin 0.0.6-alpha.191 → 0.0.6-alpha.192

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.
@@ -21,6 +21,7 @@ import { truncateToWidth } from "@oh-my-pi/pi-tui";
21
21
  import {
22
22
  accent,
23
23
  accentDim,
24
+ bgPanel,
24
25
  bold,
25
26
  cyan,
26
27
  ink,
@@ -28,10 +29,6 @@ import {
28
29
  ink3,
29
30
  ink4,
30
31
  red,
31
- DRONE_WIDTH,
32
- renderDroneFrame,
33
- renderMicroDroneFrame,
34
- RIG_SPINNER_FRAMES,
35
32
  screenGlyph,
36
33
  sectionDivider,
37
34
  statusColor,
@@ -49,6 +46,7 @@ class BoardSettingsList {
49
46
  selected = 0;
50
47
  title = "";
51
48
  emptyMessage = "nothing to show";
49
+ #rowToIndex = new Map;
52
50
  setItems(items) {
53
51
  this.items = [...items];
54
52
  if (!this.selectableAt(this.selected))
@@ -79,44 +77,77 @@ class BoardSettingsList {
79
77
  selectedItem() {
80
78
  return this.selectableAt(this.selected) ? this.items[this.selected] : null;
81
79
  }
80
+ hitTest(line) {
81
+ const index = this.#rowToIndex.get(line);
82
+ return index !== undefined && this.selectableAt(index) ? index : undefined;
83
+ }
84
+ selectIndex(index) {
85
+ if (!this.selectableAt(index))
86
+ return false;
87
+ const moved = this.selected !== index;
88
+ this.selected = index;
89
+ return moved;
90
+ }
82
91
  invalidate() {}
83
92
  render(width) {
84
93
  return this.renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
85
94
  }
86
95
  renderLines(width) {
96
+ this.#rowToIndex.clear();
87
97
  if (this.items.length === 0) {
88
98
  return ["", ` ${ink2(this.emptyMessage)}`, ""];
89
99
  }
90
- const maxVisible = Math.max(8, Math.min(16, (process.stdout.rows ?? 30) - 10));
100
+ const maxVisible = Math.max(8, Math.min(18, (process.stdout.rows ?? 30) - 10));
91
101
  const start = Math.max(0, Math.min(this.selected - Math.floor(maxVisible / 2), this.items.length - maxVisible));
92
102
  const window = this.items.slice(start, start + maxVisible);
93
- const valueWidth = Math.min(24, Math.max(10, Math.floor(width * 0.24)));
94
- const labelWidth = Math.min(24, Math.max(10, Math.floor(width * 0.26)));
95
- const lines = window.flatMap((item, index) => {
103
+ const labelWidth = Math.min(28, Math.max(10, ...window.map((item) => {
104
+ const navGlyph = item.id.startsWith("to:") ? 2 : 0;
105
+ return item.label.length + navGlyph;
106
+ })));
107
+ const actionWidth = Math.max(0, ...window.map((item) => item.values?.[0] && item.values[0] !== item.currentValue ? item.values[0].length + 2 : 0));
108
+ const valueWidth = Math.max(12, width - labelWidth - actionWidth - 8);
109
+ const lines = [""];
110
+ for (let index = 0;index < window.length; index += 1) {
111
+ const item = window[index];
96
112
  const absolute = start + index;
97
113
  const isSelected = absolute === this.selected;
98
- const marker = isSelected ? accent("\u258C") : " ";
114
+ if (item.id === "title") {
115
+ lines.push(` ${accent("\u258D")}${bold(ink(item.label))}${item.currentValue ? ` ${ink3(item.currentValue)}` : ""}`);
116
+ if (item.description)
117
+ lines.push(` ${ink4(truncateToWidth(item.description, Math.max(12, width - 4)))}`);
118
+ lines.push("");
119
+ continue;
120
+ }
121
+ if (item.heading && !item.currentValue) {
122
+ lines.push(sectionDivider(item.label.toLowerCase(), width));
123
+ continue;
124
+ }
99
125
  const navGlyph = item.id.startsWith("to:") ? `${screenGlyph(item.id.slice(3))} ` : "";
100
- const labelText = truncateToWidth(`${navGlyph}${item.label}`, labelWidth).padEnd(labelWidth);
101
- const isTitle = item.id === "title";
126
+ const labelPlain = truncateToWidth(`${navGlyph}${item.label}`, labelWidth).padEnd(labelWidth);
102
127
  const isStatus = item.id.endsWith(":status");
103
- const label = isTitle ? accent(labelText) : isSelected ? ink(labelText) : item.heading ? ink3(labelText) : ink2(labelText);
104
- const value = !item.currentValue ? ink4("".padEnd(valueWidth)) : isStatus ? statusColor(item.currentValue)(truncateToWidth(`${statusGlyph(item.currentValue)} ${item.currentValue}`, valueWidth).padEnd(valueWidth)) : item.heading ? ink2(truncateToWidth(item.currentValue, valueWidth).padEnd(valueWidth)) : ink3(truncateToWidth(item.currentValue, valueWidth).padEnd(valueWidth));
105
- const action = item.values?.[0] && item.values[0] !== item.currentValue ? isSelected ? accent(item.values[0]) : accentDim(item.values[0]) : "";
106
- const row = `${marker} ${label} ${value}${action ? ` ${action}` : ""}`;
128
+ const valuePlain = truncateToWidth(item.currentValue ?? "", valueWidth).padEnd(valueWidth);
129
+ const label = isSelected ? ink(labelPlain) : item.heading ? ink3(labelPlain) : ink2(labelPlain);
130
+ const value = !item.currentValue ? ink4(valuePlain) : isStatus ? statusColor(item.currentValue)(truncateToWidth(`${statusGlyph(item.currentValue)} ${item.currentValue}`, valueWidth).padEnd(valueWidth)) : item.heading ? ink2(valuePlain) : ink3(valuePlain);
131
+ const actionText = item.values?.[0] && item.values[0] !== item.currentValue ? item.values[0] : "";
132
+ const action = actionText ? isSelected ? accent(`${actionText} \u203A`) : accentDim(actionText) : "";
133
+ const rail = isSelected ? accent("\u258C") : " ";
134
+ const body = `${rail} ${label} ${value}${action ? ` ${action}` : ""}`;
135
+ if (!item.heading)
136
+ this.#rowToIndex.set(lines.length, absolute);
137
+ lines.push(isSelected ? bgPanel(body) : body);
107
138
  if (isSelected && item.description) {
108
- return [row, ` ${ink4(truncateToWidth(item.description, Math.max(12, width - 5)))}`];
139
+ if (!item.heading)
140
+ this.#rowToIndex.set(lines.length, absolute);
141
+ lines.push(` ${ink4("\u21B3")} ${ink4(truncateToWidth(item.description, Math.max(12, width - 6)))}`);
109
142
  }
110
- return [row];
111
- });
143
+ }
112
144
  const meta = [];
113
145
  if (this.items.length > maxVisible)
114
- meta.push(`${this.selected + 1}/${this.items.length}`);
115
- if (this.title)
116
- meta.push(this.title);
146
+ meta.push(`${this.selected + 1} of ${this.items.length}`);
117
147
  if (meta.length > 0)
118
- lines.push(ink4(` ${meta.join(" \xB7 ")}`));
119
- return ["", ...lines, ""];
148
+ lines.push("", ink4(` ${meta.join(" \xB7 ")}`));
149
+ lines.push("");
150
+ return lines;
120
151
  }
121
152
  }
122
153
 
@@ -200,6 +231,7 @@ class DetailComposer {
200
231
  selected = 0;
201
232
  tick = 0;
202
233
  bodyScroll = 0;
234
+ #lineToAction = new Map;
203
235
  setModel(model) {
204
236
  const prevId = this.enabledActions()[this.selected]?.id;
205
237
  this.model = model;
@@ -224,6 +256,17 @@ class DetailComposer {
224
256
  selectedAction() {
225
257
  return this.enabledActions()[this.selected] ?? null;
226
258
  }
259
+ hitTestAction(line) {
260
+ return this.#lineToAction.get(line);
261
+ }
262
+ selectAction(index) {
263
+ const count = this.enabledActions().length;
264
+ if (index < 0 || index >= count)
265
+ return false;
266
+ const moved = this.selected !== index;
267
+ this.selected = index;
268
+ return moved;
269
+ }
227
270
  scrollBody(delta) {
228
271
  this.bodyScroll = Math.max(0, this.bodyScroll + delta);
229
272
  }
@@ -253,21 +296,45 @@ class DetailComposer {
253
296
  }
254
297
  if (model.stages && model.stages.length > 0) {
255
298
  bottom.push(sectionDivider("pipeline", width));
256
- const parts = model.stages.map((stage) => `${statusColor(stage.status)(statusGlyph(stage.status))} ${ink3(stage.label)}`);
257
- bottom.push(truncateToWidth(` ${parts.join(ink4(" \u2192 "))}`, inner));
299
+ let linePlain = " ";
300
+ let linePainted = " ";
301
+ for (let i = 0;i < model.stages.length; i += 1) {
302
+ const stage = model.stages[i];
303
+ const sepPlain = linePlain.trim().length > 0 ? " \u2192 " : " ";
304
+ const cellPlain = `${sepPlain}? ${stage.label}`;
305
+ if (linePlain.length + cellPlain.length > inner - 2 && linePlain.trim().length > 0) {
306
+ bottom.push(linePainted);
307
+ linePlain = " ";
308
+ linePainted = " ";
309
+ }
310
+ const sep = linePlain.trim().length > 0 ? ink4(" \u2192 ") : " ";
311
+ linePlain += cellPlain;
312
+ linePainted += `${sep}${statusColor(stage.status)(statusGlyph(stage.status))} ${ink3(stage.label)}`;
313
+ }
314
+ if (linePainted.trim().length > 0)
315
+ bottom.push(linePainted);
258
316
  bottom.push("");
259
317
  }
318
+ const actionLines = [];
260
319
  if (model.actions.length > 0) {
261
320
  bottom.push(sectionDivider("actions", width));
262
321
  const selId = this.selectedAction()?.id;
322
+ let enabledIndex = -1;
263
323
  for (const action of model.actions) {
264
324
  const on = action.enabled !== false;
325
+ if (on)
326
+ enabledIndex += 1;
265
327
  const isSel = on && action.id === selId;
266
328
  const rail = isSel ? accent("\u258C") : " ";
267
329
  const labelText = `${action.glyph ? `${action.glyph} ` : ""}${action.label}`;
268
330
  const label = !on ? ink4(labelText) : action.danger ? red(labelText) : isSel ? accent(labelText) : ink2(labelText);
269
331
  const tail = !on ? ink4(action.value ?? "unavailable") : isSel ? ink3(action.hint ?? action.value ?? "") : ink3(action.value ?? "");
270
- bottom.push(`${rail} ${label}${tail ? ` ${tail}` : ""}`);
332
+ const row = `${rail} ${label}${tail ? ` ${tail}` : ""}`;
333
+ if (on)
334
+ actionLines.push(enabledIndex);
335
+ else
336
+ actionLines.push(-1);
337
+ bottom.push(isSel ? bgPanel(row) : row);
271
338
  }
272
339
  }
273
340
  const body = [];
@@ -292,10 +359,25 @@ class DetailComposer {
292
359
  }
293
360
  }
294
361
  const out = [...top, ...body, ...bottom];
362
+ const mapActions = (linesOut) => {
363
+ this.#lineToAction.clear();
364
+ if (actionLines.length === 0)
365
+ return;
366
+ const actionCount = model.actions.length;
367
+ const firstActionLine = linesOut.length - actionCount;
368
+ for (let i = 0;i < actionCount; i += 1) {
369
+ const enabledIdx = actionLines[i];
370
+ if (enabledIdx >= 0)
371
+ this.#lineToAction.set(firstActionLine + i, enabledIdx);
372
+ }
373
+ };
295
374
  if (out.length > budget) {
296
375
  const keepTail = Math.min(bottom.length, budget - top.length);
297
- return [...top, ...out.slice(out.length - keepTail)].map((line) => truncateToWidth(line, Math.max(10, width - 1)));
376
+ const clipped = [...top, ...out.slice(out.length - keepTail)];
377
+ mapActions(clipped);
378
+ return clipped.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
298
379
  }
380
+ mapActions(out);
299
381
  return out.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
300
382
  }
301
383
  }
@@ -551,9 +633,6 @@ function findScreen(catalog, screen) {
551
633
  function isRigScreen(value, catalog = DEFAULT_SCREEN_CATALOG) {
552
634
  return typeof value === "string" && catalog.some((screen) => screen.id === value);
553
635
  }
554
- function screenTitle(screen, catalog = DEFAULT_SCREEN_CATALOG) {
555
- return findScreen(catalog, screen)?.title ?? screen;
556
- }
557
636
  function screenLabel(screen, catalog = DEFAULT_SCREEN_CATALOG) {
558
637
  const descriptor = findScreen(catalog, screen);
559
638
  return descriptor?.label ?? descriptor?.id ?? screen;
@@ -567,8 +646,8 @@ function cockpitNavScreens(catalog = DEFAULT_SCREEN_CATALOG) {
567
646
  var DEFAULT_SCREEN_CATALOG;
568
647
  var init_screen_catalog = __esm(() => {
569
648
  DEFAULT_SCREEN_CATALOG = [
570
- { id: "cockpit", title: "Project Cockpit", shortcut: { key: "g", description: "Rig cockpit" } },
571
- { id: "task-detail", title: "Task Detail" },
649
+ { id: "cockpit", title: "Cockpit", label: "cockpit", shortcut: { key: "g", description: "Rig cockpit" } },
650
+ { id: "task-detail", title: "Task Detail", label: "task detail" },
572
651
  { id: "run-detail", title: "Run Detail", label: "run detail" }
573
652
  ];
574
653
  });
@@ -653,9 +732,9 @@ import { DOCTOR } from "@rig/contracts";
653
732
  import { RIG_COCKPIT_PANEL_SLOT } from "@rig/contracts";
654
733
  import { defineCapability } from "@rig/core/capability";
655
734
  import { loadCapabilityForRoot, requireCapabilityForRoot } from "@rig/core/capability-loaders";
656
- import { isKeyRelease, matchesKey } from "@oh-my-pi/pi-tui";
735
+ import { isKeyRelease, matchesKey, parseSgrMouse } from "@oh-my-pi/pi-tui";
657
736
  import { Duration, Effect, Fiber, Stream } from "effect";
658
- import { accent as accent2, accentDim as accentDim2, bold as bold2, cyan as cyan2, ink as ink5, ink2 as ink22, ink3 as ink32, ink4 as ink42, red as red2, RIG_SPINNER_FRAMES as RIG_SPINNER_FRAMES2, screenGlyph as screenGlyph2 } from "@rig/std-shared/board-theme";
737
+ import { accent as accent2, accentDim as accentDim2, bgPanel as bgPanel2, bold as bold2, cyan as cyan2, ink as ink5, ink2 as ink22, ink3 as ink32, ink4 as ink42, keyHints, red as red2, RIG_SPINNER_FRAMES, screenGlyph as screenGlyph2, yellow as yellow2 } from "@rig/std-shared/board-theme";
659
738
  import { resolvePluginHost } from "@rig/core/project-plugins";
660
739
  import { rigProjectRootFromEnv as rigProjectRoot } from "@rig/core/root-resolver";
661
740
  function isProjectedRunClassification(value) {
@@ -1328,17 +1407,6 @@ async function runCollabDoctor(ctx) {
1328
1407
  { label: "join-links", level: runs.some((run) => run.joinLink) ? "ok" : "warn", detail: `${runs.filter((run) => run.joinLink).length} joinable run(s)` }
1329
1408
  ]);
1330
1409
  }
1331
- function sessionDescription(run) {
1332
- const id = run.runId.slice(0, 8);
1333
- const cwd = run.collabCwd || run.worktreePath || run.sessionPath || "(unknown cwd)";
1334
- const link = collabLinks(run);
1335
- return link ? `${id} \xB7 ${cwd} \xB7 ${link}` : `${id} \xB7 ${cwd}`;
1336
- }
1337
- function collabLinks(run) {
1338
- const join = run.joinLink ? `joinLink ${run.joinLink}` : "";
1339
- const web = run.webLink ? `webLink ${run.webLink}` : "";
1340
- return join && web ? `${join} \xB7 ${web}` : join || web || "";
1341
- }
1342
1410
  function findRunById(runs, runId) {
1343
1411
  if (!runId)
1344
1412
  return null;
@@ -1366,15 +1434,30 @@ function runListActionHints(run, source) {
1366
1434
  function serverLabel(server) {
1367
1435
  return server?.alias?.trim() || "unselected";
1368
1436
  }
1369
- function nextStepHint(server, runs, state) {
1437
+ function nextStep(server, runs, state) {
1370
1438
  if (!server)
1371
- return "next: Server";
1372
- if (runs.some((run) => {
1439
+ return { screen: "server", label: "choose where runs execute", detail: "no target selected \u2014 open Server" };
1440
+ const live = liveRunCount(runs, state);
1441
+ if (live > 0)
1442
+ return { screen: "runs", label: `watch the fleet \u2014 ${live} live`, detail: "open Runs for live status, steer, and gates" };
1443
+ return { screen: "tasks", label: "pick a task and dispatch", detail: `read ${server.taskSource ?? "the task source"} and send a drone at one` };
1444
+ }
1445
+ function liveRunCount(runs, state) {
1446
+ let count = 0;
1447
+ for (const run of runs) {
1373
1448
  const classification = state.runClassifications?.get(run.runId);
1374
- return classification ? classification.isActive && !classification.isTerminal : false;
1375
- }))
1376
- return "next: Runs";
1377
- return "next: Tasks";
1449
+ if (classification && classification.isActive && !classification.isTerminal)
1450
+ count += 1;
1451
+ }
1452
+ return count;
1453
+ }
1454
+ function attentionRunCount(runs, state) {
1455
+ let count = 0;
1456
+ for (const run of runs) {
1457
+ if (state.runClassifications?.get(run.runId)?.isNeedsAttention)
1458
+ count += 1;
1459
+ }
1460
+ return count;
1378
1461
  }
1379
1462
  function taskDetailModel(state, server, projection) {
1380
1463
  const task = state.selectedTask ?? null;
@@ -1432,8 +1515,8 @@ function runDetailModel(run, source) {
1432
1515
  ref: `RUN ${shortId}`,
1433
1516
  title: run.title || `Run ${shortId}`,
1434
1517
  status,
1435
- body: run.errorSummary || sessionDescription(run),
1436
- bodyLabel: "summary",
1518
+ body: run.errorSummary || (classification.isTerminal ? `This run is ${status}. The facts below are its record; recovery actions appear when the read-model offers them.` : `Live ${run.source === "remote" ? "remote" : "local"} run. JOIN attaches this terminal to its OMP session; STEER drops a message into the agent's queue; gates resolve right here.`),
1519
+ bodyLabel: run.errorSummary ? "error" : "run",
1437
1520
  facts: [
1438
1521
  ...run.taskId ? [{ label: "task", value: run.taskId }] : [],
1439
1522
  { label: "cwd", value: run.collabCwd || run.worktreePath || run.sessionPath || "(unknown cwd)" },
@@ -1465,11 +1548,20 @@ function runDetailModel(run, source) {
1465
1548
  function cockpitNavValue(screen, server, runs, state) {
1466
1549
  switch (screen) {
1467
1550
  case "server":
1468
- return serverLabel(server);
1551
+ return server ? `${serverLabel(server)} \xB7 ${server.kind === "remote" ? "remote" : "this checkout"}` : "unselected";
1469
1552
  case "tasks":
1470
1553
  return server?.taskSource ?? "configured source";
1471
- case "runs":
1472
- return String(runs.length);
1554
+ case "runs": {
1555
+ const live = liveRunCount(runs, state);
1556
+ const attention = attentionRunCount(runs, state);
1557
+ if (runs.length === 0)
1558
+ return "none yet";
1559
+ const parts = [`${live} live`];
1560
+ if (attention > 0)
1561
+ parts.push(`${attention} waiting on you`);
1562
+ parts.push(`${runs.length} total`);
1563
+ return parts.join(" \xB7 ");
1564
+ }
1473
1565
  default:
1474
1566
  return "";
1475
1567
  }
@@ -1479,36 +1571,38 @@ function cockpitNavRows(server, runs, state, catalog) {
1479
1571
  id: `to:${screen}`,
1480
1572
  label: nav.label,
1481
1573
  currentValue: cockpitNavValue(screen, server, runs, state),
1482
- values: ["\u203A"],
1574
+ values: ["open"],
1483
1575
  description: nav.description
1484
1576
  }));
1485
1577
  }
1486
- function itemsFor(screen, runs, actionRowsEnabled, _selectedRunId, state = {}, catalog = DEFAULT_SCREEN_CATALOG, projection = defaultCockpitCapabilities.taskProjection) {
1578
+ function itemsFor(screen, runs, actionRowsEnabled, _selectedRunId, state = {}, catalog = DEFAULT_SCREEN_CATALOG, _projection = defaultCockpitCapabilities.taskProjection) {
1487
1579
  const server = state.server ?? null;
1488
- const projectRoot = state.projectRoot ?? rigProjectRoot();
1489
1580
  const loadError = state.error?.trim();
1490
1581
  if (screen === "task-detail" || screen === "run-detail")
1491
1582
  return [];
1492
1583
  if (loadError) {
1493
1584
  return [
1494
- { id: "title", label: screenTitle(screen, catalog), currentValue: "load failed", heading: true },
1495
- { id: `${screen}:error`, label: "LOAD FAILED", currentValue: "error", heading: true, description: loadError }
1585
+ { id: "title", label: screenLabel(screen, catalog), currentValue: "couldn't load", heading: true, description: loadError },
1586
+ { id: `${screen}:retry`, label: "RETRY", currentValue: "reload this screen", values: ["retry"], description: "press enter (or r) to reload \u2014 esc returns to the cockpit" }
1496
1587
  ];
1497
1588
  }
1498
1589
  switch (screen) {
1499
- case "cockpit":
1590
+ case "cockpit": {
1591
+ const step = nextStep(server, runs, state);
1500
1592
  return [
1501
- { id: "title", label: screenTitle("cockpit", catalog), currentValue: "", heading: true },
1502
- { id: "next", label: "NEXT", currentValue: nextStepHint(server, runs, state), heading: true, description: "guided path: Server \u2192 Tasks \u2192 Detail \u2192 Dispatch \u2192 Run Detail" },
1593
+ { id: "title", label: "cockpit", currentValue: "operate the drones", heading: true, description: "the loop: choose a target \xB7 dispatch tasks \xB7 watch runs land" },
1594
+ { id: `to:${step.screen}`, label: "NEXT", currentValue: step.label, values: ["go"], description: step.detail },
1595
+ { id: "sec:screens", label: "SCREENS", currentValue: "", heading: true },
1503
1596
  ...cockpitNavRows(server, runs, state, catalog)
1504
1597
  ];
1598
+ }
1505
1599
  default: {
1506
1600
  const producedRows = state.panelRows ?? [];
1507
1601
  if (producedRows.length > 0)
1508
1602
  return [...producedRows];
1509
1603
  return [
1510
- { id: "title", label: screenTitle(screen, catalog), currentValue: "unavailable", heading: true },
1511
- { id: `${screen}:unavailable`, label: "UNAVAILABLE", currentValue: "reload", values: ["reload"], heading: true, description: "This screen's content could not be loaded right now \u2014 press r to reload." }
1604
+ { id: "title", label: screenLabel(screen, catalog), currentValue: "nothing here yet", heading: true, description: "this screen's owner produced no content" },
1605
+ { id: `${screen}:retry`, label: "RETRY", currentValue: "reload this screen", values: ["retry"], description: "press enter (or r) to reload \u2014 esc returns to the cockpit" }
1512
1606
  ];
1513
1607
  }
1514
1608
  }
@@ -1545,6 +1639,7 @@ class RigFlowComponent {
1545
1639
  #refreshing = false;
1546
1640
  #busyDepth = 0;
1547
1641
  #animationTimer;
1642
+ #frame = { tabsLine: -1, viewStart: -1, viewEnd: -1, tabs: [] };
1548
1643
  constructor(tui, api, done, onQuit, screen, runs, state, refreshRuns, refreshState, actions, actionRowsEnabled, catalog = DEFAULT_SCREEN_CATALOG, caps = defaultCockpitCapabilities, helpCatalog) {
1549
1644
  this.tui = tui;
1550
1645
  this.api = api;
@@ -1658,10 +1753,13 @@ class RigFlowComponent {
1658
1753
  this.#genericView.setItems(itemsFor(genericScreen, this.#runs, this.actionRowsEnabled, this.#selectedRunId, this.#state, this.catalog, this.caps.taskProjection));
1659
1754
  }
1660
1755
  if (this.#view === "task-detail") {
1661
- this.#detailComposer.setModel(this.#state.error ? { screen: "task-detail", icon: screenGlyph2("task-detail"), ref: "TASK", title: "Task failed to load", body: this.#state.error, bodyLabel: "error", facts: [{ label: "hint", value: "Refresh or return to Tasks." }], actions: [] } : taskDetailModel(this.#state, this.#state.server ?? null, this.caps.taskProjection));
1756
+ const loadedId = this.caps.taskProjection.id(this.#state.selectedTask ?? null);
1757
+ const pendingId = this.#selectedTaskId;
1758
+ const stillLoading = Boolean(pendingId) && loadedId !== pendingId;
1759
+ this.#detailComposer.setModel(this.#state.error ? { screen: "task-detail", icon: screenGlyph2("task-detail"), ref: "TASK", title: "Task failed to load", body: this.#state.error, bodyLabel: "error", facts: [{ label: "hint", value: "r reloads \xB7 \u2190 returns to Tasks" }], actions: [] } : stillLoading ? { screen: "task-detail", icon: screenGlyph2("task-detail"), ref: `TASK ${pendingId}`, title: `Fetching task ${pendingId}\u2026`, body: "Reading the full task from the task source.", bodyLabel: "loading", facts: [], actions: [] } : taskDetailModel(this.#state, this.#state.server ?? null, this.caps.taskProjection));
1662
1760
  } else if (this.#view === "run-detail") {
1663
1761
  const run = findRunById(this.#runs, this.#selectedRunId);
1664
- this.#detailComposer.setModel(this.#state.error ? { screen: "run-detail", icon: screenGlyph2("run-detail"), ref: "RUN", title: "Run failed to load", body: this.#state.error, bodyLabel: "error", facts: [{ label: "hint", value: "Refresh or return to Runs." }], actions: [] } : run ? runDetailModel(run, this.#state) : { screen: "run-detail", icon: screenGlyph2("run-detail"), ref: "RUN", title: "Run unavailable", facts: [{ label: "hint", value: "The selected run is no longer discoverable; back to Runs." }], actions: [] });
1762
+ this.#detailComposer.setModel(this.#state.error ? { screen: "run-detail", icon: screenGlyph2("run-detail"), ref: "RUN", title: "Run failed to load", body: this.#state.error, bodyLabel: "error", facts: [{ label: "hint", value: "r reloads \xB7 \u2190 returns to Runs" }], actions: [] } : run ? runDetailModel(run, this.#state) : { screen: "run-detail", icon: screenGlyph2("run-detail"), ref: "RUN", title: "Run unavailable", facts: [{ label: "hint", value: "The selected run is no longer discoverable; \u2190 returns to Runs." }], actions: [] });
1665
1763
  }
1666
1764
  for (const view of this.#domainViews.values())
1667
1765
  view.setTick(this.#tick);
@@ -1680,14 +1778,16 @@ class RigFlowComponent {
1680
1778
  #setView(next) {
1681
1779
  this.#view = next;
1682
1780
  this.#notice = null;
1781
+ this.#error = "";
1683
1782
  this.#feedViews();
1684
1783
  this.tui.requestRender();
1685
1784
  }
1686
1785
  async#navigate(next, push = true) {
1687
- if (push && this.#view !== "help")
1786
+ if (push && this.#view !== "help" && this.#view !== next)
1688
1787
  this.#screenStack.push(this.#view);
1689
1788
  this.#view = next;
1690
1789
  this.#notice = null;
1790
+ this.#error = "";
1691
1791
  const endBusy = this.#beginBusy();
1692
1792
  try {
1693
1793
  await this.#refreshCurrent();
@@ -1710,10 +1810,9 @@ class RigFlowComponent {
1710
1810
  if (this.#view !== "help") {
1711
1811
  const loadedState = await this.refreshState(this.#view, this.#selectedTaskId, this.#runs);
1712
1812
  this.#state = runLoadError ? { ...loadedState, error: loadedState.error ? `${runLoadError}; ${loadedState.error}` : runLoadError } : loadedState;
1713
- if (this.#state.error)
1714
- this.#error = this.#state.error;
1715
- } else if (runLoadError) {
1716
- this.#error = runLoadError;
1813
+ this.#error = this.#state.error ?? "";
1814
+ } else {
1815
+ this.#error = runLoadError ?? "";
1717
1816
  }
1718
1817
  this.#feedViews();
1719
1818
  this.tui.requestRender();
@@ -1737,45 +1836,105 @@ class RigFlowComponent {
1737
1836
  endBusy();
1738
1837
  }
1739
1838
  }
1740
- #headerLine() {
1741
- const crumb = this.#view === "help" ? ` ${ink42("\u203A")} ${ink22("help")}` : ` ${ink42("\u203A")} ${ink22(screenLabel(this.#view, this.catalog))}`;
1742
- return ` ${accent2("\u258D")}${bold2(ink5("rig"))} ${ink32("\u2014 operate the drones")}${crumb} ${ink42("\xB7")} ${cyan2(this.#serverLabel)}`;
1839
+ #tabScreens() {
1840
+ return [
1841
+ { screen: "cockpit", label: "cockpit" },
1842
+ ...cockpitNavScreens(this.catalog).map(({ screen, nav }) => ({ screen, label: nav.label.toLowerCase() }))
1843
+ ];
1844
+ }
1845
+ #tabFor(view) {
1846
+ if (view === "run-detail")
1847
+ return "runs";
1848
+ if (view === "task-detail")
1849
+ return "tasks";
1850
+ if (view === "help")
1851
+ return "cockpit";
1852
+ return view;
1853
+ }
1854
+ #headerLine(width) {
1855
+ const screenName = this.#view === "help" ? "help" : screenLabel(this.#view, this.catalog).toLowerCase();
1856
+ const project = (this.#state.projectRoot ?? rigProjectRoot()).split("/").filter(Boolean).pop() ?? "";
1857
+ const live = liveRunCount(this.#runs, this.#state);
1858
+ const attention = attentionRunCount(this.#runs, this.#state);
1859
+ const dotPaint = attention > 0 ? yellow2 : live > 0 ? accent2 : ink42;
1860
+ const rightPlain = `\u25CF ${this.#serverLabel}${project ? ` \xB7 ${project}` : ""}`;
1861
+ const rightPainted = `${dotPaint("\u25CF")} ${cyan2(this.#serverLabel)}${project ? ` ${ink42("\xB7")} ${ink32(project)}` : ""}`;
1862
+ const leftPlain = ` \u258Drig ${screenName}`;
1863
+ const pad = Math.max(1, width - leftPlain.length - rightPlain.length - 1);
1864
+ return ` ${accent2("\u258D")}${bold2(ink5("rig"))} ${ink22(screenName)}${" ".repeat(pad)}${rightPainted}`;
1865
+ }
1866
+ #tabsLine(width) {
1867
+ const tabs = this.#tabScreens();
1868
+ const active = this.#tabFor(this.#view);
1869
+ this.#frame.tabs = [];
1870
+ let out = " ";
1871
+ let plainLength = 1;
1872
+ for (let index = 0;index < tabs.length; index += 1) {
1873
+ const tab = tabs[index];
1874
+ const text = ` ${index + 1} ${tab.label} `;
1875
+ this.#frame.tabs.push({ screen: tab.screen, start: plainLength, end: plainLength + text.length - 1 });
1876
+ out += tab.screen === active ? bgPanel2(`${accent2(` ${index + 1}`)} ${bold2(ink5(`${tab.label} `))}`) : `${ink42(` ${index + 1}`)} ${ink32(`${tab.label} `)}`;
1877
+ plainLength += text.length;
1878
+ out += " ";
1879
+ plainLength += 1;
1880
+ }
1881
+ const helpPlain = "? help";
1882
+ const pad = Math.max(1, width - plainLength - helpPlain.length - 1);
1883
+ return `${out}${" ".repeat(pad)}${accent2("?")}${ink32(" help")}`;
1743
1884
  }
1744
1885
  #noticeText() {
1745
1886
  if (!this.#notice)
1746
1887
  return null;
1747
- const prefix = this.#busyDepth > 0 ? `${RIG_SPINNER_FRAMES2[this.#tick % RIG_SPINNER_FRAMES2.length]} ` : "";
1888
+ const prefix = this.#busyDepth > 0 ? `${RIG_SPINNER_FRAMES[this.#tick % RIG_SPINNER_FRAMES.length]} ` : "";
1748
1889
  return `${prefix}${this.#notice}`;
1749
1890
  }
1750
- #footerLines() {
1891
+ #footerLines(width) {
1751
1892
  if (this.#inputLine) {
1752
1893
  const line = this.#inputLine;
1753
1894
  const label = line.kind === "search" ? `${accent2("/")}${ink5(line.buffer)}` : `${accentDim2("steer")} ${ink32(`(${line.run.runId.slice(0, 8)})`)} ${accent2("\u276F")} ${ink5(line.buffer)}`;
1754
1895
  return [
1755
1896
  ` ${label}${accent2("\u2588")}`,
1756
- ` ${accent2("enter")}${ink32(line.kind === "search" ? " keep filter" : " send steer")} ${accent2("esc")}${ink32(line.kind === "search" ? " clear" : " cancel")}`
1897
+ keyHints(line.kind === "search" ? [["enter", "keep filter"], ["esc", "clear"]] : [["enter", "send"], ["esc", "cancel"]])
1898
+ ];
1899
+ }
1900
+ if (this.#error) {
1901
+ const message = this.#error.length > width - 6 ? `${this.#error.slice(0, Math.max(8, width - 7))}\u2026` : this.#error;
1902
+ return [
1903
+ ` ${red2("\u2717")} ${red2(message)}`,
1904
+ keyHints([["esc", "dismiss"], ["r", "retry"], ["q", "quit"]])
1905
+ ];
1906
+ }
1907
+ const notice = this.#noticeText();
1908
+ if (this.#view === "help") {
1909
+ return [
1910
+ ` ${notice ? accentDim2(notice) : ink42("every key on every screen")}`,
1911
+ keyHints([["\u2191\u2193", "scroll"], ["\u2190", "back"], ["esc", "cockpit"], ["q", "quit"]])
1757
1912
  ];
1758
1913
  }
1759
- if (this.#error)
1760
- return [` ${red2(`error: ${this.#error}`)}`, ` ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`];
1761
- if (this.#view === "help")
1762
- return [` ${accent2("\u2190")}${ink32(" back")} ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`];
1763
1914
  const domainFooter = this.#domainViews.get(this.#view)?.footer;
1764
1915
  if (domainFooter)
1765
- return domainFooter(this.#noticeText());
1916
+ return domainFooter(notice);
1766
1917
  if (this.#view === "task-detail" || this.#view === "run-detail") {
1767
1918
  const isTask = this.#view === "task-detail";
1768
- const notice2 = this.#noticeText();
1769
- const hint = notice2 ? accentDim2(notice2) : ink42(isTask ? "review the task, then dispatch" : "live run controls");
1919
+ const hint = notice ? accentDim2(notice) : ink42(isTask ? "read it, then send a drone at it" : "live run controls \u2014 resolve gates in place");
1770
1920
  return [
1771
1921
  ` ${hint}`,
1772
- ` ${accent2("\u2191\u2193")}${ink32(" action")} ${accent2("enter")}${ink32(isTask ? " dispatch" : " run")} ${accent2("\u21DE\u21DF")}${ink32(" scroll")} ${accent2("\u2190")}${ink32(" back")} ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`
1922
+ keyHints([["\u2191\u2193", "action"], ["enter", isTask ? "dispatch" : "run"], ["\u21DE\u21DF", "scroll"], ["\u2190", "back"], ["q", "quit"]])
1773
1923
  ];
1774
1924
  }
1775
- const notice = this.#noticeText();
1925
+ if (this.#view === "cockpit") {
1926
+ const live = liveRunCount(this.#runs, this.#state);
1927
+ const attention = attentionRunCount(this.#runs, this.#state);
1928
+ const fleet = this.#runs.length === 0 ? "fleet idle \u2014 dispatch something" : `${live} live \xB7 ${attention} waiting on you \xB7 ${this.#runs.length} total`;
1929
+ return [
1930
+ ` ${notice ? accentDim2(notice) : ink42(fleet)}`,
1931
+ keyHints([["\u2191\u2193", "move"], ["enter", "open"], ["1-9", "jump"], ["?", "help"], ["q", "quit"]])
1932
+ ];
1933
+ }
1934
+ const purpose = this.catalog.find((entry) => entry.id === this.#view)?.nav?.description ?? "";
1776
1935
  return [
1777
- ` ${notice ? accentDim2(notice) : ink42(`screen: ${screenLabel(this.#view, this.catalog)}`)}`,
1778
- ` ${accent2("\u2191\u2193")}${ink32(" move")} ${accent2("enter")}${ink32(" act/open")} ${accent2("\u2190")}${ink32(" back")} ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`
1936
+ ` ${notice ? accentDim2(notice) : ink42(purpose || screenLabel(this.#view, this.catalog))}`,
1937
+ keyHints([["\u2191\u2193", "move"], ["enter", "act"], ["r", "reload"], ["\u2190", "back"], ["esc", "cockpit"], ["q", "quit"]])
1779
1938
  ];
1780
1939
  }
1781
1940
  async#attach(runId) {
@@ -1786,21 +1945,6 @@ class RigFlowComponent {
1786
1945
  if (!this.#error)
1787
1946
  this.done();
1788
1947
  }
1789
- async#stop(run) {
1790
- this.#notice = `stop requested for ${run.runId.slice(0, 8)}\u2026`;
1791
- this.tui.requestRender();
1792
- await this.#runAction(() => this.actions.act(`run-stop:${encodeURIComponent(run.runId)}`));
1793
- }
1794
- async#pause(run) {
1795
- this.#notice = `pause requested for ${run.runId.slice(0, 8)}\u2026`;
1796
- this.tui.requestRender();
1797
- await this.#runAction(() => this.actions.act(`run-pause:${encodeURIComponent(run.runId)}`));
1798
- }
1799
- async#resume(run) {
1800
- this.#notice = `resume requested for ${run.runId.slice(0, 8)}\u2026`;
1801
- this.tui.requestRender();
1802
- await this.#runAction(() => this.actions.act(`run-resume:${encodeURIComponent(run.runId)}`));
1803
- }
1804
1948
  async#steer(run, message) {
1805
1949
  await this.#runAction(() => this.actions.act(`run-steer:${encodeURIComponent(run.runId)}`, message));
1806
1950
  }
@@ -1834,6 +1978,10 @@ class RigFlowComponent {
1834
1978
  }
1835
1979
  return;
1836
1980
  }
1981
+ if (item.id.endsWith(":retry")) {
1982
+ await this.#runAction(async () => {});
1983
+ return;
1984
+ }
1837
1985
  if (item.id === "config:relayUrl" || item.id.startsWith("stats:") || item.id.startsWith("inspect:audit")) {
1838
1986
  this.#notice = "informational row; no cockpit action";
1839
1987
  this.tui.requestRender();
@@ -1849,12 +1997,15 @@ class RigFlowComponent {
1849
1997
  await this.#runAction((onProgress) => this.actions.act(item.id, undefined, onProgress));
1850
1998
  }
1851
1999
  render(width) {
1852
- const lines = [this.#headerLine(), hairline(width)];
2000
+ const lines = [this.#headerLine(width), this.#tabsLine(width), hairline(width)];
2001
+ this.#frame.tabsLine = 1;
1853
2002
  const view = this.#view === "help" ? this.#helpView : this.#activeView();
2003
+ this.#frame.viewStart = lines.length;
1854
2004
  for (const line of view.render(width))
1855
2005
  lines.push(line);
2006
+ this.#frame.viewEnd = lines.length - 1;
1856
2007
  lines.push(hairline(width));
1857
- for (const line of this.#footerLines())
2008
+ for (const line of this.#footerLines(width))
1858
2009
  lines.push(line);
1859
2010
  return lines;
1860
2011
  }
@@ -1864,7 +2015,95 @@ class RigFlowComponent {
1864
2015
  this.#helpView.invalidate();
1865
2016
  this.#genericView.invalidate();
1866
2017
  }
2018
+ #activateDetailAction() {
2019
+ const action = this.#detailComposer.selectedAction();
2020
+ if (!action)
2021
+ return;
2022
+ if (action.id.startsWith("run-steer:")) {
2023
+ const run = findRunById(this.#runs, this.#selectedRunId);
2024
+ if (run) {
2025
+ this.#inputLine = { kind: "steer", buffer: "", run: { runId: run.runId } };
2026
+ this.tui.requestRender();
2027
+ }
2028
+ return;
2029
+ }
2030
+ if (action.id.startsWith("task-detail:dispatch:")) {
2031
+ this.#notice = "dispatch \xB7 starting\u2026";
2032
+ this.tui.requestRender();
2033
+ }
2034
+ if (action.id.startsWith("run-stop:")) {
2035
+ this.#notice = "stop requested\u2026";
2036
+ this.tui.requestRender();
2037
+ }
2038
+ this.#actOnItem({ id: action.id, label: action.label, values: [action.value ?? "go"] });
2039
+ }
2040
+ #handleMouse(event) {
2041
+ if (this.#inputLine)
2042
+ return;
2043
+ const { tabsLine, viewStart, viewEnd, tabs } = this.#frame;
2044
+ if (event.wheel !== null) {
2045
+ if (this.#view === "help")
2046
+ this.#helpView.scroll(event.wheel * 3);
2047
+ else if (this.#view === "task-detail" || this.#view === "run-detail")
2048
+ this.#detailComposer.scrollBody(event.wheel * 2);
2049
+ else {
2050
+ const domainView2 = this.#domainViews.get(this.#view);
2051
+ if (domainView2) {
2052
+ if (!domainView2.handleMouse?.(event, Math.max(0, event.row - viewStart)))
2053
+ return;
2054
+ } else {
2055
+ this.#genericView.moveSelection(event.wheel);
2056
+ }
2057
+ }
2058
+ this.tui.requestRender();
2059
+ return;
2060
+ }
2061
+ if (!event.leftClick)
2062
+ return;
2063
+ if (event.row === tabsLine) {
2064
+ const hit = tabs.find((tab) => event.col >= tab.start && event.col <= tab.end);
2065
+ if (hit && hit.screen !== this.#view)
2066
+ this.#navigate(hit.screen);
2067
+ return;
2068
+ }
2069
+ if (event.row < viewStart || event.row > viewEnd)
2070
+ return;
2071
+ const line = event.row - viewStart;
2072
+ if (this.#view === "help")
2073
+ return;
2074
+ if (this.#view === "task-detail" || this.#view === "run-detail") {
2075
+ const index2 = this.#detailComposer.hitTestAction(line);
2076
+ if (index2 === undefined)
2077
+ return;
2078
+ const moved2 = this.#detailComposer.selectAction(index2);
2079
+ this.tui.requestRender();
2080
+ if (!moved2)
2081
+ this.#activateDetailAction();
2082
+ return;
2083
+ }
2084
+ const domainView = this.#domainViews.get(this.#view);
2085
+ if (domainView) {
2086
+ domainView.handleMouse?.(event, line);
2087
+ return;
2088
+ }
2089
+ const index = this.#genericView.hitTest(line);
2090
+ if (index === undefined)
2091
+ return;
2092
+ const moved = this.#genericView.selectIndex(index);
2093
+ this.tui.requestRender();
2094
+ if (!moved) {
2095
+ const item = this.#genericView.selectedItem();
2096
+ if (item)
2097
+ this.#actOnItem(item);
2098
+ }
2099
+ }
1867
2100
  handleInput(data) {
2101
+ if (data.startsWith("\x1B[<")) {
2102
+ const event = parseSgrMouse(data);
2103
+ if (event)
2104
+ this.#handleMouse(event);
2105
+ return;
2106
+ }
1868
2107
  if (data === "q") {
1869
2108
  this.onQuit();
1870
2109
  return;
@@ -1899,6 +2138,19 @@ class RigFlowComponent {
1899
2138
  this.done();
1900
2139
  return;
1901
2140
  }
2141
+ if (this.#error && matchesKey(data, "escape")) {
2142
+ this.#error = "";
2143
+ this.tui.requestRender();
2144
+ return;
2145
+ }
2146
+ if (/^[1-9]$/.test(data)) {
2147
+ const target = this.#tabScreens()[Number(data) - 1];
2148
+ if (target) {
2149
+ if (target.screen !== this.#view)
2150
+ this.#navigate(target.screen);
2151
+ return;
2152
+ }
2153
+ }
1902
2154
  if (this.#view === "help") {
1903
2155
  if (matchesKey(data, "up") || data === "k")
1904
2156
  this.#helpView.scroll(-1);
@@ -1927,26 +2179,7 @@ class RigFlowComponent {
1927
2179
  return;
1928
2180
  }
1929
2181
  if (matchesKey(data, "enter") || matchesKey(data, "return")) {
1930
- const action = this.#detailComposer.selectedAction();
1931
- if (action) {
1932
- if (action.id.startsWith("run-steer:")) {
1933
- const run = findRunById(this.#runs, this.#selectedRunId);
1934
- if (run) {
1935
- this.#inputLine = { kind: "steer", buffer: "", run: { runId: run.runId } };
1936
- this.tui.requestRender();
1937
- }
1938
- return;
1939
- }
1940
- if (action.id.startsWith("task-detail:dispatch:")) {
1941
- this.#notice = "dispatch \xB7 starting\u2026";
1942
- this.tui.requestRender();
1943
- }
1944
- if (action.id.startsWith("run-stop:")) {
1945
- this.#notice = "stop requested\u2026";
1946
- this.tui.requestRender();
1947
- }
1948
- this.#actOnItem({ id: action.id, label: action.label, values: [action.value ?? "go"] });
1949
- }
2182
+ this.#activateDetailAction();
1950
2183
  return;
1951
2184
  }
1952
2185
  if (matchesKey(data, "pageUp")) {