@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
@@ -1,279 +1,656 @@
1
1
  // @bun
2
- // packages/cli/src/app-opentui/runtime.ts
3
- import { existsSync } from "fs";
4
- import { resolve } from "path";
5
- import { createCliRenderer } from "@opentui/core";
6
-
7
- // packages/cli/src/app-opentui/events.ts
8
- function createAppEventBus() {
9
- const listeners = new Set;
10
- return {
11
- emit(event) {
12
- for (const listener of [...listeners])
13
- listener(event);
14
- },
15
- subscribe(listener) {
16
- listeners.add(listener);
17
- return () => listeners.delete(listener);
18
- }
19
- };
20
- }
21
- function normalizeError(error) {
22
- const message = error instanceof Error ? error.message : String(error);
23
- const rawHint = error instanceof Error ? error.hint : undefined;
24
- return {
25
- message: message || "Unknown Rig app error",
26
- ...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {},
27
- cause: error
28
- };
29
- }
2
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
30
3
 
31
- // packages/cli/src/app-opentui/intent.ts
32
- var NAV_SCENES = new Map([
33
- ["", "fleet"],
34
- ["main", "fleet"],
35
- ["home", "fleet"],
36
- ["fleet", "fleet"],
37
- ["runs", "fleet"],
38
- ["run", "fleet"],
39
- ["tasks", "tasks"],
40
- ["task", "tasks"],
41
- ["help", "help"]
42
- ]);
43
- var TASK_VIEW_COMMANDS = new Set([
44
- "",
45
- "all",
46
- "open",
47
- "ready",
48
- "blocked",
49
- "closed",
50
- "done",
51
- "mine",
52
- "me",
53
- "sort",
54
- "status",
55
- "state",
56
- "assignee",
57
- "assigned",
58
- "label",
59
- "search",
60
- "q"
61
- ]);
62
- function isTaskViewCommand(command) {
63
- return !command || TASK_VIEW_COMMANDS.has(command) || command.startsWith("-");
4
+ // packages/cli/src/app-opentui/pi-pty-host.ts
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { basename as basename2 } from "path";
7
+ import { RGBA, StyledText, TextAttributes } from "@opentui/core";
8
+ import { Terminal as XtermTerminal2 } from "@xterm/headless";
9
+ function clampCols2(cols) {
10
+ return Math.max(MIN_COLS2, Math.trunc(cols || 100));
64
11
  }
65
- function intent(scene, argv, kind, payload, label, raw) {
66
- return {
67
- scene,
68
- argv,
69
- ...raw ? { raw } : {},
70
- action: {
71
- kind,
72
- ...label ? { label } : {},
73
- ...payload ? { payload } : {}
74
- }
75
- };
12
+ function clampRows2(rows) {
13
+ return Math.max(MIN_ROWS2, Math.min(MAX_ROWS2, Math.trunc(rows || 35)));
76
14
  }
77
- function unsupportedPlainCliIntent(argv, raw) {
78
- const command = argv.join(" ").trim() || "command";
79
- return intent("help", argv, "none", { unsupported: command }, `${command} is on the plain CLI path; exit Rig shell or rerun with RIG_PLAIN=1`, raw);
15
+ function rgbaFromXtermColor(mode, value) {
16
+ if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
17
+ return RGBA.fromIndex(value);
18
+ }
19
+ if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
20
+ return RGBA.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
21
+ }
22
+ return;
80
23
  }
81
- function firstValueAfter(args, option) {
82
- const index = args.indexOf(option);
83
- return index >= 0 ? args[index + 1] : undefined;
24
+ function textAttributesFromCell(cell) {
25
+ let attributes = TextAttributes.NONE;
26
+ if (cell.isBold())
27
+ attributes |= TextAttributes.BOLD;
28
+ if (cell.isDim())
29
+ attributes |= TextAttributes.DIM;
30
+ if (cell.isItalic())
31
+ attributes |= TextAttributes.ITALIC;
32
+ if (cell.isUnderline())
33
+ attributes |= TextAttributes.UNDERLINE;
34
+ if (cell.isBlink())
35
+ attributes |= TextAttributes.BLINK;
36
+ if (cell.isInverse())
37
+ attributes |= TextAttributes.INVERSE;
38
+ if (cell.isInvisible())
39
+ attributes |= TextAttributes.HIDDEN;
40
+ if (cell.isStrikethrough())
41
+ attributes |= TextAttributes.STRIKETHROUGH;
42
+ return attributes;
84
43
  }
85
- function firstNonOption(args) {
86
- return args.find((part) => part.trim() && !part.startsWith("-"));
44
+ function sameRgba(a, b) {
45
+ if (!a && !b)
46
+ return true;
47
+ return Boolean(a && b && a.equals(b));
87
48
  }
88
- function explicitTaskIdArg(args) {
89
- const taskValue = firstValueAfter(args, "--task");
90
- if (taskValue && !taskValue.startsWith("-"))
91
- return taskValue;
92
- return args.find((part) => part.trim() && part !== "next" && part !== "--next" && part !== "--task" && !part.startsWith("-"));
49
+ function sameStyle(a, b) {
50
+ return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
93
51
  }
94
- function taskViewPayload(command, rest) {
95
- const normalized = command.toLowerCase();
96
- const args = command ? [command, ...rest] : rest;
97
- const sort = firstValueAfter(args, "--sort") ?? firstValueAfter(args, "-s");
98
- const state = firstValueAfter(args, "--state");
99
- const status = firstValueAfter(args, "--status");
100
- const assignee = firstValueAfter(args, "--assignee") ?? firstValueAfter(args, "--assigned");
101
- const label = firstValueAfter(args, "--label");
102
- const search = firstValueAfter(args, "--search") ?? firstValueAfter(args, "-q");
103
- const optionPayload = {
104
- ...sort ? { taskSort: sort } : {},
105
- ...state ? { taskState: state, taskFilterPreset: "custom" } : {},
106
- ...status ? { taskStatus: status, taskFilterPreset: "custom" } : {},
107
- ...assignee ? { taskAssignee: assignee, taskFilterPreset: "custom" } : {},
108
- ...label ? { taskLabel: label, taskFilterPreset: "custom" } : {},
109
- ...search ? { taskSearch: search, taskFilterPreset: "custom" } : {}
52
+ function styleFromCell(cell) {
53
+ return {
54
+ fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
55
+ bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
56
+ attributes: textAttributesFromCell(cell)
110
57
  };
111
- if (normalized === "sort") {
112
- const value = firstNonOption(rest) ?? sort ?? "priority";
113
- return { payload: { ...optionPayload, taskSort: value }, label: `Sorting tasks by ${value}` };
114
- }
115
- if (normalized === "status") {
116
- const value = firstNonOption(rest) ?? status ?? "open";
117
- return { payload: { ...optionPayload, taskStatus: value, taskFilterPreset: "custom" }, label: `Filtering tasks by status ${value}` };
118
- }
119
- if (normalized === "state") {
120
- const value = firstNonOption(rest) ?? state ?? "open";
121
- return { payload: { ...optionPayload, taskState: value, taskFilterPreset: "custom" }, label: `Filtering tasks by state ${value}` };
122
- }
123
- if (normalized === "assignee" || normalized === "assigned" || normalized === "mine" || normalized === "me") {
124
- const value = normalized === "mine" || normalized === "me" ? "me" : firstNonOption(rest) ?? assignee ?? "me";
125
- return { payload: { ...optionPayload, taskAssignee: value, taskState: "open", taskFilterPreset: value === "me" ? "mine" : "custom" }, label: value === "me" ? "Filtering my tasks" : `Filtering tasks for ${value}` };
126
- }
127
- if (normalized === "label") {
128
- const value = firstNonOption(rest) ?? label;
129
- return { payload: { ...optionPayload, ...value ? { taskLabel: value } : {}, taskFilterPreset: "custom" }, label: value ? `Filtering tasks labeled ${value}` : "Filtering tasks by label" };
130
- }
131
- if (normalized === "search" || normalized === "q") {
132
- const value = rest.join(" ").trim() || search;
133
- return { payload: { ...optionPayload, ...value ? { taskSearch: value } : {}, taskFilterPreset: "custom" }, label: value ? `Searching tasks for ${value}` : "Searching tasks" };
134
- }
135
- if (["all", "open", "ready", "blocked", "closed", "done"].includes(normalized)) {
136
- return { payload: { ...optionPayload, taskFilterPreset: normalized === "done" ? "closed" : normalized }, label: `Filtering tasks: ${normalized}` };
137
- }
138
- if (Object.keys(optionPayload).length > 0)
139
- return { payload: optionPayload, label: "Filtering tasks" };
140
- return { label: "Loading tasks" };
141
- }
142
- function taskViewIntent(argv, command = "", rest = [], raw) {
143
- const { payload, label } = taskViewPayload(command, rest);
144
- return intent("tasks", argv, "refresh", payload, label, raw);
145
58
  }
146
- function intentFromArgv(argv) {
147
- const [group = "", command = "", ...rest] = argv;
148
- const normalizedGroup = group.toLowerCase();
149
- const normalizedCommand = command.toLowerCase();
150
- if (!normalizedGroup)
151
- return intent("fleet", argv, "refresh", undefined, "Opening runs");
152
- if (normalizedGroup === "--help" || normalizedGroup === "-h" || normalizedGroup === "help") {
153
- return argv.length === 1 ? intent("help", argv, "none", undefined, "Help") : unsupportedPlainCliIntent(argv);
154
- }
155
- if (normalizedGroup === "task") {
156
- if (normalizedCommand === "run") {
157
- if (rest.includes("--next") || rest.includes("next")) {
158
- return intent("tasks", argv, "task-run-next", undefined, "Dispatching next task");
159
- }
160
- const task = explicitTaskIdArg(rest);
161
- if (task)
162
- return intent("tasks", argv, "task-run-id", { task }, `Dispatching ${task}`);
163
- return intent("tasks", argv, "refresh", undefined, "Select a task, then press Enter; or use run next");
59
+ function lineToStyledText(line, cols) {
60
+ if (!line)
61
+ return new StyledText([{ __isChunk: true, text: "" }]);
62
+ const chunks = [];
63
+ let run = "";
64
+ let runStyle = null;
65
+ const flush = () => {
66
+ if (!run)
67
+ return;
68
+ chunks.push({
69
+ __isChunk: true,
70
+ text: run,
71
+ ...runStyle?.fg ? { fg: runStyle.fg } : {},
72
+ ...runStyle?.bg ? { bg: runStyle.bg } : {},
73
+ ...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
74
+ });
75
+ run = "";
76
+ };
77
+ for (let index = 0;index < cols; index += 1) {
78
+ const cell = line.getCell(index);
79
+ if (!cell) {
80
+ if (runStyle !== null)
81
+ flush();
82
+ runStyle = null;
83
+ run += " ";
84
+ continue;
164
85
  }
165
- if (!isTaskViewCommand(normalizedCommand))
166
- return unsupportedPlainCliIntent(argv);
167
- return taskViewIntent(argv, normalizedCommand, rest);
168
- }
169
- if (normalizedGroup === "tasks") {
170
- if (!isTaskViewCommand(normalizedCommand))
171
- return unsupportedPlainCliIntent(argv);
172
- return taskViewIntent(argv, normalizedCommand, rest);
173
- }
174
- if (normalizedGroup === "run") {
175
- if (normalizedCommand === "attach") {
176
- const runId = firstNonOption(rest);
177
- return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
86
+ if (cell.getWidth() === 0)
87
+ continue;
88
+ const style = styleFromCell(cell);
89
+ if (!runStyle || !sameStyle(runStyle, style)) {
90
+ flush();
91
+ runStyle = style;
178
92
  }
179
- if (!normalizedCommand)
180
- return intent("fleet", argv, "refresh", undefined, "Loading fleet");
181
- return unsupportedPlainCliIntent(argv);
182
- }
183
- if (normalizedGroup === "attach") {
184
- const runId = firstNonOption([command, ...rest]);
185
- return intent("handoff", argv, "run-attach", { runId }, runId ? `Attaching ${runId}` : "Attaching run");
93
+ run += cell.getChars() || " ";
186
94
  }
187
- const nav = NAV_SCENES.get(normalizedGroup);
188
- if (nav)
189
- return intent(nav, argv, "navigate", undefined, `Opening ${nav}`);
190
- return unsupportedPlainCliIntent(argv);
191
- }
192
- function intentFromTypeBar(value) {
193
- const raw = value.trim();
194
- if (!raw)
195
- return intent("fleet", [], "refresh", undefined, "Opening runs", value);
196
- const parts = raw.split(/\s+/).filter(Boolean);
197
- const [first = "", second = "", ...rest] = parts;
198
- const firstScene = NAV_SCENES.get(first.toLowerCase());
199
- if (first === "tasks" || first === "task") {
200
- if (second === "run") {
201
- const task = explicitTaskIdArg(rest);
202
- if (rest.includes("--next") || task === "next")
203
- return intent("tasks", parts, "task-run-next", undefined, "Dispatching next task", value);
204
- if (task)
205
- return intent("tasks", parts, "task-run-id", { task }, `Dispatching ${task}`, value);
206
- return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
207
- }
208
- if (!isTaskViewCommand(second.toLowerCase()))
209
- return unsupportedPlainCliIntent(parts, value);
210
- return taskViewIntent(parts, second, rest, value);
211
- }
212
- if (first === "run") {
213
- if (second === "next" || second === "--next")
214
- return intent("tasks", parts, "task-run-next", undefined, "Dispatching next task", value);
215
- if (second === "--task") {
216
- const task = explicitTaskIdArg([second, ...rest]);
217
- if (task)
218
- return intent("tasks", parts, "task-run-id", { task }, `Dispatching ${task}`, value);
219
- return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
220
- }
221
- if (second && !second.startsWith("-"))
222
- return intent("tasks", parts, "task-run-id", { task: second }, `Dispatching ${second}`, value);
223
- return intent("tasks", parts, "refresh", undefined, "Select a task, then press Enter; or use run next", value);
224
- }
225
- if (firstScene && parts.length === 1) {
226
- return intent(firstScene, parts, "navigate", undefined, `Opening ${firstScene}`, value);
227
- }
228
- if (first === "stop")
229
- return intent("fleet", parts, "run-stop", { runId: second || undefined }, second ? `Stopping ${second}` : "Stopping run", value);
230
- if (first === "attach") {
231
- if (!second)
232
- return intent("handoff", parts, "navigate", undefined, "Select a run to attach", value);
233
- return intent("handoff", parts, "run-attach", { runId: second }, `Attaching ${second}`, value);
234
- }
235
- return intentFromArgv(parts);
236
- }
237
-
238
- // packages/cli/src/app-opentui/layout.ts
239
- function computeStageLayout(width, height) {
240
- const safeWidth = Math.max(40, Math.trunc(width || 80));
241
- const safeHeight = Math.max(18, Math.trunc(height || 24));
242
- const compact = safeWidth < 92 || safeHeight < 30;
243
- const horizontalGutter = compact ? 1 : 2;
244
- const centerWidth = Math.max(36, safeWidth - horizontalGutter * 2);
245
- const centerLeft = horizontalGutter;
246
- const typeBarTop = Math.max(0, safeHeight - 3);
247
- const footerTop = Math.max(0, safeHeight - 1);
248
- const centerTop = compact ? 1 : Math.max(1, Math.floor(safeHeight * 0.055));
249
- const centerHeight = Math.max(8, typeBarTop - centerTop - 1);
250
- return { width: safeWidth, height: safeHeight, centerWidth, centerLeft, centerTop, centerHeight, typeBarTop, footerTop, compact };
95
+ flush();
96
+ return new StyledText(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
251
97
  }
252
- function visibleWidth(text) {
253
- return [...text].length;
98
+ function childCommandPrefix2() {
99
+ const execName = basename2(process.execPath).toLowerCase();
100
+ const currentEntry = process.argv[1];
101
+ if (execName === "bun" || execName === "bun.exe") {
102
+ return currentEntry ? [process.execPath, currentEntry, "__opentui-pi-host"] : [process.execPath, fallbackChildScriptPath];
103
+ }
104
+ return [process.execPath, "__opentui-pi-host"];
254
105
  }
255
- function alignText(text, width, align = "left") {
256
- const length = visibleWidth(text);
257
- if (length >= width)
258
- return text;
259
- const pad = width - length;
260
- if (align === "right")
261
- return `${" ".repeat(pad)}${text}`;
262
- if (align === "center") {
263
- const left = Math.floor(pad / 2);
264
- return `${" ".repeat(left)}${text}${" ".repeat(pad - left)}`;
106
+ function withEnv2(base) {
107
+ const env = {};
108
+ for (const [key, value] of Object.entries(base ?? process.env)) {
109
+ if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP" || key === "NO_COLOR")
110
+ continue;
111
+ if (typeof value === "string")
112
+ env[key] = value;
265
113
  }
266
- return text;
114
+ return {
115
+ ...env,
116
+ TERM: "xterm-256color",
117
+ COLORTERM: "truecolor",
118
+ FORCE_COLOR: env.FORCE_COLOR ?? "1",
119
+ RIG_OPENTUI_PI_HOST: "1"
120
+ };
267
121
  }
268
- function truncateText(text, width) {
269
- if (visibleWidth(text) <= width)
270
- return text;
271
- if (width <= 1)
272
- return "\u2026";
273
- return `${[...text].slice(0, Math.max(0, width - 1)).join("")}\u2026`;
122
+ function getActivePiHost() {
123
+ return activeHost2 && !activeHost2.disposed ? activeHost2 : null;
124
+ }
125
+ function stopActivePiHost(reason = "detach") {
126
+ activeHost2?.dispose(reason);
127
+ activeHost2 = null;
274
128
  }
275
129
 
276
- // packages/cli/src/app-opentui/list-search.ts
130
+ class PiPtyHost {
131
+ runId;
132
+ projectRoot;
133
+ onSnapshot;
134
+ onExit;
135
+ onError;
136
+ terminal;
137
+ disposables = [];
138
+ decoder = new TextDecoder("utf-8");
139
+ proc = null;
140
+ pty = null;
141
+ status = "starting";
142
+ cols;
143
+ rows;
144
+ message = "starting bundled Pi";
145
+ exitCode;
146
+ signal;
147
+ notifyTimer = null;
148
+ _disposed = false;
149
+ constructor(options) {
150
+ this.runId = options.runId;
151
+ this.projectRoot = options.projectRoot;
152
+ this.cols = clampCols2(options.cols);
153
+ this.rows = clampRows2(options.rows);
154
+ this.onSnapshot = options.onSnapshot;
155
+ this.onExit = options.onExit;
156
+ this.onError = options.onError;
157
+ this.terminal = new XtermTerminal2({
158
+ allowProposedApi: true,
159
+ cols: this.cols,
160
+ rows: this.rows,
161
+ scrollback: 1000
162
+ });
163
+ this.registerTerminalResponders();
164
+ }
165
+ get disposed() {
166
+ return this._disposed;
167
+ }
168
+ get snapshot() {
169
+ return this.createSnapshot();
170
+ }
171
+ async start() {
172
+ if (this._disposed)
173
+ throw new Error("Pi PTY host is disposed.");
174
+ if (typeof Bun.Terminal !== "function") {
175
+ throw new Error("Bun native PTY is unavailable in this runtime. Pi host requires Bun.Terminal.");
176
+ }
177
+ const spawnOptions = {
178
+ cwd: this.projectRoot,
179
+ env: withEnv2(process.env),
180
+ terminal: {
181
+ cols: this.cols,
182
+ rows: this.rows,
183
+ name: "xterm-256color",
184
+ data: (_terminal, data) => this.handlePtyData(data)
185
+ }
186
+ };
187
+ const proc = Bun.spawn([
188
+ ...childCommandPrefix2(),
189
+ "--run-id",
190
+ this.runId,
191
+ "--project-root",
192
+ this.projectRoot
193
+ ], spawnOptions);
194
+ if (!proc.terminal)
195
+ throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
196
+ this.proc = proc;
197
+ this.pty = proc.terminal;
198
+ this.status = "running";
199
+ this.message = "bundled Pi running inside this app";
200
+ this.emitSnapshotSoon(0);
201
+ proc.exited.then((exitCode) => {
202
+ if (this._disposed)
203
+ return;
204
+ this.status = exitCode === 0 ? "exited" : "failed";
205
+ this.exitCode = exitCode;
206
+ this.signal = null;
207
+ this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
208
+ const snapshot = this.createSnapshot();
209
+ this.onSnapshot?.(snapshot);
210
+ this.onExit?.(snapshot);
211
+ if (activeHost2 === this)
212
+ activeHost2 = null;
213
+ this.dispose("exit", { kill: false, notify: false });
214
+ }).catch((error) => {
215
+ if (this._disposed)
216
+ return;
217
+ this.status = "failed";
218
+ this.message = error instanceof Error ? error.message : String(error);
219
+ const snapshot = this.createSnapshot();
220
+ this.onSnapshot?.(snapshot);
221
+ this.onError?.(error, snapshot);
222
+ if (activeHost2 === this)
223
+ activeHost2 = null;
224
+ this.dispose("error", { kill: false, notify: false });
225
+ });
226
+ }
227
+ write(data) {
228
+ if (this._disposed || !this.pty)
229
+ return;
230
+ try {
231
+ this.pty.write(data);
232
+ } catch (error) {
233
+ this.status = "failed";
234
+ this.message = error instanceof Error ? error.message : String(error);
235
+ const snapshot = this.createSnapshot();
236
+ this.onSnapshot?.(snapshot);
237
+ this.onError?.(error, snapshot);
238
+ }
239
+ }
240
+ resize(cols, rows) {
241
+ const nextCols = clampCols2(cols);
242
+ const nextRows = clampRows2(rows);
243
+ if (nextCols === this.cols && nextRows === this.rows)
244
+ return;
245
+ this.cols = nextCols;
246
+ this.rows = nextRows;
247
+ this.terminal.resize(nextCols, nextRows);
248
+ try {
249
+ this.pty?.resize(nextCols, nextRows);
250
+ } catch {}
251
+ this.emitSnapshotSoon(0);
252
+ }
253
+ detach() {
254
+ this.dispose("detach");
255
+ return this.createSnapshot("detached from bundled Pi");
256
+ }
257
+ dispose(reason = "dispose", options = {}) {
258
+ if (this._disposed)
259
+ return;
260
+ this._disposed = true;
261
+ if (this.notifyTimer)
262
+ clearTimeout(this.notifyTimer);
263
+ this.notifyTimer = null;
264
+ for (const disposable of this.disposables.splice(0)) {
265
+ try {
266
+ disposable.dispose();
267
+ } catch {}
268
+ }
269
+ if (options.kill !== false) {
270
+ try {
271
+ this.proc?.kill("SIGTERM");
272
+ } catch {}
273
+ }
274
+ try {
275
+ this.pty?.close();
276
+ } catch {}
277
+ try {
278
+ this.terminal.dispose();
279
+ } catch {}
280
+ if (activeHost2 === this)
281
+ activeHost2 = null;
282
+ if (options.notify) {
283
+ this.message = reason;
284
+ this.onSnapshot?.(this.createSnapshot(reason));
285
+ }
286
+ }
287
+ handlePtyData(data) {
288
+ if (this._disposed)
289
+ return;
290
+ const text = this.decoder.decode(data, { stream: true });
291
+ this.respondToRawTerminalQueries(text);
292
+ this.terminal.write(data, () => this.emitSnapshotSoon());
293
+ }
294
+ emitSnapshotSoon(delayMs = SNAPSHOT_DELAY_MS2) {
295
+ if (this._disposed || this.notifyTimer)
296
+ return;
297
+ this.notifyTimer = setTimeout(() => {
298
+ this.notifyTimer = null;
299
+ if (this._disposed)
300
+ return;
301
+ this.onSnapshot?.(this.createSnapshot());
302
+ }, delayMs);
303
+ }
304
+ createSnapshot(message = this.message) {
305
+ const buffer = this.terminal.buffer.active;
306
+ const end = buffer.length;
307
+ const start = Math.max(0, end - MAX_SNAPSHOT_LINES2);
308
+ const styledStart = Math.max(start, end - this.rows - STYLED_SNAPSHOT_MARGIN);
309
+ const lines = [];
310
+ const styledLines = [];
311
+ for (let row = start;row < end; row += 1) {
312
+ const line = buffer.getLine(row);
313
+ lines.push((line?.translateToString(false) ?? "").slice(0, this.cols));
314
+ if (row >= styledStart)
315
+ styledLines[lines.length - 1] = lineToStyledText(line, this.cols);
316
+ }
317
+ while (lines.length < this.rows) {
318
+ lines.push("");
319
+ styledLines[lines.length - 1] = new StyledText([{ __isChunk: true, text: "" }]);
320
+ }
321
+ return {
322
+ runId: this.runId,
323
+ status: this.status,
324
+ cols: this.cols,
325
+ rows: this.rows,
326
+ lines,
327
+ styledLines,
328
+ message,
329
+ ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
330
+ ...this.signal !== undefined ? { signal: this.signal } : {}
331
+ };
332
+ }
333
+ registerTerminalResponders() {
334
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
335
+ if (params.length === 0 || params[0] === 0)
336
+ this.write("\x1B[?62;22c");
337
+ return false;
338
+ }));
339
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
340
+ if (params.length === 0 || params[0] === 0)
341
+ this.write("\x1B[>0;0;0c");
342
+ return false;
343
+ }));
344
+ this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
345
+ if (params[0] === 5) {
346
+ this.write("\x1B[0n");
347
+ } else if (params[0] === 6) {
348
+ const row = this.terminal.buffer.active.cursorY + 1;
349
+ const col = this.terminal.buffer.active.cursorX + 1;
350
+ this.write(`\x1B[${row};${col}R`);
351
+ }
352
+ return false;
353
+ }));
354
+ }
355
+ respondToRawTerminalQueries(text) {
356
+ if (!text)
357
+ return;
358
+ const decrqm = /\x1b\[\?(\d+)\$p/g;
359
+ let match;
360
+ while ((match = decrqm.exec(text)) !== null) {
361
+ this.write(`\x1B[?${match[1]};2$y`);
362
+ }
363
+ }
364
+ }
365
+ 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;
366
+ var init_pi_pty_host = __esm(() => {
367
+ fallbackChildScriptPath = fileURLToPath2(new URL("./pi-host-child.ts", import.meta.url));
368
+ });
369
+ // packages/cli/src/app-opentui/runtime.ts
370
+ import { existsSync } from "fs";
371
+ import { resolve } from "path";
372
+ import { BoxRenderable as BoxRenderable2, RGBA as RGBA7, TextRenderable as TextRenderable3, createCliRenderer } from "@opentui/core";
373
+
374
+ // packages/cli/src/app-opentui/events.ts
375
+ function createAppEventBus() {
376
+ const listeners = new Set;
377
+ return {
378
+ emit(event) {
379
+ for (const listener of [...listeners])
380
+ listener(event);
381
+ },
382
+ subscribe(listener) {
383
+ listeners.add(listener);
384
+ return () => listeners.delete(listener);
385
+ }
386
+ };
387
+ }
388
+ function normalizeError(error) {
389
+ const message = error instanceof Error ? error.message : String(error);
390
+ const rawHint = error instanceof Error ? error.hint : undefined;
391
+ return {
392
+ message: message || "Unknown app error",
393
+ ...typeof rawHint === "string" && rawHint.trim() ? { hint: rawHint.trim() } : {},
394
+ cause: error
395
+ };
396
+ }
397
+
398
+ // packages/cli/src/app-opentui/focus-manager.ts
399
+ class AppFocusManager {
400
+ modalStack = [];
401
+ pushModal(owner) {
402
+ this.modalStack = [...this.modalStack.filter((entry) => entry !== owner), owner];
403
+ }
404
+ popModal(owner) {
405
+ this.modalStack = this.modalStack.filter((entry) => entry !== owner);
406
+ }
407
+ get modal() {
408
+ const owner = this.modalStack[this.modalStack.length - 1];
409
+ return owner ? { active: true, owner } : { active: false };
410
+ }
411
+ restoreTypeBarFocus(typeBar, options = {}) {
412
+ if (options.hidden || this.modal.active || !typeBar)
413
+ return;
414
+ if (!typeBar.input.focused)
415
+ typeBar.input.focus();
416
+ }
417
+ }
418
+
419
+ // packages/cli/src/app-opentui/command-pty-host.ts
420
+ import { basename } from "path";
421
+ import { fileURLToPath } from "url";
422
+ import { Terminal as XtermTerminal } from "@xterm/headless";
423
+ var MIN_COLS = 40;
424
+ var MIN_ROWS = 10;
425
+ var MAX_ROWS = 300;
426
+ var MAX_SNAPSHOT_LINES = 420;
427
+ var SNAPSHOT_DELAY_MS = 80;
428
+ var fallbackCliScriptPath = fileURLToPath(new URL("../../bin/rig.ts", import.meta.url));
429
+ var activeHost = null;
430
+ function clampCols(cols) {
431
+ return Math.max(MIN_COLS, Math.trunc(cols || 120));
432
+ }
433
+ function clampRows(rows) {
434
+ return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.trunc(rows || 36)));
435
+ }
436
+ function childCommandPrefix() {
437
+ const execName = basename(process.execPath).toLowerCase();
438
+ const currentEntry = process.argv[1];
439
+ if (execName === "bun" || execName === "bun.exe") {
440
+ return currentEntry ? [process.execPath, currentEntry] : [process.execPath, fallbackCliScriptPath];
441
+ }
442
+ return [process.execPath];
443
+ }
444
+ function withEnv(base) {
445
+ const env = {};
446
+ for (const [key, value] of Object.entries(base ?? process.env)) {
447
+ if (key === "NO_COLOR")
448
+ continue;
449
+ if (typeof value === "string")
450
+ env[key] = value;
451
+ }
452
+ return {
453
+ ...env,
454
+ TERM: "xterm-256color",
455
+ COLORTERM: "truecolor",
456
+ FORCE_COLOR: env.FORCE_COLOR ?? "1",
457
+ RIG_PLAIN: "1",
458
+ RIG_OPENTUI_COMMAND_HOST: "1",
459
+ RIG_CLI_PLAIN_HELP: env.RIG_CLI_PLAIN_HELP ?? "1"
460
+ };
461
+ }
462
+ function commandLabel(argv, fallback) {
463
+ const command = argv.join(" ").trim();
464
+ return fallback?.trim() || (command ? `rig ${command}` : "rig");
465
+ }
466
+ function getActiveCommandHost() {
467
+ return activeHost && !activeHost.disposed ? activeHost : null;
468
+ }
469
+ function stopActiveCommandHost(reason = "detach") {
470
+ activeHost?.dispose(reason);
471
+ activeHost = null;
472
+ }
473
+ class CommandPtyHost {
474
+ argv;
475
+ projectRoot;
476
+ label;
477
+ env;
478
+ onSnapshot;
479
+ onExit;
480
+ onError;
481
+ terminal;
482
+ decoder = new TextDecoder("utf-8");
483
+ proc = null;
484
+ pty = null;
485
+ status = "starting";
486
+ cols;
487
+ rows;
488
+ message = "starting command";
489
+ exitCode;
490
+ signal;
491
+ notifyTimer = null;
492
+ _disposed = false;
493
+ constructor(options) {
494
+ this.argv = [...options.argv];
495
+ this.projectRoot = options.projectRoot;
496
+ this.label = commandLabel(options.argv, options.label);
497
+ this.env = options.env;
498
+ this.cols = clampCols(options.cols);
499
+ this.rows = clampRows(options.rows);
500
+ this.onSnapshot = options.onSnapshot;
501
+ this.onExit = options.onExit;
502
+ this.onError = options.onError;
503
+ this.terminal = new XtermTerminal({ allowProposedApi: true, cols: this.cols, rows: this.rows, scrollback: 2000 });
504
+ }
505
+ get disposed() {
506
+ return this._disposed;
507
+ }
508
+ get snapshot() {
509
+ return this.createSnapshot();
510
+ }
511
+ async start() {
512
+ if (this._disposed)
513
+ throw new Error("Command PTY host is disposed.");
514
+ if (typeof Bun.Terminal !== "function") {
515
+ throw new Error("Bun native PTY is unavailable in this runtime. Command host requires Bun.Terminal.");
516
+ }
517
+ const spawnOptions = {
518
+ cwd: this.projectRoot,
519
+ env: withEnv(this.env),
520
+ terminal: {
521
+ cols: this.cols,
522
+ rows: this.rows,
523
+ name: "xterm-256color",
524
+ data: (_terminal, data) => this.handlePtyData(data)
525
+ }
526
+ };
527
+ const proc = Bun.spawn([...childCommandPrefix(), ...this.argv], spawnOptions);
528
+ if (!proc.terminal)
529
+ throw new Error("Bun did not attach a terminal to the command child process.");
530
+ this.proc = proc;
531
+ this.pty = proc.terminal;
532
+ this.status = "running";
533
+ this.message = "running";
534
+ this.emitSnapshotSoon(0);
535
+ proc.exited.then((exitCode) => {
536
+ if (this._disposed)
537
+ return;
538
+ this.status = exitCode === 0 ? "exited" : "failed";
539
+ this.exitCode = exitCode;
540
+ this.signal = null;
541
+ this.message = exitCode === 0 ? "completed" : `exited with code ${exitCode}`;
542
+ const snapshot = this.createSnapshot();
543
+ this.onSnapshot?.(snapshot);
544
+ this.onExit?.(snapshot);
545
+ if (activeHost === this)
546
+ activeHost = null;
547
+ this.dispose("exit", { kill: false, notify: false });
548
+ }).catch((error) => {
549
+ if (this._disposed)
550
+ return;
551
+ this.status = "failed";
552
+ this.message = error instanceof Error ? error.message : String(error);
553
+ const snapshot = this.createSnapshot();
554
+ this.onSnapshot?.(snapshot);
555
+ this.onError?.(error, snapshot);
556
+ if (activeHost === this)
557
+ activeHost = null;
558
+ this.dispose("error", { kill: false, notify: false });
559
+ });
560
+ }
561
+ write(data) {
562
+ if (this._disposed || !this.pty)
563
+ return;
564
+ try {
565
+ this.pty.write(data);
566
+ } catch (error) {
567
+ this.status = "failed";
568
+ this.message = error instanceof Error ? error.message : String(error);
569
+ const snapshot = this.createSnapshot();
570
+ this.onSnapshot?.(snapshot);
571
+ this.onError?.(error, snapshot);
572
+ }
573
+ }
574
+ resize(cols, rows) {
575
+ const nextCols = clampCols(cols);
576
+ const nextRows = clampRows(rows);
577
+ if (nextCols === this.cols && nextRows === this.rows)
578
+ return;
579
+ this.cols = nextCols;
580
+ this.rows = nextRows;
581
+ this.terminal.resize(nextCols, nextRows);
582
+ try {
583
+ this.pty?.resize(nextCols, nextRows);
584
+ } catch {}
585
+ this.emitSnapshotSoon(0);
586
+ }
587
+ dispose(reason = "dispose", options = {}) {
588
+ if (this._disposed)
589
+ return;
590
+ this._disposed = true;
591
+ if (this.notifyTimer)
592
+ clearTimeout(this.notifyTimer);
593
+ this.notifyTimer = null;
594
+ if (options.kill !== false) {
595
+ try {
596
+ this.proc?.kill("SIGTERM");
597
+ } catch {}
598
+ }
599
+ try {
600
+ this.pty?.close();
601
+ } catch {}
602
+ try {
603
+ this.terminal.dispose();
604
+ } catch {}
605
+ if (activeHost === this)
606
+ activeHost = null;
607
+ if (options.notify) {
608
+ this.message = reason;
609
+ this.onSnapshot?.(this.createSnapshot(reason));
610
+ }
611
+ }
612
+ handlePtyData(data) {
613
+ if (this._disposed)
614
+ return;
615
+ this.decoder.decode(data, { stream: true });
616
+ this.terminal.write(data, () => this.emitSnapshotSoon());
617
+ }
618
+ emitSnapshotSoon(delayMs = SNAPSHOT_DELAY_MS) {
619
+ if (this._disposed || this.notifyTimer)
620
+ return;
621
+ this.notifyTimer = setTimeout(() => {
622
+ this.notifyTimer = null;
623
+ if (this._disposed)
624
+ return;
625
+ this.onSnapshot?.(this.createSnapshot());
626
+ }, delayMs);
627
+ }
628
+ createSnapshot(message = this.message) {
629
+ const buffer = this.terminal.buffer.active;
630
+ const end = buffer.length;
631
+ const start = Math.max(0, end - MAX_SNAPSHOT_LINES);
632
+ const lines = [];
633
+ for (let row = start;row < end; row += 1) {
634
+ const line = buffer.getLine(row);
635
+ lines.push((line?.translateToString(false) ?? "").slice(0, this.cols));
636
+ }
637
+ while (lines.length < this.rows)
638
+ lines.push("");
639
+ return {
640
+ command: this.argv,
641
+ label: this.label,
642
+ status: this.status,
643
+ cols: this.cols,
644
+ rows: this.rows,
645
+ lines,
646
+ message,
647
+ ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
648
+ ...this.signal !== undefined ? { signal: this.signal } : {}
649
+ };
650
+ }
651
+ }
652
+
653
+ // packages/cli/src/app-opentui/list-search.ts
277
654
  function isListSearchScene(scene) {
278
655
  return scene === "fleet" || scene === "tasks";
279
656
  }
@@ -291,19 +668,411 @@ function valueText(value) {
291
668
  const record = value;
292
669
  return ["id", "runId", "taskId", "number", "title", "name", "summary", "status", "state", "label", "labels", "assignee", "assignees", "url"].map((key) => valueText(record[key])).join(" ");
293
670
  }
294
- return "";
295
- }
296
- function includesQuery(haystack, query) {
297
- const needle = query.trim().toLowerCase();
298
- if (!needle)
299
- return true;
300
- return haystack.map(valueText).join(" ").toLowerCase().includes(needle);
671
+ return "";
672
+ }
673
+ function includesQuery(haystack, query) {
674
+ const needle = query.trim().toLowerCase();
675
+ if (!needle)
676
+ return true;
677
+ return haystack.map(valueText).join(" ").toLowerCase().includes(needle);
678
+ }
679
+ function filterRunsForSearch(runs, query) {
680
+ return query.trim() ? runs.filter((run) => includesQuery([run.runId, run.status, run.title, run.taskId, run.raw], query)) : [...runs];
681
+ }
682
+ function filterTasksForSearch(tasks, query) {
683
+ return query.trim() ? tasks.filter((task) => includesQuery([task.id, task.status, task.title, task.priority, task.labels, task.assignees, task.raw], query)) : [...tasks];
684
+ }
685
+
686
+ // packages/cli/src/app-opentui/keymap.ts
687
+ init_pi_pty_host();
688
+ function clearTypeBar(context, message) {
689
+ const typeBar = context.getTypeBar();
690
+ if (typeBar)
691
+ typeBar.input.value = "";
692
+ context.emitTypeBarPatch({ value: "", message });
693
+ }
694
+ function handleEmbeddedTerminalKey(context, key) {
695
+ const state = context.getState();
696
+ const piHost = getActivePiHost();
697
+ const commandHost = getActiveCommandHost();
698
+ const host = state.scene === "handoff" ? piHost : state.scene === "command" ? commandHost : null;
699
+ if (!host)
700
+ return false;
701
+ if (key.ctrl && key.name === "]") {
702
+ if (state.scene === "handoff") {
703
+ stopActivePiHost("operator detach");
704
+ clearTypeBar(context, "detached from Pi");
705
+ context.runAppAction("Opening runs", () => context.runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
706
+ } else {
707
+ stopActiveCommandHost("operator detach");
708
+ clearTypeBar(context, "closed command");
709
+ context.runAppAction("Project menu", () => context.runtime.runIntent({ scene: "main", argv: ["main"], action: { kind: "none", label: "Project menu" } }));
710
+ }
711
+ return true;
712
+ }
713
+ const sequence = key.raw || key.sequence;
714
+ if (sequence)
715
+ host.write(sequence);
716
+ const typeBar = context.getTypeBar();
717
+ if (typeBar)
718
+ typeBar.input.value = "";
719
+ return true;
720
+ }
721
+ function handleAppKeyPress(context, key) {
722
+ if (handleEmbeddedTerminalKey(context, key))
723
+ return;
724
+ const renderer = context.renderer();
725
+ if (key.ctrl && key.name === "c") {
726
+ renderer?.destroy();
727
+ return;
728
+ }
729
+ const state = context.getState();
730
+ const currentScene = state.scene;
731
+ const searchScene = isListSearchScene(currentScene);
732
+ const typeBar = context.getTypeBar();
733
+ const inputValue = typeBar?.input.value ?? "";
734
+ const typeBarEmpty = inputValue.trim() === "";
735
+ if (typeBarEmpty && (key.name === "right" || key.name === "tab")) {
736
+ context.runAppAction("Navigate", () => context.runtime.runIntent(context.primaryNavIntent(context.nextPrimaryScene(context.getState().scene, 1))));
737
+ return;
738
+ }
739
+ if (typeBarEmpty && key.name === "left") {
740
+ context.runAppAction("Navigate", () => context.runtime.runIntent(context.primaryNavIntent(context.nextPrimaryScene(context.getState().scene, -1))));
741
+ return;
742
+ }
743
+ if (!searchScene && typeBarEmpty && key.name === "r") {
744
+ context.runAppAction("Opening runs", () => context.runtime.runIntent(context.primaryNavIntent("fleet")));
745
+ return;
746
+ }
747
+ if (!searchScene && typeBarEmpty && key.name === "t") {
748
+ context.runAppAction("Opening tasks", () => context.runtime.runIntent(context.primaryNavIntent("tasks")));
749
+ return;
750
+ }
751
+ if (!searchScene && typeBarEmpty && (key.name === "h" || key.sequence === "?")) {
752
+ context.runAppAction("Opening help", () => context.runtime.runIntent(context.primaryNavIntent("help")));
753
+ return;
754
+ }
755
+ if (searchScene && key.name === "up" || !searchScene && typeBarEmpty && (key.name === "up" || key.name === "k")) {
756
+ if (context.moveSelection(-1, context.getRenderedSelectableItems()))
757
+ return;
758
+ }
759
+ if (searchScene && key.name === "down" || !searchScene && typeBarEmpty && (key.name === "down" || key.name === "j")) {
760
+ if (context.moveSelection(1, context.getRenderedSelectableItems()))
761
+ return;
762
+ }
763
+ if (typeBarEmpty && (key.name === "return" || key.name === "enter" || key.name === "space" || key.sequence === " ")) {
764
+ context.runAppAction("Activate selection", () => context.activateSelection(context.getRenderedSelectableItems()));
765
+ return;
766
+ }
767
+ if (!searchScene && key.name === "q" && typeBarEmpty) {
768
+ renderer?.destroy();
769
+ return;
770
+ }
771
+ if (key.name === "escape") {
772
+ if (inputValue)
773
+ clearTypeBar(context);
774
+ else
775
+ context.runAppAction("Project menu", () => context.runtime.runIntent({ scene: "main", argv: ["main"], action: { kind: "none", label: "Project menu" } }));
776
+ }
777
+ }
778
+
779
+ // packages/cli/src/app-opentui/layout.ts
780
+ function computeStageLayout(width, height) {
781
+ const safeWidth = Math.max(40, Math.trunc(width || 80));
782
+ const safeHeight = Math.max(18, Math.trunc(height || 24));
783
+ const compact = safeWidth < 92 || safeHeight < 30;
784
+ const horizontalGutter = compact ? 1 : 2;
785
+ const centerWidth = Math.max(36, safeWidth - horizontalGutter * 2);
786
+ const centerLeft = horizontalGutter;
787
+ const footerTop = Math.max(0, safeHeight - 1);
788
+ const typeBarTop = footerTop;
789
+ const centerTop = compact ? 1 : Math.max(1, Math.floor(safeHeight * 0.055));
790
+ const centerHeight = Math.max(8, footerTop - centerTop);
791
+ return { width: safeWidth, height: safeHeight, centerWidth, centerLeft, centerTop, centerHeight, typeBarTop, footerTop, compact };
792
+ }
793
+ function visibleWidth(text) {
794
+ return [...text].length;
795
+ }
796
+ function alignText(text, width, align = "left") {
797
+ const length = visibleWidth(text);
798
+ if (length >= width)
799
+ return text;
800
+ const pad = width - length;
801
+ if (align === "right")
802
+ return `${" ".repeat(pad)}${text}`;
803
+ if (align === "center") {
804
+ const left = Math.floor(pad / 2);
805
+ return `${" ".repeat(left)}${text}${" ".repeat(pad - left)}`;
806
+ }
807
+ return text;
808
+ }
809
+ function truncateText(text, width) {
810
+ if (visibleWidth(text) <= width)
811
+ return text;
812
+ if (width <= 1)
813
+ return "\u2026";
814
+ return `${[...text].slice(0, Math.max(0, width - 1)).join("")}\u2026`;
815
+ }
816
+
817
+ // packages/cli/src/app-opentui/runtime-resources.ts
818
+ class RuntimeResources {
819
+ disposers = [];
820
+ disposed = false;
821
+ add(disposer) {
822
+ if (this.disposed) {
823
+ disposer();
824
+ return () => {
825
+ return;
826
+ };
827
+ }
828
+ this.disposers.push(disposer);
829
+ return disposer;
830
+ }
831
+ setTimeout(callback, delayMs) {
832
+ const timer = setTimeout(() => {
833
+ this.remove(dispose);
834
+ callback();
835
+ }, delayMs);
836
+ const dispose = () => clearTimeout(timer);
837
+ this.add(dispose);
838
+ return timer;
839
+ }
840
+ setInterval(callback, delayMs) {
841
+ const timer = setInterval(callback, delayMs);
842
+ this.add(() => clearInterval(timer));
843
+ return timer;
844
+ }
845
+ clearTimeout(timer) {
846
+ if (timer)
847
+ clearTimeout(timer);
848
+ }
849
+ on(emitter, event, listener) {
850
+ emitter.on(event, listener);
851
+ this.add(() => {
852
+ if (typeof emitter.off === "function")
853
+ emitter.off(event, listener);
854
+ else
855
+ emitter.removeListener?.(event, listener);
856
+ });
857
+ }
858
+ disposeAll() {
859
+ if (this.disposed)
860
+ return;
861
+ this.disposed = true;
862
+ for (let index = this.disposers.length - 1;index >= 0; index -= 1) {
863
+ try {
864
+ this.disposers[index]?.();
865
+ } catch {}
866
+ }
867
+ this.disposers.length = 0;
868
+ }
869
+ remove(disposer) {
870
+ const index = this.disposers.indexOf(disposer);
871
+ if (index >= 0)
872
+ this.disposers.splice(index, 1);
873
+ }
874
+ }
875
+
876
+ // packages/cli/src/app-opentui/intent.ts
877
+ var NAV_SCENES = new Map([
878
+ ["", "main"],
879
+ ["main", "main"],
880
+ ["home", "main"],
881
+ ["fleet", "fleet"],
882
+ ["runs", "fleet"],
883
+ ["run", "fleet"],
884
+ ["tasks", "tasks"],
885
+ ["task", "tasks"],
886
+ ["inbox", "inbox"],
887
+ ["server", "server"],
888
+ ["init", "init"],
889
+ ["doctor", "doctor"],
890
+ ["help", "help"]
891
+ ]);
892
+ var TASK_VIEW_COMMANDS = new Set([
893
+ "",
894
+ "all",
895
+ "open",
896
+ "ready",
897
+ "blocked",
898
+ "closed",
899
+ "done",
900
+ "mine",
901
+ "me",
902
+ "sort",
903
+ "status",
904
+ "state",
905
+ "assignee",
906
+ "assigned",
907
+ "label",
908
+ "search",
909
+ "q"
910
+ ]);
911
+ function isHelpCommand(command) {
912
+ return command === "help" || command === "--help" || command === "-h";
913
+ }
914
+ function isTaskViewCommand(command) {
915
+ return !command || TASK_VIEW_COMMANDS.has(command) || command.startsWith("-") && !isHelpCommand(command);
916
+ }
917
+ function intent(scene, argv, kind, payload, label, raw) {
918
+ return {
919
+ scene,
920
+ argv,
921
+ ...raw ? { raw } : {},
922
+ action: {
923
+ kind,
924
+ ...label ? { label } : {},
925
+ ...payload ? { payload } : {}
926
+ }
927
+ };
928
+ }
929
+ function commandRunIntent(argv, raw, reason) {
930
+ const command = argv.join(" ").trim();
931
+ return intent("command", argv, "command-run", { argv: [...argv], ...reason ? { reason } : {} }, command ? `Running rig ${command}` : "Running rig", raw);
932
+ }
933
+ function unsupportedPlainCliIntent(argv, raw, reason) {
934
+ return commandRunIntent(argv, raw, reason);
935
+ }
936
+ function firstValueAfter(args, option) {
937
+ const index = args.indexOf(option);
938
+ return index >= 0 ? args[index + 1] : undefined;
939
+ }
940
+ function firstNonOption(args) {
941
+ return args.find((part) => part.trim() && !part.startsWith("-"));
942
+ }
943
+ function inboxActionIntent(argv, command, rest, raw) {
944
+ const requestId = firstNonOption(rest);
945
+ if (command === "approve" || command === "approvals") {
946
+ return requestId ? intent("inbox", argv, "inbox-approve", { requestId }, `Approving ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading approvals", raw);
947
+ }
948
+ if (command === "reject") {
949
+ return requestId ? intent("inbox", argv, "inbox-reject", { requestId }, `Rejecting ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading approvals", raw);
950
+ }
951
+ if (command === "answer" || command === "inputs" || command === "respond") {
952
+ const answer = rest.slice(requestId ? 1 : 0).join(" ").trim();
953
+ return requestId && answer ? intent("inbox", argv, "inbox-answer", { requestId, answer }, `Answering ${requestId}`, raw) : intent("inbox", argv, "refresh", undefined, "Loading inbox", raw);
954
+ }
955
+ return intent("inbox", argv, "refresh", undefined, "Loading inbox", raw);
956
+ }
957
+ function taskViewPayload(command, rest) {
958
+ const normalized = command.toLowerCase();
959
+ const args = command ? [command, ...rest] : rest;
960
+ const sort = firstValueAfter(args, "--sort") ?? firstValueAfter(args, "-s");
961
+ const state = firstValueAfter(args, "--state");
962
+ const status = firstValueAfter(args, "--status");
963
+ const assignee = firstValueAfter(args, "--assignee") ?? firstValueAfter(args, "--assigned");
964
+ const label = firstValueAfter(args, "--label");
965
+ const search = firstValueAfter(args, "--search") ?? firstValueAfter(args, "-q");
966
+ const optionPayload = {
967
+ ...sort ? { taskSort: sort } : {},
968
+ ...state ? { taskState: state, taskFilterPreset: "custom" } : {},
969
+ ...status ? { taskStatus: status, taskFilterPreset: "custom" } : {},
970
+ ...assignee ? { taskAssignee: assignee, taskFilterPreset: "custom" } : {},
971
+ ...label ? { taskLabel: label, taskFilterPreset: "custom" } : {},
972
+ ...search ? { taskSearch: search, taskFilterPreset: "custom" } : {}
973
+ };
974
+ if (normalized === "sort") {
975
+ const value = firstNonOption(rest) ?? sort ?? "priority";
976
+ return { payload: { ...optionPayload, taskSort: value }, label: `Sorting tasks by ${value}` };
977
+ }
978
+ if (normalized === "status") {
979
+ const value = firstNonOption(rest) ?? status ?? "open";
980
+ return { payload: { ...optionPayload, taskStatus: value, taskFilterPreset: "custom" }, label: `Filtering tasks by status ${value}` };
981
+ }
982
+ if (normalized === "state") {
983
+ const value = firstNonOption(rest) ?? state ?? "open";
984
+ return { payload: { ...optionPayload, taskState: value, taskFilterPreset: "custom" }, label: `Filtering tasks by state ${value}` };
985
+ }
986
+ if (normalized === "assignee" || normalized === "assigned" || normalized === "mine" || normalized === "me") {
987
+ const value = normalized === "mine" || normalized === "me" ? "me" : firstNonOption(rest) ?? assignee ?? "me";
988
+ return { payload: { ...optionPayload, taskAssignee: value, taskState: "open", taskFilterPreset: value === "me" ? "mine" : "custom" }, label: value === "me" ? "Filtering my tasks" : `Filtering tasks for ${value}` };
989
+ }
990
+ if (normalized === "label") {
991
+ const value = firstNonOption(rest) ?? label;
992
+ return { payload: { ...optionPayload, ...value ? { taskLabel: value } : {}, taskFilterPreset: "custom" }, label: value ? `Filtering tasks labeled ${value}` : "Filtering tasks by label" };
993
+ }
994
+ if (normalized === "search" || normalized === "q") {
995
+ const value = rest.join(" ").trim() || search;
996
+ return { payload: { ...optionPayload, ...value ? { taskSearch: value } : {}, taskFilterPreset: "custom" }, label: value ? `Searching tasks for ${value}` : "Searching tasks" };
997
+ }
998
+ if (["all", "open", "ready", "blocked", "closed", "done"].includes(normalized)) {
999
+ return { payload: { ...optionPayload, taskFilterPreset: normalized === "done" ? "closed" : normalized }, label: `Filtering tasks: ${normalized}` };
1000
+ }
1001
+ if (Object.keys(optionPayload).length > 0)
1002
+ return { payload: optionPayload, label: "Filtering tasks" };
1003
+ return { label: "Loading tasks" };
301
1004
  }
302
- function filterRunsForSearch(runs, query) {
303
- return query.trim() ? runs.filter((run) => includesQuery([run.runId, run.status, run.title, run.taskId, run.raw], query)) : [...runs];
1005
+ function taskViewIntent(argv, command = "", rest = [], raw) {
1006
+ const { payload, label } = taskViewPayload(command, rest);
1007
+ return intent("tasks", argv, "refresh", payload, label, raw);
304
1008
  }
305
- function filterTasksForSearch(tasks, query) {
306
- return query.trim() ? tasks.filter((task) => includesQuery([task.id, task.status, task.title, task.priority, task.labels, task.assignees, task.raw], query)) : [...tasks];
1009
+ function intentFromArgv(argv) {
1010
+ const [group = "", command = "", ...rest] = argv;
1011
+ const normalizedGroup = group.toLowerCase();
1012
+ const normalizedCommand = command.toLowerCase();
1013
+ if (!normalizedGroup)
1014
+ return intent("main", argv, "none", undefined, "Project menu");
1015
+ if (normalizedGroup === "--help" || normalizedGroup === "-h" || normalizedGroup === "help") {
1016
+ return argv.length === 1 ? intent("help", argv, "none", undefined, "Help") : unsupportedPlainCliIntent(argv);
1017
+ }
1018
+ if (normalizedGroup === "task") {
1019
+ if (normalizedCommand === "run" || normalizedCommand === "ready" || normalizedCommand === "status")
1020
+ return commandRunIntent(argv);
1021
+ if (!isTaskViewCommand(normalizedCommand))
1022
+ return commandRunIntent(argv);
1023
+ return taskViewIntent(argv, normalizedCommand, rest);
1024
+ }
1025
+ if (normalizedGroup === "tasks") {
1026
+ if (!isTaskViewCommand(normalizedCommand))
1027
+ return unsupportedPlainCliIntent(argv);
1028
+ return taskViewIntent(argv, normalizedCommand, rest);
1029
+ }
1030
+ if (normalizedGroup === "run") {
1031
+ if (normalizedCommand === "attach") {
1032
+ const runId = firstNonOption(rest);
1033
+ const nativeOnly = Boolean(runId) && rest.filter((part) => part.trim()).length === 1;
1034
+ return nativeOnly ? intent("handoff", argv, "run-attach", { runId }, `Attaching ${runId}`) : commandRunIntent(argv);
1035
+ }
1036
+ if (!normalizedCommand || normalizedCommand === "list" || normalizedCommand === "status")
1037
+ return intent("fleet", argv, "refresh", undefined, "Loading fleet");
1038
+ if (normalizedCommand === "stop") {
1039
+ const runId = firstNonOption(rest);
1040
+ return runId && rest.filter((part) => part.trim()).length === 1 ? intent("fleet", argv, "run-stop", { runId }, "Stopping run") : commandRunIntent(argv);
1041
+ }
1042
+ return commandRunIntent(argv);
1043
+ }
1044
+ if (normalizedGroup === "server") {
1045
+ if (!normalizedCommand || normalizedCommand === "status")
1046
+ return intent("server", argv, "refresh", undefined, normalizedCommand ? "Checking server" : "Loading server");
1047
+ return unsupportedPlainCliIntent(argv, undefined, `server ${normalizedCommand} is not executed in the shell; exit and run it in plain mode.`);
1048
+ }
1049
+ if (normalizedGroup === "github") {
1050
+ if (normalizedCommand === "auth" && (rest[0]?.toLowerCase() ?? "status") === "status" && rest.length <= 1) {
1051
+ return intent("server", argv, "refresh", undefined, "Checking GitHub auth");
1052
+ }
1053
+ return unsupportedPlainCliIntent(argv, undefined, "github auth mutations are not executed in the shell; exit and run them in plain mode.");
1054
+ }
1055
+ if (normalizedGroup === "init")
1056
+ return commandRunIntent(argv);
1057
+ if (normalizedGroup === "doctor") {
1058
+ return normalizedCommand ? commandRunIntent(argv) : intent("doctor", argv, "doctor-run", undefined, "Running doctor");
1059
+ }
1060
+ if (normalizedGroup === "inbox") {
1061
+ if (normalizedCommand === "watch")
1062
+ return commandRunIntent(argv);
1063
+ if ((normalizedCommand === "approvals" || normalizedCommand === "inputs") && rest.length > 0)
1064
+ return commandRunIntent(argv);
1065
+ return normalizedCommand && normalizedCommand !== "approvals" && normalizedCommand !== "inputs" ? commandRunIntent(argv) : inboxActionIntent(argv, normalizedCommand, rest);
1066
+ }
1067
+ if (normalizedGroup === "attach") {
1068
+ const attachArgs = [command, ...rest].filter((part) => part.trim());
1069
+ const runId = firstNonOption(attachArgs);
1070
+ return runId && attachArgs.length === 1 ? intent("handoff", argv, "run-attach", { runId }, `Attaching ${runId}`) : commandRunIntent(argv);
1071
+ }
1072
+ const nav = NAV_SCENES.get(normalizedGroup);
1073
+ if (nav && argv.length === 1)
1074
+ return intent(nav, argv, "navigate", undefined, `Opening ${nav}`);
1075
+ return commandRunIntent(argv);
307
1076
  }
308
1077
 
309
1078
  // packages/cli/src/app-opentui/state.ts
@@ -431,7 +1200,7 @@ import {
431
1200
  dim as otuiDim,
432
1201
  fg as otuiFg,
433
1202
  t,
434
- TextAttributes
1203
+ TextAttributes as TextAttributes2
435
1204
  } from "@opentui/core";
436
1205
  var RIG_UI = {
437
1206
  bg: "#070809",
@@ -462,16 +1231,175 @@ var styles = {
462
1231
  yellow: otuiFg(RIG_UI.yellow),
463
1232
  magenta: otuiFg(RIG_UI.magenta)
464
1233
  };
1234
+ // packages/cli/src/app-opentui/adapters/tasks.ts
1235
+ function rawRecord(record, key) {
1236
+ const direct = record[key];
1237
+ if (direct !== undefined && direct !== null)
1238
+ return direct;
1239
+ const raw = record.raw;
1240
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw[key] : undefined;
1241
+ }
1242
+ function optionalString(record, keys) {
1243
+ for (const key of keys) {
1244
+ const value = rawRecord(record, key);
1245
+ if (typeof value === "string" && value.trim())
1246
+ return value.trim();
1247
+ if (typeof value === "number" && Number.isFinite(value))
1248
+ return String(value);
1249
+ }
1250
+ return;
1251
+ }
1252
+ function activeRunForRecord(record) {
1253
+ const value = rawRecord(record, "activeRun");
1254
+ if (!value || typeof value !== "object" || Array.isArray(value))
1255
+ return;
1256
+ const activeRun = value;
1257
+ const runId = optionalString(activeRun, ["runId", "id"]);
1258
+ if (!runId)
1259
+ return;
1260
+ const status = optionalString(activeRun, ["status", "state"]);
1261
+ const stage = optionalString(activeRun, ["stage", "phase"]);
1262
+ return {
1263
+ runId,
1264
+ ...status ? { status } : {},
1265
+ ...stage ? { stage } : {}
1266
+ };
1267
+ }
1268
+ function activeRunIdFromTaskLike(task) {
1269
+ return activeRunForRecord(task)?.runId;
1270
+ }
1271
+
1272
+ // packages/cli/src/app-opentui/render/graphics.ts
1273
+ import { FrameBufferRenderable, RGBA as RGBA2 } from "@opentui/core";
1274
+
1275
+ // packages/cli/src/app-opentui/render/ascii-fleet.ts
1276
+ var LEAD_DRONE = [
1277
+ " .-=-. .-=-. ",
1278
+ " ( !!! ) ( !!! ) ",
1279
+ " '-=-'._ _.'-=-' ",
1280
+ " '._ _.' ",
1281
+ " '=$$$$$$$=.' ",
1282
+ " =$$$$$$$$$$$= ",
1283
+ " $$$@@@@@@@@@@$$$ ",
1284
+ " $$$@@ @@$$$ ",
1285
+ " $$@ ? @$$$ ",
1286
+ " $$$@ '-' @$$$ ",
1287
+ " $$$@@ @@$$$ ",
1288
+ " $$$@@@@@@@@@@$$$ ",
1289
+ " =$$$$$$$$$$$= ",
1290
+ " '=$$$$$$$=.' ",
1291
+ " _.' '._ ",
1292
+ " .-=-.' '.-=-. ",
1293
+ " ( !!! ) ( !!! ) ",
1294
+ " '-=-' '-=-' "
1295
+ ];
1296
+ var MINI_DRONE = [
1297
+ "(!!!) (!!!)",
1298
+ " \\%==%/ ",
1299
+ " %%?%% ",
1300
+ " /%==%\\ ",
1301
+ "(!!!) (!!!)"
1302
+ ];
1303
+ var ROTORS = ["---", "\\\\\\", "|||", "///"];
1304
+ var FLEET_GRID_WIDTH = 52;
1305
+ var FLEET_GRID_HEIGHT = 27;
1306
+ var FLEET = [
1307
+ { art: MINI_DRONE, x: 0, y: 2, amp: 1, speed: 0.05, phase: 0, dx: 1, driftSpeed: 0.02 },
1308
+ { art: MINI_DRONE, x: 39, y: 2, amp: 1, speed: 0.044, phase: 2.1, dx: 1, driftSpeed: 0.017 },
1309
+ { art: MINI_DRONE, x: 1, y: 20, amp: 1, speed: 0.048, phase: 4.2, dx: 1, driftSpeed: 0.023 },
1310
+ { art: MINI_DRONE, x: 38, y: 20, amp: 1, speed: 0.041, phase: 1.3, dx: 1, driftSpeed: 0.015 },
1311
+ { art: LEAD_DRONE, x: 10, y: 5, amp: 2, speed: 0.04, phase: 0, dx: 0, driftSpeed: 0.011 }
1312
+ ];
1313
+ function fleetRows(tick, options = {}) {
1314
+ const animate = options.animate ?? true;
1315
+ const t2 = animate ? tick : 0;
1316
+ const blade = ROTORS[Math.floor(t2 / 4) % ROTORS.length];
1317
+ const pulse = Math.sin(t2 * 0.07);
1318
+ const eye = pulse > 0.45 ? "@" : pulse > -0.1 ? "o" : ".";
1319
+ const grid = Array.from({ length: FLEET_GRID_HEIGHT }, () => Array.from({ length: FLEET_GRID_WIDTH }, () => " "));
1320
+ for (const drone of FLEET) {
1321
+ const yOffset = drone.y + Math.round(drone.amp * Math.sin(t2 * drone.speed + drone.phase));
1322
+ const xOffset = drone.x + Math.round(drone.dx * Math.sin(t2 * drone.driftSpeed + drone.phase));
1323
+ for (let row = 0;row < drone.art.length; row += 1) {
1324
+ const source = drone.art[row];
1325
+ const targetY = yOffset + row;
1326
+ if (targetY < 0 || targetY >= FLEET_GRID_HEIGHT)
1327
+ continue;
1328
+ for (let col = 0;col < source.length; col += 1) {
1329
+ let char = source[col];
1330
+ if (char === " ")
1331
+ continue;
1332
+ const targetX = xOffset + col;
1333
+ if (targetX < 0 || targetX >= FLEET_GRID_WIDTH)
1334
+ continue;
1335
+ if (source.slice(col, col + 3) === "!!!") {
1336
+ for (let rotorIndex = 0;rotorIndex < 3; rotorIndex += 1) {
1337
+ if (targetX + rotorIndex < FLEET_GRID_WIDTH)
1338
+ grid[targetY][targetX + rotorIndex] = blade[rotorIndex];
1339
+ }
1340
+ col += 2;
1341
+ continue;
1342
+ }
1343
+ if (char === "?")
1344
+ char = eye;
1345
+ grid[targetY][targetX] = char;
1346
+ }
1347
+ }
1348
+ }
1349
+ return grid.map((row) => row.join("").replace(/\s+$/, ""));
1350
+ }
1351
+
1352
+ // packages/cli/src/app-opentui/render/panel-layout.ts
1353
+ function panelCellRect(layout, panel) {
1354
+ const left = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
1355
+ const desiredWidth = panel.width ?? layout.centerWidth;
1356
+ const width = Math.max(1, Math.min(layout.width - left, desiredWidth));
1357
+ const top = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
1358
+ const height = Math.max(1, Math.min(layout.height - top, panel.height));
1359
+ if (width <= 3 || height <= 3)
1360
+ return null;
1361
+ const right = left + width - 1;
1362
+ const bottom = top + height - 1;
1363
+ const headerHeight = panel.chrome === "ad-terminal" ? Math.max(3, Math.min(height - 2, panel.headerHeight ?? 3)) : 0;
1364
+ const dividerY = headerHeight ? Math.min(bottom - 1, top + headerHeight) : top;
1365
+ return { left, top, width, height, right, bottom, headerHeight, dividerY };
1366
+ }
1367
+ function panelContentRect(layout, panel) {
1368
+ const rect = panelCellRect(layout, panel);
1369
+ if (!rect)
1370
+ return null;
1371
+ const chromeInset = panel.chrome === "ad-terminal" ? 1 : 0;
1372
+ const left = rect.left + chromeInset;
1373
+ const top = rect.top + rect.headerHeight + chromeInset;
1374
+ const width = Math.max(1, Math.min(layout.width - left, rect.width - chromeInset * 2));
1375
+ const height = Math.max(1, Math.min(layout.height - top, rect.height - rect.headerHeight - chromeInset * 2));
1376
+ const paddingX = panel.paddingX ?? (panel.chrome === "ad-terminal" ? 5 : 2);
1377
+ const paddingY = panel.paddingY ?? (panel.chrome === "ad-terminal" ? 2 : 1);
1378
+ const contentWidth = Math.max(1, width - paddingX * 2 - 1);
1379
+ return { left, top, width, height, paddingX, paddingY, contentWidth };
1380
+ }
1381
+ function panelPixelPlacement(layout, panel, size, minPixels = 8) {
1382
+ const cellW = size.width / layout.width;
1383
+ const cellH = size.height / layout.height;
1384
+ const leftCells = layout.centerLeft + (panel.left ?? 0);
1385
+ const topCells = layout.centerTop + panel.top;
1386
+ const widthCells = panel.width ?? layout.centerWidth;
1387
+ const heightCells = panel.height;
1388
+ const left = Math.round(leftCells * cellW);
1389
+ const top = Math.round(topCells * cellH);
1390
+ const width = Math.round(widthCells * cellW);
1391
+ const height = Math.round(heightCells * cellH);
1392
+ return width > minPixels && height > minPixels ? { left, top, width, height, leftCells, topCells, widthCells, heightCells } : null;
1393
+ }
465
1394
 
466
1395
  // packages/cli/src/app-opentui/render/graphics.ts
467
- import { FrameBufferRenderable, RGBA } from "@opentui/core";
468
- var TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
469
- var BACKDROP = RGBA.fromHex(RIG_UI.bg);
470
- var PANEL_BG = RGBA.fromInts(16, 17, 21, 184);
471
- var PANEL_HEADER_BG = RGBA.fromInts(16, 17, 21, 188);
472
- var PANEL_LINE = RGBA.fromInts(255, 255, 255, 20);
473
- var PANEL_LINE_DIM = RGBA.fromInts(255, 255, 255, 8);
474
- var PANEL_TEXT_DIM = RGBA.fromInts(108, 110, 121, 255);
1396
+ var TRANSPARENT = RGBA2.fromValues(0, 0, 0, 0);
1397
+ var BACKDROP = RGBA2.fromHex(RIG_UI.bg);
1398
+ var PANEL_BG = RGBA2.fromInts(16, 17, 21, 184);
1399
+ var PANEL_HEADER_BG = RGBA2.fromInts(16, 17, 21, 188);
1400
+ var PANEL_LINE = RGBA2.fromInts(255, 255, 255, 20);
1401
+ var PANEL_LINE_DIM = RGBA2.fromInts(255, 255, 255, 8);
1402
+ var PANEL_TEXT_DIM = RGBA2.fromInts(108, 110, 121, 255);
475
1403
  var AC_RGB = [204, 255, 77];
476
1404
  var C2_RGB = [86, 216, 255];
477
1405
  var MG_RGB = [255, 121, 176];
@@ -576,7 +1504,7 @@ function paletteColor(ac, c2, mg, ink) {
576
1504
  const b = (AC_RGB[2] * ac + C2_RGB[2] * c2 + MG_RGB[2] * mg + INK_RGB[2] * ink) / total;
577
1505
  const intensity = Math.min(1, total / 4);
578
1506
  const lift = 0.34 + intensity * 0.66;
579
- return RGBA.fromInts(Math.round(r * lift), Math.round(g * lift), Math.round(b * lift), 255);
1507
+ return RGBA2.fromInts(Math.round(r * lift), Math.round(g * lift), Math.round(b * lift), 255);
580
1508
  }
581
1509
  function clearCanvas(canvas) {
582
1510
  canvas.ac.fill(0);
@@ -885,14 +1813,6 @@ function resizeGraphicsLayer(layer, layout) {
885
1813
  function clearGraphicsLayer(layer) {
886
1814
  layer.frameBuffer.clear(BACKDROP);
887
1815
  }
888
- function panelRect(layout, panel) {
889
- const left = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
890
- const desiredWidth = panel.width ?? layout.centerWidth;
891
- const width = Math.max(1, Math.min(layout.width - left, desiredWidth));
892
- const top = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
893
- const height = Math.max(1, Math.min(layout.height - top, panel.height));
894
- return width > 3 && height > 3 ? { left, top, width, height } : null;
895
- }
896
1816
  function writeCell(layer, x, y, char, fg, bg) {
897
1817
  layer.frameBuffer.setCellWithAlphaBlending(x, y, char, fg, bg);
898
1818
  }
@@ -915,14 +1835,10 @@ function drawRoundedFill(layer, left, top, right, bottom, dividerY) {
915
1835
  drawCornerCut(layer, right, bottom, "\u2598", PANEL_BG);
916
1836
  }
917
1837
  function drawAdTerminalPanel(layer, layout, panel) {
918
- const rect = panelRect(layout, panel);
1838
+ const rect = panelCellRect(layout, panel);
919
1839
  if (!rect)
920
1840
  return;
921
- const { left, top, width, height } = rect;
922
- const right = left + width - 1;
923
- const bottom = top + height - 1;
924
- const headerHeight = Math.max(3, Math.min(height - 2, panel.headerHeight ?? 3));
925
- const dividerY = Math.min(bottom - 1, top + headerHeight);
1841
+ const { left, top, width, right, bottom, headerHeight, dividerY } = rect;
926
1842
  drawRoundedFill(layer, left, top, right, bottom, dividerY);
927
1843
  if (dividerY > top && dividerY < bottom) {
928
1844
  for (let x = left + 2;x < right - 1; x += 1) {
@@ -941,14 +1857,62 @@ function drawPanelChrome(layer, layout, panels) {
941
1857
  drawAdTerminalPanel(layer, layout, panel);
942
1858
  }
943
1859
  }
944
- function drawAmbientField(layer, layout, tick, scene = "fleet") {
945
- layer.__rigMarketingCanvas?.render(layer, scene, tick);
1860
+ function staticFleetTick(scene) {
1861
+ let hash = 0;
1862
+ for (let index = 0;index < scene.length; index += 1)
1863
+ hash = hash * 33 + scene.charCodeAt(index) >>> 0;
1864
+ return 11 + hash % 97;
1865
+ }
1866
+ function asciiFleetColor(char, row) {
1867
+ if (char === "@" || char === "$" || char === "o")
1868
+ return RGBA2.fromHex(RIG_UI.lime);
1869
+ if (char === "%" || char === "!" || char === "/" || char === "\\" || char === "|")
1870
+ return RGBA2.fromHex(RIG_UI.cyan);
1871
+ if (char === "." || char === "'" || char === "_" || row < 3 || row > FLEET_GRID_HEIGHT - 4)
1872
+ return RGBA2.fromInts(108, 110, 121, 170);
1873
+ return RGBA2.fromHex(RIG_UI.limeDim);
1874
+ }
1875
+ function drawAmbientField(layer, layout, _tick, scene = "fleet") {
1876
+ const fb = layer.frameBuffer;
1877
+ fb.clear(BACKDROP);
1878
+ const rows = fleetRows(staticFleetTick(scene), { animate: true });
1879
+ const left = Math.floor((layout.width - FLEET_GRID_WIDTH) / 2);
1880
+ const top = Math.floor((layout.height - FLEET_GRID_HEIGHT) / 2);
1881
+ for (let row = 0;row < rows.length; row += 1) {
1882
+ const y = top + row;
1883
+ if (y < 0 || y >= layout.height)
1884
+ continue;
1885
+ const line = rows[row];
1886
+ for (let col = 0;col < line.length; col += 1) {
1887
+ const x = left + col;
1888
+ if (x < 0 || x >= layout.width)
1889
+ continue;
1890
+ const char = line[col];
1891
+ if (char === " ")
1892
+ continue;
1893
+ fb.setCellWithAlphaBlending(x, y, char, asciiFleetColor(char, row), TRANSPARENT);
1894
+ }
1895
+ }
946
1896
  }
947
1897
 
948
1898
  // packages/cli/src/app-opentui/render/image-visual-layer.ts
949
1899
  import { Worker } from "worker_threads";
950
1900
  import { deflateSync } from "zlib";
951
- import { allocateImageId, deleteKittyImage, getCapabilities } from "@earendil-works/pi-tui";
1901
+ import { allocateImageId, deleteKittyImage } from "@earendil-works/pi-tui";
1902
+
1903
+ // packages/cli/src/app-opentui/terminal-capabilities.ts
1904
+ import { getCapabilities } from "@earendil-works/pi-tui";
1905
+ function getRigTerminalCapabilities() {
1906
+ const imageProtocol = getCapabilities().images;
1907
+ const supportsKittyImages = imageProtocol === "kitty";
1908
+ return {
1909
+ imageProtocol,
1910
+ supportsKittyImages,
1911
+ visualMode: supportsKittyImages ? "kitty-images" : "framebuffer-fallback"
1912
+ };
1913
+ }
1914
+
1915
+ // packages/cli/src/app-opentui/render/image-visual-layer.ts
952
1916
  var BG = [7, 8, 9, 255];
953
1917
  var PANEL = [16, 17, 21, 184];
954
1918
  var PANEL_HEADER = [16, 17, 21, 190];
@@ -962,12 +1926,10 @@ var PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
962
1926
  var CRC_TABLE = makeCrcTable();
963
1927
  var MAX_IMAGE_BYTES = 48 * 1024 * 1024;
964
1928
  var KITTY_CHUNK_SIZE = 4096;
965
- var PANEL_BLUR_SCALE = 5;
1929
+ var PANEL_BLUR_SCALE = 1;
966
1930
  var PANEL_MASK_CACHE_LIMIT = 16;
967
1931
  var ZLIB_LEVEL = 1;
968
1932
  var DEFAULT_IMAGE_RESOLUTION_SCALE = 1;
969
- var DEFAULT_DRONE_LAYER_SCALE = 0.18;
970
- var DEFAULT_IMAGE_FPS = 24;
971
1933
  var panelMaskCache = new Map;
972
1934
  var BG_WORD = rgbaWord(BG);
973
1935
  function imageTransport() {
@@ -982,24 +1944,6 @@ function imageResolutionScale() {
982
1944
  return Math.max(0.25, Math.min(1, raw));
983
1945
  return DEFAULT_IMAGE_RESOLUTION_SCALE;
984
1946
  }
985
- function droneLayerScale() {
986
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_DRONE_SCALE ?? "");
987
- if (Number.isFinite(raw) && raw > 0)
988
- return Math.max(0.15, Math.min(1, raw));
989
- return DEFAULT_DRONE_LAYER_SCALE;
990
- }
991
- function scaledPixelSize(size, scale, layout) {
992
- return {
993
- width: Math.max(layout.width, Math.floor(size.width * scale)),
994
- height: Math.max(layout.height, Math.floor(size.height * scale))
995
- };
996
- }
997
- function imageFps() {
998
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_IMAGE_FPS ?? "");
999
- if (Number.isFinite(raw) && raw > 0)
1000
- return Math.max(10, Math.min(60, raw));
1001
- return DEFAULT_IMAGE_FPS;
1002
- }
1003
1947
  function rgbaWord(color) {
1004
1948
  return (color[3] << 24 | color[2] << 16 | color[1] << 8 | color[0]) >>> 0;
1005
1949
  }
@@ -1015,6 +1959,109 @@ function fillRgba(data, color, word = rgbaWord(color)) {
1015
1959
  data[i + 3] = color[3];
1016
1960
  }
1017
1961
  }
1962
+ var ASCII_GLYPHS = {
1963
+ ".": ["00000", "00000", "00000", "00000", "00000", "00100", "00100"],
1964
+ "'": ["00100", "00100", "00000", "00000", "00000", "00000", "00000"],
1965
+ "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
1966
+ _: ["00000", "00000", "00000", "00000", "00000", "00000", "11111"],
1967
+ "=": ["00000", "11111", "00000", "00000", "11111", "00000", "00000"],
1968
+ "|": ["00100", "00100", "00100", "00100", "00100", "00100", "00100"],
1969
+ "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"],
1970
+ "\\": ["10000", "01000", "01000", "00100", "00010", "00010", "00001"],
1971
+ "(": ["00010", "00100", "01000", "01000", "01000", "00100", "00010"],
1972
+ ")": ["01000", "00100", "00010", "00010", "00010", "00100", "01000"],
1973
+ "!": ["00100", "00100", "00100", "00100", "00100", "00000", "00100"],
1974
+ "%": ["11001", "11010", "00100", "01000", "10110", "00110", "00000"],
1975
+ $: ["00100", "11110", "10100", "11110", "00101", "11110", "00100"],
1976
+ "@": ["01110", "10001", "10111", "10101", "10111", "10000", "01111"],
1977
+ o: ["00000", "01110", "10001", "10001", "10001", "01110", "00000"],
1978
+ "*": ["00100", "10101", "01110", "11111", "01110", "10101", "00100"]
1979
+ };
1980
+ function sceneStaticTick(scene) {
1981
+ let hash = 0;
1982
+ for (let index = 0;index < scene.length; index += 1)
1983
+ hash = hash * 33 + scene.charCodeAt(index) >>> 0;
1984
+ return 11 + hash % 97;
1985
+ }
1986
+ function charColor(char, row, totalRows) {
1987
+ if (char === "@" || char === "$" || char === "o")
1988
+ return LIME;
1989
+ if (char === "%" || char === "!" || char === "/" || char === "\\" || char === "|")
1990
+ return CYAN;
1991
+ if (char === "." || char === "'" || char === "_" || row < 3 || row > totalRows - 4)
1992
+ return INK_DIM;
1993
+ if (char === "-" || char === "=" || char === "(" || char === ")")
1994
+ return LIME_DIM;
1995
+ return MAGENTA;
1996
+ }
1997
+ function fillRectAlpha(data, width, height, left, top, rectWidth, rectHeight, color, alpha) {
1998
+ const x0 = Math.max(0, Math.floor(left));
1999
+ const y0 = Math.max(0, Math.floor(top));
2000
+ const x1 = Math.min(width, Math.ceil(left + rectWidth));
2001
+ const y1 = Math.min(height, Math.ceil(top + rectHeight));
2002
+ for (let y = y0;y < y1; y += 1) {
2003
+ for (let x = x0;x < x1; x += 1)
2004
+ blendOver(data, (y * width + x) * 4, color, alpha);
2005
+ }
2006
+ }
2007
+ function drawAsciiGlyph(data, width, height, x, y, cellWidth, cellHeight, char, color, alpha) {
2008
+ const pattern = ASCII_GLYPHS[char] ?? ASCII_GLYPHS["*"];
2009
+ const glyphWidth = Math.max(1, cellWidth * 0.56);
2010
+ const glyphHeight = Math.max(1, cellHeight * 0.7);
2011
+ const originX = x + (cellWidth - glyphWidth) * 0.5;
2012
+ const originY = y + (cellHeight - glyphHeight) * 0.5;
2013
+ const pixelWidth = Math.max(1, glyphWidth / pattern[0].length);
2014
+ const pixelHeight = Math.max(1, glyphHeight / pattern.length);
2015
+ for (let row = 0;row < pattern.length; row += 1) {
2016
+ const bits = pattern[row];
2017
+ for (let col = 0;col < bits.length; col += 1) {
2018
+ if (bits[col] !== "1")
2019
+ continue;
2020
+ fillRectAlpha(data, width, height, originX + col * pixelWidth, originY + row * pixelHeight, pixelWidth * 0.72, pixelHeight * 0.72, color, alpha);
2021
+ }
2022
+ }
2023
+ }
2024
+ function drawStaticAsciiFleetBackground(data, width, height, scene, layout) {
2025
+ const rows = fleetRows(sceneStaticTick(scene), { animate: true });
2026
+ const cellWidth = width / Math.max(1, layout.width);
2027
+ const cellHeight = height / Math.max(1, layout.height);
2028
+ const leftCells = Math.floor((layout.width - FLEET_GRID_WIDTH) / 2);
2029
+ const topCells = Math.floor((layout.height - FLEET_GRID_HEIGHT) / 2);
2030
+ const fleetLeft = leftCells * cellWidth;
2031
+ const fleetTop = topCells * cellHeight;
2032
+ drawGlow2(data, width, height, width * 0.5, height * 0.5, Math.min(width, height) * 0.4, LIME, 0.03);
2033
+ drawGlow2(data, width, height, width * 0.18, height * 0.22, Math.min(width, height) * 0.28, CYAN, 0.022);
2034
+ drawGlow2(data, width, height, width * 0.82, height * 0.78, Math.min(width, height) * 0.22, MAGENTA, 0.014);
2035
+ for (let row = 0;row < rows.length; row += 1) {
2036
+ const line = rows[row];
2037
+ for (let col = 0;col < line.length; col += 1) {
2038
+ const char = line[col];
2039
+ if (char === " ")
2040
+ continue;
2041
+ const x = fleetLeft + col * cellWidth;
2042
+ const y = fleetTop + row * cellHeight;
2043
+ const color = charColor(char, row, rows.length);
2044
+ drawAsciiGlyph(data, width, height, x + cellWidth * 0.07, y + cellHeight * 0.05, cellWidth, cellHeight, char, color, 0.52);
2045
+ if (char === "@" || char === "$" || char === "%") {
2046
+ drawGlow2(data, width, height, x + cellWidth * 0.5, y + cellHeight * 0.5, Math.max(cellWidth, cellHeight) * 0.95, color, 0.02);
2047
+ }
2048
+ }
2049
+ }
2050
+ }
2051
+ function applyStaticAtmosphere(data, width, height) {
2052
+ const cx = width * 0.5;
2053
+ const cy = height * 0.42;
2054
+ const radius = Math.max(width, height) * 0.78;
2055
+ for (let y = 0;y < height; y += 1) {
2056
+ for (let x = 0;x < width; x += 1) {
2057
+ const distance = Math.hypot(x - cx, y - cy) / radius;
2058
+ const alpha = Math.max(0.1, Math.min(0.82, 0.12 + Math.pow(distance, 1.65) * 0.62));
2059
+ blendOver(data, (y * width + x) * 4, BG, alpha);
2060
+ }
2061
+ }
2062
+ drawGlow2(data, width, height, width * 0.82, height * 0.04, Math.min(width, height) * 0.2, LIME, 0.03);
2063
+ drawGlow2(data, width, height, width * 0.12, height * 0.18, Math.min(width, height) * 0.16, CYAN, 0.022);
2064
+ }
1018
2065
  function makeCrcTable() {
1019
2066
  const table = new Uint32Array(256);
1020
2067
  for (let n = 0;n < 256; n += 1) {
@@ -1085,9 +2132,6 @@ function kittyImageSequence(base64, options) {
1085
2132
  }
1086
2133
  return chunks.join("");
1087
2134
  }
1088
- function isModernImageTerminal(protocol) {
1089
- return protocol === "kitty";
1090
- }
1091
2135
  function clampByte(value) {
1092
2136
  return Math.max(0, Math.min(255, Math.round(value)));
1093
2137
  }
@@ -1099,23 +2143,6 @@ function blendOver(data, index, color, alphaScale = 1) {
1099
2143
  data[index + 2] = clampByte(color[2] * alpha + data[index + 2] * inv);
1100
2144
  data[index + 3] = 255;
1101
2145
  }
1102
- function drawDisc(data, width, height, cx, cy, radius, color, alpha = 1) {
1103
- const minX = Math.max(0, Math.floor(cx - radius));
1104
- const maxX = Math.min(width - 1, Math.ceil(cx + radius));
1105
- const minY = Math.max(0, Math.floor(cy - radius));
1106
- const maxY = Math.min(height - 1, Math.ceil(cy + radius));
1107
- for (let y = minY;y <= maxY; y += 1) {
1108
- for (let x = minX;x <= maxX; x += 1) {
1109
- const dx = x + 0.5 - cx;
1110
- const dy = y + 0.5 - cy;
1111
- const d = Math.sqrt(dx * dx + dy * dy);
1112
- if (d > radius)
1113
- continue;
1114
- const edge = Math.max(0, Math.min(1, radius - d));
1115
- blendOver(data, (y * width + x) * 4, color, alpha * Math.min(1, 0.35 + edge));
1116
- }
1117
- }
1118
- }
1119
2146
  function drawGlow2(data, width, height, cx, cy, radius, color, alpha = 0.18) {
1120
2147
  const minX = Math.max(0, Math.floor(cx - radius));
1121
2148
  const maxX = Math.min(width - 1, Math.ceil(cx + radius));
@@ -1133,30 +2160,6 @@ function drawGlow2(data, width, height, cx, cy, radius, color, alpha = 0.18) {
1133
2160
  }
1134
2161
  }
1135
2162
  }
1136
- function drawLine2(data, width, height, x0, y0, x1, y1, color, alpha = 0.5, thickness = 1.4) {
1137
- const dx = x1 - x0;
1138
- const dy = y1 - y0;
1139
- const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) * 1.2));
1140
- for (let i = 0;i <= steps; i += 1) {
1141
- const t2 = i / steps;
1142
- drawDisc(data, width, height, x0 + dx * t2, y0 + dy * t2, thickness, color, alpha);
1143
- }
1144
- }
1145
- function drawDrone(data, width, height, x, y, scale, tick, color) {
1146
- const wobble = Math.sin(tick * 0.055) * scale * 0.35;
1147
- const cy = y + wobble;
1148
- drawGlow2(data, width, height, x, cy, scale * 7, color, 0.18);
1149
- drawDisc(data, width, height, x, cy, scale * 1.45, color, 0.92);
1150
- drawDisc(data, width, height, x, cy, scale * 0.55, [235, 255, 176, 255], 0.85);
1151
- const arm = scale * 4.1;
1152
- const rotor = scale * 1.6;
1153
- const arms = [[-arm, -arm * 0.72], [arm, -arm * 0.72], [-arm, arm * 0.72], [arm, arm * 0.72]];
1154
- for (const [dx, dy] of arms) {
1155
- drawLine2(data, width, height, x, cy, x + dx, cy + dy, color, 0.42, Math.max(1, scale * 0.22));
1156
- drawGlow2(data, width, height, x + dx, cy + dy, rotor * 2.6, color, 0.1);
1157
- drawDisc(data, width, height, x + dx, cy + dy, rotor, color, 0.34 + 0.12 * Math.sin(tick * 0.15));
1158
- }
1159
- }
1160
2163
  function roundRectAlpha(px, py, x, y, w, h, r) {
1161
2164
  const rx = Math.max(x + r, Math.min(px, x + w - r));
1162
2165
  const ry = Math.max(y + r, Math.min(py, y + h - r));
@@ -1165,18 +2168,8 @@ function roundRectAlpha(px, py, x, y, w, h, r) {
1165
2168
  const dist = Math.sqrt(dx * dx + dy * dy);
1166
2169
  return Math.max(0, Math.min(1, r + 0.75 - dist));
1167
2170
  }
1168
- function panelRect2(layout, panel, size) {
1169
- const cellW = size.width / layout.width;
1170
- const cellH = size.height / layout.height;
1171
- const leftCells = layout.centerLeft + (panel.left ?? 0);
1172
- const topCells = layout.centerTop + panel.top;
1173
- const widthCells = panel.width ?? layout.centerWidth;
1174
- const heightCells = panel.height;
1175
- const left = Math.round(leftCells * cellW);
1176
- const top = Math.round(topCells * cellH);
1177
- const width = Math.round(widthCells * cellW);
1178
- const height = Math.round(heightCells * cellH);
1179
- return width > 8 && height > 8 ? { left, top, width, height } : null;
2171
+ function panelRect(layout, panel, size) {
2172
+ return panelPixelPlacement(layout, panel, size);
1180
2173
  }
1181
2174
  function downsampleRegion(data, canvasW, canvasH, rect, scale) {
1182
2175
  const w = Math.max(1, Math.ceil(rect.width / scale));
@@ -1325,7 +2318,8 @@ function getPanelMask(width, height, rect, headerPx, blurScale) {
1325
2318
  function compositePanel(data, width, height, rect, headerPx) {
1326
2319
  const blurScale = PANEL_BLUR_SCALE;
1327
2320
  const sampled = downsampleRegion(data, width, height, rect, blurScale);
1328
- const blurred = blurRgb(sampled.pixels, sampled.width, sampled.height, 4);
2321
+ const blurRadius = Math.max(8, Math.min(28, Math.round(Math.min(rect.width, rect.height) * 0.018)));
2322
+ const blurred = blurRgb(sampled.pixels, sampled.width, sampled.height, blurRadius);
1329
2323
  const mask = getPanelMask(width, height, rect, headerPx, blurScale);
1330
2324
  for (let i = 0;i < mask.indices.length; i += 1) {
1331
2325
  const idx = mask.indices[i];
@@ -1343,161 +2337,10 @@ function compositePanel(data, width, height, rect, headerPx) {
1343
2337
  blendOver(data, mask.dividerIndices[i], PANEL_LINE2, 0.28);
1344
2338
  }
1345
2339
  }
1346
- function visualSceneKind(scene) {
1347
- if (scene === "tasks")
1348
- return "loop";
1349
- if (scene === "help")
1350
- return "carrier";
1351
- return "dispatch";
1352
- }
1353
- function seededUnit(seed) {
1354
- let value = seed >>> 0;
1355
- value = value * 1664525 + 1013904223 >>> 0;
1356
- return value / 4294967295;
1357
- }
1358
- function sceneCore(width, height, kind) {
1359
- if (kind === "dispatch")
1360
- return { x: width * 0.3, y: height * 0.45 };
1361
- if (kind === "carrier")
1362
- return { x: width * 0.5, y: height * 0.54 };
1363
- return { x: width * 0.09, y: height * 0.56 };
1364
- }
1365
- function dispatchLane(width, index, count) {
1366
- const a = seededUnit(28672 + index * 101);
1367
- const b = seededUnit(36864 + index * 131);
1368
- const c = seededUnit(45056 + index * 151);
1369
- const ang = (-0.9 + 1.8 * (index / Math.max(1, count - 1))) * (Math.PI * 0.5);
1370
- return { ang, len: width * (0.32 + a * 0.42), curve: (b - 0.5) * 0.7, phase: c * Math.PI * 2 };
1371
- }
1372
- function lanePoint(width, height, lane, p, kind = "dispatch") {
1373
- const core = sceneCore(width, height, kind);
1374
- const dist = lane.len * p;
1375
- const bend = Math.sin(p * Math.PI) * lane.curve * Math.min(width, height) * 0.035;
1376
- const ux = Math.cos(lane.ang);
1377
- const uy = Math.sin(lane.ang);
1378
- return { x: core.x + ux * dist + -uy * bend, y: core.y + uy * dist + ux * bend };
1379
- }
1380
- function drawMarketingRails(data, width, height, kind, tick) {
1381
- const ys = kind === "loop" ? [0.18, 0.5, 0.82] : kind === "carrier" ? [0.24, 0.54, 0.82] : [0.18, 0.58, 0.82];
1382
- ys.forEach((ratio, index) => {
1383
- const y = height * ratio + Math.sin(tick * 0.018 + index) * Math.max(2, height * 0.0025);
1384
- const color = index === 2 ? CYAN : index === 1 ? LIME : LIME_DIM;
1385
- drawLine2(data, width, height, width * 0.06, y, width * 0.94, y + Math.sin(index) * height * 0.002, color, index === 1 ? 0.11 : 0.07, Math.max(0.8, height * 0.0012));
1386
- for (let p = (tick * 0.004 + index * 0.21) % 1;p < 1; p += 0.18 + index * 0.02) {
1387
- const x = width * (0.06 + 0.88 * p);
1388
- drawDisc(data, width, height, x, y, Math.max(1.2, height * (0.0017 + index * 0.0004)), color, 0.24);
1389
- drawGlow2(data, width, height, x, y, Math.max(5, height * 0.006), color, 0.035);
1390
- }
1391
- });
1392
- }
1393
- function drawDispatchScene(data, width, height, tick) {
1394
- const core = sceneCore(width, height, "dispatch");
1395
- const min = Math.min(width, height);
1396
- drawGlow2(data, width, height, core.x, core.y, min * 0.085, LIME, 0.12);
1397
- drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.85);
1398
- const laneCount = width < 1000 ? 7 : 12;
1399
- const lanes = Array.from({ length: laneCount }, (_, index) => dispatchLane(width, index, laneCount));
1400
- lanes.forEach((lane) => {
1401
- let prev = lanePoint(width, height, lane, 0);
1402
- for (let step = 1;step <= 32; step += 1) {
1403
- const next = lanePoint(width, height, lane, step / 32);
1404
- drawLine2(data, width, height, prev.x, prev.y, next.x, next.y, lane.phase > Math.PI ? CYAN : LIME_DIM, 0.045, Math.max(0.7, min * 0.0009));
1405
- prev = next;
1406
- }
1407
- });
1408
- const packets = width < 1000 ? 70 : 150;
1409
- for (let i = 0;i < packets; i += 1) {
1410
- const lane = lanes[i % lanes.length];
1411
- const speed = 0.003 + seededUnit(12288 + i * 17) * 0.009;
1412
- const p = (seededUnit(16384 + i * 19) + tick * speed) % 1;
1413
- const pt = lanePoint(width, height, lane, p);
1414
- const wobble = (seededUnit(20480 + i * 23) - 0.5) * min * 0.012;
1415
- const fade = p < 0.1 ? p / 0.1 : p > 0.88 ? (1 - p) / 0.12 : 1;
1416
- const color = seededUnit(24576 + i * 29) < 0.22 ? CYAN : LIME_DIM;
1417
- const y = pt.y + wobble * Math.sin(p * Math.PI * 2);
1418
- drawLine2(data, width, height, pt.x - min * 0.012, y - min * 0.002, pt.x, y, color, 0.08 * fade, Math.max(0.7, min * 0.0008));
1419
- drawDisc(data, width, height, pt.x, y, Math.max(1.3, min * 0.0022), color, 0.5 * fade);
1420
- drawGlow2(data, width, height, pt.x, y, Math.max(4, min * 0.008), color, 0.045 * fade);
1421
- }
1422
- for (let i = 0;i < (width < 1000 ? 4 : 8); i += 1) {
1423
- const lane = lanes[i % lanes.length];
1424
- const p = (tick * 0.002 + i * 0.13) % 1;
1425
- const pt = lanePoint(width, height, lane, p);
1426
- drawDrone(data, width, height, pt.x, pt.y + Math.sin(tick * 0.04 + i) * min * 0.004, min * (0.006 + i % 2 * 0.0015), tick + i * 11, i % 3 === 0 ? CYAN : LIME_DIM);
1427
- }
1428
- }
1429
- function drawLoopScene(data, width, height, tick) {
1430
- const min = Math.min(width, height);
1431
- const y = height * 0.56;
1432
- const x0 = width * 0.09;
1433
- const x1 = width * 0.91;
1434
- drawLine2(data, width, height, x0, y, x1, y, LIME_DIM, 0.12, Math.max(0.8, min * 0.001));
1435
- const gates = width < 1000 ? 4 : 5;
1436
- const active = Math.floor(tick / 16) % gates;
1437
- for (let i = 0;i < gates; i += 1) {
1438
- const x = x0 + (x1 - x0) * (i / Math.max(1, gates - 1));
1439
- const color = i === active ? LIME : i <= active ? LIME_DIM : INK_DIM;
1440
- drawLine2(data, width, height, x, y - min * 0.05, x, y + min * 0.05, color, i === active ? 0.25 : 0.08, Math.max(0.8, min * 0.001));
1441
- drawDisc(data, width, height, x, y, i === active ? min * 0.006 : min * 0.0035, color, i === active ? 0.72 : 0.25);
1442
- drawGlow2(data, width, height, x, y, i === active ? min * 0.018 : min * 0.01, color, i === active ? 0.1 : 0.035);
1443
- }
1444
- const packets = width < 1000 ? 36 : 84;
1445
- for (let i = 0;i < packets; i += 1) {
1446
- const p = (seededUnit(33024 + i * 13) + tick * (0.0022 + seededUnit(33280 + i * 17) * 0.004)) % 1;
1447
- const x = x0 + (x1 - x0) * p;
1448
- const segment = Math.min(gates - 1, Math.floor(p * gates));
1449
- const color = segment < active ? LIME_DIM : segment === active ? CYAN : INK_DIM;
1450
- const py = y + (seededUnit(33536 + i * 19) - 0.5) * min * 0.018 * Math.sin(p * Math.PI * 2);
1451
- drawLine2(data, width, height, x - min * 0.014, py, x, py, color, 0.1, Math.max(0.7, min * 0.0008));
1452
- drawDisc(data, width, height, x, py, Math.max(1.2, min * 0.002), color, color === INK_DIM ? 0.22 : 0.58);
1453
- }
1454
- drawDrone(data, width, height, x0 + (x1 - x0) * (tick * 0.003 % 1), y - min * 0.04, min * 0.0065, tick, LIME_DIM);
1455
- drawDrone(data, width, height, x0 + (x1 - x0) * ((tick * 0.002 + 0.5) % 1), y + min * 0.036, min * 0.0055, tick + 40, MAGENTA);
1456
- }
1457
- function drawCarrierScene(data, width, height, tick) {
1458
- const min = Math.min(width, height);
1459
- const core = sceneCore(width, height, "carrier");
1460
- drawGlow2(data, width, height, core.x, core.y, min * 0.075, LIME, 0.12);
1461
- drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.92);
1462
- const nodes = width < 1000 ? 5 : 7;
1463
- const rx = width * 0.36;
1464
- const ry = height * 0.22;
1465
- for (let i = 0;i < nodes; i += 1) {
1466
- const a = i / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
1467
- const x = core.x + Math.cos(a) * rx;
1468
- const y = core.y + Math.sin(a) * ry;
1469
- const color = i % 2 ? CYAN : LIME_DIM;
1470
- drawLine2(data, width, height, core.x, core.y, x, y, color, 0.055, Math.max(0.7, min * 0.0008));
1471
- drawGlow2(data, width, height, x, y, min * 0.018, color, 0.055);
1472
- drawDisc(data, width, height, x, y, min * 0.0048, color, 0.62);
1473
- drawDrone(data, width, height, x, y - min * 0.018, min * 0.005, tick + i * 9, color);
1474
- }
1475
- const packets = width < 1000 ? 30 : 72;
1476
- for (let i = 0;i < packets; i += 1) {
1477
- const lane = i % nodes;
1478
- const a = lane / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
1479
- const nx = core.x + Math.cos(a) * rx;
1480
- const ny = core.y + Math.sin(a) * ry;
1481
- const p = (seededUnit(37120 + i * 37) + tick * (0.003 + seededUnit(37376 + i * 43) * 0.009)) % 1;
1482
- const x = core.x + (nx - core.x) * p;
1483
- const y = core.y + (ny - core.y) * p;
1484
- const color = seededUnit(37632 + i * 47) < 0.35 ? CYAN : LIME_DIM;
1485
- drawDisc(data, width, height, x, y, Math.max(1.2, min * 0.002), color, 0.48);
1486
- drawGlow2(data, width, height, x, y, min * 0.008, color, 0.035);
1487
- }
1488
- }
1489
- function drawBackground(data, width, height, tick, scene) {
2340
+ function drawBackground(data, width, height, _tick, scene, layout) {
1490
2341
  fillRgba(data, BG, BG_WORD);
1491
- const kind = visualSceneKind(scene);
1492
- drawGlow2(data, width, height, width * 0.82, height * 0.04, Math.min(width, height) * 0.22, LIME, 0.035);
1493
- drawGlow2(data, width, height, width * 0.12, height * 0.18, Math.min(width, height) * 0.17, CYAN, 0.025);
1494
- drawMarketingRails(data, width, height, kind, tick);
1495
- if (kind === "loop")
1496
- drawLoopScene(data, width, height, tick);
1497
- else if (kind === "carrier")
1498
- drawCarrierScene(data, width, height, tick);
1499
- else
1500
- drawDispatchScene(data, width, height, tick);
2342
+ drawStaticAsciiFleetBackground(data, width, height, scene, layout);
2343
+ applyStaticAtmosphere(data, width, height);
1501
2344
  }
1502
2345
  function imageVisualFrameKey(input) {
1503
2346
  const width = Math.max(1, Math.floor(input.pixelSize.width));
@@ -1522,18 +2365,80 @@ function encodeKittyFrame(width, height, data, input) {
1522
2365
  compressed: true
1523
2366
  });
1524
2367
  }
2368
+ function cropRgba(data, sourceWidth, rect) {
2369
+ const out = new Uint8ClampedArray(rect.width * rect.height * 4);
2370
+ for (let y = 0;y < rect.height; y += 1) {
2371
+ const sourceStart = ((rect.top + y) * sourceWidth + rect.left) * 4;
2372
+ out.set(data.subarray(sourceStart, sourceStart + rect.width * 4), y * rect.width * 4);
2373
+ }
2374
+ return out;
2375
+ }
2376
+ function encodePanelPatch(data, sourceWidth, placement, imageId, zIndex) {
2377
+ const patch = cropRgba(data, sourceWidth, placement);
2378
+ const png = encodePng(placement.width, placement.height, patch);
2379
+ const sequence = kittyImageSequence(png.toString("base64"), {
2380
+ columns: Math.max(1, Math.round(placement.widthCells)),
2381
+ rows: Math.max(1, Math.round(placement.heightCells)),
2382
+ imageId,
2383
+ zIndex,
2384
+ format: 100
2385
+ });
2386
+ return `\x1B[${Math.max(1, Math.round(placement.topCells) + 1)};${Math.max(1, Math.round(placement.leftCells) + 1)}H${sequence}`;
2387
+ }
1525
2388
  function renderImageVisualFrame(input) {
1526
2389
  const width = Math.max(1, Math.floor(input.pixelSize.width));
1527
2390
  const height = Math.max(1, Math.floor(input.pixelSize.height));
1528
2391
  const byteCount = width * height * 4;
1529
2392
  if (byteCount > MAX_IMAGE_BYTES)
1530
2393
  return null;
2394
+ const layer = input.layer ?? "full";
1531
2395
  const data = new Uint8ClampedArray(byteCount);
1532
- drawBackground(data, width, height, input.tick, input.scene);
2396
+ drawBackground(data, width, height, 0, input.scene, input.layout);
2397
+ if (layer === "split") {
2398
+ const sequences = [encodeKittyFrame(width, height, data, input)];
2399
+ let panelIndex = 0;
2400
+ for (const panel of input.panels) {
2401
+ if (panel.chrome !== "ad-terminal")
2402
+ continue;
2403
+ const placement = panelPixelPlacement(input.layout, panel, { width, height });
2404
+ if (!placement)
2405
+ continue;
2406
+ const cellH = height / input.layout.height;
2407
+ const headerPx = Math.max(cellH * 3, (panel.headerHeight ?? 3) * cellH);
2408
+ compositePanel(data, width, height, placement, headerPx);
2409
+ const imageId = input.imageIds?.[panelIndex] ?? (input.imageId + 1000 + panelIndex >>> 0 || panelIndex + 1);
2410
+ sequences.push(encodePanelPatch(data, width, placement, imageId, -12));
2411
+ panelIndex += 1;
2412
+ }
2413
+ return { key: imageVisualFrameKey(input), sequence: sequences.join("") };
2414
+ }
2415
+ if (layer === "panel-chrome")
2416
+ return { key: imageVisualFrameKey(input), sequence: "" };
2417
+ if (layer === "background") {
2418
+ return { key: imageVisualFrameKey(input), sequence: encodeKittyFrame(width, height, data, input) };
2419
+ }
2420
+ if (layer === "panel-patches") {
2421
+ const sequences = [];
2422
+ let panelIndex = 0;
2423
+ for (const panel of input.panels) {
2424
+ if (panel.chrome !== "ad-terminal")
2425
+ continue;
2426
+ const placement = panelPixelPlacement(input.layout, panel, { width, height });
2427
+ if (!placement)
2428
+ continue;
2429
+ const cellH = height / input.layout.height;
2430
+ const headerPx = Math.max(cellH * 3, (panel.headerHeight ?? 3) * cellH);
2431
+ compositePanel(data, width, height, placement, headerPx);
2432
+ const imageId = input.imageIds?.[panelIndex] ?? (input.imageId + 1000 + panelIndex >>> 0 || panelIndex + 1);
2433
+ sequences.push(encodePanelPatch(data, width, placement, imageId, input.zIndex));
2434
+ panelIndex += 1;
2435
+ }
2436
+ return { key: imageVisualFrameKey(input), sequence: sequences.join("") };
2437
+ }
1533
2438
  for (const panel of input.panels) {
1534
2439
  if (panel.chrome !== "ad-terminal")
1535
2440
  continue;
1536
- const rect = panelRect2(input.layout, panel, { width, height });
2441
+ const rect = panelRect(input.layout, panel, { width, height });
1537
2442
  if (!rect)
1538
2443
  continue;
1539
2444
  const cellH = height / input.layout.height;
@@ -1542,8 +2447,11 @@ function renderImageVisualFrame(input) {
1542
2447
  }
1543
2448
  return { key: imageVisualFrameKey(input), sequence: encodeKittyFrame(width, height, data, input) };
1544
2449
  }
2450
+ function shouldRenderImagesInline() {
2451
+ return process.env.RIG_OPENTUI_IMAGE_WORKER === "0" || import.meta.url.includes("$bunfs");
2452
+ }
1545
2453
  function createImageWorker() {
1546
- if (process.env.RIG_OPENTUI_IMAGE_WORKER === "0")
2454
+ if (shouldRenderImagesInline())
1547
2455
  return null;
1548
2456
  const isTypeScriptRuntime = import.meta.url.endsWith(".ts");
1549
2457
  const workerUrl = new URL(isTypeScriptRuntime ? "./image-visual-layer-worker.ts" : "./image-visual-layer-worker.js", import.meta.url);
@@ -1559,24 +2467,8 @@ function createImageWorker() {
1559
2467
  }
1560
2468
  }
1561
2469
  var RECENT_WRITTEN_LIMIT = 24;
1562
- var DEFAULT_PANEL_FPS = 24;
1563
- var DEFAULT_PANEL_LAYER_SCALE = 0.22;
1564
2470
  var DEFAULT_IMAGE_OUTPUT_BACKLOG_BYTES = 256 * 1024;
1565
2471
  var DEFAULT_IMAGE_OUTPUT_BYTES_PER_SECOND = 850 * 1024;
1566
- var MIN_BACKGROUND_BUDGET_BYTES = 24 * 1024;
1567
- var MIN_PANEL_BUDGET_BYTES = 56 * 1024;
1568
- function panelFps() {
1569
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_FPS ?? "");
1570
- if (Number.isFinite(raw) && raw > 0)
1571
- return Math.max(4, Math.min(60, raw));
1572
- return DEFAULT_PANEL_FPS;
1573
- }
1574
- function panelLayerScale() {
1575
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_SCALE ?? "");
1576
- if (Number.isFinite(raw) && raw > 0)
1577
- return Math.max(0.18, Math.min(1, raw));
1578
- return DEFAULT_PANEL_LAYER_SCALE;
1579
- }
1580
2472
  function imageOutputBacklogBytes() {
1581
2473
  const raw = Number.parseInt(process.env.RIG_OPENTUI_IMAGE_BACKLOG_BYTES ?? "", 10);
1582
2474
  if (Number.isFinite(raw) && raw > 0)
@@ -1598,36 +2490,30 @@ class ImageVisualLayer {
1598
2490
  panelImageIds = Array.from({ length: 8 }, () => allocateImageId());
1599
2491
  panelChromeImageIds = Array.from({ length: 8 }, () => allocateImageId());
1600
2492
  stdout;
2493
+ onFrameReady;
1601
2494
  protocol;
1602
2495
  transport = imageTransport();
1603
- frameIntervalMs = 1000 / imageFps();
1604
- panelIntervalMs = 1000 / panelFps();
1605
2496
  maxOutputBacklogBytes = imageOutputBacklogBytes();
1606
2497
  outputBytesPerSecond = imageOutputBytesPerSecond();
1607
2498
  outputBudgetBytes = imageOutputBytesPerSecond();
1608
2499
  lastBudgetAtMs = Date.now();
1609
2500
  motionSlot = this.createSlot();
1610
- panelSlot = this.createSlot();
1611
- chromeSlot = this.createSlot();
2501
+ workersStarted = false;
1612
2502
  pendingFrames = new Map;
1613
2503
  recentWrittenKeys = [];
1614
2504
  imagesVisible = false;
2505
+ flushPaused = false;
1615
2506
  requestId = 0;
1616
- lastMotionFrameIndex = -1;
1617
- lastPanelFrameIndex = -1;
1618
- lastChromeKey = "";
2507
+ lastBackgroundKey = "";
2508
+ fatalWorkerError = null;
1619
2509
  destroyed = false;
1620
- constructor(stdout = process.stdout) {
2510
+ constructor(stdout = process.stdout, onFrameReady) {
1621
2511
  this.stdout = stdout;
1622
- this.protocol = getCapabilities().images;
1623
- if (this.enabled) {
1624
- this.startWorker(this.motionSlot);
1625
- this.startWorker(this.panelSlot);
1626
- this.startWorker(this.chromeSlot);
1627
- }
2512
+ this.onFrameReady = onFrameReady;
2513
+ this.protocol = getRigTerminalCapabilities().imageProtocol;
1628
2514
  }
1629
2515
  get enabled() {
1630
- return isModernImageTerminal(this.protocol);
2516
+ return this.protocol === "kitty";
1631
2517
  }
1632
2518
  clear() {
1633
2519
  if (this.enabled) {
@@ -1637,16 +2523,10 @@ class ImageVisualLayer {
1637
2523
  this.pendingFrames.clear();
1638
2524
  this.recentWrittenKeys.length = 0;
1639
2525
  this.resetSlot(this.motionSlot);
1640
- this.resetSlot(this.panelSlot);
1641
- this.resetSlot(this.chromeSlot);
1642
- this.lastMotionFrameIndex = -1;
1643
- this.lastPanelFrameIndex = -1;
1644
- this.lastChromeKey = "";
2526
+ this.lastBackgroundKey = "";
1645
2527
  this.outputBudgetBytes = this.outputBytesPerSecond;
1646
2528
  this.lastBudgetAtMs = Date.now();
1647
2529
  this.motionSlot.clearedThroughRequestId = this.requestId;
1648
- this.panelSlot.clearedThroughRequestId = this.requestId;
1649
- this.chromeSlot.clearedThroughRequestId = this.requestId;
1650
2530
  }
1651
2531
  }
1652
2532
  destroy() {
@@ -1654,19 +2534,31 @@ class ImageVisualLayer {
1654
2534
  return;
1655
2535
  this.clear();
1656
2536
  this.destroyed = true;
1657
- const workers = [this.motionSlot.worker, this.panelSlot.worker, this.chromeSlot.worker];
2537
+ const workers = [this.motionSlot.worker];
1658
2538
  this.motionSlot.worker = null;
1659
- this.panelSlot.worker = null;
1660
- this.chromeSlot.worker = null;
1661
2539
  for (const worker of workers)
1662
2540
  if (worker)
1663
2541
  worker.terminate().catch(() => {
1664
2542
  return;
1665
2543
  });
1666
2544
  }
2545
+ setFlushPaused(paused) {
2546
+ this.flushPaused = paused;
2547
+ }
2548
+ isReady(input) {
2549
+ if (!this.enabled || this.destroyed)
2550
+ return true;
2551
+ this.throwIfFatal();
2552
+ const splitKey = imageVisualFrameKey({ ...input, tick: 0, layer: "split" });
2553
+ return this.recentWrittenKeys.includes(splitKey) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === splitKey);
2554
+ }
1667
2555
  render(input) {
1668
2556
  if (!this.enabled || this.destroyed)
1669
2557
  return;
2558
+ this.throwIfFatal();
2559
+ if (this.transport !== "png")
2560
+ this.failWorker("renderer", new Error("OpenTUI image visuals require PNG transport for static-background rendering."));
2561
+ this.ensureWorkersStarted();
1670
2562
  const width = Math.max(1, Math.floor(input.pixelSize.width));
1671
2563
  const height = Math.max(1, Math.floor(input.pixelSize.height));
1672
2564
  if (width * height * 4 > MAX_IMAGE_BYTES)
@@ -1674,54 +2566,32 @@ class ImageVisualLayer {
1674
2566
  if (this.isOutputBackedUp())
1675
2567
  return;
1676
2568
  this.refillOutputBudget();
1677
- const now = Date.now();
1678
- const visualTick = Math.floor(now / (1000 / 60));
1679
- const useDynamicSplit = Boolean(this.motionSlot.worker && this.panelSlot.worker && this.chromeSlot.worker && this.transport === "png" && process.env.RIG_OPENTUI_DYNAMIC_SPLIT !== "0");
1680
- if (!useDynamicSplit) {
1681
- const frameIndex = Math.floor(now / this.frameIntervalMs);
1682
- if (!input.force && frameIndex === this.lastMotionFrameIndex)
1683
- return;
1684
- this.scheduleRender(this.motionSlot, { ...input, tick: visualTick, layer: "full" }, this.imageId, -10, input.force);
1685
- this.lastMotionFrameIndex = frameIndex;
1686
- return;
1687
- }
1688
- const motionFrameIndex = Math.floor(now / this.frameIntervalMs);
1689
- if ((input.force || motionFrameIndex !== this.lastMotionFrameIndex) && this.outputBudgetBytes >= MIN_BACKGROUND_BUDGET_BYTES) {
1690
- this.scheduleRender(this.motionSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, droneLayerScale(), input.layout), panels: [], tick: visualTick, layer: "background" }, this.imageId, -30, input.force);
1691
- this.lastMotionFrameIndex = motionFrameIndex;
1692
- }
1693
- const panelFrameIndex = Math.floor(now / this.panelIntervalMs);
1694
- if ((input.force || panelFrameIndex !== this.lastPanelFrameIndex) && this.outputBudgetBytes >= MIN_PANEL_BUDGET_BYTES) {
1695
- this.scheduleRender(this.panelSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, panelLayerScale(), input.layout), tick: visualTick, layer: "panel-patches" }, this.imageId, -12, input.force, this.panelImageIds);
1696
- this.lastPanelFrameIndex = panelFrameIndex;
1697
- }
1698
- const chromeInput = { ...input, tick: 0, layer: "panel-chrome" };
1699
- const chromeKey = imageVisualFrameKey(chromeInput);
1700
- if (input.force || chromeKey !== this.lastChromeKey) {
1701
- this.scheduleRender(this.chromeSlot, chromeInput, this.imageId, -8, input.force, this.panelChromeImageIds);
1702
- this.lastChromeKey = chromeKey;
2569
+ const splitInput = { ...input, tick: 0, layer: "split" };
2570
+ const splitKey = imageVisualFrameKey(splitInput);
2571
+ if (input.force || splitKey !== this.lastBackgroundKey) {
2572
+ this.scheduleRender(this.motionSlot, splitInput, this.imageId, -30, input.force, this.panelImageIds);
2573
+ this.lastBackgroundKey = splitKey;
1703
2574
  }
1704
2575
  }
1705
2576
  flush() {
2577
+ this.throwIfFatal();
2578
+ if (this.flushPaused)
2579
+ return false;
1706
2580
  if (this.pendingFrames.size === 0)
1707
2581
  return false;
1708
2582
  this.refillOutputBudget();
1709
2583
  if (this.isOutputBackedUp()) {
1710
- const chrome = this.pendingFrames.get("panel-chrome");
2584
+ const retainedSplit = this.pendingFrames.get("split");
1711
2585
  this.pendingFrames.clear();
1712
- if (chrome)
1713
- this.pendingFrames.set("panel-chrome", chrome);
2586
+ if (retainedSplit)
2587
+ this.pendingFrames.set("split", retainedSplit);
1714
2588
  return false;
1715
2589
  }
1716
2590
  const framePriority = (frame) => {
1717
2591
  const layer = pendingLayerKey(frame.key);
1718
- if (layer === "panel-chrome")
1719
- return 0;
1720
- if (layer === "background")
2592
+ if (layer === "split")
1721
2593
  return 1;
1722
- if (layer === "panel-patches")
1723
- return 2;
1724
- return 2;
2594
+ return 4;
1725
2595
  };
1726
2596
  const frames = Array.from(this.pendingFrames.values()).sort((a, b) => framePriority(a) - framePriority(b));
1727
2597
  const prefix = "\x1B[?25l\x1B7\x1B[s\x1B[H";
@@ -1733,12 +2603,14 @@ class ImageVisualLayer {
1733
2603
  for (const frame of frames) {
1734
2604
  const candidate = sequence + frame.sequence;
1735
2605
  const candidateBytes = Buffer.byteLength(prefix + candidate + suffix);
1736
- if (candidateBytes <= this.outputBudgetBytes) {
2606
+ const layer = pendingLayerKey(frame.key);
2607
+ const allowOneStaticFrame = selected.length === 0 && layer === "split";
2608
+ if (candidateBytes <= this.outputBudgetBytes || allowOneStaticFrame) {
1737
2609
  selected.push(frame);
1738
2610
  sequence = candidate;
1739
2611
  byteLength = candidateBytes;
1740
- } else if (pendingLayerKey(frame.key) === "panel-chrome") {
1741
- retained.set(pendingLayerKey(frame.key), frame);
2612
+ } else if (layer === "split") {
2613
+ retained.set(layer, frame);
1742
2614
  }
1743
2615
  }
1744
2616
  this.pendingFrames.clear();
@@ -1757,7 +2629,7 @@ class ImageVisualLayer {
1757
2629
  return accepted;
1758
2630
  }
1759
2631
  createSlot() {
1760
- return { worker: null, busy: false, queuedRequest: null, inFlightKey: "", queuedKey: "", clearedThroughRequestId: 0 };
2632
+ return { worker: null, busy: false, queuedRequest: null, inFlightKey: "", queuedKey: "", clearedThroughRequestId: 0, latestRequestId: 0 };
1761
2633
  }
1762
2634
  resetSlot(slot) {
1763
2635
  slot.busy = false;
@@ -1785,7 +2657,23 @@ class ImageVisualLayer {
1785
2657
  isKnownKey(slot, key) {
1786
2658
  return this.recentWrittenKeys.includes(key) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === key) || key === slot.inFlightKey || key === slot.queuedKey;
1787
2659
  }
2660
+ ensureWorkersStarted() {
2661
+ if (this.workersStarted)
2662
+ return;
2663
+ this.workersStarted = true;
2664
+ this.startWorker(this.motionSlot, "static-visuals");
2665
+ }
2666
+ failWorker(name, error) {
2667
+ const detail = error instanceof Error ? error.message : String(error);
2668
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} failed: ${detail}`);
2669
+ throw this.fatalWorkerError;
2670
+ }
2671
+ throwIfFatal() {
2672
+ if (this.fatalWorkerError)
2673
+ throw this.fatalWorkerError;
2674
+ }
1788
2675
  scheduleRender(slot, input, imageId, zIndex, force, imageIds) {
2676
+ this.throwIfFatal();
1789
2677
  const key = imageVisualFrameKey(input);
1790
2678
  if (!force && this.isKnownKey(slot, key))
1791
2679
  return;
@@ -1798,53 +2686,59 @@ class ImageVisualLayer {
1798
2686
  requestId: ++this.requestId,
1799
2687
  key
1800
2688
  };
1801
- if (slot.worker) {
1802
- this.enqueueWorkerRender(slot, request);
2689
+ slot.latestRequestId = request.requestId;
2690
+ if (!slot.worker) {
2691
+ this.renderInline(slot, request);
1803
2692
  return;
1804
2693
  }
1805
- this.renderSync(slot, request);
2694
+ this.enqueueWorkerRender(slot, request);
1806
2695
  }
1807
- startWorker(slot) {
2696
+ startWorker(slot, name) {
1808
2697
  const worker = createImageWorker();
1809
2698
  if (!worker)
1810
2699
  return;
1811
2700
  slot.worker = worker;
1812
2701
  worker.on("message", (message) => this.handleWorkerMessage(slot, message));
1813
- worker.once("error", () => this.disableWorker(slot));
1814
- worker.once("exit", () => this.disableWorker(slot));
2702
+ worker.once("error", (error) => {
2703
+ if (!this.destroyed)
2704
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} failed: ${error instanceof Error ? error.message : String(error)}`);
2705
+ });
2706
+ worker.once("exit", (code) => {
2707
+ if (!this.destroyed)
2708
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} exited unexpectedly with code ${code}.`);
2709
+ });
1815
2710
  }
1816
- disableWorker(slot) {
1817
- if (this.destroyed)
1818
- return;
1819
- slot.worker = null;
1820
- slot.busy = false;
1821
- slot.inFlightKey = "";
1822
- const queued = slot.queuedRequest;
1823
- slot.queuedRequest = null;
1824
- slot.queuedKey = "";
1825
- if (queued?.layer === "full")
1826
- this.renderSync(slot, queued);
2711
+ renderInline(slot, request) {
2712
+ try {
2713
+ const result = renderImageVisualFrame(request);
2714
+ if (result?.sequence && !this.isKnownKey(slot, result.key) && (!this.isOutputBackedUp() || pendingLayerKey(result.key) === "split")) {
2715
+ this.stageFrame({ key: result.key, sequence: result.sequence });
2716
+ this.onFrameReady?.();
2717
+ }
2718
+ } catch (error) {
2719
+ this.failWorker(String(request.layer ?? "visual"), error);
2720
+ }
1827
2721
  }
1828
2722
  enqueueWorkerRender(slot, request) {
1829
- if (!slot.worker) {
1830
- this.renderSync(slot, request);
1831
- return;
1832
- }
2723
+ this.throwIfFatal();
1833
2724
  if (slot.busy) {
1834
2725
  slot.queuedRequest = request;
1835
2726
  slot.queuedKey = request.key;
1836
2727
  return;
1837
2728
  }
1838
2729
  this.dispatchWorkerRender(slot, request);
1839
- }
1840
- dispatchWorkerRender(slot, request) {
1841
- if (!slot.worker) {
1842
- this.renderSync(slot, request);
1843
- return;
1844
- }
2730
+ }
2731
+ dispatchWorkerRender(slot, request) {
2732
+ this.throwIfFatal();
2733
+ if (!slot.worker)
2734
+ this.failWorker(String(request.layer ?? "visual"), new Error("worker is unavailable"));
1845
2735
  slot.busy = true;
1846
2736
  slot.inFlightKey = request.key;
1847
- slot.worker.postMessage(request);
2737
+ try {
2738
+ slot.worker.postMessage(request);
2739
+ } catch (error) {
2740
+ this.failWorker(String(request.layer ?? "visual"), error);
2741
+ }
1848
2742
  }
1849
2743
  handleWorkerMessage(slot, message) {
1850
2744
  if (this.destroyed || !message || typeof message !== "object")
@@ -1852,14 +2746,14 @@ class ImageVisualLayer {
1852
2746
  const response = message;
1853
2747
  slot.busy = false;
1854
2748
  slot.inFlightKey = "";
1855
- if (response.requestId <= slot.clearedThroughRequestId)
1856
- return;
1857
- if (response.error) {
1858
- this.disableWorker(slot);
1859
- return;
1860
- }
1861
- if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "panel-chrome")) {
1862
- this.stageFrame({ key: response.key, sequence: response.sequence });
2749
+ const isStale = response.requestId <= slot.clearedThroughRequestId || response.requestId < slot.latestRequestId;
2750
+ if (!isStale) {
2751
+ if (response.error)
2752
+ this.failWorker(pendingLayerKey(response.key), new Error(response.error));
2753
+ if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "split")) {
2754
+ this.stageFrame({ key: response.key, sequence: response.sequence });
2755
+ this.onFrameReady?.();
2756
+ }
1863
2757
  }
1864
2758
  const next = slot.queuedRequest;
1865
2759
  slot.queuedRequest = null;
@@ -1867,12 +2761,6 @@ class ImageVisualLayer {
1867
2761
  if (next && !this.isKnownKey(slot, next.key))
1868
2762
  this.dispatchWorkerRender(slot, next);
1869
2763
  }
1870
- renderSync(slot, request) {
1871
- const result = renderImageVisualFrame(request);
1872
- if (!result || this.isKnownKey(slot, result.key) || this.isOutputBackedUp())
1873
- return;
1874
- this.stageFrame(result);
1875
- }
1876
2764
  }
1877
2765
  function pixelSizeForRenderer(renderer) {
1878
2766
  const resolution = renderer.resolution;
@@ -1884,11 +2772,12 @@ function pixelSizeForRenderer(renderer) {
1884
2772
  };
1885
2773
  }
1886
2774
  function forceModernImageVisuals() {
1887
- return isModernImageTerminal(getCapabilities().images);
2775
+ return getRigTerminalCapabilities().supportsKittyImages;
1888
2776
  }
1889
2777
 
1890
2778
  // packages/cli/src/app-opentui/render/text.ts
1891
- import { TextAttributes as TextAttributes2, TextRenderable } from "@opentui/core";
2779
+ import { RGBA as RGBA3, TextAttributes as TextAttributes3, TextRenderable } from "@opentui/core";
2780
+ var TRANSPARENT2 = RGBA3.fromInts(0, 0, 0, 0);
1892
2781
  function selectableMouseHandler(currentLineRef, onLineMouseDown) {
1893
2782
  return (event) => {
1894
2783
  const currentLine = currentLineRef();
@@ -1939,7 +2828,8 @@ function applyTextLine(renderable, line, top, left, width) {
1939
2828
  renderable.width = width;
1940
2829
  renderable.content = line?.styledText ?? line?.text ?? "";
1941
2830
  renderable.fg = line?.fg ?? RIG_UI.ink2;
1942
- renderable.attributes = line?.bold ? TextAttributes2.BOLD : line?.dim ? TextAttributes2.DIM : 0;
2831
+ renderable.bg = line?.bg ?? TRANSPARENT2;
2832
+ renderable.attributes = line?.bold ? TextAttributes3.BOLD : line?.dim ? TextAttributes3.DIM : 0;
1943
2833
  }
1944
2834
  function applyFlowTextLine(renderable, line, width) {
1945
2835
  renderable.setRigSceneLine?.(line);
@@ -1948,7 +2838,8 @@ function applyFlowTextLine(renderable, line, width) {
1948
2838
  renderable.width = Math.max(1, width);
1949
2839
  renderable.content = line?.styledText ?? line?.text ?? "";
1950
2840
  renderable.fg = line?.fg ?? RIG_UI.ink2;
1951
- renderable.attributes = line?.bold ? TextAttributes2.BOLD : line?.dim ? TextAttributes2.DIM : 0;
2841
+ renderable.bg = line?.bg ?? TRANSPARENT2;
2842
+ renderable.attributes = line?.bold ? TextAttributes3.BOLD : line?.dim ? TextAttributes3.DIM : 0;
1952
2843
  }
1953
2844
  function clearTextLines(renderables, from = 0) {
1954
2845
  for (let index = from;index < renderables.length; index += 1) {
@@ -1958,26 +2849,27 @@ function clearTextLines(renderables, from = 0) {
1958
2849
  renderable.top = -1;
1959
2850
  renderable.left = 0;
1960
2851
  renderable.width = 1;
2852
+ renderable.bg = TRANSPARENT2;
1961
2853
  }
1962
2854
  }
1963
2855
 
1964
2856
  // packages/cli/src/app-opentui/render/panels.ts
1965
- import { RGBA as RGBA2, ScrollBoxRenderable } from "@opentui/core";
1966
- var TRANSPARENT2 = RGBA2.fromInts(0, 0, 0, 0);
2857
+ import { RGBA as RGBA4, ScrollBoxRenderable } from "@opentui/core";
2858
+ var TRANSPARENT3 = RGBA4.fromInts(0, 0, 0, 0);
1967
2859
  var MAX_PANEL_LINES = 420;
1968
2860
  function hexToRgba(hex, alpha) {
1969
2861
  const clean = hex.replace(/^#/, "");
1970
2862
  const full = clean.length === 3 ? clean.split("").map((part) => `${part}${part}`).join("") : clean;
1971
2863
  const value = Number.parseInt(full, 16);
1972
2864
  if (!Number.isFinite(value))
1973
- return RGBA2.fromInts(14, 15, 17, alpha);
1974
- return RGBA2.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255, alpha);
2865
+ return RGBA4.fromInts(14, 15, 17, alpha);
2866
+ return RGBA4.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255, alpha);
1975
2867
  }
1976
2868
  function panelBackground(panel) {
1977
2869
  return hexToRgba(panel.backgroundColor ?? RIG_UI.panel, panel.backgroundAlpha ?? 184);
1978
2870
  }
1979
2871
  function panelBorder(panel) {
1980
- return panel.borderColor ? hexToRgba(panel.borderColor, panel.borderAlpha ?? 54) : TRANSPARENT2;
2872
+ return panel.borderColor ? hexToRgba(panel.borderColor, panel.borderAlpha ?? 54) : TRANSPARENT3;
1981
2873
  }
1982
2874
  function createScrollPanelLayer(renderer, id, onLineMouseDown) {
1983
2875
  const panel = new ScrollBoxRenderable(renderer, {
@@ -1987,7 +2879,7 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
1987
2879
  top: -2,
1988
2880
  width: 1,
1989
2881
  height: 1,
1990
- backgroundColor: TRANSPARENT2,
2882
+ backgroundColor: TRANSPARENT3,
1991
2883
  border: false,
1992
2884
  shouldFill: true,
1993
2885
  opacity: 1,
@@ -2003,7 +2895,7 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
2003
2895
  visible: false,
2004
2896
  width: 1,
2005
2897
  trackOptions: {
2006
- backgroundColor: TRANSPARENT2,
2898
+ backgroundColor: TRANSPARENT3,
2007
2899
  foregroundColor: hexToRgba(RIG_UI.ink4, 84)
2008
2900
  }
2009
2901
  },
@@ -2011,18 +2903,24 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
2011
2903
  height: 0,
2012
2904
  visible: false,
2013
2905
  trackOptions: {
2014
- backgroundColor: TRANSPARENT2,
2015
- foregroundColor: TRANSPARENT2
2906
+ backgroundColor: TRANSPARENT3,
2907
+ foregroundColor: TRANSPARENT3
2016
2908
  }
2017
2909
  }
2018
2910
  });
2019
2911
  panel.__rigTextLines = [];
2020
- for (let index = 0;index < MAX_PANEL_LINES; index += 1) {
2021
- const line = createFlowTextLine(renderer, `${id}-line-${index}`, onLineMouseDown);
2912
+ panel.__rigRenderer = renderer;
2913
+ panel.__rigOnLineMouseDown = onLineMouseDown;
2914
+ return panel;
2915
+ }
2916
+ function ensurePanelLineCount(panel, desiredCount) {
2917
+ const target = Math.min(MAX_PANEL_LINES, Math.max(0, desiredCount));
2918
+ while (panel.__rigTextLines.length < target) {
2919
+ const index = panel.__rigTextLines.length;
2920
+ const line = createFlowTextLine(panel.__rigRenderer, `${panel.id}-line-${index}`, panel.__rigOnLineMouseDown);
2022
2921
  panel.__rigTextLines.push(line);
2023
2922
  panel.add(line);
2024
2923
  }
2025
- return panel;
2026
2924
  }
2027
2925
  function hidePanel(panel) {
2028
2926
  panel.visible = false;
@@ -2030,7 +2928,7 @@ function hidePanel(panel) {
2030
2928
  panel.left = 0;
2031
2929
  panel.width = 1;
2032
2930
  panel.height = 1;
2033
- panel.backgroundColor = TRANSPARENT2;
2931
+ panel.backgroundColor = TRANSPARENT3;
2034
2932
  panel.border = false;
2035
2933
  panel.__rigPanelId = undefined;
2036
2934
  panel.__rigTextLines.forEach((line) => applyFlowTextLine(line, undefined, 1));
@@ -2042,19 +2940,12 @@ function applyScrollPanels(panels, layout, scenePanels) {
2042
2940
  hidePanel(renderable);
2043
2941
  return;
2044
2942
  }
2045
- const cardLeft = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
2046
- const desiredWidth = panel.width ?? layout.centerWidth;
2047
- const cardWidth = Math.max(1, Math.min(layout.width - cardLeft, desiredWidth));
2048
- const cardTop = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
2049
- const cardHeight = Math.max(1, Math.min(layout.height - cardTop, panel.height));
2050
- const headerHeight = panel.chrome === "ad-terminal" ? Math.max(3, Math.min(cardHeight - 2, panel.headerHeight ?? 3)) : 0;
2051
- const left = cardLeft + (panel.chrome === "ad-terminal" ? 1 : 0);
2052
- const top = cardTop + headerHeight + (panel.chrome === "ad-terminal" ? 1 : 0);
2053
- const width = Math.max(1, Math.min(layout.width - left, cardWidth - (panel.chrome === "ad-terminal" ? 2 : 0)));
2054
- const height = Math.max(1, Math.min(layout.height - top, cardHeight - headerHeight - (panel.chrome === "ad-terminal" ? 2 : 0)));
2055
- const paddingX = panel.paddingX ?? (panel.chrome === "ad-terminal" ? 5 : 2);
2056
- const paddingY = panel.paddingY ?? (panel.chrome === "ad-terminal" ? 2 : 1);
2057
- const contentWidth = Math.max(1, width - paddingX * 2 - 1);
2943
+ const contentRect = panelContentRect(layout, panel);
2944
+ if (!contentRect) {
2945
+ hidePanel(renderable);
2946
+ return;
2947
+ }
2948
+ const { left, top, width, height, paddingX, paddingY, contentWidth } = contentRect;
2058
2949
  renderable.visible = true;
2059
2950
  renderable.left = left;
2060
2951
  renderable.top = top;
@@ -2062,7 +2953,7 @@ function applyScrollPanels(panels, layout, scenePanels) {
2062
2953
  renderable.height = height;
2063
2954
  renderable.zIndex = panel.zIndex ?? 6;
2064
2955
  renderable.opacity = panel.opacity ?? 1;
2065
- renderable.backgroundColor = panel.chrome === "ad-terminal" ? TRANSPARENT2 : panelBackground(panel);
2956
+ renderable.backgroundColor = panel.chrome === "ad-terminal" ? TRANSPARENT3 : panelBackground(panel);
2066
2957
  renderable.border = panel.chrome === "ad-terminal" ? false : panel.border ?? false;
2067
2958
  renderable.borderColor = panelBorder(panel);
2068
2959
  renderable.paddingX = paddingX;
@@ -2075,56 +2966,31 @@ function applyScrollPanels(panels, layout, scenePanels) {
2075
2966
  renderable.__rigPanelId = panel.id;
2076
2967
  renderable.scrollTo(0);
2077
2968
  }
2969
+ const lineCount = Math.min(panel.lines.length, MAX_PANEL_LINES);
2970
+ ensurePanelLineCount(renderable, lineCount);
2078
2971
  renderable.__rigTextLines.forEach((line, lineIndex) => {
2079
- applyFlowTextLine(line, panel.lines[lineIndex], contentWidth);
2972
+ applyFlowTextLine(line, lineIndex < lineCount ? panel.lines[lineIndex] : undefined, contentWidth);
2080
2973
  });
2081
2974
  });
2082
2975
  }
2083
2976
 
2084
2977
  // packages/cli/src/app-opentui/render/type-bar.ts
2085
- import { InputRenderable, InputRenderableEvents, TextRenderable as TextRenderable2 } from "@opentui/core";
2086
- function createTypeBar(renderer) {
2087
- const prefix = new TextRenderable2(renderer, {
2088
- id: "rig-typebar-prefix",
2089
- content: " rig \u203A",
2090
- position: "absolute",
2091
- left: 0,
2092
- top: 0,
2093
- fg: RIG_UI.lime,
2094
- selectable: true
2095
- });
2096
- const input = new InputRenderable(renderer, {
2097
- id: "rig-typebar-input",
2098
- position: "absolute",
2099
- left: 7,
2100
- top: 0,
2101
- width: 40,
2102
- placeholder: "fleet \xB7 tasks \xB7 run next \xB7 help",
2103
- textColor: RIG_UI.ink,
2104
- cursorColor: RIG_UI.lime,
2105
- cursorStyle: { style: "block", blinking: false },
2106
- showCursor: true,
2107
- selectable: true
2108
- });
2109
- const footer = new TextRenderable2(renderer, {
2110
- id: "rig-footer",
2111
- content: "",
2112
- position: "absolute",
2113
- left: 0,
2114
- top: 1,
2115
- width: "100%",
2116
- fg: RIG_UI.ink4,
2117
- selectable: true
2118
- });
2119
- input.focus();
2120
- return { input, prefix, footer };
2121
- }
2978
+ import { BoxRenderable, InputRenderable, InputRenderableEvents, RGBA as RGBA5, TextRenderable as TextRenderable2 } from "@opentui/core";
2979
+ var TYPEBAR_BG = RGBA5.fromInts(20, 25, 14, 224);
2980
+ var TYPEBAR_BORDER = RGBA5.fromInts(204, 255, 77, 92);
2122
2981
  function positionTypeBar(renderables, input, footer, width, typeBarTop, footerTop) {
2123
- renderables.prefix.content = " rig \u203A";
2982
+ renderables.background.visible = true;
2983
+ const cardTop = Math.max(0, typeBarTop - 1);
2984
+ renderables.background.left = 0;
2985
+ renderables.background.top = cardTop;
2986
+ renderables.background.width = width;
2987
+ renderables.background.height = Math.max(3, footerTop - cardTop + 1);
2988
+ renderables.prefix.content = " \u203A";
2989
+ renderables.prefix.left = 2;
2124
2990
  renderables.prefix.top = typeBarTop;
2125
2991
  renderables.input.top = typeBarTop;
2126
- renderables.input.left = 7;
2127
- renderables.input.width = Math.max(10, width - 9);
2992
+ renderables.input.left = 5;
2993
+ renderables.input.width = Math.max(10, width - 8);
2128
2994
  renderables.input.placeholder = input.placeholder;
2129
2995
  renderables.input.showCursor = true;
2130
2996
  renderables.footer.top = footerTop;
@@ -2134,6 +3000,8 @@ function positionTypeBar(renderables, input, footer, width, typeBarTop, footerTo
2134
3000
  renderables.input.focus();
2135
3001
  }
2136
3002
  function hideTypeBar(renderables, height) {
3003
+ renderables.background.visible = false;
3004
+ renderables.background.top = height + 1;
2137
3005
  renderables.prefix.content = "";
2138
3006
  renderables.prefix.top = height + 1;
2139
3007
  renderables.input.value = "";
@@ -2145,17 +3013,22 @@ function hideTypeBar(renderables, height) {
2145
3013
  renderables.footer.content = "";
2146
3014
  renderables.footer.top = height + 1;
2147
3015
  }
2148
- function bindTypeBar(renderables, handlers) {
2149
- renderables.input.on(InputRenderableEvents.INPUT, (value) => handlers.onInput(value));
2150
- renderables.input.on(InputRenderableEvents.ENTER, (value) => handlers.onEnter(value));
3016
+ function safeFooterMessage(message) {
3017
+ const trimmed = message?.trim();
3018
+ if (!trimmed)
3019
+ return null;
3020
+ if (/https?:|\b[a-z0-9-]+\.[a-z]{2,}\b|\brig\b/i.test(trimmed))
3021
+ return null;
3022
+ return trimmed;
2151
3023
  }
2152
3024
  function formatFooter(footer, width) {
3025
+ const safeMessage = safeFooterMessage(footer.message);
2153
3026
  const parts = [
2154
- footer.project ? `project ${footer.project}` : null,
2155
- footer.server ? `server ${footer.server}` : null,
2156
- footer.auth ? `auth ${footer.auth}` : null,
3027
+ footer.project ? "workspace" : null,
3028
+ footer.server ? "server selected" : null,
3029
+ footer.auth ? "auth ready" : null,
2157
3030
  footer.run ? `run ${footer.run}` : null,
2158
- footer.message ?? null
3031
+ safeMessage
2159
3032
  ].filter((part) => Boolean(part));
2160
3033
  return ` ${truncateText(parts.join(" \xB7 "), Math.max(8, width - 2))}`;
2161
3034
  }
@@ -2167,9 +3040,6 @@ function line(text, options = {}) {
2167
3040
  function blank() {
2168
3041
  return { text: "" };
2169
3042
  }
2170
- function center(text, fg = RIG_UI.ink2, bold = false) {
2171
- return { text, fg, bold, align: "center" };
2172
- }
2173
3043
  function deckRow(input) {
2174
3044
  const prefix = input.active ? "\u25B8" : " ";
2175
3045
  const label = input.label.toUpperCase().padEnd(10);
@@ -2180,6 +3050,9 @@ function deckRow(input) {
2180
3050
  activateOnClick: input.activateOnClick
2181
3051
  });
2182
3052
  }
3053
+ function kv(label, value, fg = RIG_UI.ink2) {
3054
+ return line(` ${label.toUpperCase().padEnd(10)} ${value}`, { fg });
3055
+ }
2183
3056
  function fitSceneLine(input, layout) {
2184
3057
  const fitted = truncateText(input.text, layout.centerWidth);
2185
3058
  return {
@@ -2194,9 +3067,52 @@ function makeSceneFrame(input) {
2194
3067
  return input;
2195
3068
  }
2196
3069
 
3070
+ // packages/cli/src/app-opentui/render/preloader.ts
3071
+ var PRELOADER_MIN_DURATION_MS = 520;
3072
+ var PRELOADER_MAX_DURATION_MS = 5000;
3073
+ function maybeStartPreloaderTransition(transition, scene, now = Date.now()) {
3074
+ if (transition.scene === scene)
3075
+ return;
3076
+ transition.scene = scene;
3077
+ transition.startedAtMs = now;
3078
+ transition.minUntilMs = now + PRELOADER_MIN_DURATION_MS;
3079
+ }
3080
+ function isPreloaderActive(transition, targetReady, now = Date.now()) {
3081
+ if (now < transition.minUntilMs)
3082
+ return true;
3083
+ if (!targetReady && now - transition.startedAtMs < PRELOADER_MAX_DURATION_MS)
3084
+ return true;
3085
+ return false;
3086
+ }
3087
+ function preloaderLine(line2, options = {}) {
3088
+ return { text: line2, bg: RIG_UI.bg, ...options };
3089
+ }
3090
+ function blockLine(text, leftPad, fg) {
3091
+ return preloaderLine(`${" ".repeat(Math.max(0, leftPad))}${text.padEnd(FLEET_GRID_WIDTH)}`, { fg });
3092
+ }
3093
+ function renderAsciiPreloaderFrame(scene, layout, tick) {
3094
+ const sprite = fleetRows(tick);
3095
+ const leftPad = Math.max(0, Math.floor((layout.width - FLEET_GRID_WIDTH) / 2));
3096
+ const blank2 = () => preloaderLine("");
3097
+ const body = [
3098
+ ...sprite.map((text, index) => blockLine(text, leftPad, index >= 7 && index <= 18 ? RIG_UI.lime : RIG_UI.limeDim))
3099
+ ];
3100
+ const topPadding = Math.max(0, Math.floor((layout.height - body.length) / 2));
3101
+ const bottomPadding = Math.max(0, layout.height - body.length - topPadding);
3102
+ return {
3103
+ scene,
3104
+ title: "Preloader",
3105
+ lines: [...Array.from({ length: topPadding }, blank2), ...body, ...Array.from({ length: bottomPadding }, blank2)],
3106
+ live: true,
3107
+ fullScreen: true,
3108
+ hideTypeBar: true,
3109
+ footer: { message: "" }
3110
+ };
3111
+ }
3112
+
2197
3113
  // packages/cli/src/app-opentui/drone.ts
2198
- import { RGBA as RGBA3, StyledText, TextAttributes as TextAttributes3 } from "@opentui/core";
2199
- var MINI_DRONE = [
3114
+ import { RGBA as RGBA6, StyledText as StyledText2, TextAttributes as TextAttributes4 } from "@opentui/core";
3115
+ var MINI_DRONE2 = [
2200
3116
  "(!!!) (!!!)",
2201
3117
  " \\%==%/ ",
2202
3118
  " %%?%% ",
@@ -2211,13 +3127,13 @@ var LEAD_MARK = [
2211
3127
  var BLADE_FRAMES = ["---", "\\\\\\", "|||", "///"];
2212
3128
  var EYE_FRAMES = ["o", "@", "\u2022", "."];
2213
3129
  var COLOR = {
2214
- body: RGBA3.fromHex(RIG_UI.lime),
2215
- mini: RGBA3.fromHex(RIG_UI.limeDim),
2216
- rotor: RGBA3.fromHex(RIG_UI.cyan),
2217
- path: RGBA3.fromHex(RIG_UI.cyan),
2218
- eye: RGBA3.fromHex(RIG_UI.ink),
2219
- dim: RGBA3.fromHex(RIG_UI.ink4),
2220
- ink: RGBA3.fromHex(RIG_UI.ink2)
3130
+ body: RGBA6.fromHex(RIG_UI.lime),
3131
+ mini: RGBA6.fromHex(RIG_UI.limeDim),
3132
+ rotor: RGBA6.fromHex(RIG_UI.cyan),
3133
+ path: RGBA6.fromHex(RIG_UI.cyan),
3134
+ eye: RGBA6.fromHex(RIG_UI.ink),
3135
+ dim: RGBA6.fromHex(RIG_UI.ink4),
3136
+ ink: RGBA6.fromHex(RIG_UI.ink2)
2221
3137
  };
2222
3138
  function bladeForTick(tick, phase = 0) {
2223
3139
  return BLADE_FRAMES[(Math.floor(tick / 3) + phase) % BLADE_FRAMES.length];
@@ -2227,12 +3143,12 @@ function eyeForTick(tick, phase = 0) {
2227
3143
  return pulse > 0.55 ? EYE_FRAMES[1] : pulse > 0.1 ? EYE_FRAMES[0] : pulse > -0.45 ? EYE_FRAMES[2] : EYE_FRAMES[3];
2228
3144
  }
2229
3145
  function chunk2(text, fg, bold = false, dim = false) {
2230
- let attributes = TextAttributes3.NONE;
3146
+ let attributes = TextAttributes4.NONE;
2231
3147
  if (bold)
2232
- attributes |= TextAttributes3.BOLD;
3148
+ attributes |= TextAttributes4.BOLD;
2233
3149
  if (dim)
2234
- attributes |= TextAttributes3.DIM;
2235
- return { __isChunk: true, text, fg, ...attributes !== TextAttributes3.NONE ? { attributes } : {} };
3150
+ attributes |= TextAttributes4.DIM;
3151
+ return { __isChunk: true, text, fg, ...attributes !== TextAttributes4.NONE ? { attributes } : {} };
2236
3152
  }
2237
3153
  function styledLine(text, colorFor) {
2238
3154
  const chunks = [];
@@ -2244,600 +3160,312 @@ function styledLine(text, colorFor) {
2244
3160
  chunks.push(chunk2(run, COLOR[runColor], runColor === "body" || runColor === "eye", runColor === "dim"));
2245
3161
  run = "";
2246
3162
  };
2247
- for (const char of text) {
2248
- const next = colorFor(char);
2249
- if (next !== runColor) {
2250
- flush();
2251
- runColor = next;
2252
- }
2253
- run += char;
2254
- }
2255
- flush();
2256
- return new StyledText(chunks.length > 0 ? chunks : [chunk2("", COLOR.ink)]);
2257
- }
2258
- function droneColor(char) {
2259
- if (char === "?" || char === "o" || char === "@" || char === "\u2022")
2260
- return "eye";
2261
- if (char === "%" || char === "=")
2262
- return "mini";
2263
- if (char === "$")
2264
- return "body";
2265
- if (char === "(" || char === ")" || char === "/" || char === "\\" || char === "|" || char === "-")
2266
- return "rotor";
2267
- if (char === "\xB7" || char === "\u2022" || char === "'" || char === ".")
2268
- return "path";
2269
- if (char.trim() === "")
2270
- return "dim";
2271
- return "ink";
2272
- }
2273
- function motionColor(char) {
2274
- if (char === "\u25CF" || char === "\u25C6" || char === "\u25C9")
2275
- return "body";
2276
- if (char === "\u2022" || char === "\xB7" || char === "\u2500" || char === "\u2502" || char === "\u2571" || char === "\u2572" || char === "\u256D" || char === "\u256E" || char === "\u2570" || char === "\u256F")
2277
- return "path";
2278
- if (char === "\u258C" || char === "\u2590" || char === "\u2581" || char === "\u2594")
2279
- return "mini";
2280
- return droneColor(char);
2281
- }
2282
- function materializeDrone(lines, tick, phase = 0) {
2283
- const blade = bladeForTick(tick, phase);
2284
- const eye = eyeForTick(tick, phase);
2285
- return lines.map((line2) => line2.replaceAll("!!!", blade).replaceAll("?", eye));
2286
- }
2287
- function brandFleetLines(tick, align = "center") {
2288
- const left = materializeDrone(MINI_DRONE, tick, 0);
2289
- const right = materializeDrone(MINI_DRONE, tick, 2);
2290
- const lead = materializeDrone(LEAD_MARK, tick, 1);
2291
- const packet = ["\xB7", "\u2022", "\u25CF", "\u2022"][Math.floor(tick / 2) % 4];
2292
- const bridge = [
2293
- ` ${left[0]} ${lead[0]} ${right[0]}`,
2294
- ` ${left[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${lead[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${right[1]}`,
2295
- ` ${left[2]} \xB7' '${lead[2]}' '\xB7 ${right[2]}`,
2296
- ` ${left[3]} \u2572 signal bus \u2571 ${right[3]}`,
2297
- ` ${left[4]} \xB7\u2500\u2500\u2500${packet}\u2500\u2500\u2500\u2500\u2500\u2500${packet}\u2500\u2500\u2500\xB7 ${right[4]}`
2298
- ];
2299
- return bridge.map((text) => ({
2300
- text,
2301
- styledText: styledLine(text, motionColor),
2302
- fg: RIG_UI.limeDim,
2303
- align
2304
- }));
2305
- }
2306
- function errorDrone(tick) {
2307
- return brandFleetLines(tick).map((line2, index) => ({
2308
- ...line2,
2309
- fg: index % 2 === 0 ? RIG_UI.red : line2.fg
2310
- }));
2311
- }
2312
-
2313
- // packages/cli/src/app-opentui/scenes/error.ts
2314
- function renderErrorScene(state) {
2315
- const message = state.error?.message ?? "Unknown Rig app error";
2316
- const hint = state.error?.hint ?? "type main, help, or retry after fixing the issue";
2317
- return makeSceneFrame({
2318
- scene: "error",
2319
- title: "Rig error",
2320
- lines: [
2321
- ...errorDrone(state.tick).slice(2, 16),
2322
- blank(),
2323
- center("DRONE SIGNAL DEGRADED", RIG_UI.red, true),
2324
- center(message, RIG_UI.ink2),
2325
- blank(),
2326
- center(hint, RIG_UI.limeDim)
2327
- ],
2328
- typeBarPlaceholder: hint,
2329
- live: true
2330
- });
2331
- }
2332
-
2333
- // packages/cli/src/app-opentui/scenes/help.ts
2334
- var COMMANDS = [
2335
- ["runs", "show runs"],
2336
- ["tasks", "show task source"],
2337
- ["type in Runs", "search loaded runs"],
2338
- ["type in Tasks", "search loaded tasks"],
2339
- ["tasks open", "filter to open tasks"],
2340
- ["tasks all", "show every task returned by the source"],
2341
- ["tasks mine", "show tasks assigned to @me"],
2342
- ["tasks blocked", "show blocked work"],
2343
- ["tasks closed", "show closed work"],
2344
- ["tasks sort priority", "rank by priority"],
2345
- ["tasks sort recency", "rank by update time"],
2346
- ["tasks sort status", "group by status"],
2347
- ["run <task-id>", "dispatch selected task id"],
2348
- ["run next", "dispatch next runnable task"],
2349
- ["attach <run>", "open bundled Pi for a run"],
2350
- ["tab / \u2192", "next screen"],
2351
- ["\u2190", "previous screen"],
2352
- ["\u2191 \u2193", "move selection in Runs/Tasks"],
2353
- ["j k", "move selection outside search-first list screens"],
2354
- ["enter", "activate selected row"],
2355
- ["mouse wheel", "scroll panel under cursor"],
2356
- ["ctrl-]", "detach from embedded Pi"],
2357
- ["ctrl-c / q", "exit"]
2358
- ];
2359
- function panelWidth(layout) {
2360
- return layout?.centerWidth ?? 100;
2361
- }
2362
- function panelHeight(layout, top = 3) {
2363
- return Math.max(4, (layout?.centerHeight ?? 24) - top);
2364
- }
2365
- function clip(value, width) {
2366
- if (width <= 0)
2367
- return "";
2368
- return value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}\u2026`;
2369
- }
2370
- function commandRow(width, label, detail, index) {
2371
- const keyWidth = Math.min(22, Math.max(12, Math.floor(width * 0.28)));
2372
- const key = clip(label, keyWidth).padEnd(keyWidth);
2373
- return line(`${key} ${clip(detail, Math.max(8, width - keyWidth - 4))}`, { fg: index < 3 ? RIG_UI.ink : RIG_UI.ink2 });
2374
- }
2375
- function unsupportedCommand(state) {
2376
- const value = state.intent.action.payload?.unsupported ?? state.intent.action.payload?.unknown;
2377
- return typeof value === "string" && value.trim() ? value.trim() : null;
2378
- }
2379
- function renderHelpScene(state, layout) {
2380
- const width = panelWidth(layout);
2381
- const unsupported = unsupportedCommand(state);
2382
- const panelTop = 0;
2383
- const panelLines = [
2384
- ...unsupported ? [
2385
- line(`${unsupported} uses the plain CLI path.`, { fg: RIG_UI.yellow, bold: true }),
2386
- line("Exit and rerun with RIG_PLAIN=1 if you want legacy text output.", { fg: RIG_UI.ink3 }),
2387
- line("", { fg: RIG_UI.ink3 })
2388
- ] : [],
2389
- line("COMMAND ACTION", { fg: RIG_UI.ink3, bold: true }),
2390
- line("", { fg: RIG_UI.ink3 }),
2391
- ...COMMANDS.map(([label, detail], index) => commandRow(width - 6, label, detail, index))
2392
- ];
2393
- return makeSceneFrame({
2394
- scene: "help",
2395
- title: "Rig help",
2396
- lines: [
2397
- line("", { fg: RIG_UI.ink4 }),
2398
- line(" rig help", { fg: RIG_UI.ink3 })
2399
- ],
2400
- panels: [{
2401
- id: "help-reference",
2402
- top: panelTop,
2403
- width,
2404
- height: panelHeight(layout, panelTop),
2405
- lines: panelLines,
2406
- backgroundColor: RIG_UI.panel,
2407
- backgroundAlpha: 184,
2408
- opacity: 1,
2409
- border: false,
2410
- chrome: "ad-terminal",
2411
- headerText: "rig help",
2412
- headerHeight: 3,
2413
- paddingX: layout?.compact ? 2 : 3,
2414
- paddingY: 1
2415
- }],
2416
- typeBarPlaceholder: "runs \xB7 tasks \xB7 help \xB7 attach <run>",
2417
- footer: { message: "help" },
2418
- live: true
2419
- });
2420
- }
2421
-
2422
- // packages/cli/src/app-opentui/scenes/main.ts
2423
- var CONFIGURED_OPTIONS = [
2424
- ["runs", "run list and attach"],
2425
- ["tasks", "task source and dispatch"],
2426
- ["help", "command reference"]
2427
- ];
2428
- function renderMainScene(state) {
2429
- const configured = state.data.projectConfigured === true;
2430
- const selected = configured ? Math.max(0, Math.min(CONFIGURED_OPTIONS.length - 1, state.selection.index)) : Math.max(0, Math.min(1, state.selection.index));
2431
- const lines = [
2432
- center(configured ? "rig" : "rig setup", RIG_UI.ink, true),
2433
- center(configured ? "runs \xB7 tasks \xB7 help" : "initialize this repo", RIG_UI.ink3),
2434
- blank()
2435
- ];
2436
- if (!configured) {
2437
- lines.push(deckRow({ label: "init", detail: "create config and server link", index: 0, active: selected === 0 }), deckRow({ label: "help", detail: "show command reference", index: 1, active: selected === 1 }), blank(), line("enter opens selection", { fg: RIG_UI.ink4, align: "center" }));
2438
- } else {
2439
- CONFIGURED_OPTIONS.forEach(([label, detail], index) => {
2440
- lines.push(deckRow({ label, detail, index, active: index === selected }));
2441
- });
2442
- lines.push(blank(), line(` ${state.actionLabel ?? "ready"}`, { fg: state.status === "action" ? RIG_UI.cyan : RIG_UI.ink4 }));
2443
- }
2444
- return makeSceneFrame({
2445
- scene: "main",
2446
- title: "Rig",
2447
- lines,
2448
- typeBarPlaceholder: configured ? "runs \xB7 tasks \xB7 help" : "init \xB7 help",
2449
- live: true
2450
- });
2451
- }
2452
-
2453
- // packages/cli/src/app-opentui/pi-pty-host.ts
2454
- import { fileURLToPath } from "url";
2455
- import { basename } from "path";
2456
- import { RGBA as RGBA4, StyledText as StyledText2, TextAttributes as TextAttributes4 } from "@opentui/core";
2457
- import { Terminal as XtermTerminal } from "@xterm/headless";
2458
- var MIN_COLS = 40;
2459
- var MIN_ROWS = 12;
2460
- var MAX_ROWS = 300;
2461
- var MAX_SNAPSHOT_LINES = 360;
2462
- var STYLED_SNAPSHOT_MARGIN = 24;
2463
- var SNAPSHOT_DELAY_MS = 33;
2464
- var fallbackChildScriptPath = fileURLToPath(new URL("./pi-host-child.ts", import.meta.url));
2465
- var activeHost = null;
2466
- function clampCols(cols) {
2467
- return Math.max(MIN_COLS, Math.trunc(cols || 100));
2468
- }
2469
- function clampRows(rows) {
2470
- return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.trunc(rows || 35)));
2471
- }
2472
- var XTERM_COLOR_MODE_INDEXED_16 = 16777216;
2473
- var XTERM_COLOR_MODE_INDEXED_256 = 33554432;
2474
- var XTERM_COLOR_MODE_RGB = 50331648;
2475
- function rgbaFromXtermColor(mode, value) {
2476
- if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
2477
- return RGBA4.fromIndex(value);
2478
- }
2479
- if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
2480
- return RGBA4.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
2481
- }
2482
- return;
2483
- }
2484
- function textAttributesFromCell(cell) {
2485
- let attributes = TextAttributes4.NONE;
2486
- if (cell.isBold())
2487
- attributes |= TextAttributes4.BOLD;
2488
- if (cell.isDim())
2489
- attributes |= TextAttributes4.DIM;
2490
- if (cell.isItalic())
2491
- attributes |= TextAttributes4.ITALIC;
2492
- if (cell.isUnderline())
2493
- attributes |= TextAttributes4.UNDERLINE;
2494
- if (cell.isBlink())
2495
- attributes |= TextAttributes4.BLINK;
2496
- if (cell.isInverse())
2497
- attributes |= TextAttributes4.INVERSE;
2498
- if (cell.isInvisible())
2499
- attributes |= TextAttributes4.HIDDEN;
2500
- if (cell.isStrikethrough())
2501
- attributes |= TextAttributes4.STRIKETHROUGH;
2502
- return attributes;
2503
- }
2504
- function sameRgba(a, b) {
2505
- if (!a && !b)
2506
- return true;
2507
- return Boolean(a && b && a.equals(b));
2508
- }
2509
- function sameStyle(a, b) {
2510
- return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
2511
- }
2512
- function styleFromCell(cell) {
2513
- return {
2514
- fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
2515
- bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
2516
- attributes: textAttributesFromCell(cell)
2517
- };
2518
- }
2519
- function lineToStyledText(line2, cols) {
2520
- if (!line2)
2521
- return new StyledText2([{ __isChunk: true, text: "" }]);
2522
- const chunks = [];
2523
- let run = "";
2524
- let runStyle = null;
2525
- const flush = () => {
2526
- if (!run)
2527
- return;
2528
- chunks.push({
2529
- __isChunk: true,
2530
- text: run,
2531
- ...runStyle?.fg ? { fg: runStyle.fg } : {},
2532
- ...runStyle?.bg ? { bg: runStyle.bg } : {},
2533
- ...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
2534
- });
2535
- run = "";
2536
- };
2537
- for (let index = 0;index < cols; index += 1) {
2538
- const cell = line2.getCell(index);
2539
- if (!cell) {
2540
- if (runStyle !== null)
2541
- flush();
2542
- runStyle = null;
2543
- run += " ";
2544
- continue;
2545
- }
2546
- if (cell.getWidth() === 0)
2547
- continue;
2548
- const style = styleFromCell(cell);
2549
- if (!runStyle || !sameStyle(runStyle, style)) {
3163
+ for (const char of text) {
3164
+ const next = colorFor(char);
3165
+ if (next !== runColor) {
2550
3166
  flush();
2551
- runStyle = style;
3167
+ runColor = next;
2552
3168
  }
2553
- run += cell.getChars() || " ";
3169
+ run += char;
2554
3170
  }
2555
3171
  flush();
2556
- return new StyledText2(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
2557
- }
2558
- function childCommandPrefix() {
2559
- const execName = basename(process.execPath).toLowerCase();
2560
- const currentEntry = process.argv[1];
2561
- if ((execName === "bun" || execName === "bun.exe") && currentEntry) {
2562
- return [process.execPath, currentEntry, "__opentui-pi-host"];
2563
- }
2564
- if (currentEntry)
2565
- return [process.execPath, "__opentui-pi-host"];
2566
- return [process.execPath, fallbackChildScriptPath];
3172
+ return new StyledText2(chunks.length > 0 ? chunks : [chunk2("", COLOR.ink)]);
2567
3173
  }
2568
- function withEnv(base) {
2569
- const env = {};
2570
- for (const [key, value] of Object.entries(base ?? process.env)) {
2571
- if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP")
2572
- continue;
2573
- if (typeof value === "string")
2574
- env[key] = value;
2575
- }
2576
- return {
2577
- ...env,
2578
- TERM: "xterm-256color",
2579
- COLORTERM: "truecolor",
2580
- FORCE_COLOR: env.FORCE_COLOR ?? "1",
2581
- RIG_OPENTUI_PI_HOST: "1"
2582
- };
3174
+ function droneColor(char) {
3175
+ if (char === "?" || char === "o" || char === "@" || char === "\u2022")
3176
+ return "eye";
3177
+ if (char === "%" || char === "=")
3178
+ return "mini";
3179
+ if (char === "$")
3180
+ return "body";
3181
+ if (char === "(" || char === ")" || char === "/" || char === "\\" || char === "|" || char === "-")
3182
+ return "rotor";
3183
+ if (char === "\xB7" || char === "\u2022" || char === "'" || char === ".")
3184
+ return "path";
3185
+ if (char.trim() === "")
3186
+ return "dim";
3187
+ return "ink";
2583
3188
  }
2584
- function getActivePiHost() {
2585
- return activeHost && !activeHost.disposed ? activeHost : null;
3189
+ function motionColor(char) {
3190
+ if (char === "\u25CF" || char === "\u25C6" || char === "\u25C9")
3191
+ return "body";
3192
+ if (char === "\u2022" || char === "\xB7" || char === "\u2500" || char === "\u2502" || char === "\u2571" || char === "\u2572" || char === "\u256D" || char === "\u256E" || char === "\u2570" || char === "\u256F")
3193
+ return "path";
3194
+ if (char === "\u258C" || char === "\u2590" || char === "\u2581" || char === "\u2594")
3195
+ return "mini";
3196
+ return droneColor(char);
2586
3197
  }
2587
- function stopActivePiHost(reason = "detach") {
2588
- activeHost?.dispose(reason);
2589
- activeHost = null;
3198
+ function materializeDrone(lines, tick, phase = 0) {
3199
+ const blade = bladeForTick(tick, phase);
3200
+ const eye = eyeForTick(tick, phase);
3201
+ return lines.map((line2) => line2.replaceAll("!!!", blade).replaceAll("?", eye));
2590
3202
  }
2591
-
2592
- class PiPtyHost {
2593
- runId;
2594
- projectRoot;
2595
- onSnapshot;
2596
- onExit;
2597
- onError;
2598
- terminal;
2599
- disposables = [];
2600
- decoder = new TextDecoder("utf-8");
2601
- proc = null;
2602
- pty = null;
2603
- status = "starting";
2604
- cols;
2605
- rows;
2606
- message = "starting bundled Pi";
2607
- exitCode;
2608
- signal;
2609
- notifyTimer = null;
2610
- _disposed = false;
2611
- constructor(options) {
2612
- this.runId = options.runId;
2613
- this.projectRoot = options.projectRoot;
2614
- this.cols = clampCols(options.cols);
2615
- this.rows = clampRows(options.rows);
2616
- this.onSnapshot = options.onSnapshot;
2617
- this.onExit = options.onExit;
2618
- this.onError = options.onError;
2619
- this.terminal = new XtermTerminal({
2620
- allowProposedApi: true,
2621
- cols: this.cols,
2622
- rows: this.rows,
2623
- scrollback: 5000
2624
- });
2625
- this.registerTerminalResponders();
2626
- }
2627
- get disposed() {
2628
- return this._disposed;
2629
- }
2630
- get snapshot() {
2631
- return this.createSnapshot();
2632
- }
2633
- async start() {
2634
- if (this._disposed)
2635
- throw new Error("Pi PTY host is disposed.");
2636
- if (typeof Bun.Terminal !== "function") {
2637
- throw new Error("Bun native PTY is unavailable in this runtime. Rig OpenTUI Pi host requires Bun.Terminal.");
2638
- }
2639
- const spawnOptions = {
2640
- cwd: this.projectRoot,
2641
- env: withEnv(process.env),
2642
- terminal: {
2643
- cols: this.cols,
2644
- rows: this.rows,
2645
- name: "xterm-256color",
2646
- data: (_terminal, data) => this.handlePtyData(data)
2647
- }
2648
- };
2649
- const proc = Bun.spawn([
2650
- ...childCommandPrefix(),
2651
- "--run-id",
2652
- this.runId,
2653
- "--project-root",
2654
- this.projectRoot
2655
- ], spawnOptions);
2656
- if (!proc.terminal)
2657
- throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
2658
- this.proc = proc;
2659
- this.pty = proc.terminal;
2660
- this.status = "running";
2661
- this.message = "bundled Pi running inside Rig";
2662
- this.emitSnapshotSoon(0);
2663
- proc.exited.then((exitCode) => {
2664
- if (this._disposed)
2665
- return;
2666
- this.status = exitCode === 0 ? "exited" : "failed";
2667
- this.exitCode = exitCode;
2668
- this.signal = null;
2669
- this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
2670
- const snapshot = this.createSnapshot();
2671
- this.onSnapshot?.(snapshot);
2672
- this.onExit?.(snapshot);
2673
- if (activeHost === this)
2674
- activeHost = null;
2675
- this.dispose("exit", { kill: false, notify: false });
2676
- }).catch((error) => {
2677
- if (this._disposed)
2678
- return;
2679
- this.status = "failed";
2680
- this.message = error instanceof Error ? error.message : String(error);
2681
- const snapshot = this.createSnapshot();
2682
- this.onSnapshot?.(snapshot);
2683
- this.onError?.(error, snapshot);
2684
- if (activeHost === this)
2685
- activeHost = null;
2686
- this.dispose("error", { kill: false, notify: false });
2687
- });
2688
- }
2689
- write(data) {
2690
- if (this._disposed || !this.pty)
2691
- return;
2692
- try {
2693
- this.pty.write(data);
2694
- } catch (error) {
2695
- this.status = "failed";
2696
- this.message = error instanceof Error ? error.message : String(error);
2697
- const snapshot = this.createSnapshot();
2698
- this.onSnapshot?.(snapshot);
2699
- this.onError?.(error, snapshot);
2700
- }
2701
- }
2702
- resize(cols, rows) {
2703
- const nextCols = clampCols(cols);
2704
- const nextRows = clampRows(rows);
2705
- if (nextCols === this.cols && nextRows === this.rows)
2706
- return;
2707
- this.cols = nextCols;
2708
- this.rows = nextRows;
2709
- this.terminal.resize(nextCols, nextRows);
2710
- try {
2711
- this.pty?.resize(nextCols, nextRows);
2712
- } catch {}
2713
- this.emitSnapshotSoon(0);
2714
- }
2715
- detach() {
2716
- this.dispose("detach");
2717
- return this.createSnapshot("detached from bundled Pi");
2718
- }
2719
- dispose(reason = "dispose", options = {}) {
2720
- if (this._disposed)
2721
- return;
2722
- this._disposed = true;
2723
- if (this.notifyTimer)
2724
- clearTimeout(this.notifyTimer);
2725
- this.notifyTimer = null;
2726
- for (const disposable of this.disposables.splice(0)) {
2727
- try {
2728
- disposable.dispose();
2729
- } catch {}
2730
- }
2731
- if (options.kill !== false) {
2732
- try {
2733
- this.proc?.kill("SIGTERM");
2734
- } catch {}
2735
- }
2736
- try {
2737
- this.pty?.close();
2738
- } catch {}
2739
- try {
2740
- this.terminal.dispose();
2741
- } catch {}
2742
- if (activeHost === this)
2743
- activeHost = null;
2744
- if (options.notify) {
2745
- this.message = reason;
2746
- this.onSnapshot?.(this.createSnapshot(reason));
2747
- }
2748
- }
2749
- handlePtyData(data) {
2750
- if (this._disposed)
2751
- return;
2752
- const text = this.decoder.decode(data, { stream: true });
2753
- this.respondToRawTerminalQueries(text);
2754
- this.terminal.write(data, () => this.emitSnapshotSoon());
2755
- }
2756
- emitSnapshotSoon(delayMs = SNAPSHOT_DELAY_MS) {
2757
- if (this._disposed || this.notifyTimer)
2758
- return;
2759
- this.notifyTimer = setTimeout(() => {
2760
- this.notifyTimer = null;
2761
- if (this._disposed)
2762
- return;
2763
- this.onSnapshot?.(this.createSnapshot());
2764
- }, delayMs);
2765
- }
2766
- createSnapshot(message = this.message) {
2767
- const buffer = this.terminal.buffer.active;
2768
- const end = buffer.length;
2769
- const start = Math.max(0, end - MAX_SNAPSHOT_LINES);
2770
- const styledStart = Math.max(start, end - this.rows - STYLED_SNAPSHOT_MARGIN);
2771
- const lines = [];
2772
- const styledLines = [];
2773
- for (let row = start;row < end; row += 1) {
2774
- const line2 = buffer.getLine(row);
2775
- lines.push((line2?.translateToString(false) ?? "").slice(0, this.cols));
2776
- if (row >= styledStart)
2777
- styledLines[lines.length - 1] = lineToStyledText(line2, this.cols);
2778
- }
2779
- while (lines.length < this.rows) {
2780
- lines.push("");
2781
- styledLines[lines.length - 1] = new StyledText2([{ __isChunk: true, text: "" }]);
2782
- }
2783
- return {
2784
- runId: this.runId,
2785
- status: this.status,
2786
- cols: this.cols,
2787
- rows: this.rows,
3203
+ function brandFleetLines(tick, align = "center") {
3204
+ const left = materializeDrone(MINI_DRONE2, tick, 0);
3205
+ const right = materializeDrone(MINI_DRONE2, tick, 2);
3206
+ const lead = materializeDrone(LEAD_MARK, tick, 1);
3207
+ const packet = ["\xB7", "\u2022", "\u25CF", "\u2022"][Math.floor(tick / 2) % 4];
3208
+ const bridge = [
3209
+ ` ${left[0]} ${lead[0]} ${right[0]}`,
3210
+ ` ${left[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${lead[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${right[1]}`,
3211
+ ` ${left[2]} \xB7' '${lead[2]}' '\xB7 ${right[2]}`,
3212
+ ` ${left[3]} \u2572 signal bus \u2571 ${right[3]}`,
3213
+ ` ${left[4]} \xB7\u2500\u2500\u2500${packet}\u2500\u2500\u2500\u2500\u2500\u2500${packet}\u2500\u2500\u2500\xB7 ${right[4]}`
3214
+ ];
3215
+ return bridge.map((text) => ({
3216
+ text,
3217
+ styledText: styledLine(text, motionColor),
3218
+ fg: RIG_UI.limeDim,
3219
+ align
3220
+ }));
3221
+ }
3222
+ function errorDrone(tick) {
3223
+ return brandFleetLines(tick).map((line2, index) => ({
3224
+ ...line2,
3225
+ fg: index % 2 === 0 ? RIG_UI.red : line2.fg
3226
+ }));
3227
+ }
3228
+
3229
+ // packages/cli/src/app-opentui/scenes/error.ts
3230
+ function renderErrorScene(state) {
3231
+ const message = state.error?.message ?? "Unknown app error";
3232
+ const hint = state.error?.hint ?? "Use the actions below to recover.";
3233
+ const selected = Math.max(0, Math.min(3, state.selection.index));
3234
+ return makeSceneFrame({
3235
+ scene: "error",
3236
+ title: "Error",
3237
+ lines: [
3238
+ ...errorDrone(state.tick).slice(2, 10)
3239
+ ],
3240
+ panels: [{
3241
+ id: "error-recovery",
3242
+ top: 2,
3243
+ width: 96,
3244
+ height: 18,
3245
+ lines: [
3246
+ line("DRONE SIGNAL DEGRADED", { fg: RIG_UI.red, bold: true }),
3247
+ line(message, { fg: RIG_UI.ink2 }),
3248
+ ...hint ? [line(hint, { fg: RIG_UI.yellow })] : [],
3249
+ line("", { fg: RIG_UI.ink3 }),
3250
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
3251
+ deckRow({ label: "main", detail: "return to dashboard", index: 0, active: selected === 0, activateOnClick: true }),
3252
+ deckRow({ label: "doctor", detail: "run diagnostics", index: 1, active: selected === 1, activateOnClick: true }),
3253
+ deckRow({ label: "server", detail: "open server controls", index: 2, active: selected === 2, activateOnClick: true }),
3254
+ deckRow({ label: "repair", detail: "repair project/server/auth link", index: 3, active: selected === 3, activateOnClick: true })
3255
+ ],
3256
+ backgroundColor: RIG_UI.panel,
3257
+ backgroundAlpha: 184,
3258
+ opacity: 1,
3259
+ border: false,
3260
+ chrome: "ad-terminal",
3261
+ headerText: "recovery",
3262
+ headerHeight: 3,
3263
+ paddingX: 3,
3264
+ paddingY: 1
3265
+ }],
3266
+ footer: { message: "enter/click a recovery action" },
3267
+ hideTypeBar: true,
3268
+ live: true
3269
+ });
3270
+ }
3271
+
3272
+ // packages/cli/src/app-opentui/scenes/help.ts
3273
+ var ACTIONS = [
3274
+ ["runs", "show live/completed runs"],
3275
+ ["tasks", "browse task source and dispatch work"],
3276
+ ["run next", "dispatch next runnable task"],
3277
+ ["server", "local/remote server controls"],
3278
+ ["init", "setup/reconfigure/repair wizard"],
3279
+ ["doctor", "diagnose project/server/auth"],
3280
+ ["github", "GitHub auth/repo actions"],
3281
+ ["plugin", "plugin and source actions"],
3282
+ ["setup", "bootstrap/check/preflight"],
3283
+ ["inspect", "inspect project/server state"],
3284
+ ["stats", "run/task metrics"],
3285
+ ["repo", "repo utilities"],
3286
+ ["profile", "profile/config actions"],
3287
+ ["review", "review automation"],
3288
+ ["browser", "browser tooling/wizards"],
3289
+ ["pi", "Pi install/attach/status"],
3290
+ ["queue", "queue operations"],
3291
+ ["agent", "agent/runtime operations"],
3292
+ ["inspector", "inspector tools"],
3293
+ ["dist", "distribution helpers"],
3294
+ ["workspace", "workspace/project utilities"],
3295
+ ["remote", "remote endpoint utilities"],
3296
+ ["git", "git helper actions"],
3297
+ ["harness", "harness operations"],
3298
+ ["test", "test helpers"]
3299
+ ];
3300
+ var SHORTCUTS = [
3301
+ ["tab / \u2192", "next workspace"],
3302
+ ["\u2190 / esc", "previous/back or clear search"],
3303
+ ["\u2191 \u2193 / j k", "move selection"],
3304
+ ["enter / space / click", "activate selected action"],
3305
+ ["mouse wheel", "scroll panel under cursor"],
3306
+ ["ctrl-]", "detach from embedded Pi/command terminal"],
3307
+ ["ctrl-c / q", "exit"]
3308
+ ];
3309
+ function panelWidth(layout) {
3310
+ return layout?.centerWidth ?? 100;
3311
+ }
3312
+ function panelHeight(layout, top = 1) {
3313
+ return Math.max(8, (layout?.centerHeight ?? 24) - top);
3314
+ }
3315
+ function unsupportedCommand(state) {
3316
+ const value = state.intent.action.payload?.unsupported ?? state.intent.action.payload?.unknown;
3317
+ if (typeof value !== "string" || !value.trim())
3318
+ return null;
3319
+ const reason = state.intent.action.payload?.reason;
3320
+ return { command: value.trim(), ...typeof reason === "string" && reason.trim() ? { reason: reason.trim() } : {} };
3321
+ }
3322
+ function renderHelpScene(state, layout) {
3323
+ const unsupported = unsupportedCommand(state);
3324
+ const selected = Math.max(0, Math.min(ACTIONS.length - 1, state.selection.index));
3325
+ const panelLines = [
3326
+ ...unsupported ? [
3327
+ line(`${unsupported.command} needs the action catalog.`, { fg: RIG_UI.yellow, bold: true }),
3328
+ line(unsupported.reason ?? "Pick an action below; explicit plain mode remains available with RIG_PLAIN=1.", { fg: RIG_UI.ink3 }),
3329
+ line("", { fg: RIG_UI.ink3 })
3330
+ ] : [],
3331
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
3332
+ ...ACTIONS.map(([label, detail], index) => deckRow({ label, detail, index, active: selected === index, activateOnClick: true })),
3333
+ line("", { fg: RIG_UI.ink3 }),
3334
+ line("KEYS / MOUSE", { fg: RIG_UI.ink3, bold: true }),
3335
+ ...SHORTCUTS.map(([label, detail]) => line(`${label.padEnd(22)} ${detail}`, { fg: RIG_UI.ink2 }))
3336
+ ];
3337
+ return makeSceneFrame({
3338
+ scene: "help",
3339
+ title: "Command catalog",
3340
+ lines: [
3341
+ line("", { fg: RIG_UI.ink4 }),
3342
+ line(" command catalog \xB7 TUI-first Rig", { fg: RIG_UI.ink3, bold: true })
3343
+ ],
3344
+ panels: [{
3345
+ id: "help-reference",
3346
+ top: 0,
3347
+ width: panelWidth(layout),
3348
+ height: panelHeight(layout),
3349
+ lines: panelLines,
3350
+ backgroundColor: RIG_UI.panel,
3351
+ backgroundAlpha: 184,
3352
+ opacity: 1,
3353
+ border: false,
3354
+ chrome: "ad-terminal",
3355
+ headerText: "actions",
3356
+ headerHeight: 3,
3357
+ paddingX: layout?.compact ? 2 : 3,
3358
+ paddingY: 1
3359
+ }],
3360
+ hideTypeBar: true,
3361
+ footer: { message: "enter/click opens a workspace" },
3362
+ live: true
3363
+ });
3364
+ }
3365
+
3366
+ // packages/cli/src/app-opentui/scenes/main.ts
3367
+ var CONFIGURED_MENU = [
3368
+ ["init", "setup, reconfigure, repair project state"],
3369
+ ["server", "local/remote target, auth, linkage"],
3370
+ ["github", "auth, selected repo, stored token"],
3371
+ ["tasks", "browse, filter, dispatch, attach active work"],
3372
+ ["runs", "attach, stop, resume, inspect live sessions"],
3373
+ ["inbox", "approve, reject, answer, attach"],
3374
+ ["doctor", "diagnose and repair broken surfaces"],
3375
+ ["help", "all actions, shortcuts, mouse controls"]
3376
+ ];
3377
+ var UNCONFIGURED_MENU = [
3378
+ ["init", "full setup wizard and reconfigure flow"],
3379
+ ["server", "select local/remote target"],
3380
+ ["github", "connect or import authentication"],
3381
+ ["doctor", "find missing setup pieces"],
3382
+ ["help", "all actions and controls"]
3383
+ ];
3384
+ function record(value) {
3385
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3386
+ }
3387
+ function serverSummary(state) {
3388
+ const server = record(state.data.server);
3389
+ const auth = record(server?.auth);
3390
+ const signedIn = auth?.signedIn === true || auth?.authenticated === true || auth?.status === "authenticated";
3391
+ const login = typeof auth?.login === "string" && auth.login.trim() ? auth.login.trim() : undefined;
3392
+ return {
3393
+ label: typeof server?.label === "string" && server.label.trim() ? server.label.trim() : state.footer.server ?? "not checked yet",
3394
+ target: typeof server?.kind === "string" ? `${server.kind} endpoint` : "open Server to probe",
3395
+ health: server ? server.reachable === false ? "unreachable" : "reachable" : "pending",
3396
+ auth: server ? signedIn ? `signed in${login ? ` as ${login}` : ""}` : "not signed in / unknown" : "pending"
3397
+ };
3398
+ }
3399
+ function panelWidth2(layout) {
3400
+ return layout?.centerWidth ?? 118;
3401
+ }
3402
+ function panelHeight2(layout) {
3403
+ return Math.max(22, layout?.centerHeight ?? 36);
3404
+ }
3405
+ function actionRows(state, configured) {
3406
+ const options = configured ? CONFIGURED_MENU : UNCONFIGURED_MENU;
3407
+ const selected = Math.max(0, Math.min(options.length - 1, state.selection.index));
3408
+ return options.map(([label, detail], index) => deckRow({ label, detail, index, active: index === selected, activateOnClick: true }));
3409
+ }
3410
+ function renderMainScene(state, layout) {
3411
+ const configured = state.data.projectConfigured === true;
3412
+ const server = serverSummary(state);
3413
+ const lines = [
3414
+ line("PROJECT COCKPIT", { fg: RIG_UI.ink3, bold: true }),
3415
+ line("click a row or use arrows + enter \xB7 esc returns here \xB7 ctrl-c exits", { fg: RIG_UI.ink4 }),
3416
+ blank(),
3417
+ line("ACTIONS", { fg: RIG_UI.ink3, bold: true }),
3418
+ ...actionRows(state, configured),
3419
+ blank(),
3420
+ line("CONTEXT", { fg: RIG_UI.ink3, bold: true }),
3421
+ kv("workspace", "current repository", RIG_UI.ink2),
3422
+ kv("config", configured ? "configured" : "needs setup", configured ? RIG_UI.limeDim : RIG_UI.yellow),
3423
+ kv("server", server.label, server.health === "reachable" ? RIG_UI.limeDim : RIG_UI.yellow),
3424
+ kv("target", server.target, RIG_UI.ink3),
3425
+ kv("github", server.auth, server.auth.startsWith("signed in") ? RIG_UI.limeDim : RIG_UI.yellow),
3426
+ blank(),
3427
+ line(state.actionLabel ?? "ready", { fg: state.status === "action" ? RIG_UI.cyan : RIG_UI.ink4 })
3428
+ ];
3429
+ return makeSceneFrame({
3430
+ scene: "main",
3431
+ title: "Project cockpit",
3432
+ lines: [],
3433
+ panels: [{
3434
+ id: "project-cockpit",
3435
+ top: 0,
3436
+ width: panelWidth2(layout),
3437
+ height: panelHeight2(layout),
2788
3438
  lines,
2789
- styledLines,
2790
- message,
2791
- ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
2792
- ...this.signal !== undefined ? { signal: this.signal } : {}
2793
- };
2794
- }
2795
- registerTerminalResponders() {
2796
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
2797
- if (params.length === 0 || params[0] === 0)
2798
- this.write("\x1B[?62;22c");
2799
- return false;
2800
- }));
2801
- this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
2802
- if (params.length === 0 || params[0] === 0)
2803
- this.write("\x1B[>0;0;0c");
2804
- return false;
2805
- }));
2806
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
2807
- if (params[0] === 5) {
2808
- this.write("\x1B[0n");
2809
- } else if (params[0] === 6) {
2810
- const row = this.terminal.buffer.active.cursorY + 1;
2811
- const col = this.terminal.buffer.active.cursorX + 1;
2812
- this.write(`\x1B[${row};${col}R`);
2813
- }
2814
- return false;
2815
- }));
2816
- }
2817
- respondToRawTerminalQueries(text) {
2818
- if (!text)
2819
- return;
2820
- const decrqm = /\x1b\[\?(\d+)\$p/g;
2821
- let match;
2822
- while ((match = decrqm.exec(text)) !== null) {
2823
- this.write(`\x1B[?${match[1]};2$y`);
2824
- }
2825
- }
3439
+ backgroundColor: RIG_UI.panel,
3440
+ backgroundAlpha: 184,
3441
+ opacity: 0.98,
3442
+ border: false,
3443
+ chrome: "ad-terminal",
3444
+ headerText: "project cockpit",
3445
+ paddingX: layout?.compact ? 3 : 5,
3446
+ paddingY: 2
3447
+ }],
3448
+ live: true,
3449
+ hideTypeBar: true
3450
+ });
2826
3451
  }
2827
3452
 
2828
3453
  // packages/cli/src/app-opentui/runtime.ts
3454
+ init_pi_pty_host();
2829
3455
  var MAX_SCENE_LINES = 200;
2830
3456
  var MAX_SCENE_PANELS = 5;
2831
- var FRAME_MS = 16;
2832
- var ANIMATION_SLOWDOWN = 3;
3457
+ var ANIMATION_SLOWDOWN = 1;
3458
+ var PRELOADER_BACKDROP = RGBA7.fromHex(RIG_UI.bg);
2833
3459
  var PRIMARY_NAV = [
3460
+ { scene: "main", argv: ["main"], label: "Project menu" },
2834
3461
  { scene: "fleet", argv: ["runs"], label: "Opening runs" },
2835
3462
  { scene: "tasks", argv: ["tasks"], label: "Opening tasks" },
2836
3463
  { scene: "help", argv: ["help"], label: "Opening help" }
2837
3464
  ];
2838
3465
  function primaryNavIntent(scene) {
2839
3466
  const entry = PRIMARY_NAV.find((item) => item.scene === scene) ?? PRIMARY_NAV[0];
2840
- return { scene: entry.scene, argv: entry.argv, action: { kind: entry.scene === "help" ? "none" : "refresh", label: entry.label } };
3467
+ const kind = entry.scene === "help" || entry.scene === "main" ? "none" : "refresh";
3468
+ return { scene: entry.scene, argv: entry.argv, action: { kind, label: entry.label } };
2841
3469
  }
2842
3470
  function nextPrimaryScene(current, delta) {
2843
3471
  const currentIndex = PRIMARY_NAV.findIndex((entry) => entry.scene === current);
@@ -2858,10 +3486,10 @@ function defaultSceneRenderer(state) {
2858
3486
  scene: state.scene,
2859
3487
  title,
2860
3488
  lines: [
2861
- { text: title.toUpperCase(), fg: RIG_UI.ink, bold: true, align: "center" },
2862
- { text: state.actionLabel ?? "adapter scene pending", fg: RIG_UI.limeDim, align: "center" },
2863
- { text: "support state", fg: RIG_UI.ink3, align: "center" },
2864
- { text: "runs \xB7 tasks \xB7 help", fg: RIG_UI.ink4, align: "center" }
3489
+ { text: ` ${title.toUpperCase()}`, fg: RIG_UI.ink, bold: true },
3490
+ { text: ` ${state.actionLabel ?? "adapter scene pending"}`, fg: RIG_UI.limeDim },
3491
+ { text: " choose an action from the available rows", fg: RIG_UI.ink3 },
3492
+ { text: " esc returns home \xB7 arrows select \xB7 enter activates", fg: RIG_UI.ink4 }
2865
3493
  ],
2866
3494
  typeBarPlaceholder: "runs \xB7 tasks \xB7 help",
2867
3495
  live: false
@@ -2887,9 +3515,9 @@ function requestRender(renderer) {
2887
3515
  function recordArray(value) {
2888
3516
  return Array.isArray(value) ? value.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
2889
3517
  }
2890
- function stringField(record, keys) {
3518
+ function stringField2(record2, keys) {
2891
3519
  for (const key of keys) {
2892
- const value = record[key];
3520
+ const value = record2[key];
2893
3521
  if (typeof value === "string" && value.trim())
2894
3522
  return value.trim();
2895
3523
  if (typeof value === "number" && Number.isFinite(value))
@@ -2897,33 +3525,84 @@ function stringField(record, keys) {
2897
3525
  }
2898
3526
  return;
2899
3527
  }
2900
- function mainSelectableItems(_state) {
3528
+ function mainSelectableItems(state) {
3529
+ const configured = state.data.projectConfigured === true;
3530
+ const common = [
3531
+ { id: "init", label: "init", intent: { scene: "command", argv: ["init"], action: { kind: "command-run", label: "Run init" } }, message: "run setup/reconfigure flow" },
3532
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Checking server" } }, message: "check selected server" },
3533
+ { id: "github", label: "github", intent: { scene: "server", argv: ["github", "auth", "status"], action: { kind: "refresh", label: "Checking GitHub auth" } }, message: "check GitHub auth" }
3534
+ ];
3535
+ if (!configured) {
3536
+ return [
3537
+ ...common,
3538
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Running doctor" } }, message: "diagnose missing setup" },
3539
+ { id: "help", label: "help", intent: { scene: "help", argv: ["help"], action: { kind: "none", label: "Opening help" } }, message: "selected help" }
3540
+ ];
3541
+ }
2901
3542
  return [
2902
- { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }, message: "selected runs" },
3543
+ ...common,
2903
3544
  { id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Opening tasks" } }, message: "selected tasks" },
3545
+ { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }, message: "selected runs" },
3546
+ { id: "inbox", label: "inbox", intent: { scene: "inbox", argv: ["inbox"], action: { kind: "refresh", label: "Loading inbox" } }, message: "selected inbox" },
3547
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Running doctor" } }, message: "diagnose project" },
2904
3548
  { id: "help", label: "help", intent: { scene: "help", argv: ["help"], action: { kind: "none", label: "Opening help" } }, message: "selected help" }
2905
3549
  ];
2906
3550
  }
3551
+ function fleetRecoveryItems() {
3552
+ return [
3553
+ { id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Open tasks" } }, message: "open tasks and dispatch work" },
3554
+ { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair project link" } }, message: "repair project/server/auth link" },
3555
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "server controls" },
3556
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "diagnose project" }
3557
+ ];
3558
+ }
2907
3559
  function fleetSelectableItems(state) {
2908
3560
  const fleet = state.data.fleet;
2909
3561
  const runs = fleet && typeof fleet === "object" && !Array.isArray(fleet) ? recordArray(fleet.runs) : [];
2910
- return filterRunsForSearch(runs, listSearchQuery(state)).flatMap((run) => {
2911
- const runId = stringField(run, ["runId", "id"]);
3562
+ const visibleRuns = filterRunsForSearch(runs, listSearchQuery(state));
3563
+ if (visibleRuns.length === 0)
3564
+ return fleetRecoveryItems();
3565
+ return visibleRuns.flatMap((run) => {
3566
+ const runId = stringField2(run, ["runId", "id"]);
2912
3567
  if (!runId)
2913
3568
  return [];
2914
- const title = stringField(run, ["title", "taskTitle", "taskId"]) ?? "run";
3569
+ const title = stringField2(run, ["title", "taskTitle", "taskId"]) ?? "run";
2915
3570
  return [{ id: runId, label: title, data: { selectedRunId: runId }, intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: `attach Pi to ${runId.slice(0, 8)}` }];
2916
3571
  });
2917
3572
  }
3573
+ function taskRecoveryItems() {
3574
+ return [
3575
+ { id: "refresh", label: "refresh", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Refresh tasks" } }, message: "refresh task source" },
3576
+ { id: "next", label: "next", intent: { scene: "command", argv: ["task", "run", "--next"], action: { kind: "command-run", label: "Run next task" } }, message: "dispatch next runnable task" },
3577
+ { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair task source" } }, message: "repair project/task source" },
3578
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "server controls" },
3579
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "diagnose task source" }
3580
+ ];
3581
+ }
2918
3582
  function taskSelectableItems(state) {
2919
3583
  const tasks = state.data.tasks;
2920
3584
  const records = tasks && typeof tasks === "object" && !Array.isArray(tasks) ? recordArray(tasks.records) : [];
2921
- return filterTasksForSearch(records, listSearchQuery(state)).flatMap((task) => {
2922
- const taskId = stringField(task, ["id", "taskId", "number"]);
3585
+ const visibleTasks = filterTasksForSearch(records, listSearchQuery(state));
3586
+ if (visibleTasks.length === 0)
3587
+ return taskRecoveryItems();
3588
+ return visibleTasks.flatMap((task) => {
3589
+ const taskId = stringField2(task, ["id", "taskId", "number"]);
2923
3590
  if (!taskId)
2924
3591
  return [];
2925
- const title = stringField(task, ["title", "name", "summary"]) ?? "task";
2926
- return [{ id: taskId, label: title, data: { selectedTaskId: taskId }, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` }];
3592
+ const title = stringField2(task, ["title", "name", "summary"]) ?? "task";
3593
+ const activeRunId = activeRunIdFromTaskLike(task);
3594
+ if (activeRunId) {
3595
+ const item2 = {
3596
+ id: taskId,
3597
+ label: title,
3598
+ data: { selectedTaskId: taskId, selectedRunId: activeRunId },
3599
+ intent: { scene: "handoff", argv: ["attach", activeRunId], action: { kind: "run-attach", payload: { runId: activeRunId }, label: `Attach Pi ${activeRunId.slice(0, 8)}` } },
3600
+ message: `attach Pi to ${activeRunId.slice(0, 8)}`
3601
+ };
3602
+ return [item2];
3603
+ }
3604
+ const item = { id: taskId, label: title, data: { selectedTaskId: taskId }, intent: { scene: "tasks", argv: ["run", taskId], action: { kind: "task-run-id", payload: { task: taskId }, label: `Dispatching ${taskId}` } }, message: `dispatch task ${taskId}` };
3605
+ return [item];
2927
3606
  });
2928
3607
  }
2929
3608
  function selectedRunId(state) {
@@ -2944,21 +3623,126 @@ function selectedRunId(state) {
2944
3623
  }
2945
3624
  return;
2946
3625
  }
3626
+ function inboxRecoveryItems() {
3627
+ return [
3628
+ { id: "refresh", label: "refresh", intent: { scene: "inbox", argv: ["inbox"], action: { kind: "refresh", label: "Refresh inbox" } }, message: "refresh inbox" },
3629
+ { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Open runs" } }, message: "open runs" },
3630
+ { id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Open tasks" } }, message: "open tasks" },
3631
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "diagnose inbox" }
3632
+ ];
3633
+ }
2947
3634
  function inboxSelectableItems(state) {
2948
3635
  const inbox = state.data.inbox;
2949
3636
  if (!inbox || typeof inbox !== "object" || Array.isArray(inbox))
2950
- return [];
2951
- const record = inbox;
2952
- const approvals = recordArray(record.approvals).map((entry) => ({ ...entry, kind: "approvals" }));
2953
- const inputs = recordArray(record.inputs).map((entry) => ({ ...entry, kind: "inputs" }));
3637
+ return inboxRecoveryItems();
3638
+ const record2 = inbox;
3639
+ const approvals = recordArray(record2.approvals).map((entry) => ({ ...entry, kind: "approvals" }));
3640
+ const inputs = recordArray(record2.inputs).map((entry) => ({ ...entry, kind: "inputs" }));
3641
+ if (approvals.length === 0 && inputs.length === 0)
3642
+ return inboxRecoveryItems();
2954
3643
  return [...approvals, ...inputs].flatMap((item) => {
2955
- const requestId = stringField(item, ["requestId", "id"]);
2956
- if (!requestId)
3644
+ const requestId = stringField2(item, ["requestId", "id"]);
3645
+ const runId = stringField2(item, ["runId"]);
3646
+ if (!requestId || !runId)
2957
3647
  return [];
2958
- const kind = item.kind === "inputs" ? "answer" : "approve/reject";
2959
- return [{ id: requestId, label: kind, data: { selectedInboxRequestId: requestId }, message: `selected request ${requestId}; type ${kind} ${requestId}` }];
3648
+ if (item.kind === "approvals") {
3649
+ return [
3650
+ { id: `${requestId}:approve`, label: "approve", data: { selectedInboxRequestId: requestId }, intent: { scene: "inbox", argv: ["inbox", "approve", requestId], action: { kind: "inbox-approve", payload: { requestId }, label: `Approve ${requestId}` } }, message: `approve ${requestId}` },
3651
+ { id: `${requestId}:reject`, label: "reject", data: { selectedInboxRequestId: requestId }, intent: { scene: "inbox", argv: ["inbox", "reject", requestId], action: { kind: "inbox-reject", payload: { requestId }, label: `Reject ${requestId}` } }, message: `reject ${requestId}` },
3652
+ { id: `${requestId}:attach`, label: "attach", data: { selectedInboxRequestId: requestId, selectedRunId: runId }, intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: `attach run ${runId.slice(0, 8)}` }
3653
+ ];
3654
+ }
3655
+ return [
3656
+ { id: `${requestId}:answer`, label: "answer", data: { selectedInboxRequestId: requestId, selectedRunId: runId }, intent: { scene: "command", argv: ["inbox", "respond", "--run", runId, "--request", requestId], action: { kind: "command-run", label: `Answer ${requestId}` } }, message: `answer ${requestId}` },
3657
+ { id: `${requestId}:attach`, label: "attach", data: { selectedInboxRequestId: requestId, selectedRunId: runId }, intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: `attach run ${runId.slice(0, 8)}` }
3658
+ ];
3659
+ });
3660
+ }
3661
+ function serverSelectableItems(_state) {
3662
+ return [
3663
+ { id: "refresh", label: "refresh", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Refresh server" } }, message: "refresh selected server" },
3664
+ { id: "local", label: "local", intent: { scene: "command", argv: ["server", "use", "local"], action: { kind: "command-run", label: "Use local server" } }, message: "switch this repo to local" },
3665
+ { id: "remote", label: "remote", intent: { scene: "command", argv: ["server", "use"], action: { kind: "command-run", label: "Choose remote server" } }, message: "choose a saved remote alias" },
3666
+ { id: "list", label: "list", intent: { scene: "command", argv: ["server", "list"], action: { kind: "command-run", label: "List saved servers" } }, message: "list saved server aliases" },
3667
+ { id: "auth", label: "auth", intent: { scene: "command", argv: ["github", "auth", "import-gh"], action: { kind: "command-run", label: "Import GitHub auth" } }, message: "import gh auth" },
3668
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "verify server/auth" }
3669
+ ];
3670
+ }
3671
+ function initSelectableItems(_state) {
3672
+ return [
3673
+ { id: "wizard", label: "wizard", intent: { scene: "command", argv: ["init"], action: { kind: "command-run", label: "Run init wizard" } }, message: "full init wizard" },
3674
+ { id: "reconfigure", label: "reconfigure", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Reconfigure project" } }, message: "verify or rewrite generated config" },
3675
+ { id: "demo", label: "demo", intent: { scene: "command", argv: ["init", "--demo", "--repair"], action: { kind: "command-run", label: "Seed demo project" } }, message: "offline demo project" },
3676
+ { id: "remote", label: "remote", intent: { scene: "command", argv: ["init"], action: { kind: "command-run", label: "Run init remote wizard" } }, message: "choose remote server in init wizard" },
3677
+ { id: "auth", label: "auth", intent: { scene: "command", argv: ["github", "auth", "import-gh"], action: { kind: "command-run", label: "Import GitHub auth" } }, message: "import gh auth" },
3678
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "verify setup" }
3679
+ ];
3680
+ }
3681
+ var CLI_CATALOG_ACTIONS = [
3682
+ { id: "runs", label: "runs", argv: ["runs"], scene: "fleet", kind: "refresh", detail: "show live/completed runs" },
3683
+ { id: "tasks", label: "tasks", argv: ["tasks"], scene: "tasks", kind: "refresh", detail: "browse task source" },
3684
+ { id: "run-next", label: "run next", argv: ["task", "run", "--next"], detail: "dispatch next runnable task" },
3685
+ { id: "server", label: "server", argv: ["server", "status"], scene: "server", kind: "refresh", detail: "local/remote controls" },
3686
+ { id: "init", label: "init", argv: ["init"], detail: "setup/reconfigure/repair wizard" },
3687
+ { id: "doctor", label: "doctor", argv: ["doctor"], scene: "doctor", kind: "doctor-run", detail: "diagnose project/server/auth" },
3688
+ { id: "github", label: "github", argv: ["github"], detail: "GitHub auth/repo actions" },
3689
+ { id: "plugin", label: "plugin", argv: ["plugin"], detail: "plugin/source actions" },
3690
+ { id: "setup", label: "setup", argv: ["setup"], detail: "bootstrap/check/preflight" },
3691
+ { id: "inspect", label: "inspect", argv: ["inspect"], detail: "inspect project/server state" },
3692
+ { id: "stats", label: "stats", argv: ["stats"], detail: "run/task metrics" },
3693
+ { id: "repo", label: "repo", argv: ["repo"], detail: "repo utilities" },
3694
+ { id: "profile", label: "profile", argv: ["profile"], detail: "profile/config actions" },
3695
+ { id: "review", label: "review", argv: ["review"], detail: "review automation" },
3696
+ { id: "browser", label: "browser", argv: ["browser"], detail: "browser tooling/wizards" },
3697
+ { id: "pi", label: "pi", argv: ["pi"], detail: "Pi install/attach/status" },
3698
+ { id: "queue", label: "queue", argv: ["queue"], detail: "queue operations" },
3699
+ { id: "agent", label: "agent", argv: ["agent"], detail: "agent/runtime operations" },
3700
+ { id: "inspector", label: "inspector", argv: ["inspector"], detail: "inspector tools" },
3701
+ { id: "dist", label: "dist", argv: ["dist"], detail: "distribution helpers" },
3702
+ { id: "workspace", label: "workspace", argv: ["workspace"], detail: "workspace/project utilities" },
3703
+ { id: "remote", label: "remote", argv: ["remote"], detail: "remote endpoint utilities" },
3704
+ { id: "git", label: "git", argv: ["git"], detail: "git helper actions" },
3705
+ { id: "harness", label: "harness", argv: ["harness"], detail: "harness operations" },
3706
+ { id: "test", label: "test", argv: ["test"], detail: "test helpers" }
3707
+ ];
3708
+ function helpSelectableItems() {
3709
+ return CLI_CATALOG_ACTIONS.map((entry) => {
3710
+ const scene = entry.scene ?? "command";
3711
+ const kind = entry.kind ?? "command-run";
3712
+ return {
3713
+ id: entry.id,
3714
+ label: entry.label,
3715
+ intent: { scene, argv: entry.argv, action: { kind, label: `Open ${entry.label}` } },
3716
+ message: entry.detail
3717
+ };
2960
3718
  });
2961
3719
  }
3720
+ function handoffSelectableItems(state) {
3721
+ const runId = typeof state.data.selectedRunId === "string" ? state.data.selectedRunId : undefined;
3722
+ return runId ? [
3723
+ { id: "attach", label: "attach", intent: { scene: "handoff", argv: ["attach", runId], action: { kind: "run-attach", payload: { runId }, label: `Attach Pi ${runId.slice(0, 8)}` } }, message: "open bundled Pi" },
3724
+ { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["fleet"], action: { kind: "refresh", label: "Back to fleet" } }, message: "back to fleet" }
3725
+ ] : [
3726
+ { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["fleet"], action: { kind: "refresh", label: "Open runs" } }, message: "select a run first" },
3727
+ { id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Open tasks" } }, message: "open tasks" }
3728
+ ];
3729
+ }
3730
+ function errorSelectableItems() {
3731
+ return [
3732
+ { id: "main", label: "main", intent: { scene: "main", argv: [], action: { kind: "refresh", label: "Back to dashboard" } }, message: "back to dashboard" },
3733
+ { id: "doctor", label: "doctor", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "run diagnostics" },
3734
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "open server controls" },
3735
+ { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair project link" } }, message: "repair project link" }
3736
+ ];
3737
+ }
3738
+ function doctorSelectableItems() {
3739
+ return [
3740
+ { id: "run", label: "run", intent: { scene: "doctor", argv: ["doctor"], action: { kind: "doctor-run", label: "Run doctor" } }, message: "run diagnostics" },
3741
+ { id: "server", label: "server", intent: { scene: "server", argv: ["server", "status"], action: { kind: "refresh", label: "Open server controls" } }, message: "open server controls" },
3742
+ { id: "repair", label: "repair", intent: { scene: "command", argv: ["init", "--repair"], action: { kind: "command-run", label: "Repair project link" } }, message: "repair project link" },
3743
+ { id: "tasks", label: "tasks", intent: { scene: "tasks", argv: ["tasks"], action: { kind: "refresh", label: "Open tasks" } }, message: "open tasks" }
3744
+ ];
3745
+ }
2962
3746
  function runDetailSelectableItems(state) {
2963
3747
  const runId = selectedRunId(state);
2964
3748
  if (!runId)
@@ -2973,12 +3757,24 @@ function selectableItems(state) {
2973
3757
  switch (state.scene) {
2974
3758
  case "main":
2975
3759
  return mainSelectableItems(state);
3760
+ case "error":
3761
+ return errorSelectableItems();
3762
+ case "help":
3763
+ return helpSelectableItems();
2976
3764
  case "fleet":
2977
3765
  return fleetSelectableItems(state);
2978
3766
  case "tasks":
2979
3767
  return taskSelectableItems(state);
2980
3768
  case "inbox":
2981
3769
  return inboxSelectableItems(state);
3770
+ case "handoff":
3771
+ return handoffSelectableItems(state);
3772
+ case "server":
3773
+ return serverSelectableItems(state);
3774
+ case "init":
3775
+ return initSelectableItems(state);
3776
+ case "doctor":
3777
+ return doctorSelectableItems();
2982
3778
  case "run-detail":
2983
3779
  return runDetailSelectableItems(state);
2984
3780
  default:
@@ -3058,13 +3854,18 @@ async function launchRigOpenTuiApp(options) {
3058
3854
  let renderer = null;
3059
3855
  let graphics = null;
3060
3856
  let imageVisuals = null;
3857
+ let preloaderBackdrop = null;
3061
3858
  const scrollPanels = [];
3062
3859
  let typeBar = null;
3860
+ let brandText = null;
3063
3861
  const textLines = [];
3064
- let tickTimer = null;
3862
+ let renderTimer = null;
3065
3863
  let destroyed = false;
3066
3864
  let runnerPromise = null;
3067
3865
  let renderedSelectableItems = [];
3866
+ const preloaderTransition = { scene: null, startedAtMs: 0, minUntilMs: 0 };
3867
+ const resources = new RuntimeResources;
3868
+ const focusManager = new AppFocusManager;
3068
3869
  const emitAsyncError = (label, error) => {
3069
3870
  const normalized = normalizeError(error);
3070
3871
  events.emit({ type: "action.failed", label, message: normalized.message, hint: normalized.hint, cause: normalized.cause });
@@ -3072,6 +3873,23 @@ async function launchRigOpenTuiApp(options) {
3072
3873
  const runAppAction = (label, action) => {
3073
3874
  action().catch((error) => emitAsyncError(label, error));
3074
3875
  };
3876
+ const renderApp = () => {
3877
+ if (!renderer || destroyed)
3878
+ return;
3879
+ const currentState = store.getState();
3880
+ renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, preloaderBackdrop, scrollPanels, typeBar, brandText, textLines, currentState, options.sceneRenderers, preloaderTransition, scheduleAppRender);
3881
+ focusManager.restoreTypeBarFocus(typeBar, { hidden: currentState.scene === "handoff" });
3882
+ };
3883
+ const scheduleAppRender = (delayMs = 0) => {
3884
+ if (destroyed)
3885
+ return;
3886
+ if (renderTimer)
3887
+ return;
3888
+ renderTimer = resources.setTimeout(() => {
3889
+ renderTimer = null;
3890
+ renderApp();
3891
+ }, Math.max(0, delayMs));
3892
+ };
3075
3893
  const runtime = {
3076
3894
  getState: () => store.getState(),
3077
3895
  patchState: (patch) => store.patch(patch),
@@ -3098,29 +3916,35 @@ async function launchRigOpenTuiApp(options) {
3098
3916
  },
3099
3917
  getRunnerContext: async () => {
3100
3918
  if (!options.initializeRuntime)
3101
- throw new Error("Rig runtime initializer was not provided to the OpenTUI app.");
3919
+ throw new Error("Runtime initializer was not provided to the OpenTUI app.");
3102
3920
  runnerPromise ??= options.initializeRuntime();
3103
3921
  return runnerPromise;
3104
3922
  },
3105
3923
  suspend: () => renderer?.suspend(),
3106
3924
  resume: () => {
3107
3925
  renderer?.resume();
3108
- if (renderer && !destroyed)
3109
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3926
+ renderApp();
3110
3927
  },
3111
3928
  destroy: () => renderer?.destroy()
3112
3929
  };
3113
- events.subscribe((event) => store.patch(reduceAppEvent(store.getState(), event)));
3114
- store.subscribe(() => {
3115
- if (!destroyed && renderer)
3116
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3117
- });
3930
+ resources.add(events.subscribe((event) => store.patch(reduceAppEvent(store.getState(), event))));
3931
+ resources.add(store.subscribe(() => {
3932
+ scheduleAppRender(0);
3933
+ }));
3934
+ resources.setInterval(() => {
3935
+ const state = store.getState();
3936
+ if (state.scene === "handoff" && getActivePiHost())
3937
+ return;
3938
+ if (state.scene === "command" && getActiveCommandHost())
3939
+ return;
3940
+ store.patch({ tick: state.tick + 1 });
3941
+ }, 96);
3118
3942
  try {
3119
3943
  renderer = await createCliRenderer({
3120
3944
  screenMode: "alternate-screen",
3121
3945
  exitOnCtrlC: false,
3122
- targetFps: 60,
3123
- maxFps: 60,
3946
+ targetFps: 30,
3947
+ maxFps: 30,
3124
3948
  useMouse: true,
3125
3949
  autoFocus: false,
3126
3950
  useKittyKeyboard: {
@@ -3132,24 +3956,41 @@ async function launchRigOpenTuiApp(options) {
3132
3956
  openConsoleOnError: false,
3133
3957
  backgroundColor: "transparent"
3134
3958
  });
3135
- renderer.setTerminalTitle?.("Rig \u2014 operator cockpit");
3959
+ renderer.setTerminalTitle?.("Operator cockpit");
3136
3960
  const layout = computeStageLayout(renderer.width, renderer.height);
3137
- imageVisuals = forceModernImageVisuals() ? new ImageVisualLayer(process.stdout) : null;
3961
+ imageVisuals = forceModernImageVisuals() ? new ImageVisualLayer(process.stdout, () => scheduleAppRender(0)) : null;
3138
3962
  if (imageVisuals?.enabled) {
3139
- renderer.setFrameCallback(async () => {
3963
+ const flushImageVisuals = async () => {
3140
3964
  imageVisuals?.flush();
3141
- });
3965
+ };
3966
+ renderer.setFrameCallback(flushImageVisuals);
3967
+ resources.add(() => renderer?.removeFrameCallback(flushImageVisuals));
3142
3968
  }
3143
3969
  if (!imageVisuals?.enabled) {
3144
3970
  graphics = createGraphicsLayer(renderer, layout);
3145
3971
  renderer.root.add(graphics);
3146
3972
  }
3973
+ preloaderBackdrop = new BoxRenderable2(renderer, {
3974
+ id: "rig-preloader-backdrop",
3975
+ position: "absolute",
3976
+ left: 0,
3977
+ top: 0,
3978
+ width: layout.width,
3979
+ height: layout.height,
3980
+ backgroundColor: PRELOADER_BACKDROP,
3981
+ border: false,
3982
+ zIndex: 100,
3983
+ visible: false
3984
+ });
3985
+ renderer.root.add(preloaderBackdrop);
3147
3986
  for (let index = 0;index < MAX_SCENE_PANELS; index += 1) {
3148
3987
  const panel = createScrollPanelLayer(renderer, `rig-scroll-panel-${index}`, (line2) => {
3149
3988
  const selectableIndex = line2.selectableIndex;
3150
3989
  if (selectableIndex === undefined)
3151
3990
  return;
3152
3991
  selectIndex(store, selectableIndex, renderedSelectableItems);
3992
+ if (line2.activateOnClick)
3993
+ runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3153
3994
  });
3154
3995
  scrollPanels.push(panel);
3155
3996
  renderer.root.add(panel);
@@ -3160,146 +4001,84 @@ async function launchRigOpenTuiApp(options) {
3160
4001
  if (selectableIndex === undefined)
3161
4002
  return;
3162
4003
  selectIndex(store, selectableIndex, renderedSelectableItems);
4004
+ if (line2.activateOnClick)
4005
+ runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3163
4006
  });
3164
4007
  textLines.push(textLine);
3165
4008
  renderer.root.add(textLine);
3166
4009
  }
3167
- typeBar = createTypeBar(renderer);
3168
- renderer.root.add(typeBar.prefix);
3169
- renderer.root.add(typeBar.input);
3170
- renderer.root.add(typeBar.footer);
3171
- bindTypeBar(typeBar, {
3172
- onInput(value) {
3173
- const scene = store.getState().scene;
3174
- events.emit({
3175
- type: "typebar.patch",
3176
- typeBar: {
3177
- value,
3178
- ...isListSearchScene(scene) ? { message: value.trim() ? `search ${JSON.stringify(value.trim())}` : undefined } : {}
3179
- }
3180
- });
3181
- },
3182
- onEnter(value) {
3183
- const scene = store.getState().scene;
3184
- if (isListSearchScene(scene) && value.trim()) {
3185
- if (renderedSelectableItems.length === 0) {
3186
- events.emit({ type: "typebar.patch", typeBar: { message: "no search match" } });
3187
- return;
3188
- }
3189
- const snapshot = renderedSelectableItems;
3190
- typeBar?.input && (typeBar.input.value = "");
3191
- events.emit({ type: "typebar.patch", typeBar: { value: "" } });
3192
- runAppAction("Activate search result", () => activateSelection(runtime, snapshot));
3193
- return;
3194
- }
3195
- typeBar?.input && (typeBar.input.value = "");
3196
- events.emit({ type: "typebar.patch", typeBar: { value: "" } });
3197
- if (value.trim() === "") {
3198
- runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3199
- return;
3200
- }
3201
- runAppAction("Run command", () => runtime.runIntent(intentFromTypeBar(value)));
3202
- }
4010
+ brandText = new TextRenderable3(renderer, {
4011
+ id: "brand-mark",
4012
+ content: "RIG",
4013
+ position: "absolute",
4014
+ left: layout.centerLeft,
4015
+ top: Math.max(0, layout.centerTop - 1),
4016
+ fg: RIG_UI.lime,
4017
+ zIndex: 8,
4018
+ selectable: false
3203
4019
  });
3204
- renderer.keyInput.on("keypress", (key) => {
3205
- const host = getActivePiHost();
3206
- if (host && store.getState().scene === "handoff") {
3207
- if (key.ctrl && key.name === "]") {
3208
- stopActivePiHost("operator detach");
3209
- events.emit({ type: "typebar.patch", typeBar: { value: "", message: "detached from Pi" } });
3210
- runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
3211
- return;
3212
- }
3213
- const sequence = key.raw || key.sequence;
3214
- if (sequence)
3215
- host.write(sequence);
3216
- if (typeBar)
3217
- typeBar.input.value = "";
3218
- return;
3219
- }
3220
- if (key.ctrl && key.name === "c") {
3221
- renderer?.destroy();
3222
- return;
3223
- }
3224
- const currentScene = store.getState().scene;
3225
- const searchScene = isListSearchScene(currentScene);
3226
- const inputValue = typeBar?.input.value ?? "";
3227
- const typeBarEmpty = inputValue.trim() === "";
3228
- if (typeBarEmpty && (key.name === "right" || key.name === "tab")) {
3229
- runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, 1))));
3230
- return;
3231
- }
3232
- if (typeBarEmpty && key.name === "left") {
3233
- runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, -1))));
3234
- return;
3235
- }
3236
- if (!searchScene && typeBarEmpty && key.name === "r") {
3237
- runAppAction("Opening runs", () => runtime.runIntent(primaryNavIntent("fleet")));
3238
- return;
3239
- }
3240
- if (!searchScene && typeBarEmpty && key.name === "t") {
3241
- runAppAction("Opening tasks", () => runtime.runIntent(primaryNavIntent("tasks")));
3242
- return;
3243
- }
3244
- if (!searchScene && typeBarEmpty && (key.name === "h" || key.sequence === "?")) {
3245
- runAppAction("Opening help", () => runtime.runIntent(primaryNavIntent("help")));
3246
- return;
3247
- }
3248
- if (searchScene && key.name === "up" || !searchScene && typeBarEmpty && (key.name === "up" || key.name === "k")) {
3249
- if (moveSelection(store, -1, renderedSelectableItems))
4020
+ renderer.root.add(brandText);
4021
+ typeBar = null;
4022
+ const onKeyPress = (key) => {
4023
+ handleAppKeyPress({
4024
+ renderer: () => renderer,
4025
+ runtime,
4026
+ getState: () => store.getState(),
4027
+ getTypeBar: () => typeBar,
4028
+ getRenderedSelectableItems: () => renderedSelectableItems,
4029
+ emitTypeBarPatch: (typeBarPatch) => events.emit({ type: "typebar.patch", typeBar: typeBarPatch }),
4030
+ runAppAction,
4031
+ primaryNavIntent,
4032
+ nextPrimaryScene,
4033
+ moveSelection: (delta, items) => moveSelection(store, delta, items),
4034
+ activateSelection: (items) => activateSelection(runtime, items)
4035
+ }, key);
4036
+ };
4037
+ renderer.keyInput.on("keypress", onKeyPress);
4038
+ resources.add(() => renderer?.keyInput.off("keypress", onKeyPress));
4039
+ const onPaste = (event) => {
4040
+ const state = store.getState();
4041
+ if (state.scene === "handoff") {
4042
+ const host = getActivePiHost();
4043
+ if (!host)
3250
4044
  return;
3251
- }
3252
- if (searchScene && key.name === "down" || !searchScene && typeBarEmpty && (key.name === "down" || key.name === "j")) {
3253
- if (moveSelection(store, 1, renderedSelectableItems))
4045
+ host.write(event.bytes);
4046
+ } else if (state.scene === "command") {
4047
+ const host = getActiveCommandHost();
4048
+ if (!host)
3254
4049
  return;
4050
+ host.write(event.bytes);
3255
4051
  }
3256
- if (typeBarEmpty && (key.name === "return" || key.name === "enter")) {
3257
- runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3258
- return;
3259
- }
3260
- if (!searchScene && key.name === "q" && typeBarEmpty) {
3261
- renderer?.destroy();
3262
- return;
3263
- }
3264
- if (key.name === "escape") {
3265
- if (inputValue) {
3266
- if (typeBar)
3267
- typeBar.input.value = "";
3268
- events.emit({ type: "typebar.patch", typeBar: { value: "", message: undefined } });
3269
- } else {
3270
- runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
3271
- }
3272
- }
3273
- });
3274
- renderer.keyInput.on("paste", (event) => {
3275
- const host = getActivePiHost();
3276
- if (!host || store.getState().scene !== "handoff")
3277
- return;
3278
- host.write(event.bytes);
3279
- if (typeBar)
3280
- typeBar.input.value = "";
3281
- });
3282
- renderer.on("selection", (selection) => {
4052
+ };
4053
+ renderer.keyInput.on("paste", onPaste);
4054
+ resources.add(() => renderer?.keyInput.off("paste", onPaste));
4055
+ const onSelection = (selection) => {
3283
4056
  const selectedText = selection.getSelectedText();
3284
4057
  if (!selectedText)
3285
4058
  return;
3286
4059
  renderer?.copyToClipboardOSC52?.(selectedText);
3287
4060
  events.emit({ type: "typebar.patch", typeBar: { message: "copied selection" } });
3288
- });
3289
- renderer.on("resize", () => {
3290
- if (renderer)
3291
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3292
- });
3293
- renderer.on("destroy", () => {
4061
+ };
4062
+ renderer.on("selection", onSelection);
4063
+ resources.add(() => renderer?.off("selection", onSelection));
4064
+ const onResize = () => {
4065
+ renderApp();
4066
+ };
4067
+ renderer.on("resize", onResize);
4068
+ resources.add(() => renderer?.off("resize", onResize));
4069
+ const onDestroy = () => {
3294
4070
  destroyed = true;
3295
4071
  stopActivePiHost("renderer destroyed");
4072
+ stopActiveCommandHost("renderer destroyed");
3296
4073
  imageVisuals?.destroy();
3297
- if (tickTimer)
3298
- clearInterval(tickTimer);
3299
- tickTimer = null;
3300
- });
3301
- renderer.requestLive?.();
3302
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
4074
+ if (renderTimer)
4075
+ clearTimeout(renderTimer);
4076
+ renderTimer = null;
4077
+ resources.disposeAll();
4078
+ };
4079
+ renderer.on("destroy", onDestroy);
4080
+ resources.add(() => renderer?.off("destroy", onDestroy));
4081
+ renderApp();
3303
4082
  requestRender(renderer);
3304
4083
  queueMicrotask(() => {
3305
4084
  (async () => {
@@ -3317,46 +4096,77 @@ async function launchRigOpenTuiApp(options) {
3317
4096
  }
3318
4097
  })();
3319
4098
  });
3320
- tickTimer = setInterval(() => {
3321
- if (destroyed)
3322
- return;
3323
- const state = store.getState();
3324
- if (state.scene === "handoff" && getActivePiHost())
3325
- return;
3326
- store.patch({ tick: state.tick + 1 });
3327
- }, FRAME_MS);
3328
- await new Promise((resolveDestroy) => renderer?.on("destroy", resolveDestroy));
4099
+ await new Promise((resolveDestroy) => {
4100
+ renderer?.on("destroy", resolveDestroy);
4101
+ resources.add(() => renderer?.off("destroy", resolveDestroy));
4102
+ });
3329
4103
  } finally {
3330
4104
  destroyed = true;
3331
- if (tickTimer)
3332
- clearInterval(tickTimer);
3333
- tickTimer = null;
4105
+ if (renderTimer)
4106
+ clearTimeout(renderTimer);
4107
+ renderTimer = null;
4108
+ resources.disposeAll();
3334
4109
  imageVisuals?.destroy();
3335
4110
  stopActivePiHost("app shutdown");
3336
- try {
3337
- renderer?.dropLive?.();
3338
- } catch {}
4111
+ stopActiveCommandHost("app shutdown");
3339
4112
  if (renderer && !renderer.isDestroyed)
3340
4113
  renderer.destroy();
3341
4114
  }
3342
4115
  }
3343
- function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, state, renderers) {
4116
+ function renderNow(renderer, graphics, imageVisuals, preloaderBackdrop, scrollPanels, typeBar, brandText, textLines, state, renderers, preloaderTransition, scheduleRenderAfter) {
3344
4117
  const layout = computeStageLayout(renderer.width, renderer.height);
3345
4118
  const animationTick = Math.floor(state.tick / ANIMATION_SLOWDOWN);
3346
4119
  const renderState = animationTick === state.tick ? state : { ...state, tick: animationTick };
3347
- const frame = rendererForScene(renderState.scene, renderers)(renderState, layout);
3348
- if (frame.terminalActive) {
4120
+ const now = Date.now();
4121
+ const sceneJustChanged = preloaderTransition.scene !== renderState.scene;
4122
+ maybeStartPreloaderTransition(preloaderTransition, renderState.scene, now);
4123
+ const targetFrame = rendererForScene(renderState.scene, renderers)(renderState, layout);
4124
+ const targetContentLayout = targetFrame.fullScreen ? { ...layout, centerWidth: layout.width, centerLeft: 0, centerTop: 0, centerHeight: Math.min(layout.height, MAX_SCENE_LINES) } : layout;
4125
+ const targetPanelsForImages = (targetFrame.panels ?? []).map((panel) => ({ ...panel }));
4126
+ let targetVisualReady = true;
4127
+ if (targetFrame.terminalActive) {
3349
4128
  getActivePiHost()?.resize(layout.width, Math.max(1, layout.height));
3350
4129
  }
4130
+ if (renderState.scene === "command") {
4131
+ getActiveCommandHost()?.resize(Math.max(40, targetContentLayout.centerWidth - 12), Math.max(10, targetContentLayout.centerHeight - 6));
4132
+ }
4133
+ if (imageVisuals && targetFrame.terminalActive) {
4134
+ imageVisuals.clear();
4135
+ } else if (imageVisuals && !targetFrame.fullScreen && !sceneJustChanged) {
4136
+ const visualInput = {
4137
+ layout: targetContentLayout,
4138
+ pixelSize: pixelSizeForRenderer(renderer),
4139
+ panels: targetPanelsForImages,
4140
+ scene: state.scene,
4141
+ tick: state.tick
4142
+ };
4143
+ imageVisuals.render(visualInput);
4144
+ targetVisualReady = imageVisuals.isReady(visualInput);
4145
+ }
4146
+ const preloaderActive = sceneJustChanged || isPreloaderActive(preloaderTransition, targetVisualReady, now);
4147
+ imageVisuals?.setFlushPaused(preloaderActive);
4148
+ const frame = preloaderActive ? renderAsciiPreloaderFrame(renderState.scene, layout, state.tick) : targetFrame;
3351
4149
  if (graphics) {
3352
4150
  resizeGraphicsLayer(graphics, layout);
3353
- if (frame.terminalActive || frame.fullScreen)
4151
+ if (targetFrame.terminalActive || targetFrame.fullScreen || preloaderActive)
3354
4152
  clearGraphicsLayer(graphics);
3355
4153
  else
3356
4154
  drawAmbientField(graphics, layout, animationTick, state.scene);
3357
4155
  }
3358
- if (imageVisuals && (frame.terminalActive || frame.fullScreen)) {
3359
- imageVisuals.clear();
4156
+ if (preloaderBackdrop) {
4157
+ preloaderBackdrop.visible = preloaderActive;
4158
+ preloaderBackdrop.left = 0;
4159
+ preloaderBackdrop.top = 0;
4160
+ preloaderBackdrop.width = layout.width;
4161
+ preloaderBackdrop.height = layout.height;
4162
+ preloaderBackdrop.zIndex = 100;
4163
+ preloaderBackdrop.backgroundColor = PRELOADER_BACKDROP;
4164
+ }
4165
+ if (brandText) {
4166
+ brandText.visible = !targetFrame.terminalActive && !preloaderActive;
4167
+ brandText.content = "RIG";
4168
+ brandText.left = layout.centerLeft;
4169
+ brandText.top = Math.max(0, layout.centerTop - 1);
3360
4170
  }
3361
4171
  const contentLayout = frame.fullScreen ? { ...layout, centerWidth: layout.width, centerLeft: 0, centerTop: 0, centerHeight: Math.min(layout.height, MAX_SCENE_LINES) } : layout;
3362
4172
  const sceneLines = fitSceneLines(frame.lines, contentLayout);
@@ -3382,18 +4192,13 @@ function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, text
3382
4192
  ...panel,
3383
4193
  lines: panel.lines.map(remapSelectableLine)
3384
4194
  }));
3385
- if (imageVisuals && !frame.terminalActive && !frame.fullScreen) {
3386
- imageVisuals.render({
3387
- layout: contentLayout,
3388
- pixelSize: pixelSizeForRenderer(renderer),
3389
- panels: renderedPanels,
3390
- scene: state.scene,
3391
- tick: state.tick
3392
- });
3393
- }
3394
4195
  if (graphics && !frame.terminalActive)
3395
4196
  drawPanelChrome(graphics, contentLayout, renderedPanels);
3396
- renderedSceneLines.forEach((entry, index) => applyTextLine(textLines[index], entry, contentLayout.centerTop + index, contentLayout.centerLeft, contentLayout.centerWidth));
4197
+ renderedSceneLines.forEach((entry, index) => {
4198
+ const textLine = textLines[index];
4199
+ textLine.zIndex = preloaderActive ? 102 : 5;
4200
+ applyTextLine(textLine, entry, contentLayout.centerTop + index, contentLayout.centerLeft, contentLayout.centerWidth);
4201
+ });
3397
4202
  clearTextLines(textLines, renderedSceneLines.length);
3398
4203
  applyScrollPanels(scrollPanels, contentLayout, renderedPanels);
3399
4204
  if (typeBar) {
@@ -3407,6 +4212,10 @@ function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, text
3407
4212
  }
3408
4213
  }
3409
4214
  requestRender(renderer);
4215
+ if (preloaderActive) {
4216
+ const minDelay = Math.max(0, preloaderTransition.minUntilMs - Date.now());
4217
+ scheduleRenderAfter?.(sceneJustChanged ? 0 : Math.max(32, Math.min(160, minDelay || 96)));
4218
+ }
3410
4219
  return renderedSelectableItems;
3411
4220
  }
3412
4221
  export {