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