@h-rig/cli 0.0.6-alpha.82 → 0.0.6-alpha.83

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 (100) hide show
  1. package/dist/bin/build-rig-binaries.js +40 -8
  2. package/dist/bin/rig.js +2798 -1440
  3. package/dist/src/app/board.js +62 -13
  4. package/dist/src/app-opentui/adapters/command.d.ts +2 -0
  5. package/dist/src/app-opentui/adapters/command.js +329 -0
  6. package/dist/src/app-opentui/adapters/common.js +2 -2
  7. package/dist/src/app-opentui/adapters/doctor.js +63 -14
  8. package/dist/src/app-opentui/adapters/fleet.js +84 -20
  9. package/dist/src/app-opentui/adapters/inbox.js +83 -19
  10. package/dist/src/app-opentui/adapters/init.js +87 -23
  11. package/dist/src/app-opentui/adapters/pi-attach.js +96 -34
  12. package/dist/src/app-opentui/adapters/run-detail.js +83 -19
  13. package/dist/src/app-opentui/adapters/server.js +100 -23
  14. package/dist/src/app-opentui/adapters/tasks.d.ts +11 -0
  15. package/dist/src/app-opentui/adapters/tasks.js +742 -94
  16. package/dist/src/app-opentui/bootstrap.d.ts +1 -6
  17. package/dist/src/app-opentui/bootstrap.js +13769 -12368
  18. package/dist/src/app-opentui/command-pty-host.d.ts +62 -0
  19. package/dist/src/app-opentui/command-pty-host.js +248 -0
  20. package/dist/src/app-opentui/events.js +1 -1
  21. package/dist/src/app-opentui/focus-manager.d.ts +14 -0
  22. package/dist/src/app-opentui/focus-manager.js +24 -0
  23. package/dist/src/app-opentui/index.js +3802 -2882
  24. package/dist/src/app-opentui/intent.js +154 -48
  25. package/dist/src/app-opentui/keymap.d.ts +20 -0
  26. package/dist/src/app-opentui/keymap.js +707 -0
  27. package/dist/src/app-opentui/launch-routing.d.ts +16 -0
  28. package/dist/src/app-opentui/launch-routing.js +55 -0
  29. package/dist/src/app-opentui/layout.js +2 -2
  30. package/dist/src/app-opentui/pi-host-child.js +66 -16
  31. package/dist/src/app-opentui/pi-pty-host.d.ts +9 -0
  32. package/dist/src/app-opentui/pi-pty-host.js +9 -11
  33. package/dist/src/app-opentui/registry.js +3231 -2403
  34. package/dist/src/app-opentui/render/ascii-fleet.d.ts +15 -0
  35. package/dist/src/app-opentui/render/ascii-fleet.js +82 -0
  36. package/dist/src/app-opentui/render/graphics.d.ts +1 -1
  37. package/dist/src/app-opentui/render/graphics.js +131 -16
  38. package/dist/src/app-opentui/render/image-visual-layer-worker.js +408 -957
  39. package/dist/src/app-opentui/render/image-visual-layer.d.ts +18 -10
  40. package/dist/src/app-opentui/render/image-visual-layer.js +386 -356
  41. package/dist/src/app-opentui/render/panel-layout.d.ts +38 -0
  42. package/dist/src/app-opentui/render/panel-layout.js +48 -0
  43. package/dist/src/app-opentui/render/panels.d.ts +2 -0
  44. package/dist/src/app-opentui/render/panels.js +62 -29
  45. package/dist/src/app-opentui/render/preloader.d.ts +10 -0
  46. package/dist/src/app-opentui/render/preloader.js +163 -0
  47. package/dist/src/app-opentui/render/scene.d.ts +3 -0
  48. package/dist/src/app-opentui/render/scene.js +12 -0
  49. package/dist/src/app-opentui/render/text.js +5 -1
  50. package/dist/src/app-opentui/render/type-bar.d.ts +2 -1
  51. package/dist/src/app-opentui/render/type-bar.js +51 -16
  52. package/dist/src/app-opentui/runtime-resources.d.ts +16 -0
  53. package/dist/src/app-opentui/runtime-resources.js +62 -0
  54. package/dist/src/app-opentui/runtime.js +2322 -1513
  55. package/dist/src/app-opentui/scenes/command.d.ts +3 -0
  56. package/dist/src/app-opentui/scenes/command.js +103 -0
  57. package/dist/src/app-opentui/scenes/doctor.d.ts +2 -1
  58. package/dist/src/app-opentui/scenes/doctor.js +53 -14
  59. package/dist/src/app-opentui/scenes/error.js +44 -14
  60. package/dist/src/app-opentui/scenes/fleet.js +40 -21
  61. package/dist/src/app-opentui/scenes/handoff.js +142 -27
  62. package/dist/src/app-opentui/scenes/help.js +63 -48
  63. package/dist/src/app-opentui/scenes/inbox.d.ts +2 -1
  64. package/dist/src/app-opentui/scenes/inbox.js +64 -17
  65. package/dist/src/app-opentui/scenes/init.d.ts +2 -1
  66. package/dist/src/app-opentui/scenes/init.js +62 -21
  67. package/dist/src/app-opentui/scenes/main.d.ts +2 -1
  68. package/dist/src/app-opentui/scenes/main.js +80 -24
  69. package/dist/src/app-opentui/scenes/run-detail.d.ts +2 -1
  70. package/dist/src/app-opentui/scenes/run-detail.js +39 -25
  71. package/dist/src/app-opentui/scenes/server.d.ts +2 -1
  72. package/dist/src/app-opentui/scenes/server.js +71 -20
  73. package/dist/src/app-opentui/scenes/tasks.js +44 -22
  74. package/dist/src/app-opentui/state.js +70 -30
  75. package/dist/src/app-opentui/terminal-capabilities.d.ts +7 -0
  76. package/dist/src/app-opentui/terminal-capabilities.js +15 -0
  77. package/dist/src/app-opentui/types.d.ts +10 -2
  78. package/dist/src/commands/_doctor-checks.js +62 -13
  79. package/dist/src/commands/_operator-view.js +63 -13
  80. package/dist/src/commands/_pi-frontend.js +63 -13
  81. package/dist/src/commands/_preflight.js +64 -14
  82. package/dist/src/commands/_server-client.js +82 -18
  83. package/dist/src/commands/_snapshot-upload.js +62 -13
  84. package/dist/src/commands/connect.js +7 -1
  85. package/dist/src/commands/doctor.js +62 -13
  86. package/dist/src/commands/github.js +144 -23
  87. package/dist/src/commands/inbox.js +62 -13
  88. package/dist/src/commands/init.js +82 -18
  89. package/dist/src/commands/inspect.js +62 -13
  90. package/dist/src/commands/run.js +66 -13
  91. package/dist/src/commands/server.js +69 -14
  92. package/dist/src/commands/setup.js +62 -13
  93. package/dist/src/commands/stats.js +62 -13
  94. package/dist/src/commands/task-run-driver.js +62 -13
  95. package/dist/src/commands/task.js +65 -14
  96. package/dist/src/commands.js +227 -92
  97. package/dist/src/index.js +234 -99
  98. package/package.json +8 -9
  99. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.d.ts +0 -2
  100. package/dist/src/app-opentui/render/image-visual-layer-native-canvas.js +0 -1484
