@h-rig/cli 0.0.6-alpha.86 → 0.0.6-alpha.88

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.
Files changed (80) hide show
  1. package/dist/bin/rig.js +287 -578
  2. package/dist/src/app-opentui/adapters/common.d.ts +3 -0
  3. package/dist/src/app-opentui/adapters/common.js +4 -0
  4. package/dist/src/app-opentui/adapters/family.js +31 -4
  5. package/dist/src/app-opentui/adapters/pi-attach.d.ts +7 -0
  6. package/dist/src/app-opentui/adapters/pi-attach.js +527 -473
  7. package/dist/src/app-opentui/adapters/tasks.js +82 -468
  8. package/dist/src/app-opentui/bootstrap.js +287 -578
  9. package/dist/src/app-opentui/command-palette.js +1 -0
  10. package/dist/src/app-opentui/drone.js +1 -0
  11. package/dist/src/app-opentui/index.js +127 -446
  12. package/dist/src/app-opentui/keymap.js +2 -387
  13. package/dist/src/app-opentui/list-search.d.ts +5 -1
  14. package/dist/src/app-opentui/list-search.js +2 -2
  15. package/dist/src/app-opentui/pi-host-child.js +31 -4
  16. package/dist/src/app-opentui/pi-pty-host.d.ts +14 -64
  17. package/dist/src/app-opentui/pi-pty-host.js +3 -397
  18. package/dist/src/app-opentui/react/App.js +107 -433
  19. package/dist/src/app-opentui/react/Backdrop.js +1 -0
  20. package/dist/src/app-opentui/react/ChromeHost.js +34 -410
  21. package/dist/src/app-opentui/react/SceneFrameView.js +55 -5
  22. package/dist/src/app-opentui/react/launch.js +171 -472
  23. package/dist/src/app-opentui/react/syntax.js +1 -0
  24. package/dist/src/app-opentui/registry.js +99 -487
  25. package/dist/src/app-opentui/render/graphics.js +1 -0
  26. package/dist/src/app-opentui/render/hover.d.ts +11 -0
  27. package/dist/src/app-opentui/render/hover.js +34 -0
  28. package/dist/src/app-opentui/render/native-host.js +1 -0
  29. package/dist/src/app-opentui/render/panels.js +50 -3
  30. package/dist/src/app-opentui/render/preloader.js +1 -0
  31. package/dist/src/app-opentui/render/scene.js +1 -0
  32. package/dist/src/app-opentui/render/terminal-handoff.d.ts +16 -0
  33. package/dist/src/app-opentui/render/terminal-handoff.js +14 -0
  34. package/dist/src/app-opentui/render/text.d.ts +13 -0
  35. package/dist/src/app-opentui/render/text.js +54 -5
  36. package/dist/src/app-opentui/render/type-bar.js +1 -0
  37. package/dist/src/app-opentui/runtime.js +127 -446
  38. package/dist/src/app-opentui/scenes/command.js +1 -0
  39. package/dist/src/app-opentui/scenes/doctor.js +1 -0
  40. package/dist/src/app-opentui/scenes/error.js +1 -0
  41. package/dist/src/app-opentui/scenes/family-domains/agent.js +1 -0
  42. package/dist/src/app-opentui/scenes/family-domains/browser.js +1 -0
  43. package/dist/src/app-opentui/scenes/family-domains/dist.js +1 -0
  44. package/dist/src/app-opentui/scenes/family-domains/git.js +1 -0
  45. package/dist/src/app-opentui/scenes/family-domains/github.js +1 -0
  46. package/dist/src/app-opentui/scenes/family-domains/harness.js +1 -0
  47. package/dist/src/app-opentui/scenes/family-domains/index.js +1 -0
  48. package/dist/src/app-opentui/scenes/family-domains/kit.js +1 -0
  49. package/dist/src/app-opentui/scenes/family-domains/profile.js +1 -0
  50. package/dist/src/app-opentui/scenes/family-domains/queue.js +1 -0
  51. package/dist/src/app-opentui/scenes/family-domains/remote.js +1 -0
  52. package/dist/src/app-opentui/scenes/family-domains/review.js +1 -0
  53. package/dist/src/app-opentui/scenes/family-domains/setup.js +1 -0
  54. package/dist/src/app-opentui/scenes/family-domains/stats.js +1 -0
  55. package/dist/src/app-opentui/scenes/family.js +1 -0
  56. package/dist/src/app-opentui/scenes/fleet.js +3 -5
  57. package/dist/src/app-opentui/scenes/handoff.js +1 -0
  58. package/dist/src/app-opentui/scenes/help.js +1 -0
  59. package/dist/src/app-opentui/scenes/inbox.js +1 -0
  60. package/dist/src/app-opentui/scenes/init.js +1 -0
  61. package/dist/src/app-opentui/scenes/inspect.js +1 -0
  62. package/dist/src/app-opentui/scenes/main.js +1 -0
  63. package/dist/src/app-opentui/scenes/pi.js +1 -0
  64. package/dist/src/app-opentui/scenes/plugin.js +1 -0
  65. package/dist/src/app-opentui/scenes/repo.js +1 -0
  66. package/dist/src/app-opentui/scenes/run-detail.js +1 -0
  67. package/dist/src/app-opentui/scenes/server.js +1 -0
  68. package/dist/src/app-opentui/scenes/tasks.js +3 -5
  69. package/dist/src/app-opentui/scenes/workspace.js +1 -0
  70. package/dist/src/app-opentui/selectable.js +1 -0
  71. package/dist/src/app-opentui/theme.d.ts +1 -0
  72. package/dist/src/app-opentui/theme.js +1 -0
  73. package/dist/src/commands/_operator-view.js +31 -4
  74. package/dist/src/commands/_pi-frontend.d.ts +25 -0
  75. package/dist/src/commands/_pi-frontend.js +32 -4
  76. package/dist/src/commands/run.js +31 -4
  77. package/dist/src/commands/task.js +31 -4
  78. package/dist/src/commands.js +31 -4
  79. package/dist/src/index.js +31 -4
  80. package/package.json +8 -8
package/dist/bin/rig.js CHANGED
@@ -1346,6 +1346,14 @@ var init__async_ui = __esm(() => {
1346
1346
  });
1347
1347
 
1348
1348
  // packages/cli/src/commands/_pi-frontend.ts
1349
+ var exports__pi_frontend = {};
1350
+ __export(exports__pi_frontend, {
1351
+ shouldRequireOperatorTranscript: () => shouldRequireOperatorTranscript,
1352
+ runWithProcessExitGuard: () => runWithProcessExitGuard,
1353
+ missingOperatorTranscriptMessage: () => missingOperatorTranscriptMessage,
1354
+ buildOperatorPiEnv: () => buildOperatorPiEnv,
1355
+ attachRunBundledPiFrontend: () => attachRunBundledPiFrontend
1356
+ });
1349
1357
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1350
1358
  import { homedir as homedir2, tmpdir } from "os";
1351
1359
  import { join } from "path";
@@ -1458,6 +1466,29 @@ function installRigPiTheme() {
1458
1466
  }
1459
1467
  } catch {}
1460
1468
  }
