@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.
@@ -21,7 +21,7 @@ import { DOCTOR } from "@rig/contracts";
21
21
  import { RIG_COCKPIT_PANEL_SLOT } from "@rig/contracts";
22
22
  import { defineCapability } from "@rig/core/capability";
23
23
  import { loadCapabilityForRoot, requireCapabilityForRoot } from "@rig/core/capability-loaders";
24
- import { isKeyRelease, matchesKey } from "@oh-my-pi/pi-tui";
24
+ import { isKeyRelease, matchesKey, parseSgrMouse } from "@oh-my-pi/pi-tui";
25
25
  import { Duration, Effect, Fiber, Stream } from "effect";
26
26
 
27
27
  // packages/omp-extension-plugin/src/cockpit/board-views.ts
@@ -29,6 +29,7 @@ import { truncateToWidth } from "@oh-my-pi/pi-tui";
29
29
  import {
30
30
  accent,
31
31
  accentDim,
32
+ bgPanel,
32
33
  bold,
33
34
  cyan,
34
35
  ink,
@@ -36,10 +37,6 @@ import {
36
37
  ink3,
37
38
  ink4,
38
39
  red,
39
- DRONE_WIDTH,
40
- renderDroneFrame,
41
- renderMicroDroneFrame,
42
- RIG_SPINNER_FRAMES,
43
40
  screenGlyph,
44
41
  sectionDivider,
45
42
  statusColor,
@@ -51,11 +48,13 @@ import {
51
48
  function hairline(width) {
52
49
  return ink4("\u2500".repeat(Math.max(0, width)));
53
50
  }
51
+
54
52
  class BoardSettingsList {
55
53
  items = [];
56
54
  selected = 0;
57
55
  title = "";
58
56
  emptyMessage = "nothing to show";
57
+ #rowToIndex = new Map;
59
58
  setItems(items) {
60
59
  this.items = [...items];
61
60
  if (!this.selectableAt(this.selected))
@@ -86,44 +85,77 @@ class BoardSettingsList {
86
85
  selectedItem() {
87
86
  return this.selectableAt(this.selected) ? this.items[this.selected] : null;
88
87
  }
88
+ hitTest(line) {
89
+ const index = this.#rowToIndex.get(line);
90
+ return index !== undefined && this.selectableAt(index) ? index : undefined;
91
+ }
92
+ selectIndex(index) {
93
+ if (!this.selectableAt(index))
94
+ return false;
95
+ const moved = this.selected !== index;
96
+ this.selected = index;
97
+ return moved;
98
+ }
89
99
  invalidate() {}
90
100
  render(width) {
91
101
  return this.renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
92
102
  }
93
103
  renderLines(width) {
104
+ this.#rowToIndex.clear();
94
105
  if (this.items.length === 0) {
95
106
  return ["", ` ${ink2(this.emptyMessage)}`, ""];
96
107
  }
97
- const maxVisible = Math.max(8, Math.min(16, (process.stdout.rows ?? 30) - 10));
108
+ const maxVisible = Math.max(8, Math.min(18, (process.stdout.rows ?? 30) - 10));
98
109
  const start = Math.max(0, Math.min(this.selected - Math.floor(maxVisible / 2), this.items.length - maxVisible));
99
110
  const window = this.items.slice(start, start + maxVisible);
100
- const valueWidth = Math.min(24, Math.max(10, Math.floor(width * 0.24)));
101
- const labelWidth = Math.min(24, Math.max(10, Math.floor(width * 0.26)));
102
- const lines = window.flatMap((item, index) => {
111
+ const labelWidth = Math.min(28, Math.max(10, ...window.map((item) => {
112
+ const navGlyph = item.id.startsWith("to:") ? 2 : 0;
113
+ return item.label.length + navGlyph;
114
+ })));
115
+ const actionWidth = Math.max(0, ...window.map((item) => item.values?.[0] && item.values[0] !== item.currentValue ? item.values[0].length + 2 : 0));
116
+ const valueWidth = Math.max(12, width - labelWidth - actionWidth - 8);
117
+ const lines = [""];
118
+ for (let index = 0;index < window.length; index += 1) {
119
+ const item = window[index];
103
120
  const absolute = start + index;
104
121
  const isSelected = absolute === this.selected;
105
- const marker = isSelected ? accent("\u258C") : " ";
122
+ if (item.id === "title") {
123
+ lines.push(` ${accent("\u258D")}${bold(ink(item.label))}${item.currentValue ? ` ${ink3(item.currentValue)}` : ""}`);
124
+ if (item.description)
125
+ lines.push(` ${ink4(truncateToWidth(item.description, Math.max(12, width - 4)))}`);
126
+ lines.push("");
127
+ continue;
128
+ }
129
+ if (item.heading && !item.currentValue) {
130
+ lines.push(sectionDivider(item.label.toLowerCase(), width));
131
+ continue;
132
+ }
106
133
  const navGlyph = item.id.startsWith("to:") ? `${screenGlyph(item.id.slice(3))} ` : "";
107
- const labelText = truncateToWidth(`${navGlyph}${item.label}`, labelWidth).padEnd(labelWidth);
108
- const isTitle = item.id === "title";
134
+ const labelPlain = truncateToWidth(`${navGlyph}${item.label}`, labelWidth).padEnd(labelWidth);
109
135
  const isStatus = item.id.endsWith(":status");
110
- const label = isTitle ? accent(labelText) : isSelected ? ink(labelText) : item.heading ? ink3(labelText) : ink2(labelText);
111
- 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));
112
- const action = item.values?.[0] && item.values[0] !== item.currentValue ? isSelected ? accent(item.values[0]) : accentDim(item.values[0]) : "";
113
- const row = `${marker} ${label} ${value}${action ? ` ${action}` : ""}`;
136
+ const valuePlain = truncateToWidth(item.currentValue ?? "", valueWidth).padEnd(valueWidth);
137
+ const label = isSelected ? ink(labelPlain) : item.heading ? ink3(labelPlain) : ink2(labelPlain);
138
+ 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);
139
+ const actionText = item.values?.[0] && item.values[0] !== item.currentValue ? item.values[0] : "";
140
+ const action = actionText ? isSelected ? accent(`${actionText} \u203A`) : accentDim(actionText) : "";
141
+ const rail = isSelected ? accent("\u258C") : " ";
142
+ const body = `${rail} ${label} ${value}${action ? ` ${action}` : ""}`;
143
+ if (!item.heading)
144
+ this.#rowToIndex.set(lines.length, absolute);
145
+ lines.push(isSelected ? bgPanel(body) : body);
114
146
  if (isSelected && item.description) {
115
- return [row, ` ${ink4(truncateToWidth(item.description, Math.max(12, width - 5)))}`];
147
+ if (!item.heading)
148
+ this.#rowToIndex.set(lines.length, absolute);
149
+ lines.push(` ${ink4("\u21B3")} ${ink4(truncateToWidth(item.description, Math.max(12, width - 6)))}`);
116
150
  }
117
- return [row];
118
- });
151
+ }
119
152
  const meta = [];
120
153
  if (this.items.length > maxVisible)
121
- meta.push(`${this.selected + 1}/${this.items.length}`);
122
- if (this.title)
123
- meta.push(this.title);
154
+ meta.push(`${this.selected + 1} of ${this.items.length}`);
124
155
  if (meta.length > 0)
125
- lines.push(ink4(` ${meta.join(" \xB7 ")}`));
126
- return ["", ...lines, ""];
156
+ lines.push("", ink4(` ${meta.join(" \xB7 ")}`));
157
+ lines.push("");
158
+ return lines;
127
159
  }
128
160
  }
129
161
  class HelpView {
@@ -207,6 +239,7 @@ class DetailComposer {
207
239
  selected = 0;
208
240
  tick = 0;
209
241
  bodyScroll = 0;
242
+ #lineToAction = new Map;
210
243
  setModel(model) {
211
244
  const prevId = this.enabledActions()[this.selected]?.id;
212
245
  this.model = model;
@@ -231,6 +264,17 @@ class DetailComposer {
231
264
  selectedAction() {
232
265
  return this.enabledActions()[this.selected] ?? null;
233
266
  }
267
+ hitTestAction(line) {
268
+ return this.#lineToAction.get(line);
269
+ }
270
+ selectAction(index) {
271
+ const count = this.enabledActions().length;
272
+ if (index < 0 || index >= count)
273
+ return false;
274
+ const moved = this.selected !== index;
275
+ this.selected = index;
276
+ return moved;
277
+ }
234
278
  scrollBody(delta) {
235
279
  this.bodyScroll = Math.max(0, this.bodyScroll + delta);
236
280
  }
@@ -260,21 +304,45 @@ class DetailComposer {
260
304
  }
261
305
  if (model.stages && model.stages.length > 0) {
262
306
  bottom.push(sectionDivider("pipeline", width));
263
- const parts = model.stages.map((stage) => `${statusColor(stage.status)(statusGlyph(stage.status))} ${ink3(stage.label)}`);
264
- bottom.push(truncateToWidth(` ${parts.join(ink4(" \u2192 "))}`, inner));
307
+ let linePlain = " ";
308
+ let linePainted = " ";
309
+ for (let i = 0;i < model.stages.length; i += 1) {
310
+ const stage = model.stages[i];
311
+ const sepPlain = linePlain.trim().length > 0 ? " \u2192 " : " ";
312
+ const cellPlain = `${sepPlain}? ${stage.label}`;
313
+ if (linePlain.length + cellPlain.length > inner - 2 && linePlain.trim().length > 0) {
314
+ bottom.push(linePainted);
315
+ linePlain = " ";
316
+ linePainted = " ";
317
+ }
318
+ const sep = linePlain.trim().length > 0 ? ink4(" \u2192 ") : " ";
319
+ linePlain += cellPlain;
320
+ linePainted += `${sep}${statusColor(stage.status)(statusGlyph(stage.status))} ${ink3(stage.label)}`;
321
+ }
322
+ if (linePainted.trim().length > 0)
323
+ bottom.push(linePainted);
265
324
  bottom.push("");
266
325
  }
326
+ const actionLines = [];
267
327
  if (model.actions.length > 0) {
268
328
  bottom.push(sectionDivider("actions", width));
269
329
  const selId = this.selectedAction()?.id;
330
+ let enabledIndex = -1;
270
331
  for (const action of model.actions) {
271
332
  const on = action.enabled !== false;
333
+ if (on)
334
+ enabledIndex += 1;
272
335
  const isSel = on && action.id === selId;
273
336
  const rail = isSel ? accent("\u258C") : " ";
274
337
  const labelText = `${action.glyph ? `${action.glyph} ` : ""}${action.label}`;
275
338
  const label = !on ? ink4(labelText) : action.danger ? red(labelText) : isSel ? accent(labelText) : ink2(labelText);
276
339
  const tail = !on ? ink4(action.value ?? "unavailable") : isSel ? ink3(action.hint ?? action.value ?? "") : ink3(action.value ?? "");
277
- bottom.push(`${rail} ${label}${tail ? ` ${tail}` : ""}`);
340
+ const row = `${rail} ${label}${tail ? ` ${tail}` : ""}`;
341
+ if (on)
342
+ actionLines.push(enabledIndex);
343
+ else
344
+ actionLines.push(-1);
345
+ bottom.push(isSel ? bgPanel(row) : row);
278
346
  }
279
347
  }
280
348
  const body = [];
@@ -299,16 +367,31 @@ class DetailComposer {
299
367
  }
300
368
  }
301
369
  const out = [...top, ...body, ...bottom];
370
+ const mapActions = (linesOut) => {
371
+ this.#lineToAction.clear();
372
+ if (actionLines.length === 0)
373
+ return;
374
+ const actionCount = model.actions.length;
375
+ const firstActionLine = linesOut.length - actionCount;
376
+ for (let i = 0;i < actionCount; i += 1) {
377
+ const enabledIdx = actionLines[i];
378
+ if (enabledIdx >= 0)
379
+ this.#lineToAction.set(firstActionLine + i, enabledIdx);
380
+ }
381
+ };
302
382
  if (out.length > budget) {
303
383
  const keepTail = Math.min(bottom.length, budget - top.length);
304
- return [...top, ...out.slice(out.length - keepTail)].map((line) => truncateToWidth(line, Math.max(10, width - 1)));
384
+ const clipped = [...top, ...out.slice(out.length - keepTail)];
385
+ mapActions(clipped);
386
+ return clipped.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
305
387
  }
388
+ mapActions(out);
306
389
  return out.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
307
390
  }
308
391
  }
309
392
 
310
393
  // packages/omp-extension-plugin/src/cockpit/extension.ts
311
- 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";
394
+ 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";
312
395
 
313
396
  // packages/omp-extension-plugin/src/cockpit/drone-preloader.ts
314
397
  var ART = [
@@ -529,8 +612,8 @@ class DronePreloaderComponent {
529
612
 
530
613
  // packages/omp-extension-plugin/src/cockpit/screen-catalog.ts
531
614
  var DEFAULT_SCREEN_CATALOG = [
532
- { id: "cockpit", title: "Project Cockpit", shortcut: { key: "g", description: "Rig cockpit" } },
533
- { id: "task-detail", title: "Task Detail" },
615
+ { id: "cockpit", title: "Cockpit", label: "cockpit", shortcut: { key: "g", description: "Rig cockpit" } },
616
+ { id: "task-detail", title: "Task Detail", label: "task detail" },
534
617
  { id: "run-detail", title: "Run Detail", label: "run detail" }
535
618
  ];
536
619
  function buildScreenFromPanel(panel) {
@@ -569,9 +652,6 @@ function findScreen(catalog, screen) {
569
652
  function isRigScreen(value, catalog = DEFAULT_SCREEN_CATALOG) {
570
653
  return typeof value === "string" && catalog.some((screen) => screen.id === value);
571
654
  }
572
- function screenTitle(screen, catalog = DEFAULT_SCREEN_CATALOG) {
573
- return findScreen(catalog, screen)?.title ?? screen;
574
- }
575
655
  function screenLabel(screen, catalog = DEFAULT_SCREEN_CATALOG) {
576
656
  const descriptor = findScreen(catalog, screen);
577
657
  return descriptor?.label ?? descriptor?.id ?? screen;
@@ -1323,17 +1403,6 @@ async function runCollabDoctor(ctx) {
1323
1403
  ]);
1324
1404
  }
1325
1405
  var commandFlowActions = defaultFlowActions;
1326
- function sessionDescription(run) {
1327
- const id = run.runId.slice(0, 8);
1328
- const cwd = run.collabCwd || run.worktreePath || run.sessionPath || "(unknown cwd)";
1329
- const link = collabLinks(run);
1330
- return link ? `${id} \xB7 ${cwd} \xB7 ${link}` : `${id} \xB7 ${cwd}`;
1331
- }
1332
- function collabLinks(run) {
1333
- const join = run.joinLink ? `joinLink ${run.joinLink}` : "";
1334
- const web = run.webLink ? `webLink ${run.webLink}` : "";
1335
- return join && web ? `${join} \xB7 ${web}` : join || web || "";
1336
- }
1337
1406
  function findRunById(runs, runId) {
1338
1407
  if (!runId)
1339
1408
  return null;
@@ -1361,15 +1430,30 @@ function runListActionHints(run, source) {
1361
1430
  function serverLabel(server) {
1362
1431
  return server?.alias?.trim() || "unselected";
1363
1432
  }
1364
- function nextStepHint(server, runs, state) {
1433
+ function nextStep(server, runs, state) {
1365
1434
  if (!server)
1366
- return "next: Server";
1367
- if (runs.some((run) => {
1435
+ return { screen: "server", label: "choose where runs execute", detail: "no target selected \u2014 open Server" };
1436
+ const live = liveRunCount(runs, state);
1437
+ if (live > 0)
1438
+ return { screen: "runs", label: `watch the fleet \u2014 ${live} live`, detail: "open Runs for live status, steer, and gates" };
1439
+ return { screen: "tasks", label: "pick a task and dispatch", detail: `read ${server.taskSource ?? "the task source"} and send a drone at one` };
1440
+ }
1441
+ function liveRunCount(runs, state) {
1442
+ let count = 0;
1443
+ for (const run of runs) {
1368
1444
  const classification = state.runClassifications?.get(run.runId);
1369
- return classification ? classification.isActive && !classification.isTerminal : false;
1370
- }))
1371
- return "next: Runs";
1372
- return "next: Tasks";
1445
+ if (classification && classification.isActive && !classification.isTerminal)
1446
+ count += 1;
1447
+ }
1448
+ return count;
1449
+ }
1450
+ function attentionRunCount(runs, state) {
1451
+ let count = 0;
1452
+ for (const run of runs) {
1453
+ if (state.runClassifications?.get(run.runId)?.isNeedsAttention)
1454
+ count += 1;
1455
+ }
1456
+ return count;
1373
1457
  }
1374
1458
  function taskDetailModel(state, server, projection) {
1375
1459
  const task = state.selectedTask ?? null;
@@ -1427,8 +1511,8 @@ function runDetailModel(run, source) {
1427
1511
  ref: `RUN ${shortId}`,
1428
1512
  title: run.title || `Run ${shortId}`,
1429
1513
  status,
1430
- body: run.errorSummary || sessionDescription(run),
1431
- bodyLabel: "summary",
1514
+ 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.`),
1515
+ bodyLabel: run.errorSummary ? "error" : "run",
1432
1516
  facts: [
1433
1517
  ...run.taskId ? [{ label: "task", value: run.taskId }] : [],
1434
1518
  { label: "cwd", value: run.collabCwd || run.worktreePath || run.sessionPath || "(unknown cwd)" },
@@ -1460,11 +1544,20 @@ function runDetailModel(run, source) {
1460
1544
  function cockpitNavValue(screen, server, runs, state) {
1461
1545
  switch (screen) {
1462
1546
  case "server":
1463
- return serverLabel(server);
1547
+ return server ? `${serverLabel(server)} \xB7 ${server.kind === "remote" ? "remote" : "this checkout"}` : "unselected";
1464
1548
  case "tasks":
1465
1549
  return server?.taskSource ?? "configured source";
1466
- case "runs":
1467
- return String(runs.length);
1550
+ case "runs": {
1551
+ const live = liveRunCount(runs, state);
1552
+ const attention = attentionRunCount(runs, state);
1553
+ if (runs.length === 0)
1554
+ return "none yet";
1555
+ const parts = [`${live} live`];
1556
+ if (attention > 0)
1557
+ parts.push(`${attention} waiting on you`);
1558
+ parts.push(`${runs.length} total`);
1559
+ return parts.join(" \xB7 ");
1560
+ }
1468
1561
  default:
1469
1562
  return "";
1470
1563
  }
@@ -1474,36 +1567,38 @@ function cockpitNavRows(server, runs, state, catalog) {
1474
1567
  id: `to:${screen}`,
1475
1568
  label: nav.label,
1476
1569
  currentValue: cockpitNavValue(screen, server, runs, state),
1477
- values: ["\u203A"],
1570
+ values: ["open"],
1478
1571
  description: nav.description
1479
1572
  }));
1480
1573
  }
1481
- function itemsFor(screen, runs, actionRowsEnabled, _selectedRunId, state = {}, catalog = DEFAULT_SCREEN_CATALOG, projection = defaultCockpitCapabilities.taskProjection) {
1574
+ function itemsFor(screen, runs, actionRowsEnabled, _selectedRunId, state = {}, catalog = DEFAULT_SCREEN_CATALOG, _projection = defaultCockpitCapabilities.taskProjection) {
1482
1575
  const server = state.server ?? null;
1483
- const projectRoot = state.projectRoot ?? rigProjectRoot();
1484
1576
  const loadError = state.error?.trim();
1485
1577
  if (screen === "task-detail" || screen === "run-detail")
1486
1578
  return [];
1487
1579
  if (loadError) {
1488
1580
  return [
1489
- { id: "title", label: screenTitle(screen, catalog), currentValue: "load failed", heading: true },
1490
- { id: `${screen}:error`, label: "LOAD FAILED", currentValue: "error", heading: true, description: loadError }
1581
+ { id: "title", label: screenLabel(screen, catalog), currentValue: "couldn't load", heading: true, description: loadError },
1582
+ { id: `${screen}:retry`, label: "RETRY", currentValue: "reload this screen", values: ["retry"], description: "press enter (or r) to reload \u2014 esc returns to the cockpit" }
1491
1583
  ];
1492
1584
  }
1493
1585
  switch (screen) {
1494
- case "cockpit":
1586
+ case "cockpit": {
1587
+ const step = nextStep(server, runs, state);
1495
1588
  return [
1496
- { id: "title", label: screenTitle("cockpit", catalog), currentValue: "", heading: true },
1497
- { id: "next", label: "NEXT", currentValue: nextStepHint(server, runs, state), heading: true, description: "guided path: Server \u2192 Tasks \u2192 Detail \u2192 Dispatch \u2192 Run Detail" },
1589
+ { id: "title", label: "cockpit", currentValue: "operate the drones", heading: true, description: "the loop: choose a target \xB7 dispatch tasks \xB7 watch runs land" },
1590
+ { id: `to:${step.screen}`, label: "NEXT", currentValue: step.label, values: ["go"], description: step.detail },
1591
+ { id: "sec:screens", label: "SCREENS", currentValue: "", heading: true },
1498
1592
  ...cockpitNavRows(server, runs, state, catalog)
1499
1593
  ];
1594
+ }
1500
1595
  default: {
1501
1596
  const producedRows = state.panelRows ?? [];
1502
1597
  if (producedRows.length > 0)
1503
1598
  return [...producedRows];
1504
1599
  return [
1505
- { id: "title", label: screenTitle(screen, catalog), currentValue: "unavailable", heading: true },
1506
- { 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." }
1600
+ { id: "title", label: screenLabel(screen, catalog), currentValue: "nothing here yet", heading: true, description: "this screen's owner produced no content" },
1601
+ { id: `${screen}:retry`, label: "RETRY", currentValue: "reload this screen", values: ["retry"], description: "press enter (or r) to reload \u2014 esc returns to the cockpit" }
1507
1602
  ];
1508
1603
  }
1509
1604
  }
@@ -1540,6 +1635,7 @@ class RigFlowComponent {
1540
1635
  #refreshing = false;
1541
1636
  #busyDepth = 0;
1542
1637
  #animationTimer;
1638
+ #frame = { tabsLine: -1, viewStart: -1, viewEnd: -1, tabs: [] };
1543
1639
  constructor(tui, api, done, onQuit, screen, runs, state, refreshRuns, refreshState, actions, actionRowsEnabled, catalog = DEFAULT_SCREEN_CATALOG, caps = defaultCockpitCapabilities, helpCatalog) {
1544
1640
  this.tui = tui;
1545
1641
  this.api = api;
@@ -1653,10 +1749,13 @@ class RigFlowComponent {
1653
1749
  this.#genericView.setItems(itemsFor(genericScreen, this.#runs, this.actionRowsEnabled, this.#selectedRunId, this.#state, this.catalog, this.caps.taskProjection));
1654
1750
  }
1655
1751
  if (this.#view === "task-detail") {
1656
- 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));
1752
+ const loadedId = this.caps.taskProjection.id(this.#state.selectedTask ?? null);
1753
+ const pendingId = this.#selectedTaskId;
1754
+ const stillLoading = Boolean(pendingId) && loadedId !== pendingId;
1755
+ 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));
1657
1756
  } else if (this.#view === "run-detail") {
1658
1757
  const run = findRunById(this.#runs, this.#selectedRunId);
1659
- 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: [] });
1758
+ 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: [] });
1660
1759
  }
1661
1760
  for (const view of this.#domainViews.values())
1662
1761
  view.setTick(this.#tick);
@@ -1675,14 +1774,16 @@ class RigFlowComponent {
1675
1774
  #setView(next) {
1676
1775
  this.#view = next;
1677
1776
  this.#notice = null;
1777
+ this.#error = "";
1678
1778
  this.#feedViews();
1679
1779
  this.tui.requestRender();
1680
1780
  }
1681
1781
  async#navigate(next, push = true) {
1682
- if (push && this.#view !== "help")
1782
+ if (push && this.#view !== "help" && this.#view !== next)
1683
1783
  this.#screenStack.push(this.#view);
1684
1784
  this.#view = next;
1685
1785
  this.#notice = null;
1786
+ this.#error = "";
1686
1787
  const endBusy = this.#beginBusy();
1687
1788
  try {
1688
1789
  await this.#refreshCurrent();
@@ -1705,10 +1806,9 @@ class RigFlowComponent {
1705
1806
  if (this.#view !== "help") {
1706
1807
  const loadedState = await this.refreshState(this.#view, this.#selectedTaskId, this.#runs);
1707
1808
  this.#state = runLoadError ? { ...loadedState, error: loadedState.error ? `${runLoadError}; ${loadedState.error}` : runLoadError } : loadedState;
1708
- if (this.#state.error)
1709
- this.#error = this.#state.error;
1710
- } else if (runLoadError) {
1711
- this.#error = runLoadError;
1809
+ this.#error = this.#state.error ?? "";
1810
+ } else {
1811
+ this.#error = runLoadError ?? "";
1712
1812
  }
1713
1813
  this.#feedViews();
1714
1814
  this.tui.requestRender();
@@ -1732,45 +1832,105 @@ class RigFlowComponent {
1732
1832
  endBusy();
1733
1833
  }
1734
1834
  }
1735
- #headerLine() {
1736
- const crumb = this.#view === "help" ? ` ${ink42("\u203A")} ${ink22("help")}` : ` ${ink42("\u203A")} ${ink22(screenLabel(this.#view, this.catalog))}`;
1737
- return ` ${accent2("\u258D")}${bold2(ink5("rig"))} ${ink32("\u2014 operate the drones")}${crumb} ${ink42("\xB7")} ${cyan2(this.#serverLabel)}`;
1835
+ #tabScreens() {
1836
+ return [
1837
+ { screen: "cockpit", label: "cockpit" },
1838
+ ...cockpitNavScreens(this.catalog).map(({ screen, nav }) => ({ screen, label: nav.label.toLowerCase() }))
1839
+ ];
1840
+ }
1841
+ #tabFor(view) {
1842
+ if (view === "run-detail")
1843
+ return "runs";
1844
+ if (view === "task-detail")
1845
+ return "tasks";
1846
+ if (view === "help")
1847
+ return "cockpit";
1848
+ return view;
1849
+ }
1850
+ #headerLine(width) {
1851
+ const screenName = this.#view === "help" ? "help" : screenLabel(this.#view, this.catalog).toLowerCase();
1852
+ const project = (this.#state.projectRoot ?? rigProjectRoot()).split("/").filter(Boolean).pop() ?? "";
1853
+ const live = liveRunCount(this.#runs, this.#state);
1854
+ const attention = attentionRunCount(this.#runs, this.#state);
1855
+ const dotPaint = attention > 0 ? yellow2 : live > 0 ? accent2 : ink42;
1856
+ const rightPlain = `\u25CF ${this.#serverLabel}${project ? ` \xB7 ${project}` : ""}`;
1857
+ const rightPainted = `${dotPaint("\u25CF")} ${cyan2(this.#serverLabel)}${project ? ` ${ink42("\xB7")} ${ink32(project)}` : ""}`;
1858
+ const leftPlain = ` \u258Drig ${screenName}`;
1859
+ const pad = Math.max(1, width - leftPlain.length - rightPlain.length - 1);
1860
+ return ` ${accent2("\u258D")}${bold2(ink5("rig"))} ${ink22(screenName)}${" ".repeat(pad)}${rightPainted}`;
1861
+ }
1862
+ #tabsLine(width) {
1863
+ const tabs = this.#tabScreens();
1864
+ const active = this.#tabFor(this.#view);
1865
+ this.#frame.tabs = [];
1866
+ let out = " ";
1867
+ let plainLength = 1;
1868
+ for (let index = 0;index < tabs.length; index += 1) {
1869
+ const tab = tabs[index];
1870
+ const text = ` ${index + 1} ${tab.label} `;
1871
+ this.#frame.tabs.push({ screen: tab.screen, start: plainLength, end: plainLength + text.length - 1 });
1872
+ out += tab.screen === active ? bgPanel2(`${accent2(` ${index + 1}`)} ${bold2(ink5(`${tab.label} `))}`) : `${ink42(` ${index + 1}`)} ${ink32(`${tab.label} `)}`;
1873
+ plainLength += text.length;
1874
+ out += " ";
1875
+ plainLength += 1;
1876
+ }
1877
+ const helpPlain = "? help";
1878
+ const pad = Math.max(1, width - plainLength - helpPlain.length - 1);
1879
+ return `${out}${" ".repeat(pad)}${accent2("?")}${ink32(" help")}`;
1738
1880
  }
1739
1881
  #noticeText() {
1740
1882
  if (!this.#notice)
1741
1883
  return null;
1742
- const prefix = this.#busyDepth > 0 ? `${RIG_SPINNER_FRAMES2[this.#tick % RIG_SPINNER_FRAMES2.length]} ` : "";
1884
+ const prefix = this.#busyDepth > 0 ? `${RIG_SPINNER_FRAMES[this.#tick % RIG_SPINNER_FRAMES.length]} ` : "";
1743
1885
  return `${prefix}${this.#notice}`;
1744
1886
  }
1745
- #footerLines() {
1887
+ #footerLines(width) {
1746
1888
  if (this.#inputLine) {
1747
1889
  const line = this.#inputLine;
1748
1890
  const label = line.kind === "search" ? `${accent2("/")}${ink5(line.buffer)}` : `${accentDim2("steer")} ${ink32(`(${line.run.runId.slice(0, 8)})`)} ${accent2("\u276F")} ${ink5(line.buffer)}`;
1749
1891
  return [
1750
1892
  ` ${label}${accent2("\u2588")}`,
1751
- ` ${accent2("enter")}${ink32(line.kind === "search" ? " keep filter" : " send steer")} ${accent2("esc")}${ink32(line.kind === "search" ? " clear" : " cancel")}`
1893
+ keyHints(line.kind === "search" ? [["enter", "keep filter"], ["esc", "clear"]] : [["enter", "send"], ["esc", "cancel"]])
1894
+ ];
1895
+ }
1896
+ if (this.#error) {
1897
+ const message = this.#error.length > width - 6 ? `${this.#error.slice(0, Math.max(8, width - 7))}\u2026` : this.#error;
1898
+ return [
1899
+ ` ${red2("\u2717")} ${red2(message)}`,
1900
+ keyHints([["esc", "dismiss"], ["r", "retry"], ["q", "quit"]])
1901
+ ];
1902
+ }
1903
+ const notice = this.#noticeText();
1904
+ if (this.#view === "help") {
1905
+ return [
1906
+ ` ${notice ? accentDim2(notice) : ink42("every key on every screen")}`,
1907
+ keyHints([["\u2191\u2193", "scroll"], ["\u2190", "back"], ["esc", "cockpit"], ["q", "quit"]])
1752
1908
  ];
1753
1909
  }
1754
- if (this.#error)
1755
- return [` ${red2(`error: ${this.#error}`)}`, ` ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`];
1756
- if (this.#view === "help")
1757
- return [` ${accent2("\u2190")}${ink32(" back")} ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`];
1758
1910
  const domainFooter = this.#domainViews.get(this.#view)?.footer;
1759
1911
  if (domainFooter)
1760
- return domainFooter(this.#noticeText());
1912
+ return domainFooter(notice);
1761
1913
  if (this.#view === "task-detail" || this.#view === "run-detail") {
1762
1914
  const isTask = this.#view === "task-detail";
1763
- const notice2 = this.#noticeText();
1764
- const hint = notice2 ? accentDim2(notice2) : ink42(isTask ? "review the task, then dispatch" : "live run controls");
1915
+ const hint = notice ? accentDim2(notice) : ink42(isTask ? "read it, then send a drone at it" : "live run controls \u2014 resolve gates in place");
1765
1916
  return [
1766
1917
  ` ${hint}`,
1767
- ` ${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")}`
1918
+ keyHints([["\u2191\u2193", "action"], ["enter", isTask ? "dispatch" : "run"], ["\u21DE\u21DF", "scroll"], ["\u2190", "back"], ["q", "quit"]])
1768
1919
  ];
1769
1920
  }
1770
- const notice = this.#noticeText();
1921
+ if (this.#view === "cockpit") {
1922
+ const live = liveRunCount(this.#runs, this.#state);
1923
+ const attention = attentionRunCount(this.#runs, this.#state);
1924
+ const fleet = this.#runs.length === 0 ? "fleet idle \u2014 dispatch something" : `${live} live \xB7 ${attention} waiting on you \xB7 ${this.#runs.length} total`;
1925
+ return [
1926
+ ` ${notice ? accentDim2(notice) : ink42(fleet)}`,
1927
+ keyHints([["\u2191\u2193", "move"], ["enter", "open"], ["1-9", "jump"], ["?", "help"], ["q", "quit"]])
1928
+ ];
1929
+ }
1930
+ const purpose = this.catalog.find((entry) => entry.id === this.#view)?.nav?.description ?? "";
1771
1931
  return [
1772
- ` ${notice ? accentDim2(notice) : ink42(`screen: ${screenLabel(this.#view, this.catalog)}`)}`,
1773
- ` ${accent2("\u2191\u2193")}${ink32(" move")} ${accent2("enter")}${ink32(" act/open")} ${accent2("\u2190")}${ink32(" back")} ${accent2("esc")}${ink32(" cockpit")} ${accent2("q")}${ink32(" quit")}`
1932
+ ` ${notice ? accentDim2(notice) : ink42(purpose || screenLabel(this.#view, this.catalog))}`,
1933
+ keyHints([["\u2191\u2193", "move"], ["enter", "act"], ["r", "reload"], ["\u2190", "back"], ["esc", "cockpit"], ["q", "quit"]])
1774
1934
  ];
1775
1935
  }
1776
1936
  async#attach(runId) {
@@ -1781,21 +1941,6 @@ class RigFlowComponent {
1781
1941
  if (!this.#error)
1782
1942
  this.done();
1783
1943
  }
1784
- async#stop(run) {
1785
- this.#notice = `stop requested for ${run.runId.slice(0, 8)}\u2026`;
1786
- this.tui.requestRender();
1787
- await this.#runAction(() => this.actions.act(`run-stop:${encodeURIComponent(run.runId)}`));
1788
- }
1789
- async#pause(run) {
1790
- this.#notice = `pause requested for ${run.runId.slice(0, 8)}\u2026`;
1791
- this.tui.requestRender();
1792
- await this.#runAction(() => this.actions.act(`run-pause:${encodeURIComponent(run.runId)}`));
1793
- }
1794
- async#resume(run) {
1795
- this.#notice = `resume requested for ${run.runId.slice(0, 8)}\u2026`;
1796
- this.tui.requestRender();
1797
- await this.#runAction(() => this.actions.act(`run-resume:${encodeURIComponent(run.runId)}`));
1798
- }
1799
1944
  async#steer(run, message) {
1800
1945
  await this.#runAction(() => this.actions.act(`run-steer:${encodeURIComponent(run.runId)}`, message));
1801
1946
  }
@@ -1829,6 +1974,10 @@ class RigFlowComponent {
1829
1974
  }
1830
1975
  return;
1831
1976
  }
1977
+ if (item.id.endsWith(":retry")) {
1978
+ await this.#runAction(async () => {});
1979
+ return;
1980
+ }
1832
1981
  if (item.id === "config:relayUrl" || item.id.startsWith("stats:") || item.id.startsWith("inspect:audit")) {
1833
1982
  this.#notice = "informational row; no cockpit action";
1834
1983
  this.tui.requestRender();
@@ -1844,12 +1993,15 @@ class RigFlowComponent {
1844
1993
  await this.#runAction((onProgress) => this.actions.act(item.id, undefined, onProgress));
1845
1994
  }
1846
1995
  render(width) {
1847
- const lines = [this.#headerLine(), hairline(width)];
1996
+ const lines = [this.#headerLine(width), this.#tabsLine(width), hairline(width)];
1997
+ this.#frame.tabsLine = 1;
1848
1998
  const view = this.#view === "help" ? this.#helpView : this.#activeView();
1999
+ this.#frame.viewStart = lines.length;
1849
2000
  for (const line of view.render(width))
1850
2001
  lines.push(line);
2002
+ this.#frame.viewEnd = lines.length - 1;
1851
2003
  lines.push(hairline(width));
1852
- for (const line of this.#footerLines())
2004
+ for (const line of this.#footerLines(width))
1853
2005
  lines.push(line);
1854
2006
  return lines;
1855
2007
  }
@@ -1859,7 +2011,95 @@ class RigFlowComponent {
1859
2011
  this.#helpView.invalidate();
1860
2012
  this.#genericView.invalidate();
1861
2013
  }
2014
+ #activateDetailAction() {
2015
+ const action = this.#detailComposer.selectedAction();
2016
+ if (!action)
2017
+ return;
2018
+ if (action.id.startsWith("run-steer:")) {
2019
+ const run = findRunById(this.#runs, this.#selectedRunId);
2020
+ if (run) {
2021
+ this.#inputLine = { kind: "steer", buffer: "", run: { runId: run.runId } };
2022
+ this.tui.requestRender();
2023
+ }
2024
+ return;
2025
+ }
2026
+ if (action.id.startsWith("task-detail:dispatch:")) {
2027
+ this.#notice = "dispatch \xB7 starting\u2026";
2028
+ this.tui.requestRender();
2029
+ }
2030
+ if (action.id.startsWith("run-stop:")) {
2031
+ this.#notice = "stop requested\u2026";
2032
+ this.tui.requestRender();
2033
+ }
2034
+ this.#actOnItem({ id: action.id, label: action.label, values: [action.value ?? "go"] });
2035
+ }
2036
+ #handleMouse(event) {
2037
+ if (this.#inputLine)
2038
+ return;
2039
+ const { tabsLine, viewStart, viewEnd, tabs } = this.#frame;
2040
+ if (event.wheel !== null) {
2041
+ if (this.#view === "help")
2042
+ this.#helpView.scroll(event.wheel * 3);
2043
+ else if (this.#view === "task-detail" || this.#view === "run-detail")
2044
+ this.#detailComposer.scrollBody(event.wheel * 2);
2045
+ else {
2046
+ const domainView2 = this.#domainViews.get(this.#view);
2047
+ if (domainView2) {
2048
+ if (!domainView2.handleMouse?.(event, Math.max(0, event.row - viewStart)))
2049
+ return;
2050
+ } else {
2051
+ this.#genericView.moveSelection(event.wheel);
2052
+ }
2053
+ }
2054
+ this.tui.requestRender();
2055
+ return;
2056
+ }
2057
+ if (!event.leftClick)
2058
+ return;
2059
+ if (event.row === tabsLine) {
2060
+ const hit = tabs.find((tab) => event.col >= tab.start && event.col <= tab.end);
2061
+ if (hit && hit.screen !== this.#view)
2062
+ this.#navigate(hit.screen);
2063
+ return;
2064
+ }
2065
+ if (event.row < viewStart || event.row > viewEnd)
2066
+ return;
2067
+ const line = event.row - viewStart;
2068
+ if (this.#view === "help")
2069
+ return;
2070
+ if (this.#view === "task-detail" || this.#view === "run-detail") {
2071
+ const index2 = this.#detailComposer.hitTestAction(line);
2072
+ if (index2 === undefined)
2073
+ return;
2074
+ const moved2 = this.#detailComposer.selectAction(index2);
2075
+ this.tui.requestRender();
2076
+ if (!moved2)
2077
+ this.#activateDetailAction();
2078
+ return;
2079
+ }
2080
+ const domainView = this.#domainViews.get(this.#view);
2081
+ if (domainView) {
2082
+ domainView.handleMouse?.(event, line);
2083
+ return;
2084
+ }
2085
+ const index = this.#genericView.hitTest(line);
2086
+ if (index === undefined)
2087
+ return;
2088
+ const moved = this.#genericView.selectIndex(index);
2089
+ this.tui.requestRender();
2090
+ if (!moved) {
2091
+ const item = this.#genericView.selectedItem();
2092
+ if (item)
2093
+ this.#actOnItem(item);
2094
+ }
2095
+ }
1862
2096
  handleInput(data) {
2097
+ if (data.startsWith("\x1B[<")) {
2098
+ const event = parseSgrMouse(data);
2099
+ if (event)
2100
+ this.#handleMouse(event);
2101
+ return;
2102
+ }
1863
2103
  if (data === "q") {
1864
2104
  this.onQuit();
1865
2105
  return;
@@ -1894,6 +2134,19 @@ class RigFlowComponent {
1894
2134
  this.done();
1895
2135
  return;
1896
2136
  }
2137
+ if (this.#error && matchesKey(data, "escape")) {
2138
+ this.#error = "";
2139
+ this.tui.requestRender();
2140
+ return;
2141
+ }
2142
+ if (/^[1-9]$/.test(data)) {
2143
+ const target = this.#tabScreens()[Number(data) - 1];
2144
+ if (target) {
2145
+ if (target.screen !== this.#view)
2146
+ this.#navigate(target.screen);
2147
+ return;
2148
+ }
2149
+ }
1897
2150
  if (this.#view === "help") {
1898
2151
  if (matchesKey(data, "up") || data === "k")
1899
2152
  this.#helpView.scroll(-1);
@@ -1922,26 +2175,7 @@ class RigFlowComponent {
1922
2175
  return;
1923
2176
  }
1924
2177
  if (matchesKey(data, "enter") || matchesKey(data, "return")) {
1925
- const action = this.#detailComposer.selectedAction();
1926
- if (action) {
1927
- if (action.id.startsWith("run-steer:")) {
1928
- const run = findRunById(this.#runs, this.#selectedRunId);
1929
- if (run) {
1930
- this.#inputLine = { kind: "steer", buffer: "", run: { runId: run.runId } };
1931
- this.tui.requestRender();
1932
- }
1933
- return;
1934
- }
1935
- if (action.id.startsWith("task-detail:dispatch:")) {
1936
- this.#notice = "dispatch \xB7 starting\u2026";
1937
- this.tui.requestRender();
1938
- }
1939
- if (action.id.startsWith("run-stop:")) {
1940
- this.#notice = "stop requested\u2026";
1941
- this.tui.requestRender();
1942
- }
1943
- this.#actOnItem({ id: action.id, label: action.label, values: [action.value ?? "go"] });
1944
- }
2178
+ this.#activateDetailAction();
1945
2179
  return;
1946
2180
  }
1947
2181
  if (matchesKey(data, "pageUp")) {