@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
@@ -44,42 +44,93 @@ function line(text, options = {}) {
44
44
  function blank() {
45
45
  return { text: "" };
46
46
  }
47
- function center(text, fg = RIG_UI.ink2, bold = false) {
48
- return { text, fg, bold, align: "center" };
47
+ function deckRow(input) {
48
+ const prefix = input.active ? "\u25B8" : " ";
49
+ const label = input.label.toUpperCase().padEnd(10);
50
+ return line(`${prefix} ${label} ${input.detail}`, {
51
+ fg: input.active ? RIG_UI.lime : RIG_UI.ink2,
52
+ bold: input.active,
53
+ selectableIndex: input.index,
54
+ activateOnClick: input.activateOnClick
55
+ });
49
56
  }
50
57
  function makeSceneFrame(input) {
51
58
  return input;
52
59
  }
53
60
 
54
61
  // packages/cli/src/app-opentui/scenes/server.ts
62
+ function cleanVisible(value, fallback = "unknown") {
63
+ return value?.trim() || fallback;
64
+ }
65
+ function targetLabel(server) {
66
+ if (!server)
67
+ return "not checked";
68
+ return server.kind === "remote" ? "remote endpoint" : server.kind === "local" ? "local endpoint" : cleanVisible(server.baseUrl);
69
+ }
55
70
  function serverState(state) {
56
71
  const server = state.data.server;
57
72
  return server && typeof server === "object" && !Array.isArray(server) ? server : null;
58
73
  }
59
- function renderServerScene(state) {
60
- const server = serverState(state);
74
+ function panelWidth(layout) {
75
+ return layout?.centerWidth ?? 118;
76
+ }
77
+ function panelHeight(layout) {
78
+ return Math.max(18, layout?.centerHeight ?? 34);
79
+ }
80
+ function selectedIndex(state, count) {
81
+ return Math.max(0, Math.min(count - 1, state.selection.index));
82
+ }
83
+ function statusRows(server) {
61
84
  const auth = server?.auth;
62
85
  const signedIn = auth && (auth.signedIn === true || auth.authenticated === true || auth.status === "authenticated") ? "authenticated" : "auth unknown";
86
+ return [
87
+ line(`selected ${cleanVisible(server?.label, "not checked")}`, { fg: RIG_UI.ink2 }),
88
+ line(`target ${targetLabel(server)}`, { fg: RIG_UI.ink3 }),
89
+ line(`mode ${server?.kind ?? "local or remote"}`, { fg: RIG_UI.ink3 }),
90
+ line(`health ${server ? server.reachable ? "reachable" : "unreachable" : "check pending"}`, { fg: server?.reachable ? RIG_UI.limeDim : server ? RIG_UI.red : RIG_UI.yellow }),
91
+ line(`github ${signedIn}`, { fg: signedIn === "authenticated" ? RIG_UI.limeDim : RIG_UI.ink4 }),
92
+ ...server?.error ? [line(`attention ${server.error}`, { fg: RIG_UI.yellow })] : []
93
+ ];
94
+ }
95
+ function renderServerScene(state, layout) {
96
+ const server = serverState(state);
97
+ const selected = selectedIndex(state, 6);
98
+ const panelLines = [
99
+ line("server controls", { fg: RIG_UI.ink3 }),
100
+ blank(),
101
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
102
+ deckRow({ label: "refresh", detail: "probe selected server and GitHub auth", index: 0, active: selected === 0, activateOnClick: true }),
103
+ deckRow({ label: "local", detail: "select local server for this repo", index: 1, active: selected === 1, activateOnClick: true }),
104
+ deckRow({ label: "remote", detail: "choose from saved remote servers", index: 2, active: selected === 2, activateOnClick: true }),
105
+ deckRow({ label: "list", detail: "show saved server aliases", index: 3, active: selected === 3, activateOnClick: true }),
106
+ deckRow({ label: "auth", detail: "import GitHub auth to selected server", index: 4, active: selected === 4, activateOnClick: true }),
107
+ deckRow({ label: "doctor", detail: "diagnose server/auth/project linkage", index: 5, active: selected === 5, activateOnClick: true }),
108
+ blank(),
109
+ line("STATUS", { fg: RIG_UI.ink3, bold: true }),
110
+ ...statusRows(server),
111
+ blank(),
112
+ line("enter/click runs action \xB7 esc returns home", { fg: RIG_UI.ink4 })
113
+ ];
63
114
  return makeSceneFrame({
64
115
  scene: "server",
65
116
  title: "Server",
66
- lines: [
67
- center("server", RIG_UI.ink, true),
68
- center(server?.label ?? "loading connection", RIG_UI.ink3),
69
- blank(),
70
- ...server ? [
71
- line(`selected ${server.label}`, { fg: RIG_UI.ink2, align: "center" }),
72
- line(`target ${server.baseUrl ?? "unknown"}`, { fg: RIG_UI.ink3, align: "center" }),
73
- line(`mode ${server.kind ?? "unknown"}`, { fg: RIG_UI.ink3, align: "center" }),
74
- line(`health ${server.reachable ? "reachable" : "unreachable"}`, { fg: server.reachable ? RIG_UI.limeDim : RIG_UI.red, align: "center" }),
75
- line(`latency ${server.latencyMs ?? "\u2014"}ms`, { fg: RIG_UI.cyan, align: "center" }),
76
- line(`github ${signedIn}`, { fg: RIG_UI.ink4, align: "center" }),
77
- blank(),
78
- line(server.error ? `attention ${server.error}` : "ready", { fg: server.error ? RIG_UI.yellow : RIG_UI.ink4, align: "center" })
79
- ] : [line("loading server status", { fg: RIG_UI.ink3, align: "center" })]
80
- ],
117
+ lines: [],
118
+ panels: [{
119
+ id: "server-actions",
120
+ top: 0,
121
+ width: panelWidth(layout),
122
+ height: panelHeight(layout),
123
+ lines: panelLines,
124
+ backgroundColor: RIG_UI.panel,
125
+ backgroundAlpha: 184,
126
+ opacity: 0.98,
127
+ border: false,
128
+ chrome: "ad-terminal",
129
+ headerText: "rig server \xB7 select / repair / auth",
130
+ paddingX: 5,
131
+ paddingY: 2
132
+ }],
81
133
  footer: { server: server?.label, message: server?.latencyMs !== undefined ? `${server.latencyMs}ms` : undefined },
82
- typeBarPlaceholder: "server \xB7 doctor \xB7 tasks \xB7 runs",
83
134
  live: state.status === "action" || !server
84
135
  });
85
136
  }
@@ -67,6 +67,16 @@ function statusColor(status) {
67
67
  function line(text, options = {}) {
68
68
  return { text, ...options };
69
69
  }
70
+ function deckRow(input) {
71
+ const prefix = input.active ? "\u25B8" : " ";
72
+ const label = input.label.toUpperCase().padEnd(10);
73
+ return line(`${prefix} ${label} ${input.detail}`, {
74
+ fg: input.active ? RIG_UI.lime : RIG_UI.ink2,
75
+ bold: input.active,
76
+ selectableIndex: input.index,
77
+ activateOnClick: input.activateOnClick
78
+ });
79
+ }
70
80
  function makeSceneFrame(input) {
71
81
  return input;
72
82
  }
@@ -148,35 +158,41 @@ function taskRow(width, task, index, active) {
148
158
  const priority = task.priority === undefined ? "prio --" : `prio ${String(task.priority).slice(0, 3).padStart(2)}`;
149
159
  const marker = active ? "\u258C" : " ";
150
160
  const prefix = `${marker} ${id} ${status} ${priority.padEnd(8)} `;
161
+ const activeRun = task.activeRun;
162
+ const activeSuffix = activeRun ? ` \xB7 attach ${activeRun.runId.slice(0, 8)}${activeRun.status ? ` ${activeRun.status}` : ""}` : "";
151
163
  const titleWidth = Math.max(12, width - prefix.length - 2);
152
- return line(`${prefix}${clip(task.title, titleWidth)}`, {
164
+ return line(`${prefix}${clip(`${task.title}${activeSuffix}`, titleWidth)}`, {
153
165
  fg: active ? RIG_UI.ink : statusColor(task.status),
154
166
  bold: active,
155
- selectableIndex: index
167
+ selectableIndex: index,
168
+ activateOnClick: true
156
169
  });
157
170
  }
158
- function emptyRows(query, total) {
171
+ function recoveryRows(selected, reason) {
172
+ return [
173
+ ...reason ? [line(reason, { fg: RIG_UI.yellow }), line("", { fg: RIG_UI.ink3 })] : [],
174
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
175
+ deckRow({ label: "refresh", detail: "reload task source", index: 0, active: selected === 0, activateOnClick: true }),
176
+ deckRow({ label: "next", detail: "dispatch next runnable task", index: 1, active: selected === 1, activateOnClick: true }),
177
+ deckRow({ label: "repair", detail: "repair project/task source link", index: 2, active: selected === 2, activateOnClick: true }),
178
+ deckRow({ label: "server", detail: "open server controls", index: 3, active: selected === 3, activateOnClick: true }),
179
+ deckRow({ label: "doctor", detail: "diagnose task source", index: 4, active: selected === 4, activateOnClick: true })
180
+ ];
181
+ }
182
+ function emptyRows(query, total, selected) {
183
+ if (!query.trim() && total === 0)
184
+ return recoveryRows(selected, "No tasks loaded.");
159
185
  if (query.trim() && total > 0) {
160
186
  return [
161
- line("No matching tasks.", { fg: RIG_UI.ink2, bold: true }),
162
- line(`search ${JSON.stringify(query)} returned 0/${total}`, { fg: RIG_UI.ink3 }),
163
- line("escape clears search", { fg: RIG_UI.ink4 })
187
+ ...recoveryRows(selected, `No matching tasks for ${JSON.stringify(query)} (${total} total).`)
164
188
  ];
165
189
  }
166
- return [
167
- line("No tasks loaded.", { fg: RIG_UI.ink2, bold: true }),
168
- line("Check the task source or switch filters from Help.", { fg: RIG_UI.ink3 }),
169
- line("tab switches screens", { fg: RIG_UI.ink4 })
170
- ];
190
+ return recoveryRows(selected, "No tasks are visible with this filter.");
171
191
  }
172
- function degradedRows(state) {
192
+ function degradedRows(state, selected) {
173
193
  if (!state.error)
174
194
  return [];
175
- return [
176
- line(`status: ${state.error.message}`, { fg: RIG_UI.red, bold: true }),
177
- ...state.error.hint ? [line(state.error.hint, { fg: RIG_UI.yellow })] : [],
178
- line("", { fg: RIG_UI.ink3 })
179
- ];
195
+ return recoveryRows(selected, state.error.message);
180
196
  }
181
197
  function renderTasksScene(state, layout) {
182
198
  const width = panelWidth(layout);
@@ -186,6 +202,7 @@ function renderTasksScene(state, layout) {
186
202
  const tasks = filterTasksForSearch(allTasks, query);
187
203
  const selected = typeof state.data.selectedTaskId === "string" && tasks.some((task) => task.id === state.data.selectedTaskId) ? state.data.selectedTaskId : tasks[0]?.id;
188
204
  const selectedIndex = Math.max(0, tasks.findIndex((task) => task.id === selected));
205
+ const recoveryIndex = Math.max(0, Math.min(4, state.selection.index));
189
206
  const dispatch = state.data.dispatchingRun;
190
207
  const dispatchLine = dispatch && typeof dispatch === "object" && !Array.isArray(dispatch) ? ` \xB7 ${dispatch.runId ?? "new"} ${dispatch.status ?? "dispatching"}` : "";
191
208
  const panelTop = 0;
@@ -193,16 +210,21 @@ function renderTasksScene(state, layout) {
193
210
  const panelLines = [
194
211
  line("TASK STATUS PRIORITY TITLE", { fg: RIG_UI.ink3, bold: true }),
195
212
  line("", { fg: RIG_UI.ink3 }),
196
- ...degradedRows(state),
197
- ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
198
- ...tasks.length > 0 ? tasks.map((task, index) => taskRow(contentWidth, task, index, index === selectedIndex)) : emptyRows(query, allTasks.length)
213
+ ...tasks.length > 0 ? [
214
+ ...degradedRows(state, recoveryIndex),
215
+ ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
216
+ ...tasks.map((task, index) => taskRow(contentWidth, task, index, index === selectedIndex))
217
+ ] : state.error ? degradedRows(state, recoveryIndex) : [
218
+ ...query.trim() ? [line(searchSummary("tasks", query, tasks.length, allTasks.length), { fg: RIG_UI.ink4 }), line("", { fg: RIG_UI.ink3 })] : [],
219
+ ...emptyRows(query, allTasks.length, recoveryIndex)
220
+ ]
199
221
  ];
200
222
  return makeSceneFrame({
201
223
  scene: "tasks",
202
224
  title: "Tasks",
203
225
  lines: [
204
226
  line("", { fg: RIG_UI.ink4 }),
205
- line(query.trim() ? ` rig tasks --search ${JSON.stringify(query)}` : ` rig tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`, { fg: RIG_UI.ink3 })
227
+ line(query.trim() ? ` tasks --search ${JSON.stringify(query)}` : ` tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`, { fg: RIG_UI.ink3 })
206
228
  ],
207
229
  panels: [{
208
230
  id: "tasks-list",
@@ -215,7 +237,7 @@ function renderTasksScene(state, layout) {
215
237
  opacity: 1,
216
238
  border: false,
217
239
  chrome: "ad-terminal",
218
- headerText: query.trim() ? `rig tasks --search ${JSON.stringify(query)}` : `rig tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`,
240
+ headerText: query.trim() ? `tasks --search ${JSON.stringify(query)}` : `tasks \xB7 ${taskViewLabel(view)}${dispatchLine}`,
219
241
  headerHeight: 3,
220
242
  paddingX: layout?.compact ? 2 : 3,
221
243
  paddingY: 1
@@ -1,14 +1,18 @@
1
1
  // @bun
2
2
  // packages/cli/src/app-opentui/intent.ts
3
3
  var NAV_SCENES = new Map([
4
- ["", "fleet"],
5
- ["main", "fleet"],
6
- ["home", "fleet"],
4
+ ["", "main"],
5
+ ["main", "main"],
6
+ ["home", "main"],
7
7
  ["fleet", "fleet"],
8
8
  ["runs", "fleet"],
9
9
  ["run", "fleet"],
10
10
  ["tasks", "tasks"],
11
11
  ["task", "tasks"],
12
+ ["inbox", "inbox"],
13
+ ["server", "server"],
14
+ ["init", "init"],
15
+ ["doctor", "doctor"],
12
16
  ["help", "help"]
13
17
  ]);
14
18
  var TASK_VIEW_COMMANDS = new Set([
@@ -30,8 +34,11 @@ var TASK_VIEW_COMMANDS = new Set([
30
34
  "search",
31
35
  "q"
32
36
  ]);
37
+ function isHelpCommand(command) {
38
+ return command === "help" || command === "--help" || command === "-h";
39
+ }
33
40
  function isTaskViewCommand(command) {
34
- return !command || TASK_VIEW_COMMANDS.has(command) || command.startsWith("-");
41
+ return !command || TASK_VIEW_COMMANDS.has(command) || command.startsWith("-") && !isHelpCommand(command);
35
42
  }
36
43
  function intent(scene, argv, kind, payload, label, raw) {
37
44
  return {
@@ -45,9 +52,12 @@ function intent(scene, argv, kind, payload, label, raw) {
45
52
  }
46
53
  };
47
54
  }
48
- function unsupportedPlainCliIntent(argv, raw) {
49
- const command = argv.join(" ").trim() || "command";
50
- return intent("help", argv, "none", { unsupported: command }, `${command} is on the plain CLI path; exit Rig shell or rerun with RIG_PLAIN=1`, raw);
55
+ function commandRunIntent(argv, raw, reason) {
56
+ const command = argv.join(" ").trim();
57
+ return intent("command", argv, "command-run", { argv: [...argv], ...reason ? { reason } : {} }, command ? `Running rig ${command}` : "Running rig", raw);
58
+ }
59
+ function unsupportedPlainCliIntent(argv, raw, reason) {
60
+ return commandRunIntent(argv, raw, reason);
51
61
  }
52
62
  function firstValueAfter(args, option) {
53
63
  const index = args.indexOf(option);
@@ -56,11 +66,19 @@ function firstValueAfter(args, option) {
56
66
  function firstNonOption(args) {
57
67
  return args.find((part) => part.trim() && !part.startsWith("-"));
58
68
  }
59
- function explicitTaskIdArg(args) {
60
- const taskValue = firstValueAfter(args, "--task");
61
- if (taskValue && !taskValue.startsWith("-"))
62
- return taskValue;
63
- return args.find((part) => part.trim() && part !== "next" && part !== "--next" && part !== "--task" && !part.startsWith("-"));
69
+ function inboxActionIntent(argv, command, rest, raw) {
70
+ const requestId = firstNonOption(rest);
71
+ if (command === "approve" || command === "approvals") {
72
+ return requestId ? intent("inbox", argv, "inbox-approve", { requestId }, `Approving ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading approvals", raw);
73
+ }
74
+ if (command === "reject") {
75
+ return requestId ? intent("inbox", argv, "inbox-reject", { requestId }, `Rejecting ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading approvals", raw);
76
+ }
77
+ if (command === "answer" || command === "inputs" || command === "respond") {
78
+ const answer = rest.slice(requestId ? 1 : 0).join(" ").trim();
79
+ return requestId && answer ? intent("inbox", argv, "inbox-answer", { requestId, answer }, `Answering ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading inbox", raw);
80
+ }
81
+ return intent("inbox", argv, "refresh", undefined, "Loading inbox", raw);
64
82
  }
65
83
  function taskViewPayload(command, rest) {
66
84
  const normalized = command.toLowerCase();
@@ -119,22 +137,15 @@ function intentFromArgv(argv) {
119
137
  const normalizedGroup = group.toLowerCase();
120
138
  const normalizedCommand = command.toLowerCase();
121
139
  if (!normalizedGroup)
122
- return intent("fleet", argv, "refresh", undefined, "Opening runs");
140
+ return intent("main", argv, "none", undefined, "Project menu");
123
141
  if (normalizedGroup === "--help" || normalizedGroup === "-h" || normalizedGroup === "help") {
124
142
  return argv.length === 1 ? intent("help", argv, "none", undefined, "Help") : unsupportedPlainCliIntent(argv);
125
143
  }
126
144
  if (normalizedGroup === "task") {
127
- if (normalizedCommand === "run") {
128
- if (rest.includes("--next") || rest.includes("next")) {
129
- return intent("tasks", argv, "task-run-next", undefined, "Dispatching next task");
130
- }
131
- const task = explicitTaskIdArg(rest);
132
- if (task)
133
- return intent("tasks", argv, "task-run-id", { task }, `Dispatching ${task}`);
134
- return intent("tasks", argv, "refresh", undefined, "Select a task, then press Enter; or use run next");
135
- }
145
+ if (normalizedCommand === "run" || normalizedCommand === "ready" || normalizedCommand === "status")
146
+ return commandRunIntent(argv);
136
147
  if (!isTaskViewCommand(normalizedCommand))
137
- return unsupportedPlainCliIntent(argv);
148
+ return commandRunIntent(argv);
138
149
  return taskViewIntent(argv, normalizedCommand, rest);
139
150
  }
140
151
  if (normalizedGroup === "tasks") {
@@ -145,20 +156,49 @@ function intentFromArgv(argv) {
145
156
  if (normalizedGroup === "run") {
146
157
  if (normalizedCommand === "attach") {
147
158
  const runId = firstNonOption(rest);
148
- return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
159
+ const nativeOnly = Boolean(runId) && rest.filter((part) => part.trim()).length === 1;
160
+ return nativeOnly ? intent("handoff", argv, "run-attach", { runId }, `Attaching ${runId}`) : commandRunIntent(argv);
149
161
  }
150
- if (!normalizedCommand)
162
+ if (!normalizedCommand || normalizedCommand === "list" || normalizedCommand === "status")
151
163
  return intent("fleet", argv, "refresh", undefined, "Loading fleet");
152
- return unsupportedPlainCliIntent(argv);
164
+ if (normalizedCommand === "stop") {
165
+ const runId = firstNonOption(rest);
166
+ return runId && rest.filter((part) => part.trim()).length === 1 ? intent("fleet", argv, "run-stop", { runId }, "Stopping run") : commandRunIntent(argv);
167
+ }
168
+ return commandRunIntent(argv);
169
+ }
170
+ if (normalizedGroup === "server") {
171
+ if (!normalizedCommand || normalizedCommand === "status")
172
+ return intent("server", argv, "refresh", undefined, normalizedCommand ? "Checking server" : "Loading server");
173
+ return unsupportedPlainCliIntent(argv, undefined, `server ${normalizedCommand} is not executed in the shell; exit and run it in plain mode.`);
174
+ }
175
+ if (normalizedGroup === "github") {
176
+ if (normalizedCommand === "auth" && (rest[0]?.toLowerCase() ?? "status") === "status" && rest.length <= 1) {
177
+ return intent("server", argv, "refresh", undefined, "Checking GitHub auth");
178
+ }
179
+ return unsupportedPlainCliIntent(argv, undefined, "github auth mutations are not executed in the shell; exit and run them in plain mode.");
180
+ }
181
+ if (normalizedGroup === "init")
182
+ return commandRunIntent(argv);
183
+ if (normalizedGroup === "doctor") {
184
+ return normalizedCommand ? commandRunIntent(argv) : intent("doctor", argv, "doctor-run", undefined, "Running doctor");
185
+ }
186
+ if (normalizedGroup === "inbox") {
187
+ if (normalizedCommand === "watch")
188
+ return commandRunIntent(argv);
189
+ if ((normalizedCommand === "approvals" || normalizedCommand === "inputs") && rest.length > 0)
190
+ return commandRunIntent(argv);
191
+ return normalizedCommand && normalizedCommand !== "approvals" && normalizedCommand !== "inputs" ? commandRunIntent(argv) : inboxActionIntent(argv, normalizedCommand, rest);
153
192
  }
154
193
  if (normalizedGroup === "attach") {
155
- const runId = firstNonOption([command, ...rest]);
156
- return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
194
+ const attachArgs = [command, ...rest].filter((part) => part.trim());
195
+ const runId = firstNonOption(attachArgs);
196
+ return runId && attachArgs.length === 1 ? intent("handoff", argv, "run-attach", { runId }, `Attaching ${runId}`) : commandRunIntent(argv);
157
197
  }
158
198
  const nav = NAV_SCENES.get(normalizedGroup);
159
- if (nav)
199
+ if (nav && argv.length === 1)
160
200
  return intent(nav, argv, "navigate", undefined, `Opening ${nav}`);
161
- return unsupportedPlainCliIntent(argv);
201
+ return commandRunIntent(argv);
162
202
  }
163
203
 
164
204
  // packages/cli/src/app-opentui/state.ts
@@ -0,0 +1,7 @@
1
+ import { type ImageProtocol } from "@earendil-works/pi-tui";
2
+ export type RigTerminalCapabilities = {
3
+ readonly imageProtocol: ImageProtocol;
4
+ readonly supportsKittyImages: boolean;
5
+ readonly visualMode: "kitty-images" | "framebuffer-fallback";
6
+ };
7
+ export declare function getRigTerminalCapabilities(): RigTerminalCapabilities;
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ // packages/cli/src/app-opentui/terminal-capabilities.ts
3
+ import { getCapabilities } from "@earendil-works/pi-tui";
4
+ function getRigTerminalCapabilities() {
5
+ const imageProtocol = getCapabilities().images;
6
+ const supportsKittyImages = imageProtocol === "kitty";
7
+ return {
8
+ imageProtocol,
9
+ supportsKittyImages,
10
+ visualMode: supportsKittyImages ? "kitty-images" : "framebuffer-fallback"
11
+ };
12
+ }
13
+ export {
14
+ getRigTerminalCapabilities
15
+ };
@@ -1,8 +1,8 @@
1
1
  import type { StyledText } from "@opentui/core";
2
2
  import type { RunnerContext } from "../runner";
3
3
  import type { StageLayout } from "./layout";
4
- export type AppSceneId = "main" | "fleet" | "tasks" | "inbox" | "server" | "init" | "doctor" | "run-detail" | "handoff" | "help" | "error";
5
- export type AppActionKind = "none" | "refresh" | "navigate" | "task-run-next" | "task-run-id" | "run-stop" | "run-attach" | "run-steer" | "inbox-approve" | "inbox-reject" | "inbox-answer" | "init-start" | "doctor-run";
4
+ export type AppSceneId = "main" | "fleet" | "tasks" | "inbox" | "server" | "init" | "doctor" | "command" | "run-detail" | "handoff" | "help" | "error";
5
+ export type AppActionKind = "none" | "refresh" | "navigate" | "task-run-next" | "task-run-id" | "run-stop" | "run-attach" | "run-steer" | "inbox-approve" | "inbox-reject" | "inbox-answer" | "init-start" | "command-run" | "doctor-run";
6
6
  export type AppIntent = {
7
7
  readonly scene: AppSceneId;
8
8
  readonly argv: readonly string[];
@@ -36,6 +36,13 @@ export type AppSelectionState = {
36
36
  readonly index: number;
37
37
  readonly count: number;
38
38
  };
39
+ export type AppSelectableItem = {
40
+ readonly id: string;
41
+ readonly label: string;
42
+ readonly data?: Record<string, unknown>;
43
+ readonly intent?: AppIntent;
44
+ readonly message?: string;
45
+ };
39
46
  export type AppState = {
40
47
  readonly projectRoot: string;
41
48
  readonly scene: AppSceneId;
@@ -112,6 +119,7 @@ export type AppSceneLine = {
112
119
  readonly text: string;
113
120
  readonly styledText?: StyledText;
114
121
  readonly fg?: string;
122
+ readonly bg?: string;
115
123
  readonly bold?: boolean;
116
124
  readonly dim?: boolean;
117
125
  readonly align?: "left" | "center" | "right";
@@ -134,17 +134,21 @@ function cleanToken(value) {
134
134
  const trimmed = value?.trim();
135
135
  return trimmed ? trimmed : null;
136
136
  }
137
- function readPrivateRemoteSessionToken(projectRoot) {
137
+ function readRemoteAuthState(projectRoot) {
138
138
  const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
139
139
  if (!existsSync2(path))
140
140
  return null;
141
141
  try {
142
142
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
143
- return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
143
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
144
144
  } catch {
145
145
  return null;
146
146
  }
147
147
  }
148
+ function readPrivateRemoteSessionToken(projectRoot) {
149
+ const parsed = readRemoteAuthState(projectRoot);
150
+ return parsed ? cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined) : null;
151
+ }
148
152
  function readGitHubBearerTokenForRemote(projectRoot) {
149
153
  const scopedKey = resolve2(projectRoot);
150
154
  if (scopedGitHubBearerTokens.has(scopedKey))
@@ -155,15 +159,25 @@ function readGitHubBearerTokenForRemote(projectRoot) {
155
159
  return cleanToken(process.env.RIG_SERVER_AUTH_TOKEN) ?? cleanToken(process.env.RIG_REMOTE_AUTH_TOKEN);
156
160
  }
157
161
  function readStoredGitHubAuthToken(projectRoot) {
158
- const path = resolve2(projectRoot, ".rig", "state", "github-auth.json");
159
- if (!existsSync2(path))
162
+ const parsed = readRemoteAuthState(projectRoot);
163
+ return parsed ? cleanToken(typeof parsed.token === "string" ? parsed.token : undefined) : null;
164
+ }
165
+ function inferRemoteServerProjectRootFromAuthState(projectRoot) {
166
+ const repo = readRepoConnection(projectRoot);
167
+ const slug = repo?.project?.trim();
168
+ if (!slug || !/^[^/]+\/[^/]+$/.test(slug))
160
169
  return null;
161
- try {
162
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
163
- return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
164
- } catch {
170
+ const auth = readRemoteAuthState(projectRoot);
171
+ const authUpdatedAt = typeof auth?.updatedAt === "string" ? Date.parse(auth.updatedAt) : Number.NaN;
172
+ const repoLinkedAt = typeof repo?.linkedAt === "string" ? Date.parse(repo.linkedAt) : Number.NaN;
173
+ if (Number.isFinite(authUpdatedAt) && Number.isFinite(repoLinkedAt) && authUpdatedAt + 1000 < repoLinkedAt)
165
174
  return null;
166
- }
175
+ const checkoutBaseDir = typeof auth?.checkoutBaseDir === "string" && auth.checkoutBaseDir.trim() ? auth.checkoutBaseDir.trim() : null;
176
+ if (!checkoutBaseDir)
177
+ return null;
178
+ const inferred = `${checkoutBaseDir.replace(/\/+$/, "")}/${slug}`;
179
+ writeRepoServerProjectRoot(projectRoot, inferred);
180
+ return inferred;
167
181
  }
168
182
  function readLocalConnectionFallbackToken(projectRoot) {
169
183
  return readGitHubBearerTokenForRemote(projectRoot) ?? cleanToken(process.env.RIG_GITHUB_TOKEN) ?? readStoredGitHubAuthToken(projectRoot);
@@ -174,7 +188,7 @@ async function ensureServerForCli(projectRoot) {
174
188
  if (selected?.connection.kind === "remote") {
175
189
  reportServerPhase(`Connecting to ${selected.alias}\u2026`);
176
190
  const authToken = readGitHubBearerTokenForRemote(projectRoot);
177
- const serverProjectRoot = selected.serverProjectRoot ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
191
+ const serverProjectRoot = selected.serverProjectRoot ?? inferRemoteServerProjectRootFromAuthState(projectRoot) ?? await backfillRemoteServerProjectRoot(projectRoot, selected.connection.baseUrl, authToken);
178
192
  return {
179
193
  baseUrl: selected.connection.baseUrl,
180
194
  authToken,
@@ -203,7 +217,10 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
203
217
  if (!slug)
204
218
  return null;
205
219
  try {
206
- const response = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`, {
220
+ const url = new URL(`${baseUrl}/api/projects/${encodeURIComponent(slug)}`);
221
+ if (authToken && queryAuthFallbackEnabled())
222
+ url.searchParams.set("rt", authToken);
223
+ const response = await fetch(url, {
207
224
  headers: mergeHeaders(undefined, authToken)
208
225
  });
209
226
  if (!response.ok)
@@ -220,10 +237,23 @@ async function backfillRemoteServerProjectRoot(projectRoot, baseUrl, authToken)
220
237
  return null;
221
238
  }
222
239
  }
240
+ function mergeCookie(existing, name, value) {
241
+ const encoded = `${name}=${encodeURIComponent(value)}`;
242
+ if (!existing?.trim())
243
+ return encoded;
244
+ const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
245
+ return [...parts, encoded].join("; ");
246
+ }
247
+ function queryAuthFallbackEnabled(env = process.env) {
248
+ return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
249
+ }
223
250
  function mergeHeaders(headers, authToken) {
224
251
  const merged = new Headers(headers);
225
252
  if (authToken) {
226
- merged.set("authorization", `Bearer ${authToken}`);
253
+ const bearer = `Bearer ${authToken}`;
254
+ merged.set("authorization", bearer);
255
+ merged.set("x-auth", bearer);
256
+ merged.set("cookie", mergeCookie(merged.get("cookie"), "rig_auth", authToken));
227
257
  }
228
258
  return merged;
229
259
  }
@@ -284,15 +314,34 @@ async function buildServerFailureContext(projectRoot, server) {
284
314
  hint: "Check the selected server with `rig server status`, or switch with `rig server use <alias|local>`."
285
315
  };
286
316
  }
317
+ function isLoopbackRemoteBaseUrl(baseUrl) {
318
+ try {
319
+ const host = new URL(baseUrl).hostname.toLowerCase();
320
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ function canUseRemoteWithoutProjectRoot(pathname) {
326
+ 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/");
327
+ }
287
328
  async function requestServerJson(context, pathname, init = {}) {
288
329
  const server = await ensureServerForCli(context.projectRoot);
330
+ const requestUrl = new URL(`${server.baseUrl}${pathname}`);
331
+ if (server.connectionKind === "remote" && !server.serverProjectRoot && !canUseRemoteWithoutProjectRoot(requestUrl.pathname) && (server.authToken || !isLoopbackRemoteBaseUrl(server.baseUrl))) {
332
+ const repo = readRepoConnection(context.projectRoot);
333
+ 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." });
334
+ }
289
335
  const headers = mergeHeaders(init.headers, server.authToken);
290
336
  if (server.serverProjectRoot)
291
337
  headers.set("x-rig-project-root", server.serverProjectRoot);
338
+ if (server.connectionKind === "remote" && server.authToken && queryAuthFallbackEnabled()) {
339
+ requestUrl.searchParams.set("rt", server.authToken);
340
+ }
292
341
  reportServerPhase(`${(init.method ?? "GET").toUpperCase()} ${pathname.split("?")[0]}\u2026`);
293
342
  let response;
294
343
  try {
295
- response = await fetch(`${server.baseUrl}${pathname}`, {
344
+ response = await fetch(requestUrl, {
296
345
  ...init,
297
346
  headers
298
347
  });