1469
+ async function runWithProcessExitGuard(body) {
1470
+ const realExit = process.exit;
1471
+ let exitCode = 0;
1472
+ let signalQuit = () => {};
1473
+ const quit = new Promise((resolve4) => {
1474
+ signalQuit = resolve4;
1475
+ });
1476
+ const guardedExit = (code) => {
1477
+ exitCode = typeof code === "number" ? code : 0;
1478
+ signalQuit();
1479
+ return;
1480
+ };
1481
+ process.exit = guardedExit;
1482
+ try {
1483
+ await Promise.race([Promise.resolve().then(body), quit]);
1484
+ } finally {
1485
+ process.exit = realExit;
1486
+ }
1487
+ return exitCode;
1488
+ }
1489
+ function runPiMainReturningOnQuit(args, options) {
1490
+ return runWithProcessExitGuard(() => runPiMain(args, options));
1491
+ }
1461
1492
  async function attachRunBundledPiFrontend(context, input) {
1462
1493
  const tempSessionDir = mkdtempSync(join(tmpdir(), "rig-pi-frontend-sessions-"));
1463
1494
  const { server, sessionFileArg } = await withSpinner(`Opening Pi console for run ${input.runId}\u2026`, () => prepareOperatorConsole(context, input.runId, tempSessionDir), { outputMode: context.outputMode });
@@ -1473,16 +1504,20 @@ async function attachRunBundledPiFrontend(context, input) {
1473
1504
  };
1474
1505
  let detached = false;
1475
1506
  try {
1476
- await runPiMain([
1507
+ const piArgs = [
1477
1508
  "--offline",
1478
1509
  "--no-extensions",
1479
1510
  "--no-skills",
1480
1511
  "--no-prompt-templates",
1481
1512
  "--no-context-files",
1482
1513
  ...sessionFileArg
1483
- ], {
1484
- extensionFactories: [piRigExtensionFactory]
1485
- });
1514
+ ];
1515
+ const piOptions = { extensionFactories: [piRigExtensionFactory] };
1516
+ if (input.returnOnQuit) {
1517
+ await runPiMainReturningOnQuit(piArgs, piOptions);
1518
+ } else {
1519
+ await runPiMain(piArgs, piOptions);
1520
+ }
1486
1521
  detached = true;
1487
1522
  } finally {
1488
1523
  restoreEnv();
@@ -1695,6 +1730,21 @@ async function captureConsole(fn) {
1695
1730
  console.error = original.error;
1696
1731
  }
1697
1732
  }
1733
+ async function releaseRendererForExternalTui(ctx) {
1734
+ const renderer = ctx.renderer;
1735
+ if (!renderer)
1736
+ return;
1737
+ if (renderer.suspend) {
1738
+ await renderer.suspend();
1739
+ return;
1740
+ }
1741
+ if (renderer.destroy) {
1742
+ await renderer.destroy();
1743
+ }
1744
+ }
1745
+ async function resumeRendererAfterExternalTui(ctx) {
1746
+ await ctx.renderer?.resume?.();
1747
+ }
1698
1748
  function arrayFromPayload(value) {
1699
1749
  if (Array.isArray(value)) {
1700
1750
  return value.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
@@ -15341,397 +15391,6 @@ async function runInitYes(ctx, args = ["--yes"]) {
15341
15391
  }
15342
15392
  var init_init2 = () => {};
15343
15393
 
15344
- // packages/cli/src/app-opentui/pi-pty-host.ts
15345
- import { fileURLToPath as fileURLToPath2 } from "url";
15346
- import { basename as basename4 } from "path";
15347
- import { RGBA, StyledText, TextAttributes } from "@opentui/core";
15348
- import { Terminal as XtermTerminal2 } from "@xterm/headless";
15349
- function clampCols2(cols) {
15350
- return Math.max(MIN_COLS2, Math.trunc(cols || 100));
15351
- }
15352
- function clampRows2(rows) {
15353
- return Math.max(MIN_ROWS2, Math.min(MAX_ROWS2, Math.trunc(rows || 35)));
15354
- }
15355
- function rgbaFromXtermColor(mode, value) {
15356
- if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
15357
- return RGBA.fromIndex(value);
15358
- }
15359
- if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
15360
- return RGBA.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
15361
- }
15362
- return;
15363
- }
15364
- function textAttributesFromCell(cell) {
15365
- let attributes = TextAttributes.NONE;
15366
- if (cell.isBold())
15367
- attributes |= TextAttributes.BOLD;
15368
- if (cell.isDim())
15369
- attributes |= TextAttributes.DIM;
15370
- if (cell.isItalic())
15371
- attributes |= TextAttributes.ITALIC;
15372
- if (cell.isUnderline())
15373
- attributes |= TextAttributes.UNDERLINE;
15374
- if (cell.isBlink())
15375
- attributes |= TextAttributes.BLINK;
15376
- if (cell.isInverse())
15377
- attributes |= TextAttributes.INVERSE;
15378
- if (cell.isInvisible())
15379
- attributes |= TextAttributes.HIDDEN;
15380
- if (cell.isStrikethrough())
15381
- attributes |= TextAttributes.STRIKETHROUGH;
15382
- return attributes;
15383
- }
15384
- function sameRgba(a, b) {
15385
- if (!a && !b)
15386
- return true;
15387
- return Boolean(a && b && a.equals(b));
15388
- }
15389
- function sameStyle(a, b) {
15390
- return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
15391
- }
15392
- function styleFromCell(cell) {
15393
- return {
15394
- fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
15395
- bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
15396
- attributes: textAttributesFromCell(cell)
15397
- };
15398
- }
15399
- function lineToStyledText(line, cols) {
15400
- if (!line)
15401
- return new StyledText([{ __isChunk: true, text: "" }]);
15402
- const chunks = [];
15403
- let run = "";
15404
- let runStyle = null;
15405
- const flush = () => {
15406
- if (!run)
15407
- return;
15408
- chunks.push({
15409
- __isChunk: true,
15410
- text: run,
15411
- ...runStyle?.fg ? { fg: runStyle.fg } : {},
15412
- ...runStyle?.bg ? { bg: runStyle.bg } : {},
15413
- ...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
15414
- });
15415
- run = "";
15416
- };
15417
- for (let index = 0;index < cols; index += 1) {
15418
- const cell = line.getCell(index);
15419
- if (!cell) {
15420
- if (runStyle !== null)
15421
- flush();
15422
- runStyle = null;
15423
- run += " ";
15424
- continue;
15425
- }
15426
- if (cell.getWidth() === 0)
15427
- continue;
15428
- const style = styleFromCell(cell);
15429
- if (!runStyle || !sameStyle(runStyle, style)) {
15430
- flush();
15431
- runStyle = style;
15432
- }
15433
- run += cell.getChars() || " ";
15434
- }
15435
- flush();
15436
- return new StyledText(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
15437
- }
15438
- function childCommandPrefix2() {
15439
- const execName = basename4(process.execPath).toLowerCase();
15440
- const currentEntry = process.argv[1];
15441
- if (execName === "bun" || execName === "bun.exe") {
15442
- return currentEntry ? [process.execPath, currentEntry, "__opentui-pi-host"] : [process.execPath, fallbackChildScriptPath];
15443
- }
15444
- return [process.execPath, "__opentui-pi-host"];
15445
- }
15446
- function withEnv2(base) {
15447
- const env = {};
15448
- for (const [key, value] of Object.entries(base ?? process.env)) {
15449
- if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP" || key === "NO_COLOR")
15450
- continue;
15451
- if (typeof value === "string")
15452
- env[key] = value;
15453
- }
15454
- return {
15455
- ...env,
15456
- TERM: "xterm-256color",
15457
- COLORTERM: "truecolor",
15458
- FORCE_COLOR: env.FORCE_COLOR ?? "1",
15459
- RIG_OPENTUI_PI_HOST: "1"
15460
- };
15461
- }
15462
- function getActivePiHost() {
15463
- return activeHost2 && !activeHost2.disposed ? activeHost2 : null;
15464
- }
15465
- async function startPiPtyHost(options) {
15466
- activeHost2?.dispose("replace");
15467
- const host = new PiPtyHost(options);
15468
- activeHost2 = host;
15469
- await host.start();
15470
- return host;
15471
- }
15472
- function stopActivePiHost(reason = "detach") {
15473
- activeHost2?.dispose(reason);
15474
- activeHost2 = null;
15475
- }
15476
-
15477
- class PiPtyHost {
15478
- runId;
15479
- projectRoot;
15480
- onSnapshot;
15481
- onExit;
15482
- onError;
15483
- terminal;
15484
- disposables = [];
15485
- decoder = new TextDecoder("utf-8");
15486
- proc = null;
15487
- pty = null;
15488
- status = "starting";
15489
- cols;
15490
- rows;
15491
- message = "starting bundled Pi";
15492
- lastResizeError = null;
15493
- exitCode;
15494
- signal;
15495
- notifyTimer = null;
15496
- lastStreamKey = "";
15497
- _disposed = false;
15498
- constructor(options) {
15499
- this.runId = options.runId;
15500
- this.projectRoot = options.projectRoot;
15501
- this.cols = clampCols2(options.cols);
15502
- this.rows = clampRows2(options.rows);
15503
- this.onSnapshot = options.onSnapshot;
15504
- this.onExit = options.onExit;
15505
- this.onError = options.onError;
15506
- this.terminal = new XtermTerminal2({
15507
- allowProposedApi: true,
15508
- cols: this.cols,
15509
- rows: this.rows,
15510
- scrollback: 1000
15511
- });
15512
- this.registerTerminalResponders();
15513
- this.disposables.push(this.terminal.onWriteParsed(() => {
15514
- if (this._disposed)
15515
- return;
15516
- const snapshot = this.createSnapshot();
15517
- const key = snapshot.lines.join(`
15518
- `);
15519
- if (key === this.lastStreamKey)
15520
- return;
15521
- this.lastStreamKey = key;
15522
- this.onSnapshot?.(snapshot);
15523
- }));
15524
- }
15525
- get disposed() {
15526
- return this._disposed;
15527
- }
15528
- get snapshot() {
15529
- return this.createSnapshot();
15530
- }
15531
- async start() {
15532
- if (this._disposed)
15533
- throw new Error("Pi PTY host is disposed.");
15534
- if (typeof Bun.Terminal !== "function") {
15535
- throw new Error("Bun native PTY is unavailable in this runtime. Pi host requires Bun.Terminal.");
15536
- }
15537
- const spawnOptions = {
15538
- cwd: this.projectRoot,
15539
- env: withEnv2(process.env),
15540
- terminal: {
15541
- cols: this.cols,
15542
- rows: this.rows,
15543
- name: "xterm-256color",
15544
- data: (_terminal, data) => this.handlePtyData(data)
15545
- }
15546
- };
15547
- const proc = Bun.spawn([
15548
- ...childCommandPrefix2(),
15549
- "--run-id",
15550
- this.runId,
15551
- "--project-root",
15552
- this.projectRoot
15553
- ], spawnOptions);
15554
- if (!proc.terminal)
15555
- throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
15556
- this.proc = proc;
15557
- this.pty = proc.terminal;
15558
- this.status = "running";
15559
- this.message = "bundled Pi running inside this app";
15560
- this.emitSnapshotSoon(0);
15561
- proc.exited.then((exitCode) => {
15562
- if (this._disposed)
15563
- return;
15564
- this.status = exitCode === 0 ? "exited" : "failed";
15565
- this.exitCode = exitCode;
15566
- this.signal = null;
15567
- this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
15568
- const snapshot = this.createSnapshot();
15569
- this.onSnapshot?.(snapshot);
15570
- this.onExit?.(snapshot);
15571
- if (activeHost2 === this)
15572
- activeHost2 = null;
15573
- this.dispose("exit", { kill: false, notify: false });
15574
- }).catch((error) => {
15575
- if (this._disposed)
15576
- return;
15577
- this.status = "failed";
15578
- this.message = error instanceof Error ? error.message : String(error);
15579
- const snapshot = this.createSnapshot();
15580
- this.onSnapshot?.(snapshot);
15581
- this.onError?.(error, snapshot);
15582
- if (activeHost2 === this)
15583
- activeHost2 = null;
15584
- this.dispose("error", { kill: false, notify: false });
15585
- });
15586
- }
15587
- write(data) {
15588
- if (this._disposed || !this.pty)
15589
- return;
15590
- try {
15591
- this.pty.write(data);
15592
- } catch (error) {
15593
- this.status = "failed";
15594
- this.message = error instanceof Error ? error.message : String(error);
15595
- const snapshot = this.createSnapshot();
15596
- this.onSnapshot?.(snapshot);
15597
- this.onError?.(error, snapshot);
15598
- }
15599
- }
15600
- resize(cols, rows) {
15601
- const nextCols = clampCols2(cols);
15602
- const nextRows = clampRows2(rows);
15603
- if (nextCols === this.cols && nextRows === this.rows)
15604
- return;
15605
- this.cols = nextCols;
15606
- this.rows = nextRows;
15607
- this.terminal.resize(nextCols, nextRows);
15608
- try {
15609
- this.pty?.resize(nextCols, nextRows);
15610
- this.lastResizeError = null;
15611
- } catch (error) {
15612
- this.lastResizeError = error instanceof Error ? error.message : String(error);
15613
- }
15614
- this.emitSnapshotSoon(0);
15615
- }
15616
- detach() {
15617
- this.dispose("detach");
15618
- return this.createSnapshot("detached from bundled Pi");
15619
- }
15620
- dispose(reason = "dispose", options = {}) {
15621
- if (this._disposed)
15622
- return;
15623
- this._disposed = true;
15624
- if (this.notifyTimer)
15625
- clearTimeout(this.notifyTimer);
15626
- this.notifyTimer = null;
15627
- for (const disposable of this.disposables.splice(0)) {
15628
- try {
15629
- disposable.dispose();
15630
- } catch {}
15631
- }
15632
- if (options.kill !== false) {
15633
- try {
15634
- this.proc?.kill("SIGTERM");
15635
- } catch {}
15636
- }
15637
- try {
15638
- this.pty?.close();
15639
- } catch {}
15640
- try {
15641
- this.terminal.dispose();
15642
- } catch {}
15643
- if (activeHost2 === this)
15644
- activeHost2 = null;
15645
- if (options.notify) {
15646
- this.message = reason;
15647
- this.onSnapshot?.(this.createSnapshot(reason));
15648
- }
15649
- }
15650
- handlePtyData(data) {
15651
- if (this._disposed)
15652
- return;
15653
- const text2 = this.decoder.decode(data, { stream: true });
15654
- this.respondToRawTerminalQueries(text2);
15655
- this.terminal.write(data);
15656
- }
15657
- emitSnapshotSoon(delayMs = SNAPSHOT_DELAY_MS2) {
15658
- if (this._disposed || this.notifyTimer)
15659
- return;
15660
- this.notifyTimer = setTimeout(() => {
15661
- this.notifyTimer = null;
15662
- if (this._disposed)
15663
- return;
15664
- this.onSnapshot?.(this.createSnapshot());
15665
- }, delayMs);
15666
- }
15667
- createSnapshot(message2 = this.message) {
15668
- const buffer = this.terminal.buffer.active;
15669
- const end = buffer.length;
15670
- const start = Math.max(0, end - MAX_SNAPSHOT_LINES2);
15671
- const styledStart = Math.max(start, end - this.rows - STYLED_SNAPSHOT_MARGIN);
15672
- const lines = [];
15673
- const styledLines = [];
15674
- for (let row = start;row < end; row += 1) {
15675
- const line = buffer.getLine(row);
15676
- lines.push((line?.translateToString(false) ?? "").slice(0, this.cols));
15677
- if (row >= styledStart)
15678
- styledLines[lines.length - 1] = lineToStyledText(line, this.cols);
15679
- }
15680
- while (lines.length < this.rows) {
15681
- lines.push("");
15682
- styledLines[lines.length - 1] = new StyledText([{ __isChunk: true, text: "" }]);
15683
- }
15684
- const resolvedMessage = this.lastResizeError ? `resize degraded: ${this.lastResizeError}` : message2;
15685
- return {
15686
- runId: this.runId,
15687
- status: this.status,
15688
- cols: this.cols,
15689
- rows: this.rows,
15690
- lines,
15691
- styledLines,
15692
- message: resolvedMessage,
15693
- ...this.lastResizeError ? { resizeError: this.lastResizeError } : {},
15694
- ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
15695
- ...this.signal !== undefined ? { signal: this.signal } : {}
15696
- };
15697
- }
15698
- registerTerminalResponders() {
15699
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
15700
- if (params.length === 0 || params[0] === 0)
15701
- this.write("\x1B[?62;22c");
15702
- return false;
15703
- }));
15704
- this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
15705
- if (params.length === 0 || params[0] === 0)
15706
- this.write("\x1B[>0;0;0c");
15707
- return false;
15708
- }));
15709
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
15710
- if (params[0] === 5) {
15711
- this.write("\x1B[0n");
15712
- } else if (params[0] === 6) {
15713
- const row = this.terminal.buffer.active.cursorY + 1;
15714
- const col = this.terminal.buffer.active.cursorX + 1;
15715
- this.write(`\x1B[${row};${col}R`);
15716
- }
15717
- return false;
15718
- }));
15719
- }
15720
- respondToRawTerminalQueries(text2) {
15721
- if (!text2)
15722
- return;
15723
- const decrqm = /\x1b\[\?(\d+)\$p/g;
15724
- let match;
15725
- while ((match = decrqm.exec(text2)) !== null) {
15726
- this.write(`\x1B[?${match[1]};2$y`);
15727
- }
15728
- }
15729
- }
15730
- var MIN_COLS2 = 40, MIN_ROWS2 = 12, MAX_ROWS2 = 300, MAX_SNAPSHOT_LINES2 = 360, STYLED_SNAPSHOT_MARGIN = 6, SNAPSHOT_DELAY_MS2 = 120, fallbackChildScriptPath, activeHost2 = null, XTERM_COLOR_MODE_INDEXED_16 = 16777216, XTERM_COLOR_MODE_INDEXED_256 = 33554432, XTERM_COLOR_MODE_RGB = 50331648;
15731
- var init_pi_pty_host = __esm(() => {
15732
- fallbackChildScriptPath = fileURLToPath2(new URL("./pi-host-child.ts", import.meta.url));
15733
- });
15734
-
15735
15394
  // packages/cli/src/app-opentui/adapters/inspect.ts