@@ -15,6 +15,89 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
+ // packages/cli/src/app-opentui/adapters/common.ts
19
+ function adapterContextFromRuntime(runtime, renderer) {
20
+ return {
21
+ projectRoot: runtime.getState().projectRoot,
22
+ renderer,
23
+ ensureRuntime: () => runtime.getRunnerContext(),
24
+ emit: (event) => runtime.emit(event),
25
+ getState: () => runtime.getState()
26
+ };
27
+ }
28
+ function projectRootOf(ctx) {
29
+ const root = ctx.rig?.projectRoot ?? ctx.projectRoot ?? ctx.getState().projectRoot;
30
+ if (typeof root === "string" && root.trim())
31
+ return root;
32
+ throw new Error("App adapter requires a projectRoot.");
33
+ }
34
+ async function runtimeOf(ctx) {
35
+ if (ctx.rig && "runId" in ctx.rig && ctx.ensureRuntime === undefined) {
36
+ return ctx.rig;
37
+ }
38
+ if (ctx.ensureRuntime)
39
+ return ctx.ensureRuntime();
40
+ throw new Error("App adapter requires ensureRuntime() before this action can run.");
41
+ }
42
+ function normalizeAppError(error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ const rawHint = error instanceof Error ? error.hint : undefined;
45
+ return {
46
+ message,
47
+ ...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {}
48
+ };
49
+ }
50
+ function emitStarted(ctx, label, optimistic) {
51
+ ctx.emit({ type: "action.started", label, ...optimistic ? { optimistic } : {} });
52
+ }
53
+ function emitProgress(ctx, label, detail, data) {
54
+ ctx.emit({ type: "action.progress", label, ...detail ? { detail } : {}, ...data ? { data } : {} });
55
+ }
56
+ function emitCompleted(ctx, label, data) {
57
+ ctx.emit({ type: "action.completed", label, ...data ? { data } : {} });
58
+ }
59
+ function emitFailed(ctx, label, error, data) {
60
+ if (data)
61
+ patchData(ctx, data);
62
+ const normalized = normalizeAppError(error);
63
+ ctx.emit({
64
+ type: "action.failed",
65
+ label,
66
+ message: normalized.message,
67
+ ...normalized.hint ? { hint: normalized.hint } : {},
68
+ cause: error
69
+ });
70
+ }
71
+ function patchData(ctx, data) {
72
+ ctx.emit({ type: "data.patch", data });
73
+ }
74
+ function patchFooter(ctx, footer) {
75
+ ctx.emit({ type: "footer.patch", footer });
76
+ }
77
+ function stringField(record, keys, fallback = "") {
78
+ for (const key of keys) {
79
+ const value = record[key];
80
+ if (typeof value === "string" && value.trim())
81
+ return value.trim();
82
+ if (typeof value === "number" && Number.isFinite(value))
83
+ return String(value);
84
+ }
85
+ return fallback;
86
+ }
87
+ function compactRecord(record, keys) {
88
+ const out = {};
89
+ for (const key of keys) {
90
+ const value = record[key];
91
+ if (value !== undefined && value !== null && value !== "")
92
+ out[key] = value;
93
+ }
94
+ return out;
95
+ }
96
+ function payloadString(intent, key) {
97
+ const value = intent.action.payload?.[key];
98
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
99
+ }
100
+
18
101
  // packages/cli/src/runner.ts
19
102
  import { EventBus } from "@rig/runtime/control-plane/runtime/events";
20
103
  import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
@@ -237,17 +320,21 @@ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
237
320
  const scopedKey = resolve2(projectRoot ?? process.cwd());
238
321
  scopedGitHubBearerTokens.set(scopedKey, cleanToken(token ?? undefined));
239
322
  }
240
- function readPrivateRemoteSessionToken(projectRoot) {
323
+ function readRemoteAuthState(projectRoot) {
241
324
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
242
325
  if (!existsSync2(path))
243
326
  return null;
244
327
  try {
245
328
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
246
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
329
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
247
330
  } catch {
248
331
  return null;
249
332
  }
250
333
  }
334
+ function readPrivateRemoteSessionToken(projectRoot) {
335
+ const parsed = readRemoteAuthState(projectRoot);
336
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
337
+ }
251
338
  function readGitHubBearerTokenForRemote(projectRoot) {
252
339
  const scopedKey = resolve2(projectRoot);
253
340
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -258,15 +345,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
258
345
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
259
346
  }
260
347
  function readStoredGitHubAuthToken(projectRoot) {
261
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
262
- if (!existsSync2(path))
348
+ const parsed = readRemoteAuthState(projectRoot);
349
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
350
+ }
351
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
352
+ const repo = readRepoConnection(projectRoot);
353
+ const slug = repo?.project?.trim();
354
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
263
355
  return null;
264
- try {
265
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
266
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
267
- } catch {
356
+ const auth = readRemoteAuthState(projectRoot);
357
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
358
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
359
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
268
360
  return null;
269
- }
361
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
362
+ if (!checkoutBaseDir)
363
+ return null;
364
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
365
+ writeRepoServerProjectRoot(projectRoot, inferred);
366
+ return inferred;
270
367
  }