15736
15395
  var exports_inspect = {};
15737
15396
  __export(exports_inspect, {
@@ -15954,14 +15613,6 @@ function recordStep(ctx, runId, label, emitLabel) {
15954
15613
  });
15955
15614
  emitProgress(ctx, emitLabel, label);
15956
15615
  }
15957
- function settleSteps(ctx, runId) {
15958
- const now = Date.now();
15959
- const previous = currentAttachState(ctx, runId);
15960
- if (!previous?.steps)
15961
- return;
15962
- const steps = previous.steps.map((step) => step.status === "running" ? { ...step, status: "done", endedAtMs: step.endedAtMs ?? now } : step);
15963
- patchData(ctx, { piAttach: { ...previous, steps } });
15964
- }
15965
15616
  async function preparePiAttachHandoff(ctx, runId) {
15966
15617
  const cleanRunId = runId.trim();
15967
15618
  const label = `Preparing Pi ${cleanRunId.slice(0, 8)}`;
@@ -16009,85 +15660,41 @@ async function attachRunWithBundledPi(ctx, runId) {
16009
15660
  throw error;
16010
15661
  }
16011
15662
  emitStarted(ctx, label, { piAttach: { runId: cleanRunId, status: "entering-pi" } });
16012
- patchData(ctx, { piAttach: { runId: cleanRunId, status: "entering-pi", message: "starting bundled Pi", steps: [] } });
15663
+ patchData(ctx, { piAttach: { runId: cleanRunId, status: "entering-pi", message: "handing the terminal to worker Pi\u2026" } });
15664
+ const projectRoot = projectRootOf(ctx);
15665
+ const outputMode = ctx.rig?.outputMode ?? "text";
15666
+ await releaseRendererForExternalTui(ctx);
15667
+ let outcome = null;
15668
+ let attachError = null;
16013
15669
  try {
16014
- recordStep(ctx, cleanRunId, "connecting to server and fetching run transcript", label);
16015
- await preparePiAttachHandoff(ctx, cleanRunId).catch((error) => {
16016
- patchData(ctx, { lastRefreshError: normalizeAppError(error).message });
16017
- return null;
16018
- });
16019
- recordStep(ctx, cleanRunId, "spawning Bun PTY host for bundled Pi", label);
16020
- const projectRoot = projectRootOf(ctx);
16021
- const cols = typeof process.stdout.columns === "number" ? process.stdout.columns : 100;
16022
- const rows = Math.max(1, (typeof process.stdout.rows === "number" ? process.stdout.rows : 35) - 1);
16023
- const patchTerminal = (snapshot) => {
16024
- const previousSteps = currentAttachState(ctx, cleanRunId)?.steps;
16025
- const failed = snapshot.status === "failed";
16026
- const reason = failed ? snapshot.message ?? `bundled Pi exited with code ${snapshot.exitCode ?? "unknown"}` : undefined;
16027
- patchData(ctx, {
16028
- piTerminal: snapshot,
16029
- piAttach: {
16030
- runId: cleanRunId,
16031
- status: failed ? "failed" : snapshot.status === "exited" ? "returned" : "entering-pi",
16032
- message: snapshot.message ?? "bundled Pi running",
16033
- result: { runId: cleanRunId, status: snapshot.status, exitCode: snapshot.exitCode },
16034
- ...previousSteps ? { steps: previousSteps } : {},
16035
- ...reason ? { failureReason: reason } : {}
16036
- }
16037
- });
16038
- };
16039
- recordStep(ctx, cleanRunId, "starting bundled Pi", label);
16040
- await startPiPtyHost({
16041
- runId: cleanRunId,
16042
- projectRoot,
16043
- cols,
16044
- rows,
16045
- onSnapshot: (snapshot) => {
16046
- if (snapshot.status === "running" && snapshot.lines.some((entry) => entry.trim().length > 0)) {
16047
- settleSteps(ctx, cleanRunId);
16048
- }
16049
- patchTerminal(snapshot);
16050
- },
16051
- onExit: (snapshot) => {
16052
- patchTerminal(snapshot);
16053
- if (snapshot.status === "failed") {
16054
- Promise.resolve().then(() => (init_inspect2(), exports_inspect)).then(({ loadInspect: loadInspect2 }) => loadInspect2(ctx, cleanRunId)).catch(() => null);
16055
- emitFailed(ctx, label, new Error(snapshot.message ?? `bundled Pi exited with code ${snapshot.exitCode ?? "unknown"}`), { runId: cleanRunId, result: { status: snapshot.status, exitCode: snapshot.exitCode } });
16056
- return;
16057
- }
16058
- emitCompleted(ctx, label, { runId: cleanRunId, result: { status: snapshot.status, exitCode: snapshot.exitCode } });
16059
- ctx.emit({ type: "scene.change", scene: "fleet", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } } });
16060
- refreshFleet(ctx).catch((error) => emitFailed(ctx, "Refresh runs", error));
16061
- },
16062
- onError: (error, snapshot) => {
16063
- patchTerminal(snapshot);
16064
- emitFailed(ctx, label, error, { runId: cleanRunId });
16065
- }
16066
- });
16067
- const record = {
16068
- runId: cleanRunId,
16069
- status: "running",
16070
- rendered: "bundled Pi embedded via Bun.Terminal + @xterm/headless"
16071
- };
16072
- emitProgress(ctx, label, "bundled Pi is running", { piAttach: { runId: cleanRunId, status: "entering-pi", message: "bundled Pi running", result: record } });
16073
- return record;
15670
+ const { attachRunBundledPiFrontend: attachRunBundledPiFrontend2 } = await Promise.resolve().then(() => (init__pi_frontend(), exports__pi_frontend));
15671
+ outcome = await attachRunBundledPiFrontend2({ projectRoot, outputMode }, { runId: cleanRunId, returnOnQuit: true });
16074
15672
  } catch (error) {
16075
- const reason = error instanceof Error ? error.message : String(error);
15673
+ attachError = error;
15674
+ } finally {
15675
+ await resumeRendererAfterExternalTui(ctx);
15676
+ }
15677
+ if (attachError) {
15678
+ const reason = normalizeAppError(attachError).message;
16076
15679
  patchData(ctx, {
16077
- piAttach: {
16078
- runId: cleanRunId,
16079
- status: "failed",
16080
- message: reason,
16081
- failureReason: reason,
16082
- ...currentAttachState(ctx, cleanRunId)?.steps ? { steps: currentAttachState(ctx, cleanRunId).steps } : {}
16083
- }
15680
+ piTerminal: undefined,
15681
+ piAttach: { runId: cleanRunId, status: "failed", message: reason, failureReason: reason }
16084
15682
  });
16085
- emitFailed(ctx, label, error, { runId: cleanRunId });
16086
- throw error;
15683
+ Promise.resolve().then(() => (init_inspect2(), exports_inspect)).then(({ loadInspect: loadInspect2 }) => loadInspect2(ctx, cleanRunId)).catch(() => null);
15684
+ emitFailed(ctx, label, attachError, { runId: cleanRunId });
15685
+ throw attachError;
16087
15686
  }