271
368
  function readLocalConnectionFallbackToken(projectRoot) {
272
369
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -277,7 +374,7 @@ async function ensureServerForCli(projectRoot) {
277
374
  if (selected?.connection.kind === "remote") {
278
375
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
279
376
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
280
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
377
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
281
378
  return {
282
379
  baseUrl: selected.connection.baseUrl,
283
380
  authToken,
@@ -306,7 +403,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
306
403
  if (!slug)
307
404
  return null;
308
405
  try {
309
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
406
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
407
+ if (authToken && queryAuthFallbackEnabled())
408
+ url.searchParams.set("rt", authToken);
409
+ const response = await fetch(url, {
310
410
  headers: mergeHeaders(undefined, authToken)
311
411
  });
312
412
  if (!response.ok)
@@ -333,10 +433,23 @@ function appendTaskFilterParams(url, filters) {
333
433
  if (filters.limit !== undefined)
334
434
  url.searchParams.set("limit", String(filters.limit));
335
435
  }
436
+ function mergeCookie(existing, name, value) {
437
+ const encoded = `${name}=${encodeURIComponent(value)}`;
438
+ if (!existing?.trim())
439
+ return encoded;
440
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
441
+ return [...parts, encoded].join("; ");
442
+ }
443
+ function queryAuthFallbackEnabled(env = process.env) {
444
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
445
+ }
336
446
  function mergeHeaders(headers, authToken) {
337
447
  const merged = new Headers(headers);
338
448
  if (authToken) {
339
- merged.set("authorization", `Bearer ${authToken}`);
449
+ const bearer = `Bearer ${authToken}`;
450
+ merged.set("authorization", bearer);
451
+ merged.set("x-auth", bearer);
452
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
340
453
  }
341
454
  return merged;
342
455
  }
@@ -396,15 +509,34 @@ async function buildServerFailureContext(projectRoot, server) {
396
509
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
397
510
  };
398
511
  }
512
+ function isLoopbackRemoteBaseUrl(baseUrl) {
513
+ try {
514
+ const host = new URL(baseUrl).hostname.toLowerCase();
515
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ function canUseRemoteWithoutProjectRoot(pathname) {
521
+ return pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/server/project-root" || pathname.startsWith("/api/github/auth/") || pathname === "/api/projects" || pathname.startsWith("/api/projects/");
522
+ }
399
523
  async function requestServerJson(context, pathname, init = {}) {
400
524
  const server = await ensureServerForCli(context.projectRoot);
525
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
526
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
527
+ const repo = readRepoConnection(context.projectRoot);
528
+ throw new CliError(`Remote server is selected for ${repo?.project ?? "this repo"}, but this checkout has no server-host project root link.`, 1, { hint: "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to repair the remote project link before task/run commands." });
529
+ }
401
530
  const headers = mergeHeaders(init.headers, server.authToken);
402
531
  if (server.serverProjectRoot)
403
532
  headers.set("x-rig-project-root", server.serverProjectRoot);
533
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
534
+ requestUrl.searchParams.set("rt", server.authToken);
535
+ }
404
536
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
405
537
  let response;
406
538
  try {
407
- response = await fetch(`${server.baseUrl}${pathname}`, {
539
+ response = await fetch(requestUrl, {
408
540
  ...init,
409
541
  headers
410
542
  });
@@ -466,11 +598,26 @@ async function getGitHubAuthStatusViaServer(context) {
466
598
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
467
599
  }
468
600
  async function postGitHubTokenViaServer(context, token, options = {}) {
469
- const payload = await requestServerJson(context, "/api/github/auth/token", {
470
- method: "POST",
471
- headers: { "content-type": "application/json" },
472
- body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot })
473
- });
601
+ const server = await ensureServerForCli(context.projectRoot);
602
+ const requestUrl = new URL(`${server.baseUrl}/api/github/auth/token`);
603
+ reportServerPhase("POST /api/github/auth/token\u2026");
604
+ let response;
605
+ try {
606
+ response = await fetch(requestUrl, {
607
+ method: "POST",
608
+ headers: { "content-type": "application/json" },
609
+ body: JSON.stringify({ token, selectedRepo: options.selectedRepo, projectRoot: options.projectRoot ?? server.serverProjectRoot ?? undefined })
610
+ });
611
+ } catch (error) {
612
+ const failure = await buildServerFailureContext(context.projectRoot, server);
613
+ throw new CliError(`Rig server auth bootstrap failed: ${error instanceof Error ? error.message : String(error)}
614
+ ${failure.contextLine}`, 1, { hint: failure.hint });
615
+ }
616
+ const payload = await response.json().catch(() => null);
617
+ if (!response.ok) {
618
+ const detail = payload && typeof payload === "object" && !Array.isArray(payload) ? String(payload.error ?? JSON.stringify(payload)) : `HTTP ${response.status}`;
619
+ throw new CliError(`Rig server auth bootstrap failed (${response.status}): ${detail}`, 1, { hint: "Re-run `rig github auth import-gh`, or check the selected server with `rig server status`." });
620
+ }
474
621
  return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
475
622
  }
476
623
  async function prepareRemoteCheckoutViaServer(context, input) {
@@ -1107,7 +1254,8 @@ async function runFastTaskRunPreflight(context, options = {}) {
1107
1254
  }
1108
1255
  }
1109
1256
  const repo = readRepoConnection(context.projectRoot);
1110
- checks.push(repo ? preflightCheck("project-link", "project linked to Rig server", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to Rig server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
1257
+ const remoteWithoutProjectLink = selectedServer?.connectionKind === "remote" && !repo?.project;
1258
+ checks.push(repo ? preflightCheck("project-link", "project linked to selected server", repo.project ? "pass" : remoteWithoutProjectLink ? "fail" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : " (missing project link)"}`, remoteWithoutProjectLink ? "Run `rig init --repair` or `rig init --yes --repo owner/repo --server remote` to restore the remote project link." : "Run `rig init --yes --repo owner/repo` to record the GitHub repo slug.") : preflightCheck("project-link", "project linked to selected server", legacyServerCompatibility ? "warn" : "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig server use <alias|local>`."));
1111
1259
  try {
1112
1260
  const auth = await request("/api/github/auth/status");
1113
1261
  checks.push(isAuthenticated(auth) ? preflightCheck("github-auth", "GitHub auth valid", "pass") : preflightCheck("github-auth", "GitHub auth valid", legacyServerCompatibility ? "warn" : "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
@@ -1682,6 +1830,7 @@ async function attachRunBundledPiFrontend(context, input) {
1682
1830
  let detached = false;
1683
1831
  try {
1684
1832
  await runPiMain([
1833
+ "--offline",
1685
1834
  "--no-extensions",
1686
1835
  "--no-skills",
1687
1836
  "--no-prompt-templates",
@@ -2990,93 +3139,536 @@ var init_task = __esm(() => {
2990
3139
  init__connection_state();
2991
3140
  });
2992
3141
 
2993
- // packages/cli/src/app-opentui/adapters/tasks.ts
2994
- import { randomUUID } from "crypto";
2995
-
2996
- // packages/cli/src/app-opentui/adapters/common.ts
2997
- function adapterContextFromRuntime(runtime, renderer) {
3142
+ // packages/cli/src/app-opentui/pi-pty-host.ts
3143
+ import { fileURLToPath } from "url";
3144
+ import { basename } from "path";
3145
+ import { RGBA, StyledText, TextAttributes } from "@opentui/core";
3146
+ import { Terminal as XtermTerminal } from "@xterm/headless";
3147
+ function clampCols(cols) {
3148
+ return Math.max(MIN_COLS, Math.trunc(cols || 100));
3149
+ }
3150
+ function clampRows(rows) {
3151
+ return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.trunc(rows || 35)));
3152
+ }
3153
+ function rgbaFromXtermColor(mode, value) {
3154
+ if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
3155
+ return RGBA.fromIndex(value);
3156
+ }
3157
+ if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
3158
+ return RGBA.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
3159
+ }
3160
+ return;
3161
+ }
3162
+ function textAttributesFromCell(cell) {
3163
+ let attributes = TextAttributes.NONE;
3164
+ if (cell.isBold())
3165
+ attributes |= TextAttributes.BOLD;
3166
+ if (cell.isDim())
3167
+ attributes |= TextAttributes.DIM;
3168
+ if (cell.isItalic())
3169
+ attributes |= TextAttributes.ITALIC;
3170
+ if (cell.isUnderline())
3171
+ attributes |= TextAttributes.UNDERLINE;
3172
+ if (cell.isBlink())
3173
+ attributes |= TextAttributes.BLINK;
3174
+ if (cell.isInverse())
3175
+ attributes |= TextAttributes.INVERSE;
3176
+ if (cell.isInvisible())
3177
+ attributes |= TextAttributes.HIDDEN;
3178
+ if (cell.isStrikethrough())
3179
+ attributes |= TextAttributes.STRIKETHROUGH;
3180
+ return attributes;
3181
+ }
3182
+ function sameRgba(a, b) {
3183
+ if (!a && !b)
3184
+ return true;
3185
+ return Boolean(a && b && a.equals(b));
3186
+ }
3187
+ function sameStyle(a, b) {
3188
+ return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
3189
+ }
3190
+ function styleFromCell(cell) {
2998
3191
  return {
2999
- projectRoot: runtime.getState().projectRoot,
3000
- renderer,
3001
- ensureRuntime: () => runtime.getRunnerContext(),
3002
- emit: (event) => runtime.emit(event),
3003
- getState: () => runtime.getState()
3192
+ fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
3193
+ bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
3194
+ attributes: textAttributesFromCell(cell)
3004
3195
  };
3005
3196
  }
3006
- function projectRootOf(ctx) {
3007
- const root = ctx.rig?.projectRoot ?? ctx.projectRoot ?? ctx.getState().projectRoot;
3008
- if (typeof root === "string" && root.trim())
3009
- return root;
3010
- throw new Error("Rig app adapter requires a projectRoot.");
3011
- }
3012
- async function runtimeOf(ctx) {
3013
- if (ctx.rig && "runId" in ctx.rig && ctx.ensureRuntime === undefined) {
3014
- return ctx.rig;
3197
+ function lineToStyledText(line, cols) {
3198
+ if (!line)
3199
+ return new StyledText([{ __isChunk: true, text: "" }]);
3200
+ const chunks = [];
3201
+ let run = "";
3202
+ let runStyle = null;
3203
+ const flush = () => {
3204
+ if (!run)
3205
+ return;
3206
+ chunks.push({
3207
+ __isChunk: true,
3208
+ text: run,
3209
+ ...runStyle?.fg ? { fg: runStyle.fg } : {},
3210
+ ...runStyle?.bg ? { bg: runStyle.bg } : {},
3211
+ ...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
3212
+ });
3213
+ run = "";
3214
+ };
3215
+ for (let index = 0;index < cols; index += 1) {
3216
+ const cell = line.getCell(index);
3217
+ if (!cell) {
3218
+ if (runStyle !== null)
3219
+ flush();
3220
+ runStyle = null;
3221
+ run += " ";
3222
+ continue;
3223
+ }
3224
+ if (cell.getWidth() === 0)
3225
+ continue;
3226
+ const style = styleFromCell(cell);
3227
+ if (!runStyle || !sameStyle(runStyle, style)) {
3228
+ flush();
3229
+ runStyle = style;
3230
+ }
3231
+ run += cell.getChars() || " ";
3015
3232
  }
3016
- if (ctx.ensureRuntime)
3017
- return ctx.ensureRuntime();
3018
- throw new Error("Rig app adapter requires ensureRuntime() before this action can run.");
3233
+ flush();
3234
+ return new StyledText(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
3235
+ }
3236
+ function childCommandPrefix() {
3237
+ const execName = basename(process.execPath).toLowerCase();
3238
+ const currentEntry = process.argv[1];
3239
+ if (execName === "bun" || execName === "bun.exe") {
3240
+ return currentEntry ? [process.execPath, currentEntry, "__opentui-pi-host"] : [process.execPath, fallbackChildScriptPath];
3241
+ }
3242
+ return [process.execPath, "__opentui-pi-host"];
3019
3243
  }
3020
- function normalizeAppError(error) {
3021
- const message = error instanceof Error ? error.message : String(error);
3022
- const rawHint = error instanceof Error ? error.hint : undefined;
3244
+ function withEnv(base) {
3245
+ const env = {};
3246
+ for (const [key, value] of Object.entries(base ?? process.env)) {
3247
+ if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP" || key === "NO_COLOR")
3248
+ continue;
3249
+ if (typeof value === "string")
3250
+ env[key] = value;
3251
+ }
3023
3252
  return {
3024
- message,
3025
- ...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {}
3253
+ ...env,
3254
+ TERM: "xterm-256color",
3255
+ COLORTERM: "truecolor",
3256
+ FORCE_COLOR: env.FORCE_COLOR ?? "1",
3257
+ RIG_OPENTUI_PI_HOST: "1"
3026
3258
  };
3027
3259
  }
3028
- function emitStarted(ctx, label, optimistic) {
3029
- ctx.emit({ type: "action.started", label, ...optimistic ? { optimistic } : {} });
3030
- }
3031
- function emitProgress(ctx, label, detail, data) {
3032
- ctx.emit({ type: "action.progress", label, ...detail ? { detail } : {}, ...data ? { data } : {} });
3260
+ async function startPiPtyHost(options) {
3261
+ activeHost?.dispose("replace");
3262
+ const host = new PiPtyHost(options);
3263
+ activeHost = host;
3264
+ await host.start();
3265
+ return host;
3033
3266
  }
3034
- function emitCompleted(ctx, label, data) {
3035
- ctx.emit({ type: "action.completed", label, ...data ? { data } : {} });
3267
+
3268
+ class PiPtyHost {
3269
+ runId;
3270
+ projectRoot;
3271
+ onSnapshot;
3272
+ onExit;
3273
+ onError;
3274
+ terminal;
3275
+ disposables = [];
3276
+ decoder = new TextDecoder("utf-8");
3277
+ proc = null;
3278
+ pty = null;
3279
+ status = "starting";
3280
+ cols;
3281
+ rows;
3282
+ message = "starting bundled Pi";
3283
+ exitCode;
3284
+ signal;
3285
+ notifyTimer = null;
3286
+ _disposed = false;
3287
+ constructor(options) {
3288
+ this.runId = options.runId;
3289
+ this.projectRoot = options.projectRoot;
3290
+ this.cols = clampCols(options.cols);
3291
+ this.rows = clampRows(options.rows);
3292
+ this.onSnapshot = options.onSnapshot;
3293
+ this.onExit = options.onExit;
3294
+ this.onError = options.onError;
3295
+ this.terminal = new XtermTerminal({
3296
+ allowProposedApi: true,
3297
+ cols: this.cols,
3298
+ rows: this.rows,
3299
+ scrollback: 1000
3300
+ });
3301
+ this.registerTerminalResponders();
3302
+ }
3303
+ get disposed() {
3304
+ return this._disposed;
3305
+ }
3306
+ get snapshot() {
3307
+ return this.createSnapshot();
3308
+ }
3309
+ async start() {
3310
+ if (this._disposed)
3311
+ throw new Error("Pi PTY host is disposed.");
3312
+ if (typeof Bun.Terminal !== "function") {
3313
+ throw new Error("Bun native PTY is unavailable in this runtime. Pi host requires Bun.Terminal.");
3314
+ }
3315
+ const spawnOptions = {
3316
+ cwd: this.projectRoot,
3317
+ env: withEnv(process.env),
3318
+ terminal: {
3319
+ cols: this.cols,
3320
+ rows: this.rows,
3321
+ name: "xterm-256color",
3322
+ data: (_terminal, data) => this.handlePtyData(data)
3323
+ }
3324
+ };
3325
+ const proc = Bun.spawn([
3326
+ ...childCommandPrefix(),
3327
+ "--run-id",
3328
+ this.runId,
3329
+ "--project-root",
3330
+ this.projectRoot
3331
+ ], spawnOptions);
3332
+ if (!proc.terminal)
3333
+ throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
3334
+ this.proc = proc;
3335
+ this.pty = proc.terminal;
3336
+ this.status = "running";
3337
+ this.message = "bundled Pi running inside this app";
3338
+ this.emitSnapshotSoon(0);
3339
+ proc.exited.then((exitCode) => {
3340
+ if (this._disposed)
3341
+ return;
3342
+ this.status = exitCode === 0 ? "exited" : "failed";
3343
+ this.exitCode = exitCode;
3344
+ this.signal = null;
3345
+ this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
3346
+ const snapshot = this.createSnapshot();
3347
+ this.onSnapshot?.(snapshot);
3348
+ this.onExit?.(snapshot);
3349
+ if (activeHost === this)
3350
+ activeHost = null;
3351
+ this.dispose("exit", { kill: false, notify: false });
3352
+ }).catch((error) => {
3353
+ if (this._disposed)
3354
+ return;
3355
+ this.status = "failed";
3356
+ this.message = error instanceof Error ? error.message : String(error);
3357
+ const snapshot = this.createSnapshot();
3358
+ this.onSnapshot?.(snapshot);
3359
+ this.onError?.(error, snapshot);
3360
+ if (activeHost === this)
3361
+ activeHost = null;
3362
+ this.dispose("error", { kill: false, notify: false });
3363
+ });
3364
+ }
3365
+ write(data) {
3366
+ if (this._disposed || !this.pty)
3367
+ return;
3368
+ try {
3369
+ this.pty.write(data);
3370
+ } catch (error) {
3371
+ this.status = "failed";
3372
+ this.message = error instanceof Error ? error.message : String(error);
3373
+ const snapshot = this.createSnapshot();
3374
+ this.onSnapshot?.(snapshot);
3375
+ this.onError?.(error, snapshot);
3376
+ }
3377
+ }
3378
+ resize(cols, rows) {
3379
+ const nextCols = clampCols(cols);
3380
+ const nextRows = clampRows(rows);
3381
+ if (nextCols === this.cols && nextRows === this.rows)
3382
+ return;
3383
+ this.cols = nextCols;
3384
+ this.rows = nextRows;
3385
+ this.terminal.resize(nextCols, nextRows);
3386
+ try {
3387
+ this.pty?.resize(nextCols, nextRows);
3388
+ } catch {}
3389
+ this.emitSnapshotSoon(0);
3390
+ }
3391
+ detach() {
3392
+ this.dispose("detach");
3393
+ return this.createSnapshot("detached from bundled Pi");
3394
+ }
3395
+ dispose(reason = "dispose", options = {}) {
3396
+ if (this._disposed)
3397
+ return;
3398
+ this._disposed = true;
3399
+ if (this.notifyTimer)
3400
+ clearTimeout(this.notifyTimer);
3401
+ this.notifyTimer = null;
3402
+ for (const disposable of this.disposables.splice(0)) {
3403
+ try {
3404
+ disposable.dispose();
3405
+ } catch {}
3406
+ }
3407
+ if (options.kill !== false) {
3408
+ try {
3409
+ this.proc?.kill("SIGTERM");
3410
+ } catch {}
3411
+ }
3412
+ try {
3413
+ this.pty?.close();
3414
+ } catch {}
3415
+ try {
3416
+ this.terminal.dispose();
3417
+ } catch {}
3418
+ if (activeHost === this)
3419
+ activeHost = null;
3420
+ if (options.notify) {
3421
+ this.message = reason;
3422
+ this.onSnapshot?.(this.createSnapshot(reason));
3423
+ }
3424
+ }
3425
+ handlePtyData(data) {
3426
+ if (this._disposed)
3427
+ return;
3428
+ const text = this.decoder.decode(data, { stream: true });
3429
+ this.respondToRawTerminalQueries(text);
3430
+ this.terminal.write(data, () => this.emitSnapshotSoon());
3431
+ }
3432
+ emitSnapshotSoon(delayMs = SNAPSHOT_DELAY_MS) {
3433
+ if (this._disposed || this.notifyTimer)
3434
+ return;
3435
+ this.notifyTimer = setTimeout(() => {
3436
+ this.notifyTimer = null;
3437
+ if (this._disposed)
3438
+ return;
3439
+ this.onSnapshot?.(this.createSnapshot());
3440
+ }, delayMs);
3441
+ }
3442
+ createSnapshot(message2 = this.message) {
3443
+ const buffer = this.terminal.buffer.active;
3444
+ const end = buffer.length;
3445
+ const start = Math.max(0, end - MAX_SNAPSHOT_LINES);
3446
+ const styledStart = Math.max(start, end - this.rows - STYLED_SNAPSHOT_MARGIN);
3447
+ const lines = [];
3448
+ const styledLines = [];
3449
+ for (let row = start;row < end; row += 1) {
3450
+ const line = buffer.getLine(row);
3451
+ lines.push((line?.translateToString(false) ?? "").slice(0, this.cols));
3452
+ if (row >= styledStart)
3453
+ styledLines[lines.length - 1] = lineToStyledText(line, this.cols);
3454
+ }
3455
+ while (lines.length < this.rows) {
3456
+ lines.push("");
3457
+ styledLines[lines.length - 1] = new StyledText([{ __isChunk: true, text: "" }]);
3458
+ }
3459
+ return {
3460
+ runId: this.runId,
3461
+ status: this.status,
3462
+ cols: this.cols,
3463
+ rows: this.rows,
3464
+ lines,
3465
+ styledLines,
3466
+ message: message2,
3467
+ ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
3468
+ ...this.signal !== undefined ? { signal: this.signal } : {}
3469
+ };
3470
+ }
3471
+ registerTerminalResponders() {
3472
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
3473
+ if (params.length === 0 || params[0] === 0)
3474
+ this.write("\x1B[?62;22c");
3475
+ return false;
3476
+ }));
3477
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
3478
+ if (params.length === 0 || params[0] === 0)
3479
+ this.write("\x1B[>0;0;0c");
3480
+ return false;
3481
+ }));
3482
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
3483
+ if (params[0] === 5) {
3484
+ this.write("\x1B[0n");
3485
+ } else if (params[0] === 6) {
3486
+ const row = this.terminal.buffer.active.cursorY + 1;
3487
+ const col = this.terminal.buffer.active.cursorX + 1;
3488
+ this.write(`\x1B[${row};${col}R`);
3489
+ }
3490
+ return false;
3491
+ }));
3492
+ }
3493
+ respondToRawTerminalQueries(text) {
3494
+ if (!text)
3495
+ return;
3496
+ const decrqm = /\x1b\[\?(\d+)\$p/g;
3497
+ let match;
3498
+ while ((match = decrqm.exec(text)) !== null) {
3499
+ this.write(`\x1B[?${match[1]};2$y`);
3500
+ }
3501
+ }
3036
3502
  }
3037
- function emitFailed(ctx, label, error, data) {
3038
- if (data)
3039
- patchData(ctx, data);
3040
- const normalized = normalizeAppError(error);
3041
- ctx.emit({
3042
- type: "action.failed",
3043
- label,
3044
- message: normalized.message,
3045
- ...normalized.hint ? { hint: normalized.hint } : {},
3046
- cause: error
3047
- });
3503
+ var MIN_COLS = 40, MIN_ROWS = 12, MAX_ROWS = 300, MAX_SNAPSHOT_LINES = 360, STYLED_SNAPSHOT_MARGIN = 6, SNAPSHOT_DELAY_MS = 120, fallbackChildScriptPath, activeHost = null, XTERM_COLOR_MODE_INDEXED_16 = 16777216, XTERM_COLOR_MODE_INDEXED_256 = 33554432, XTERM_COLOR_MODE_RGB = 50331648;
3504
+ var init_pi_pty_host = __esm(() => {
3505
+ fallbackChildScriptPath = fileURLToPath(new URL("./pi-host-child.ts", import.meta.url));
3506
+ });
3507
+
3508
+ // packages/cli/src/app-opentui/adapters/fleet.ts
3509
+ function normalizeRunRecord(record) {
3510
+ const runId = stringField(record, ["runId", "id"]);
3511
+ if (!runId)
3512
+ return null;
3513
+ const title = stringField(record, ["title", "taskTitle", "taskId"], "untitled run");
3514
+ const taskId3 = stringField(record, ["taskId"]);
3515
+ const updatedAt = stringField(record, ["updatedAt", "createdAt"]);
3516
+ return {
3517
+ runId,
3518
+ status: stringField(record, ["status", "state"], "unknown"),
3519
+ title,
3520
+ raw: record,
3521
+ ...taskId3 ? { taskId: taskId3 } : {},
3522
+ ...updatedAt ? { updatedAt } : {}
3523
+ };
3048
3524
  }
3049
- function patchData(ctx, data) {
3050
- ctx.emit({ type: "data.patch", data });
3525
+ async function refreshFleet(ctx, options = {}) {
3526
+ const label = "Refreshing fleet";
3527
+ emitStarted(ctx, label);
3528
+ try {
3529
+ const { listRunsViaServer: listRunsViaServer2, resolveServerConnectionLabel: resolveServerConnectionLabel2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
3530
+ const projectRoot = projectRootOf(ctx);
3531
+ emitProgress(ctx, label, "contacting selected server");
3532
+ const [runs, serverLabel] = await Promise.all([
3533
+ listRunsViaServer2({ projectRoot }, { limit: options.limit ?? 50 }),
3534
+ resolveServerConnectionLabel2(projectRoot)
3535
+ ]);
3536
+ const normalized = runs.flatMap((run) => {
3537
+ const entry = normalizeRunRecord(run);
3538
+ return entry ? [entry] : [];
3539
+ });
3540
+ patchData(ctx, { fleet: { runs: normalized, refreshedAt: new Date().toISOString() } });
3541
+ patchFooter(ctx, { server: serverLabel, runs: String(normalized.length) });
3542
+ emitCompleted(ctx, label, { count: normalized.length });
3543
+ return normalized;
3544
+ } catch (error) {
3545
+ emitFailed(ctx, label, error);
3546
+ throw error;
3547
+ }
3051
3548
  }
3052
- function patchFooter(ctx, footer) {
3053
- ctx.emit({ type: "footer.patch", footer });
3549
+ var init_fleet = () => {};
3550
+
3551
+ // packages/cli/src/app-opentui/adapters/pi-attach.ts
3552
+ var exports_pi_attach = {};
3553
+ __export(exports_pi_attach, {
3554
+ preparePiAttachHandoff: () => preparePiAttachHandoff,
3555
+ createPiAttachAdapter: () => createPiAttachAdapter,
3556
+ attachRunWithBundledPi: () => attachRunWithBundledPi
3557
+ });
3558
+ function createPiAttachAdapter(renderer) {
3559
+ return {
3560
+ id: "pi-attach",
3561
+ async handleIntent(runtime, intent) {
3562
+ if (intent.scene !== "handoff" && intent.action.kind !== "run-attach")
3563
+ return false;
3564
+ const ctx = adapterContextFromRuntime(runtime, renderer);
3565
+ const runId = payloadString(intent, "runId") ?? runtime.getState().data.selectedRunId;
3566
+ if (intent.action.kind === "run-attach" && runId) {
3567
+ await attachRunWithBundledPi(ctx, runId);
3568
+ return true;
3569
+ }
3570
+ if (runId) {
3571
+ await preparePiAttachHandoff(ctx, runId);
3572
+ return true;
3573
+ }
3574
+ return false;
3575
+ }
3576
+ };
3054
3577
  }
3055
- function stringField(record, keys, fallback = "") {
3056
- for (const key of keys) {
3057
- const value = record[key];
3058
- if (typeof value === "string" && value.trim())
3059
- return value.trim();
3060
- if (typeof value === "number" && Number.isFinite(value))
3061
- return String(value);
3578
+ async function preparePiAttachHandoff(ctx, runId) {
3579
+ const cleanRunId = runId.trim();
3580
+ const label = `Preparing Pi ${cleanRunId.slice(0, 8)}`;
3581
+ emitStarted(ctx, label, { piAttach: { runId: cleanRunId, status: "preparing" } });
3582
+ try {
3583
+ const { getRunDetailsViaServer: getRunDetailsViaServer2, getRunPiStatusViaServer: getRunPiStatusViaServer2, resolveServerConnectionLabel: resolveServerConnectionLabel2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
3584
+ const projectRoot = projectRootOf(ctx);
3585
+ emitProgress(ctx, label, "checking run and Pi session readiness");
3586
+ const [run, piStatus, server] = await Promise.all([
3587
+ getRunDetailsViaServer2({ projectRoot }, cleanRunId).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })),
3588
+ getRunPiStatusViaServer2({ projectRoot }, cleanRunId).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })),
3589
+ resolveServerConnectionLabel2(projectRoot)
3590
+ ]);
3591
+ const state = {
3592
+ runId: cleanRunId,
3593
+ status: "preparing",
3594
+ message: "ready to enter worker Pi",
3595
+ result: { run, piStatus, server },
3596
+ ready: true
3597
+ };
3598
+ patchData(ctx, { piAttach: state });
3599
+ emitCompleted(ctx, label, { runId: cleanRunId, server });
3600
+ return state;
3601
+ } catch (error) {
3602
+ const state = { runId: cleanRunId, status: "failed", message: error instanceof Error ? error.message : String(error) };
3603
+ patchData(ctx, { piAttach: state });
3604
+ emitFailed(ctx, label, error, { runId: cleanRunId });
3605
+ throw error;
3062
3606
  }
3063
- return fallback;
3064
3607
  }
3065
- function compactRecord(record, keys) {
3066
- const out = {};
3067
- for (const key of keys) {
3068
- const value = record[key];
3069
- if (value !== undefined && value !== null && value !== "")
3070
- out[key] = value;
3608
+ async function attachRunWithBundledPi(ctx, runId) {
3609
+ const cleanRunId = runId.trim();
3610
+ const label = `Entering Pi ${cleanRunId.slice(0, 8)}`;
3611
+ if (!cleanRunId) {
3612
+ const error = new Error("Attach requires a run id.");
3613
+ emitFailed(ctx, label, error);
3614
+ throw error;
3615
+ }
3616
+ emitStarted(ctx, label, { piAttach: { runId: cleanRunId, status: "entering-pi" } });
3617
+ patchData(ctx, { piAttach: { runId: cleanRunId, status: "entering-pi", message: "starting bundled Pi" } });
3618
+ try {
3619
+ await preparePiAttachHandoff(ctx, cleanRunId).catch(() => null);
3620
+ emitProgress(ctx, label, "starting Bun PTY host for bundled Pi");
3621
+ const projectRoot = projectRootOf(ctx);
3622
+ const cols = typeof process.stdout.columns === "number" ? process.stdout.columns : 100;
3623
+ const rows = Math.max(1, (typeof process.stdout.rows === "number" ? process.stdout.rows : 35) - 1);
3624
+ const patchTerminal = (snapshot) => {
3625
+ patchData(ctx, {
3626
+ piTerminal: snapshot,
3627
+ piAttach: {
3628
+ runId: cleanRunId,
3629
+ status: snapshot.status === "failed" ? "failed" : snapshot.status === "exited" ? "returned" : "entering-pi",
3630
+ message: snapshot.message ?? "bundled Pi running",
3631
+ result: { runId: cleanRunId, status: snapshot.status, exitCode: snapshot.exitCode }
3632
+ }
3633
+ });
3634
+ };
3635
+ await startPiPtyHost({
3636
+ runId: cleanRunId,
3637
+ projectRoot,
3638
+ cols,
3639
+ rows,
3640
+ onSnapshot: patchTerminal,
3641
+ onExit: (snapshot) => {
3642
+ patchTerminal(snapshot);
3643
+ emitCompleted(ctx, label, { runId: cleanRunId, result: { status: snapshot.status, exitCode: snapshot.exitCode } });
3644
+ ctx.emit({ type: "scene.change", scene: "fleet", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } } });
3645
+ refreshFleet(ctx).catch((error) => emitFailed(ctx, "Refresh runs", error));
3646
+ },
3647
+ onError: (error, snapshot) => {
3648
+ patchTerminal(snapshot);
3649
+ emitFailed(ctx, label, error, { runId: cleanRunId });
3650
+ }
3651
+ });
3652
+ const record = {
3653
+ runId: cleanRunId,
3654
+ status: "running",
3655
+ rendered: "bundled Pi embedded via Bun.Terminal + @xterm/headless"
3656
+ };
3657
+ emitProgress(ctx, label, "bundled Pi is running", { piAttach: { runId: cleanRunId, status: "entering-pi", message: "bundled Pi running", result: record } });
3658
+ return record;
3659
+ } catch (error) {
3660
+ patchData(ctx, { piAttach: { runId: cleanRunId, status: "failed", message: error instanceof Error ? error.message : String(error) } });
3661
+ emitFailed(ctx, label, error, { runId: cleanRunId });
3662
+ throw error;
3071
3663
  }
3072
- return out;
3073
- }
3074
- function payloadString(intent, key) {
3075
- const value = intent.action.payload?.[key];
3076
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
3077
3664
  }
3665
+ var init_pi_attach = __esm(() => {
3666
+ init_pi_pty_host();
3667
+ init_fleet();
3668
+ });
3078
3669
 
3079
3670
  // packages/cli/src/app-opentui/adapters/tasks.ts
3671
+ import { randomUUID } from "crypto";
3080
3672
  function createTasksAdapter() {
3081
3673
  return {
3082
3674
  id: "tasks",
@@ -3084,12 +3676,13 @@ function createTasksAdapter() {
3084
3676
  if (intent.scene !== "tasks" && intent.action.kind !== "task-run-next" && intent.action.kind !== "task-run-id")
3085
3677
  return false;
3086
3678
  const ctx = adapterContextFromRuntime(runtime);
3679
+ const detach = intent.action.payload?.detach === true;
3087
3680
  if (intent.action.kind === "task-run-next") {
3088
- await submitNextTaskRun(ctx);
3681
+ await submitNextTaskRun(ctx, {}, { detach });
3089
3682
  return true;
3090
3683
  }
3091
3684
  if (intent.action.kind === "task-run-id") {
3092
- await submitTaskRun(ctx, { taskId: payloadString(intent, "task") });
3685
+ await submitTaskRun(ctx, { taskId: payloadString(intent, "task"), detach });
3093
3686
  return true;
3094
3687
  }
3095
3688
  if (intent.action.kind === "refresh" || intent.action.kind === "navigate") {
@@ -3150,6 +3743,30 @@ function labelsFor(record) {
3150
3743
  function assigneesFor(record) {
3151
3744
  return stringList(rawRecord(record, "assignees"));
3152
3745
  }
3746
+ function activeRunForRecord(record) {
3747
+ const value = rawRecord(record, "activeRun");
3748
+ if (!value || typeof value !== "object" || Array.isArray(value))
3749
+ return;
3750
+ const activeRun = value;
3751
+ const runId = optionalString(activeRun, ["runId", "id"]);
3752
+ if (!runId)
3753
+ return;
3754
+ const status = optionalString(activeRun, ["status", "state"]);
3755
+ const stage = optionalString(activeRun, ["stage", "phase"]);
3756
+ return {
3757
+ runId,
3758
+ ...status ? { status } : {},
3759
+ ...stage ? { stage } : {}
3760
+ };
3761
+ }
3762
+ function activeRunIdFromTaskLike(task) {
3763
+ return activeRunForRecord(task)?.runId;
3764
+ }
3765
+ function activeRunIdFromTaskRunError(error) {
3766
+ const message2 = error instanceof Error ? error.message : String(error);
3767
+ const match = message2.match(/already has an active (?:Rig )?run:\s*([^\s"'}]+)/i);
3768
+ return match?.[1]?.replace(/[),.;]+$/g, "") ?? null;
3769
+ }
3153
3770
  function normalizeTaskRecord(record) {
3154
3771
  const id = stringField(record, ["id", "taskId", "number"]);
3155
3772
  if (!id)
@@ -3157,6 +3774,7 @@ function normalizeTaskRecord(record) {
3157
3774
  const priority = optionalNumber(record, ["priority", "rank", "score"]);
3158
3775
  const updatedAt = optionalString(record, ["updatedAt", "updated_at", "modifiedAt"]);
3159
3776
  const createdAt = optionalString(record, ["createdAt", "created_at"]);
3777
+ const activeRun = activeRunForRecord(record);
3160
3778
  return {
3161
3779
  id,
3162
3780
  status: stringField(record, ["status", "state"], "unknown"),
@@ -3166,6 +3784,7 @@ function normalizeTaskRecord(record) {
3166
3784
  ...createdAt ? { createdAt } : {},
3167
3785
  labels: labelsFor(record),
3168
3786
  assignees: assigneesFor(record),
3787
+ ...activeRun ? { activeRun } : {},
3169
3788
  raw: record
3170
3789
  };
3171
3790
  }
@@ -3393,7 +4012,7 @@ async function submitTaskRun(ctx, input = {}) {
3393
4012
  try {
3394
4013
  const { submitTaskRunViaServer: submitTaskRunViaServer2 } = await Promise.resolve().then(() => (init__server_client(), exports__server_client));
3395
4014
  const defaults = await loadRunDefaults(projectRoot);
3396
- emitProgress(ctx, label, "submitting run to selected Rig server", { runId, taskId: taskId3 ?? null });
4015
+ emitProgress(ctx, label, "submitting run to selected server", { runId, taskId: taskId3 ?? null });
3397
4016
  const submitted = await submitTaskRunViaServer2({ projectRoot }, {
3398
4017
  runId,
3399
4018
  taskId: taskId3 || undefined,
@@ -3413,14 +4032,41 @@ async function submitTaskRun(ctx, input = {}) {
3413
4032
  latestSubmittedRun: { runId: submitted.runId, taskId: taskId3 ?? null },
3414
4033
  runDetail: { runId: submitted.runId, status: "submitted", title: optimisticTitle, run: submitted, logs: [], timeline: [], timelineCursor: null }
3415
4034
  });
3416
- patchFooter(ctx, { run: `${submitted.runId.slice(0, 8)} submitted` });
4035
+ patchFooter(ctx, { run: `${submitted.runId.slice(0, 8)} ${input.detach ? "submitted" : "attaching"}` });
4036
+ if (!input.detach) {
4037
+ ctx.emit({
4038
+ type: "scene.change",
4039
+ scene: "handoff",
4040
+ intent: { scene: "handoff", argv: ["attach", submitted.runId], action: { kind: "run-attach", payload: { runId: submitted.runId }, label: `Attach Pi ${submitted.runId.slice(0, 8)}` } }
4041
+ });
4042
+ const { attachRunWithBundledPi: attachRunWithBundledPi2 } = await Promise.resolve().then(() => (init_pi_attach(), exports_pi_attach));
4043
+ await attachRunWithBundledPi2(ctx, submitted.runId);
4044
+ }
3417
4045
  return submitted;
3418
4046
  } catch (error) {
4047
+ const activeRunId = activeRunIdFromTaskRunError(error);
4048
+ if (activeRunId) {
4049
+ emitProgress(ctx, label, `task already has active run ${activeRunId.slice(0, 8)}; attaching Pi`, {
4050
+ selectedRunId: activeRunId,
4051
+ dispatchingRun: null,
4052
+ latestSubmittedRun: { runId: activeRunId, taskId: taskId3 ?? null }
4053
+ });
4054
+ patchData(ctx, { selectedRunId: activeRunId, dispatchingRun: null });
4055
+ patchFooter(ctx, { run: `${activeRunId.slice(0, 8)} active` });
4056
+ ctx.emit({
4057
+ type: "scene.change",
4058
+ scene: "handoff",
4059
+ intent: { scene: "handoff", argv: ["attach", activeRunId], action: { kind: "run-attach", payload: { runId: activeRunId }, label: `Attach Pi ${activeRunId.slice(0, 8)}` } }
4060
+ });
4061
+ const { attachRunWithBundledPi: attachRunWithBundledPi2 } = await Promise.resolve().then(() => (init_pi_attach(), exports_pi_attach));
4062
+ await attachRunWithBundledPi2(ctx, activeRunId);
4063
+ return { runId: activeRunId };
4064
+ }
3419
4065
  emitFailed(ctx, label, error, { runId, taskId: taskId3 ?? null, revert: { dispatchingRun: null } });
3420
4066
  throw error;
3421
4067
  }
3422
4068
  }
3423
- async function submitNextTaskRun(ctx, filters = {}) {
4069
+ async function submitNextTaskRun(ctx, filters = {}, options = {}) {
3424
4070
  const label = "Selecting next task";
3425
4071
  emitStarted(ctx, label, { dispatch: { status: "selecting-next" } });
3426
4072
  try {
@@ -3436,7 +4082,7 @@ async function submitNextTaskRun(ctx, filters = {}) {
3436
4082
  throw error;
3437
4083
  }
3438
4084
  emitCompleted(ctx, label, { taskId: task.id, count: selected.count });
3439
- const submitted = await submitTaskRun(ctx, { taskId: task.id, title: task.title });
4085
+ const submitted = await submitTaskRun(ctx, { taskId: task.id, title: task.title, detach: options.detach });
3440
4086
  return { ...submitted, task };
3441
4087
  } catch (error) {
3442
4088
  emitFailed(ctx, label, error);
@@ -3468,5 +4114,7 @@ export {
3468
4114
  normalizeTaskRecord,
3469
4115
  loadTaskPreview,
3470
4116
  ensureTaskRuntime,
3471
- createTasksAdapter
4117
+ createTasksAdapter,
4118
+ activeRunIdFromTaskRunError,
4119
+ activeRunIdFromTaskLike
3472
4120
  };