15687
+ const detached = outcome?.detached === true;
15688
+ patchData(ctx, {
15689
+ piTerminal: undefined,
15690
+ piAttach: { runId: cleanRunId, status: "returned", message: detached ? "detached from worker Pi" : "returned from worker Pi" }
15691
+ });
15692
+ emitCompleted(ctx, label, { runId: cleanRunId, result: { runId: cleanRunId, detached } });
15693
+ ctx.emit({ type: "scene.change", scene: "fleet", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } } });
15694
+ refreshFleet(ctx).catch((error) => emitFailed(ctx, "Refresh runs", error));
15695
+ return { runId: cleanRunId, status: "returned", detached };
16088
15696
  }
16089
15697
  var init_pi_attach = __esm(() => {
16090
- init_pi_pty_host();
16091
15698
  init_fleet();
16092
15699
  });
16093
15700
 
@@ -17378,7 +16985,7 @@ import {
17378
16985
  dim as otuiDim,
17379
16986
  fg as otuiFg,
17380
16987
  t,
17381
- TextAttributes as TextAttributes2
16988
+ TextAttributes
17382
16989
  } from "@opentui/core";
17383
16990
  function statusColor3(status) {
17384
16991
  switch (status) {
@@ -17453,6 +17060,7 @@ var init_theme2 = __esm(() => {
17453
17060
  bg2: "#0b0c0e",
17454
17061
  panel: "#101115",
17455
17062
  panel2: "#14161b",
17063
+ hover: "#181b24",
17456
17064
  glass: "#1e2230",
17457
17065
  border: "#2a2e3a",
17458
17066
  ink: "#f4f5f8",
@@ -17482,7 +17090,7 @@ var init_theme2 = __esm(() => {
17482
17090
  });
17483
17091
 
17484
17092
  // packages/cli/src/app-opentui/drone.ts
17485
- import { RGBA as RGBA2, StyledText as StyledText2, TextAttributes as TextAttributes3 } from "@opentui/core";
17093
+ import { RGBA, StyledText, TextAttributes as TextAttributes2 } from "@opentui/core";
17486
17094
  function bladeForTick(tick, phase = 0) {
17487
17095
  return BLADE_FRAMES2[(Math.floor(tick / 3) + phase) % BLADE_FRAMES2.length];
17488
17096
  }
@@ -17491,12 +17099,12 @@ function eyeForTick(tick, phase = 0) {
17491
17099
  return pulse > 0.55 ? EYE_FRAMES2[1] : pulse > 0.1 ? EYE_FRAMES2[0] : pulse > -0.45 ? EYE_FRAMES2[2] : EYE_FRAMES2[3];
17492
17100
  }
17493
17101
  function chunk(text2, fg2, bold2 = false, dim = false) {
17494
- let attributes = TextAttributes3.NONE;
17102
+ let attributes = TextAttributes2.NONE;
17495
17103
  if (bold2)
17496
- attributes |= TextAttributes3.BOLD;
17104
+ attributes |= TextAttributes2.BOLD;
17497
17105
  if (dim)
17498
- attributes |= TextAttributes3.DIM;
17499
- return { __isChunk: true, text: text2, fg: fg2, ...attributes !== TextAttributes3.NONE ? { attributes } : {} };
17106
+ attributes |= TextAttributes2.DIM;
17107
+ return { __isChunk: true, text: text2, fg: fg2, ...attributes !== TextAttributes2.NONE ? { attributes } : {} };
17500
17108
  }
17501
17109
  function styledLine(text2, colorFor) {
17502
17110
  const chunks = [];
@@ -17517,7 +17125,7 @@ function styledLine(text2, colorFor) {
17517
17125
  run += char;
17518
17126
  }
17519
17127
  flush();
17520
- return new StyledText2(chunks.length > 0 ? chunks : [chunk("", COLOR.ink)]);
17128
+ return new StyledText(chunks.length > 0 ? chunks : [chunk("", COLOR.ink)]);
17521
17129
  }
17522
17130
  function droneColor(char) {
17523
17131
  if (char === "?" || char === "o" || char === "@" || char === "\u2022")
@@ -17609,13 +17217,13 @@ var init_drone = __esm(() => {
17609
17217
  BLADE_FRAMES2 = ["---", "\\\\\\", "|||", "///"];
17610
17218
  EYE_FRAMES2 = ["o", "@", "\u2022", "."];
17611
17219
  COLOR = {
17612
- body: RGBA2.fromHex(RIG_UI.lime),
17613
- mini: RGBA2.fromHex(RIG_UI.limeDim),
17614
- rotor: RGBA2.fromHex(RIG_UI.cyan),
17615
- path: RGBA2.fromHex(RIG_UI.cyan),
17616
- eye: RGBA2.fromHex(RIG_UI.ink),
17617
- dim: RGBA2.fromHex(RIG_UI.ink4),
17618
- ink: RGBA2.fromHex(RIG_UI.ink2)
17220
+ body: RGBA.fromHex(RIG_UI.lime),
17221
+ mini: RGBA.fromHex(RIG_UI.limeDim),
17222
+ rotor: RGBA.fromHex(RIG_UI.cyan),
17223
+ path: RGBA.fromHex(RIG_UI.cyan),
17224
+ eye: RGBA.fromHex(RIG_UI.ink),
17225
+ dim: RGBA.fromHex(RIG_UI.ink4),
17226
+ ink: RGBA.fromHex(RIG_UI.ink2)
17619
17227
  };
17620
17228
  });
17621
17229
 
@@ -19802,11 +19410,8 @@ var init_family2 = __esm(() => {
19802
19410
  });
19803
19411
 
19804
19412
  // packages/cli/src/app-opentui/list-search.ts
19805
- function isListSearchScene(scene) {
19806
- return scene === "fleet" || scene === "tasks";
19807
- }
19808
- function listSearchQuery(state) {
19809
- return isListSearchScene(state.scene) ? state.typeBar.value.trim() : "";
19413
+ function listSearchQuery(_state) {
19414
+ return "";
19810
19415
  }
19811
19416
  function valueText(value) {
19812
19417
  if (value === undefined || value === null)
@@ -23048,7 +22653,7 @@ var init__json_output = __esm(() => {
23048
22653
 
23049
22654
  // packages/cli/src/launcher.ts
23050
22655
  import { existsSync as existsSync22 } from "fs";
23051
- import { basename as basename5, resolve as resolve29 } from "path";
22656
+ import { basename as basename4, resolve as resolve29 } from "path";
23052
22657
  import { loadDotEnvSecrets } from "@rig/runtime/baked-secrets";
23053
22658
  import { RIG_DEFINITION_DIRNAME, RIG_STATE_DIRNAME, resolveNearestRigProjectRoot } from "@rig/runtime/layout";
23054
22659
  function parsePolicyMode(value) {
@@ -23073,7 +22678,7 @@ function resolveProjectRoot({
23073
22678
  return resolve29(cwd, envProjectRoot);
23074
22679
  }
23075
22680
  const fallbackImportDir = importDir ?? cwd;
23076
- const execName = basename5(execPath).toLowerCase();
22681
+ const execName = basename4(execPath).toLowerCase();
23077
22682
  const execCandidates = execName === "rig" || execName === "rig.exe" ? [resolve29(execPath, "..", "..")] : [];
23078
22683
  const candidates = [cwd, ...execCandidates, resolve29(fallbackImportDir, "..")];
23079
22684
  for (const candidate of candidates) {
@@ -23942,6 +23547,12 @@ var init_autocomplete = __esm(() => {
23942
23547
  NAV_VERBS = ["runs", "tasks", "inbox", "server", "doctor", "help", "init", "main"];
23943
23548
  });
23944
23549
 
23550
+ // packages/cli/src/app-opentui/pi-pty-host.ts
23551
+ function getActivePiHost() {
23552
+ return null;
23553
+ }
23554
+ function stopActivePiHost(_reason) {}
23555
+
23945
23556
  // packages/cli/src/app-opentui/keymap.ts
23946
23557
  function clearTypeBar(context, message2) {
23947
23558
  const typeBar = context.getTypeBar();
@@ -24174,7 +23785,6 @@ var init_keymap = __esm(() => {
24174
23785
  init_autocomplete();
24175
23786
  init_command_pty_host();
24176
23787
  init_intent();
24177
- init_pi_pty_host();
24178
23788
  });
24179
23789
 
24180
23790
  // packages/cli/src/app-opentui/runtime-resources.ts
@@ -24331,7 +23941,7 @@ var init_constants = __esm(() => {
24331
23941
  });
24332
23942
 
24333
23943
  // packages/cli/src/app-opentui/render/graphics.ts
24334
- import { FrameBufferRenderable, RGBA as RGBA3 } from "@opentui/core";
23944
+ import { FrameBufferRenderable, RGBA as RGBA2 } from "@opentui/core";
24335
23945
  function sceneKind(scene) {
24336
23946
  if (scene === "tasks")
24337
23947
  return "loop";
@@ -24426,7 +24036,7 @@ function paletteColor(ac, c2, mg, ink5) {
24426
24036
  const b = (AC_RGB[2] * ac + C2_RGB[2] * c2 + MG_RGB[2] * mg + INK_RGB[2] * ink5) / total;
24427
24037
  const intensity = Math.min(1, total / 4);
24428
24038
  const lift = 0.34 + intensity * 0.66;
24429
- return RGBA3.fromInts(Math.round(r * lift), Math.round(g * lift), Math.round(b * lift), 255);
24039
+ return RGBA2.fromInts(Math.round(r * lift), Math.round(g * lift), Math.round(b * lift), 255);
24430
24040
  }
24431
24041
  function clearCanvas(canvas) {
24432
24042
  canvas.ac.fill(0);
@@ -24781,8 +24391,8 @@ function staticFleetTick(scene) {
24781
24391
  }
24782
24392
  function withAlpha(hex, alpha) {
24783
24393
  const a = Math.max(0, Math.min(255, Math.round(alpha * 255)));
24784
- const [r, g, b] = RGBA3.fromHex(hex).toInts();
24785
- return RGBA3.fromInts(r, g, b, a);
24394
+ const [r, g, b] = RGBA2.fromHex(hex).toInts();
24395
+ return RGBA2.fromInts(r, g, b, a);
24786
24396
  }
24787
24397
  function asciiFleetColor(char, row, alpha) {
24788
24398
  if (char === "@" || char === "$" || char === "o")
@@ -24790,7 +24400,7 @@ function asciiFleetColor(char, row, alpha) {
24790
24400
  if (char === "%" || char === "!" || char === "/" || char === "\\" || char === "|")
24791
24401
  return withAlpha(RIG_UI.cyan, alpha);
24792
24402
  if (char === "." || char === "'" || char === "_" || row < 3 || row > FLEET_GRID_HEIGHT - 4) {
24793
- return RGBA3.fromInts(108, 110, 121, Math.min(170, Math.max(0, Math.min(255, Math.round(alpha * 255)))));
24403
+ return RGBA2.fromInts(108, 110, 121, Math.min(170, Math.max(0, Math.min(255, Math.round(alpha * 255)))));
24794
24404
  }
24795
24405
  return withAlpha(RIG_UI.limeDim, alpha);
24796
24406
  }
@@ -24844,13 +24454,13 @@ var init_graphics = __esm(() => {
24844
24454
  init_theme2();
24845
24455
  init_ascii_fleet();
24846
24456
  init_constants();
24847
- TRANSPARENT = RGBA3.fromValues(0, 0, 0, 0);
24848
- BACKDROP = RGBA3.fromHex(RIG_UI.bg);
24849
- PANEL_BG = RGBA3.fromInts(16, 17, 21, 184);
24850
- PANEL_HEADER_BG = RGBA3.fromInts(16, 17, 21, 188);
24851
- PANEL_LINE = RGBA3.fromInts(255, 255, 255, 20);
24852
- PANEL_LINE_DIM = RGBA3.fromInts(255, 255, 255, 8);
24853
- PANEL_TEXT_DIM = RGBA3.fromInts(108, 110, 121, 255);
24457
+ TRANSPARENT = RGBA2.fromValues(0, 0, 0, 0);
24458
+ BACKDROP = RGBA2.fromHex(RIG_UI.bg);
24459
+ PANEL_BG = RGBA2.fromInts(16, 17, 21, 184);
24460
+ PANEL_HEADER_BG = RGBA2.fromInts(16, 17, 21, 188);
24461
+ PANEL_LINE = RGBA2.fromInts(255, 255, 255, 20);
24462
+ PANEL_LINE_DIM = RGBA2.fromInts(255, 255, 255, 8);
24463
+ PANEL_TEXT_DIM = RGBA2.fromInts(108, 110, 121, 255);
24854
24464
  AC_RGB = [204, 255, 77];
24855
24465
  C2_RGB = [86, 216, 255];
24856
24466
  MG_RGB = [255, 121, 176];
@@ -24863,16 +24473,69 @@ var init_graphics = __esm(() => {
24863
24473
  ];
24864
24474
  });
24865
24475
 
24476
+ // packages/cli/src/app-opentui/render/hover.ts
24477
+ function getHoveredSelectableId() {
24478
+ return hoveredId;
24479
+ }
24480
+ function isHovered(id) {
24481
+ return id !== undefined && id === hoveredId;
24482
+ }
24483
+ function setHoveredSelectableId(id) {
24484
+ if (hoveredId === id)
24485
+ return;
24486
+ hoveredId = id;
24487
+ for (const listener of listeners)
24488
+ listener();
24489
+ }
24490
+ function clearHoveredSelectableId(id) {
24491
+ if (id !== undefined && hoveredId === id)
24492
+ setHoveredSelectableId(undefined);
24493
+ }
24494
+ function subscribeHover(listener) {
24495
+ listeners.add(listener);
24496
+ return () => {
24497
+ listeners.delete(listener);
24498
+ };
24499
+ }
24500
+ var hoveredId, listeners;
24501
+ var init_hover = __esm(() => {
24502
+ listeners = new Set;
24503
+ });
24504
+
24866
24505
  // packages/cli/src/app-opentui/render/text.ts
24867
- import { RGBA as RGBA4, TextAttributes as TextAttributes4, TextRenderable } from "@opentui/core";
24506
+ import { RGBA as RGBA3, TextAttributes as TextAttributes3, TextRenderable } from "@opentui/core";
24507
+ function lineIsClickable(line2) {
24508
+ return line2?.selectable !== undefined || line2?.selectableIndex !== undefined;
24509
+ }
24510
+ function lineSelectableId(line2) {
24511
+ return line2?.selectable?.id ?? line2?.selectableId;
24512
+ }
24513
+ function lineBackground(line2) {
24514
+ if (line2?.bg)
24515
+ return RGBA3.fromHex(line2.bg);
24516
+ const id = lineSelectableId(line2);
24517
+ if (id !== undefined && isHovered(id))
24518
+ return HOVER_BG;
24519
+ return TRANSPARENT2;
24520
+ }
24868
24521
  function selectableMouseHandler(currentLineRef, onLineMouseDown) {
24869
24522
  return (event) => {
24870
24523
  const currentLine = currentLineRef();
24871
- if (currentLine?.selectableIndex === undefined)
24524
+ if (!lineIsClickable(currentLine))
24872
24525
  return;
24873
24526
  onLineMouseDown?.(currentLine, event);
24874
24527
  };
24875
24528
  }
24529
+ function hoverHandlers(currentLineRef) {
24530
+ return {
24531
+ onMouseOver: () => {
24532
+ const id = lineSelectableId(currentLineRef());
24533
+ if (id !== undefined)
24534
+ setHoveredSelectableId(id);
24535
+ },
24536
+ onMouseOut: () => clearHoveredSelectableId(lineSelectableId(currentLineRef()))
24537
+ };
24538
+ }
24876
24539
  function createTextLine(renderer, id, top, onLineMouseDown) {
24877
24540
  let currentLine;
24878
24541
  const renderable = new TextRenderable(renderer, {
@@ -24884,7 +24547,8 @@ function createTextLine(renderer, id, top, onLineMouseDown) {
24884
24547
  width: 1,
24885
24548
  fg: RIG_UI.ink2,
24886
24549
  selectable: true,
24887
- onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown)
24550
+ onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown),
24551
+ ...hoverHandlers(() => currentLine)
24888
24552
  });
24889
24553
  renderable.setRigSceneLine = (line2) => {
24890
24554
  currentLine = line2;
@@ -24901,7 +24565,8 @@ function createFlowTextLine(renderer, id, onLineMouseDown) {
24901
24565
  flexShrink: 0,
24902
24566
  fg: RIG_UI.ink2,
24903
24567
  selectable: true,
24904
- onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown)
24568
+ onMouseDown: selectableMouseHandler(() => currentLine, onLineMouseDown),
24569
+ ...hoverHandlers(() => currentLine)
24905
24570
  });
24906
24571
  renderable.setRigSceneLine = (line2) => {
24907
24572
  currentLine = line2;
@@ -24915,8 +24580,8 @@ function applyTextLine(renderable, line2, top, left, width) {
24915
24580
  renderable.width = width;
24916
24581
  renderable.content = line2?.styledText ?? line2?.text ?? "";
24917
24582
  renderable.fg = line2?.fg ?? RIG_UI.ink2;
24918
- renderable.bg = line2?.bg ?? TRANSPARENT2;
24919
- renderable.attributes = line2?.bold ? TextAttributes4.BOLD : line2?.dim ? TextAttributes4.DIM : 0;
24583
+ renderable.bg = lineBackground(line2);
24584
+ renderable.attributes = line2?.bold ? TextAttributes3.BOLD : line2?.dim ? TextAttributes3.DIM : 0;
24920
24585
  }
24921
24586
  function applyFlowTextLine(renderable, line2, width) {
24922
24587
  renderable.setRigSceneLine?.(line2);
@@ -24925,8 +24590,8 @@ function applyFlowTextLine(renderable, line2, width) {
24925
24590
  renderable.width = Math.max(1, width);
24926
24591
  renderable.content = line2?.styledText ?? line2?.text ?? "";
24927
24592
  renderable.fg = line2?.fg ?? RIG_UI.ink2;
24928
- renderable.bg = line2?.bg ?? TRANSPARENT2;
24929
- renderable.attributes = line2?.bold ? TextAttributes4.BOLD : line2?.dim ? TextAttributes4.DIM : 0;
24593
+ renderable.bg = lineBackground(line2);
24594
+ renderable.attributes = line2?.bold ? TextAttributes3.BOLD : line2?.dim ? TextAttributes3.DIM : 0;
24930
24595
  }
24931
24596
  function clearTextLines(renderables, from = 0) {
24932
24597
  for (let index = from;index < renderables.length; index += 1) {
@@ -24939,21 +24604,34 @@ function clearTextLines(renderables, from = 0) {
24939
24604
  renderable.bg = TRANSPARENT2;
24940
24605
  }
24941
24606
  }
24942
- var TRANSPARENT2;
24607
+ var TRANSPARENT2, HOVER_BG;
24943
24608
  var init_text = __esm(() => {
24944
24609
  init_theme2();
24945
- TRANSPARENT2 = RGBA4.fromInts(0, 0, 0, 0);
24610
+ init_hover();
24611
+ TRANSPARENT2 = RGBA3.fromInts(0, 0, 0, 0);
24612
+ HOVER_BG = RGBA3.fromHex(RIG_UI.hover);
24946
24613
  });
24947
24614
 
24615
+ // packages/cli/src/app-opentui/render/terminal-handoff.ts
24616
+ function resumeRendererClean(renderer) {
24617
+ try {
24618
+ renderer.pendingSuspendedTerminalSetup = true;
24619
+ } catch {}
24620
+ renderer.resume();
24621
+ try {
24622
+ renderer.requestRender?.();
24623
+ } catch {}
24624
+ }
24625
+
24948
24626
  // packages/cli/src/app-opentui/render/panels.ts
24949
- import { RGBA as RGBA5, ScrollBoxRenderable } from "@opentui/core";
24627
+ import { RGBA as RGBA4, ScrollBoxRenderable } from "@opentui/core";
24950
24628
  function hexToRgba(hex, alpha) {
24951
24629
  const clean = hex.replace(/^#/, "");
24952
24630
  const full = clean.length === 3 ? clean.split("").map((part) => `${part}${part}`).join("") : clean;
24953
24631
  const value = Number.parseInt(full, 16);
24954
24632
  if (!Number.isFinite(value))
24955
- return RGBA5.fromInts(14, 15, 17, alpha);
24956
- return RGBA5.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255, alpha);
24633
+ return RGBA4.fromInts(14, 15, 17, alpha);
24634
+ return RGBA4.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255, alpha);
24957
24635
  }
24958
24636
  function panelBackground(panel) {
24959
24637
  return hexToRgba(panel.backgroundColor ?? RIG_UI.panel, PANEL_OPAQUE_ALPHA);
@@ -25070,12 +24748,12 @@ var TRANSPARENT3, MAX_PANEL_LINES = 420, PANEL_OPAQUE_ALPHA = 255, PANEL_BORDER;
25070
24748
  var init_panels = __esm(() => {
25071
24749
  init_theme2();
25072
24750
  init_text();
25073
- TRANSPARENT3 = RGBA5.fromInts(0, 0, 0, 0);
24751
+ TRANSPARENT3 = RGBA4.fromInts(0, 0, 0, 0);
25074
24752
  PANEL_BORDER = hexToRgba(RIG_UI.border, 255);
25075
24753
  });
25076
24754
 
25077
24755
  // packages/cli/src/app-opentui/render/type-bar.ts
25078
- import { BoxRenderable, InputRenderable, InputRenderableEvents, RGBA as RGBA6, StyledText as StyledText3, TextRenderable as TextRenderable2 } from "@opentui/core";
24756
+ import { BoxRenderable, InputRenderable, InputRenderableEvents, RGBA as RGBA5, StyledText as StyledText2, TextRenderable as TextRenderable2 } from "@opentui/core";
25079
24757
  function createTypeBar(renderer) {
25080
24758
  const background = new BoxRenderable(renderer, {
25081
24759
  id: "typebar-card",
@@ -25222,13 +24900,13 @@ function formatFooter(footer, width) {
25222
24900
  chunks.push(cell4.style(text2));
25223
24901
  used += visibleWidth(text2);
25224
24902
  });
25225
- return new StyledText3(chunks);
24903
+ return new StyledText2(chunks);
25226
24904
  }
25227
24905
  var TYPEBAR_BG, TYPEBAR_BORDER, FOOTER_SEPARATOR = " \xB7 ";
25228
24906
  var init_type_bar = __esm(() => {
25229
24907
  init_theme2();
25230
- TYPEBAR_BG = RGBA6.fromInts(20, 25, 14, 224);
25231
- TYPEBAR_BORDER = RGBA6.fromInts(204, 255, 77, 92);
24908
+ TYPEBAR_BG = RGBA5.fromInts(20, 25, 14, 224);
24909
+ TYPEBAR_BORDER = RGBA5.fromInts(204, 255, 77, 92);
25232
24910
  });
25233
24911
 
25234
24912
  // packages/cli/src/app-opentui/render/native-host.ts
@@ -25236,7 +24914,7 @@ import {
25236
24914
  BoxRenderable as BoxRenderable2,
25237
24915
  CodeRenderable,
25238
24916
  DiffRenderable,
25239
- RGBA as RGBA7,
24917
+ RGBA as RGBA6,
25240
24918
  ScrollBoxRenderable as ScrollBoxRenderable2,
25241
24919
  SyntaxStyle,
25242
24920
  TextRenderable as TextRenderable3,
@@ -25246,19 +24924,19 @@ function rigSyntaxStyle() {
25246
24924
  if (syntaxStyle)
25247
24925
  return syntaxStyle;
25248
24926
  syntaxStyle = SyntaxStyle.fromStyles({
25249
- keyword: { fg: RGBA7.fromHex(RIG_UI.magenta), bold: true },
25250
- string: { fg: RGBA7.fromHex(RIG_UI.lime) },
25251
- number: { fg: RGBA7.fromHex(RIG_UI.cyan) },
25252
- comment: { fg: RGBA7.fromHex(RIG_UI.ink4), italic: true },
25253
- function: { fg: RGBA7.fromHex(RIG_UI.cyan) },
25254
- type: { fg: RGBA7.fromHex(RIG_UI.limeDim) },
25255
- constant: { fg: RGBA7.fromHex(RIG_UI.yellow) },
25256
- default: { fg: RGBA7.fromHex(RIG_UI.ink2) }
24927
+ keyword: { fg: RGBA6.fromHex(RIG_UI.magenta), bold: true },
24928
+ string: { fg: RGBA6.fromHex(RIG_UI.lime) },
24929
+ number: { fg: RGBA6.fromHex(RIG_UI.cyan) },
24930
+ comment: { fg: RGBA6.fromHex(RIG_UI.ink4), italic: true },
24931
+ function: { fg: RGBA6.fromHex(RIG_UI.cyan) },
24932
+ type: { fg: RGBA6.fromHex(RIG_UI.limeDim) },
24933
+ constant: { fg: RGBA6.fromHex(RIG_UI.yellow) },
24934
+ default: { fg: RGBA6.fromHex(RIG_UI.ink2) }
25257
24935
  });
25258
24936
  return syntaxStyle;
25259
24937
  }
25260
24938
  function tableContent(rows) {
25261
- return rows.map((row, rowIndex) => row.map((cell4) => [{ __isChunk: true, text: cell4, fg: RGBA7.fromHex(rowIndex === 0 ? RIG_UI.ink3 : RIG_UI.ink2) }]));
24939
+ return rows.map((row, rowIndex) => row.map((cell4) => [{ __isChunk: true, text: cell4, fg: RGBA6.fromHex(rowIndex === 0 ? RIG_UI.ink3 : RIG_UI.ink2) }]));
25262
24940
  }
25263
24941
  function createNativeHost(renderer) {
25264
24942
  try {
@@ -25786,7 +25464,7 @@ __export(exports_runtime, {
25786
25464
  });
25787
25465
  import { existsSync as existsSync23 } from "fs";
25788
25466
  import { resolve as resolve30 } from "path";
25789
- import { BoxRenderable as BoxRenderable3, RGBA as RGBA8, StyledText as StyledText4, TextRenderable as TextRenderable4, createCliRenderer } from "@opentui/core";
25467
+ import { BoxRenderable as BoxRenderable3, RGBA as RGBA7, StyledText as StyledText3, TextRenderable as TextRenderable4, createCliRenderer } from "@opentui/core";
25790
25468
  function inboxPendingCount(state) {
25791
25469
  const inbox = state.data.inbox;
25792
25470
  if (!inbox || typeof inbox !== "object" || Array.isArray(inbox))
@@ -25955,7 +25633,7 @@ function buildNavStrip(state) {
25955
25633
  chunks.push(otuiBold(styles.yellow(String(badgeCount))));
25956
25634
  }
25957
25635
  });
25958
- return new StyledText4(chunks);
25636
+ return new StyledText3(chunks);
25959
25637
  }
25960
25638
  function requestRender(renderer) {
25961
25639
  renderer.requestRender?.();
@@ -26183,7 +25861,8 @@ async function launchRigOpenTuiApp(options) {
26183
25861
  },
26184
25862
  suspend: () => renderer?.suspend(),
26185
25863
  resume: () => {
26186
- renderer?.resume();
25864
+ if (renderer)
25865
+ resumeRendererClean(renderer);
26187
25866
  renderApp();
26188
25867
  },
26189
25868
  destroy: () => renderer?.destroy()
@@ -26290,8 +25969,7 @@ async function launchRigOpenTuiApp(options) {
26290
25969
  } else if (!selectById(store, id, renderedSelectableItems)) {
26291
25970
  return;
26292
25971
  }
26293
- if (line2.activateOnClick)
26294
- runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
25972
+ runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
26295
25973
  };
26296
25974
  for (let index = 0;index < MAX_SCENE_PANELS; index += 1) {
26297
25975
  const panel = createScrollPanelLayer(renderer, `rig-scroll-panel-${index}`, onLineClick);
@@ -26358,6 +26036,7 @@ async function launchRigOpenTuiApp(options) {
26358
26036
  };
26359
26037
  renderer.keyInput.on("keypress", onKeyPress);
26360
26038
  resources.add(() => renderer?.keyInput.off("keypress", onKeyPress));
26039
+ resources.add(subscribeHover(() => scheduleAppRender(0)));
26361
26040
  const onPaste = (event) => {
26362
26041
  const state = store.getState();
26363
26042
  if (state.scene === "handoff") {
@@ -26599,6 +26278,7 @@ var init_runtime = __esm(() => {
26599
26278
  init_graphics();
26600
26279
  init_fleet_stats();
26601
26280
  init_text();
26281
+ init_hover();
26602
26282
  init_panels();
26603
26283
  init_type_bar();
26604
26284
  init_scene();
@@ -26608,8 +26288,7 @@ var init_runtime = __esm(() => {
26608
26288
  init_help();
26609
26289
  init_main();
26610
26290
  init_selectable();
26611
- init_pi_pty_host();
26612
- PRELOADER_BACKDROP = RGBA8.fromHex(RIG_UI.bg);
26291
+ PRELOADER_BACKDROP = RGBA7.fromHex(RIG_UI.bg);
26613
26292
  PRIMARY_NAV = [
26614
26293
  { id: "cockpit", label: "Cockpit", scene: "main", argv: ["main"], enterLabel: "Cockpit", member: ["main"] },
26615
26294
  { id: "runs", label: "Runs", scene: "fleet", argv: ["runs"], enterLabel: "Runs", member: ["fleet", "run-detail", "inspect", "handoff"] },
@@ -26726,23 +26405,26 @@ var init_scroll = __esm(() => {
26726
26405
  // packages/cli/src/app-opentui/react/SceneFrameView.tsx
26727
26406
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
26728
26407
  import { useRenderer as useRenderer2 } from "@opentui/react";
26729
- import { TextAttributes as TextAttributes5 } from "@opentui/core";
26408
+ import { TextAttributes as TextAttributes4 } from "@opentui/core";
26730
26409
  import { jsxDEV, Fragment } from "@opentui/react/jsx-dev-runtime";
26731
26410
  function lineAttributes(line2) {
26732
- return line2?.bold ? TextAttributes5.BOLD : line2?.dim ? TextAttributes5.DIM : 0;
26411
+ return line2?.bold ? TextAttributes4.BOLD : line2?.dim ? TextAttributes4.DIM : 0;
26733
26412
  }
26734
26413
  function lineContent(line2) {
26735
26414
  return line2.styledText ?? line2.text ?? "";
26736
26415
  }
26737
26416
  function LineView(props) {
26738
26417
  const { line: line2, onLineClick } = props;
26418
+ const id = lineSelectableId(line2);
26739
26419
  return /* @__PURE__ */ jsxDEV("text", {
26740
26420
  style: { position: "absolute", left: props.left, top: props.top, width: props.width, zIndex: 5 },
26741
26421
  fg: line2.fg ?? RIG_UI.ink2,
26742
- bg: line2.bg ?? TRANSPARENT2,
26422
+ bg: lineBackground(line2),
26743
26423
  attributes: lineAttributes(line2),
26744
26424
  content: lineContent(line2),
26745
- onMouseDown: onLineClick ? () => onLineClick(line2) : undefined
26425
+ onMouseDown: onLineClick ? () => onLineClick(line2) : undefined,
26426
+ onMouseOver: id !== undefined ? () => setHoveredSelectableId(id) : undefined,
26427
+ onMouseOut: id !== undefined ? () => clearHoveredSelectableId(id) : undefined
26746
26428
  }, undefined, false, undefined, this);
26747
26429
  }
26748
26430
  function PanelHost(props) {
@@ -26829,6 +26511,7 @@ var init_SceneFrameView = __esm(() => {
26829
26511
  init_theme2();
26830
26512
  init_scene();
26831
26513
  init_text();
26514
+ init_hover();
26832
26515
  init_panels();
26833
26516
  init_native_host();
26834
26517
  init_scroll();
@@ -26982,7 +26665,7 @@ var init_nav = __esm(() => {
26982
26665
  });
26983
26666
 
26984
26667
  // packages/cli/src/app-opentui/react/App.tsx
26985
- import { useMemo, useRef as useRef4 } from "react";
26668
+ import { useMemo, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
26986
26669
  import { useKeyboard, useRenderer as useRenderer4, useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
26987
26670
  import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
26988
26671
  function defaultFrame(state) {
@@ -27000,6 +26683,7 @@ function App(props) {
27000
26683
  const runAppAction = useRunAppAction();
27001
26684
  const store = useStore();
27002
26685
  const renderer = useRenderer4();
26686
+ useSyncExternalStore2(subscribeHover, getHoveredSelectableId, getHoveredSelectableId);
27003
26687
  const dims = useTerminalDimensions2();
27004
26688
  const layout = computeStageLayout(dims.width, dims.height);
27005
26689
  const registry = useMemo(() => ({ ...BUILTIN, ...props.sceneRenderers ?? {} }), [props.sceneRenderers]);
@@ -27034,8 +26718,7 @@ function App(props) {
27034
26718
  } else {
27035
26719
  return;
27036
26720
  }
27037
- if (line2.activateOnClick)
27038
- runAppAction("Activate selection", () => activateSelection(runtime, items));
26721
+ runAppAction("Activate selection", () => activateSelection(runtime, items));
27039
26722
  };
27040
26723
  return /* @__PURE__ */ jsxDEV3("box", {
27041
26724
  style: { position: "absolute", left: 0, top: 0, width: "100%", height: "100%" },
@@ -27085,6 +26768,7 @@ var init_App = __esm(() => {
27085
26768
  init_SceneFrameView();
27086
26769
  init_ChromeHost();
27087
26770
  init_scroll();
26771
+ init_hover();
27088
26772
  init_context();
27089
26773
  init_nav();
27090
26774
  BUILTIN = {
@@ -27107,8 +26791,48 @@ async function launchRigReactApp(options) {
27107
26791
  const store = createAppStore(initialState);
27108
26792
  const events = createAppEventBus();
27109
26793
  let renderer = null;
26794
+ let root = null;
27110
26795
  let runnerPromise = null;
27111
26796
  let suppressHistoryPush = false;
26797
+ let resolveExit = () => {};
26798
+ const appExit = new Promise((resolve31) => {
26799
+ resolveExit = resolve31;
26800
+ });
26801
+ let relaunching = false;
26802
+ const mountRenderer = async () => {
26803
+ renderer = await createCliRenderer2({
26804
+ screenMode: "alternate-screen",
26805
+ exitOnCtrlC: false,
26806
+ targetFps: 30,
26807
+ maxFps: 30,
26808
+ useMouse: true,
26809
+ autoFocus: false,
26810
+ useKittyKeyboard: { disambiguate: true }
26811
+ });
26812
+ renderer.on("destroy", () => {
26813
+ if (!relaunching)
26814
+ resolveExit();
26815
+ });
26816
+ root = createRoot(renderer);
26817
+ root.render(/* @__PURE__ */ jsxDEV4(RigAppProvider, {
26818
+ value: { store, runtime, runAppAction },
26819
+ children: /* @__PURE__ */ jsxDEV4(App, {
26820
+ sceneRenderers: options.sceneRenderers
26821
+ }, undefined, false, undefined, this)
26822
+ }, undefined, false, undefined, this));
26823
+ relaunching = false;
26824
+ };
26825
+ const teardownRenderer = () => {
26826
+ relaunching = true;
26827
+ try {
26828
+ root?.unmount();
26829
+ } catch {}
26830
+ root = null;
26831
+ try {
26832
+ renderer?.destroy();
26833
+ } catch {}
26834
+ renderer = null;
26835
+ };
27112
26836
  const runAppAction = (label, action) => {
27113
26837
  action().catch((error) => {
27114
26838
  const n = normalizeError(error);
@@ -27150,8 +26874,10 @@ async function launchRigReactApp(options) {
27150
26874
  runnerPromise ??= options.initializeRuntime();
27151
26875
  return runnerPromise;
27152
26876
  },
27153
- suspend: () => renderer?.suspend(),
27154
- resume: () => renderer?.resume(),
26877
+ suspend: () => teardownRenderer(),
26878
+ resume: async () => {
26879
+ await mountRenderer();
26880
+ },
27155
26881
  destroy: () => renderer?.destroy()
27156
26882
  };
27157
26883
  events.subscribe((event) => store.patch(reduceAppEvent(store.getState(), event)));
@@ -27159,15 +26885,7 @@ async function launchRigReactApp(options) {
27159
26885
  if (typeof tick.unref === "function") {
27160
26886
  tick.unref();
27161
26887
  }
27162
- renderer = await createCliRenderer2({
27163
- screenMode: "alternate-screen",
27164
- exitOnCtrlC: false,
27165
- targetFps: 30,
27166
- maxFps: 30,
27167
- useMouse: true,
27168
- autoFocus: false,
27169
- useKittyKeyboard: { disambiguate: true }
27170
- });
26888
+ await mountRenderer();
27171
26889
  let liveEvents = null;
27172
26890
  const canAutoRefresh = (state) => {
27173
26891
  if (state.status === "action" || state.status === "loading")
@@ -27198,12 +26916,6 @@ async function launchRigReactApp(options) {
27198
26916
  }, FALLBACK_REFRESH_MS);
27199
26917
  if (typeof poll.unref === "function")
27200
26918
  poll.unref();
27201
- createRoot(renderer).render(/* @__PURE__ */ jsxDEV4(RigAppProvider, {
27202
- value: { store, runtime, runAppAction },
27203
- children: /* @__PURE__ */ jsxDEV4(App, {
27204
- sceneRenderers: options.sceneRenderers
27205
- }, undefined, false, undefined, this)
27206
- }, undefined, false, undefined, this));
27207
26919
  queueMicrotask(() => {
27208
26920
  (async () => {
27209
26921
  try {
@@ -27231,14 +26943,11 @@ async function launchRigReactApp(options) {
27231
26943
  }
27232
26944
  })();
27233
26945
  });
27234
- await new Promise((resolve31) => {
27235
- renderer?.on("destroy", () => {
27236
- clearInterval(tick);
27237
- clearInterval(poll);
27238
- liveEvents?.close();
27239
- resolve31();
27240
- });
27241
- });
26946
+ await appExit;
26947
+ clearInterval(tick);
26948
+ clearInterval(poll);
26949
+ liveEvents?.close();
26950
+ teardownRenderer();
27242
26951
  }
27243
26952
  var POLLABLE_SCENES;
27244
26953
  var init_launch = __esm(() => {
@@ -27258,7 +26967,7 @@ __export(exports_bootstrap, {
27258
26967
  buildAppLaunchOptions: () => buildAppLaunchOptions
27259
26968
  });
27260
26969
  import { existsSync as existsSync24, readFileSync as readFileSync15 } from "fs";
27261
- import { basename as basename6, resolve as resolve31 } from "path";
26970
+ import { basename as basename5, resolve as resolve31 } from "path";
27262
26971
  async function launchRigOpenTuiBootstrap(options) {
27263
26972
  const launchOptions = buildAppLaunchOptions(options);
27264
26973
  await launchOpenTuiApp(launchOptions);
@@ -27364,7 +27073,7 @@ function parsePolicyMode2(value) {
27364
27073
  function resolveProjectRoot2(input) {
27365
27074
  if (input.envProjectRoot)
27366
27075
  return resolve31(input.cwd, input.envProjectRoot);
27367
- const execName = basename6(input.execPath).toLowerCase();
27076
+ const execName = basename5(input.execPath).toLowerCase();
27368
27077
  const execCandidates = execName === "rig" || execName === "rig.exe" ? [resolve31(input.execPath, "..", "..")] : [];
27369
27078
  const candidates = [input.cwd, ...execCandidates, resolve31(input.importDir, "..")];
27370
27079
  for (const candidate of candidates) {