@h-rig/cli 0.0.6-alpha.81 → 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 +2807 -1443
  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 +102 -36
  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 +13776 -12369
  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 +3806 -2878
  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 +15 -13
  33. package/dist/src/app-opentui/registry.js +3228 -2396
  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 +413 -958
  39. package/dist/src/app-opentui/render/image-visual-layer.d.ts +19 -10
  40. package/dist/src/app-opentui/render/image-visual-layer.js +391 -357
  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 +2329 -1512
  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 -1480
@@ -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,52 +2490,43 @@ 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 = [];
2504
+ imagesVisible = false;
2505
+ flushPaused = false;
1614
2506
  requestId = 0;
1615
- lastMotionFrameIndex = -1;
1616
- lastPanelFrameIndex = -1;
1617
- lastChromeKey = "";
2507
+ lastBackgroundKey = "";
2508
+ fatalWorkerError = null;
1618
2509
  destroyed = false;
1619
- constructor(stdout = process.stdout) {
2510
+ constructor(stdout = process.stdout, onFrameReady) {
1620
2511
  this.stdout = stdout;
1621
- this.protocol = getCapabilities().images;
1622
- if (this.enabled) {
1623
- this.startWorker(this.motionSlot);
1624
- this.startWorker(this.panelSlot);
1625
- this.startWorker(this.chromeSlot);
1626
- }
2512
+ this.onFrameReady = onFrameReady;
2513
+ this.protocol = getRigTerminalCapabilities().imageProtocol;
1627
2514
  }
1628
2515
  get enabled() {
1629
- return isModernImageTerminal(this.protocol);
2516
+ return this.protocol === "kitty";
1630
2517
  }
1631
2518
  clear() {
1632
2519
  if (this.enabled) {
1633
- this.stdout.write([this.imageId, ...this.panelImageIds, ...this.panelChromeImageIds].map((id) => deleteKittyImage(id)).join("") + "\x1B[?25h");
2520
+ if (this.imagesVisible)
2521
+ this.stdout.write([this.imageId, ...this.panelImageIds, ...this.panelChromeImageIds].map((id) => deleteKittyImage(id)).join("") + "\x1B[?25h");
2522
+ this.imagesVisible = false;
1634
2523
  this.pendingFrames.clear();
1635
2524
  this.recentWrittenKeys.length = 0;
1636
2525
  this.resetSlot(this.motionSlot);
1637
- this.resetSlot(this.panelSlot);
1638
- this.resetSlot(this.chromeSlot);
1639
- this.lastMotionFrameIndex = -1;
1640
- this.lastPanelFrameIndex = -1;
1641
- this.lastChromeKey = "";
2526
+ this.lastBackgroundKey = "";
1642
2527
  this.outputBudgetBytes = this.outputBytesPerSecond;
1643
2528
  this.lastBudgetAtMs = Date.now();
1644
2529
  this.motionSlot.clearedThroughRequestId = this.requestId;
1645
- this.panelSlot.clearedThroughRequestId = this.requestId;
1646
- this.chromeSlot.clearedThroughRequestId = this.requestId;
1647
2530
  }
1648
2531
  }
1649
2532
  destroy() {
@@ -1651,19 +2534,31 @@ class ImageVisualLayer {
1651
2534
  return;
1652
2535
  this.clear();
1653
2536
  this.destroyed = true;
1654
- const workers = [this.motionSlot.worker, this.panelSlot.worker, this.chromeSlot.worker];
2537
+ const workers = [this.motionSlot.worker];
1655
2538
  this.motionSlot.worker = null;
1656
- this.panelSlot.worker = null;
1657
- this.chromeSlot.worker = null;
1658
2539
  for (const worker of workers)
1659
2540
  if (worker)
1660
2541
  worker.terminate().catch(() => {
1661
2542
  return;
1662
2543
  });
1663
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
+ }
1664
2555
  render(input) {
1665
2556
  if (!this.enabled || this.destroyed)
1666
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();
1667
2562
  const width = Math.max(1, Math.floor(input.pixelSize.width));
1668
2563
  const height = Math.max(1, Math.floor(input.pixelSize.height));
1669
2564
  if (width * height * 4 > MAX_IMAGE_BYTES)
@@ -1671,54 +2566,32 @@ class ImageVisualLayer {
1671
2566
  if (this.isOutputBackedUp())
1672
2567
  return;
1673
2568
  this.refillOutputBudget();
1674
- const now = Date.now();
1675
- const visualTick = Math.floor(now / (1000 / 60));
1676
- const useDynamicSplit = Boolean(this.motionSlot.worker && this.panelSlot.worker && this.chromeSlot.worker && this.transport === "png" && process.env.RIG_OPENTUI_DYNAMIC_SPLIT !== "0");
1677
- if (!useDynamicSplit) {
1678
- const frameIndex = Math.floor(now / this.frameIntervalMs);
1679
- if (!input.force && frameIndex === this.lastMotionFrameIndex)
1680
- return;
1681
- this.scheduleRender(this.motionSlot, { ...input, tick: visualTick, layer: "full" }, this.imageId, -10, input.force);
1682
- this.lastMotionFrameIndex = frameIndex;
1683
- return;
1684
- }
1685
- const motionFrameIndex = Math.floor(now / this.frameIntervalMs);
1686
- if ((input.force || motionFrameIndex !== this.lastMotionFrameIndex) && this.outputBudgetBytes >= MIN_BACKGROUND_BUDGET_BYTES) {
1687
- this.scheduleRender(this.motionSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, droneLayerScale(), input.layout), panels: [], tick: visualTick, layer: "background" }, this.imageId, -30, input.force);
1688
- this.lastMotionFrameIndex = motionFrameIndex;
1689
- }
1690
- const panelFrameIndex = Math.floor(now / this.panelIntervalMs);
1691
- if ((input.force || panelFrameIndex !== this.lastPanelFrameIndex) && this.outputBudgetBytes >= MIN_PANEL_BUDGET_BYTES) {
1692
- this.scheduleRender(this.panelSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, panelLayerScale(), input.layout), tick: visualTick, layer: "panel-patches" }, this.imageId, -12, input.force, this.panelImageIds);
1693
- this.lastPanelFrameIndex = panelFrameIndex;
1694
- }
1695
- const chromeInput = { ...input, tick: 0, layer: "panel-chrome" };
1696
- const chromeKey = imageVisualFrameKey(chromeInput);
1697
- if (input.force || chromeKey !== this.lastChromeKey) {
1698
- this.scheduleRender(this.chromeSlot, chromeInput, this.imageId, -8, input.force, this.panelChromeImageIds);
1699
- 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;
1700
2574
  }
1701
2575
  }
1702
2576
  flush() {
2577
+ this.throwIfFatal();
2578
+ if (this.flushPaused)
2579
+ return false;
1703
2580
  if (this.pendingFrames.size === 0)
1704
2581
  return false;
1705
2582
  this.refillOutputBudget();
1706
2583
  if (this.isOutputBackedUp()) {
1707
- const chrome = this.pendingFrames.get("panel-chrome");
2584
+ const retainedSplit = this.pendingFrames.get("split");
1708
2585
  this.pendingFrames.clear();
1709
- if (chrome)
1710
- this.pendingFrames.set("panel-chrome", chrome);
2586
+ if (retainedSplit)
2587
+ this.pendingFrames.set("split", retainedSplit);
1711
2588
  return false;
1712
2589
  }
1713
2590
  const framePriority = (frame) => {
1714
2591
  const layer = pendingLayerKey(frame.key);
1715
- if (layer === "panel-chrome")
1716
- return 0;
1717
- if (layer === "background")
2592
+ if (layer === "split")
1718
2593
  return 1;
1719
- if (layer === "panel-patches")
1720
- return 2;
1721
- return 2;
2594
+ return 4;
1722
2595
  };
1723
2596
  const frames = Array.from(this.pendingFrames.values()).sort((a, b) => framePriority(a) - framePriority(b));
1724
2597
  const prefix = "\x1B[?25l\x1B7\x1B[s\x1B[H";
@@ -1730,12 +2603,14 @@ class ImageVisualLayer {
1730
2603
  for (const frame of frames) {
1731
2604
  const candidate = sequence + frame.sequence;
1732
2605
  const candidateBytes = Buffer.byteLength(prefix + candidate + suffix);
1733
- if (candidateBytes <= this.outputBudgetBytes) {
2606
+ const layer = pendingLayerKey(frame.key);
2607
+ const allowOneStaticFrame = selected.length === 0 && layer === "split";
2608
+ if (candidateBytes <= this.outputBudgetBytes || allowOneStaticFrame) {
1734
2609
  selected.push(frame);
1735
2610
  sequence = candidate;
1736
2611
  byteLength = candidateBytes;
1737
- } else if (pendingLayerKey(frame.key) === "panel-chrome") {
1738
- retained.set(pendingLayerKey(frame.key), frame);
2612
+ } else if (layer === "split") {
2613
+ retained.set(layer, frame);
1739
2614
  }
1740
2615
  }
1741
2616
  this.pendingFrames.clear();
@@ -1750,10 +2625,11 @@ class ImageVisualLayer {
1750
2625
  this.stdout.cork?.();
1751
2626
  const accepted = this.stdout.write(payload);
1752
2627
  this.stdout.uncork?.();
2628
+ this.imagesVisible = true;
1753
2629
  return accepted;
1754
2630
  }
1755
2631
  createSlot() {
1756
- 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 };
1757
2633
  }
1758
2634
  resetSlot(slot) {
1759
2635
  slot.busy = false;
@@ -1781,7 +2657,23 @@ class ImageVisualLayer {
1781
2657
  isKnownKey(slot, key) {
1782
2658
  return this.recentWrittenKeys.includes(key) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === key) || key === slot.inFlightKey || key === slot.queuedKey;
1783
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
+ }
1784
2675
  scheduleRender(slot, input, imageId, zIndex, force, imageIds) {
2676
+ this.throwIfFatal();
1785
2677
  const key = imageVisualFrameKey(input);
1786
2678
  if (!force && this.isKnownKey(slot, key))
1787
2679
  return;
@@ -1794,38 +2686,41 @@ class ImageVisualLayer {
1794
2686
  requestId: ++this.requestId,
1795
2687
  key
1796
2688
  };
1797
- if (slot.worker) {
1798
- this.enqueueWorkerRender(slot, request);
2689
+ slot.latestRequestId = request.requestId;
2690
+ if (!slot.worker) {
2691
+ this.renderInline(slot, request);
1799
2692
  return;
1800
2693
  }
1801
- this.renderSync(slot, request);
2694
+ this.enqueueWorkerRender(slot, request);
1802
2695
  }
1803
- startWorker(slot) {
2696
+ startWorker(slot, name) {
1804
2697
  const worker = createImageWorker();
1805
2698
  if (!worker)
1806
2699
  return;
1807
2700
  slot.worker = worker;
1808
2701
  worker.on("message", (message) => this.handleWorkerMessage(slot, message));
1809
- worker.once("error", () => this.disableWorker(slot));
1810
- 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
+ });
1811
2710
  }
1812
- disableWorker(slot) {
1813
- if (this.destroyed)
1814
- return;
1815
- slot.worker = null;
1816
- slot.busy = false;
1817
- slot.inFlightKey = "";
1818
- const queued = slot.queuedRequest;
1819
- slot.queuedRequest = null;
1820
- slot.queuedKey = "";
1821
- if (queued?.layer === "full")
1822
- 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
+ }
1823
2721
  }
1824
2722
  enqueueWorkerRender(slot, request) {
1825
- if (!slot.worker) {
1826
- this.renderSync(slot, request);
1827
- return;
1828
- }
2723
+ this.throwIfFatal();
1829
2724
  if (slot.busy) {
1830
2725
  slot.queuedRequest = request;
1831
2726
  slot.queuedKey = request.key;
@@ -1834,13 +2729,16 @@ class ImageVisualLayer {
1834
2729
  this.dispatchWorkerRender(slot, request);
1835
2730
  }
1836
2731
  dispatchWorkerRender(slot, request) {
1837
- if (!slot.worker) {
1838
- this.renderSync(slot, request);
1839
- return;
2732
+ this.throwIfFatal();
2733
+ if (!slot.worker)
2734
+ this.failWorker(String(request.layer ?? "visual"), new Error("worker is unavailable"));
2735
+ slot.busy = true;
2736
+ slot.inFlightKey = request.key;
2737
+ try {
2738
+ slot.worker.postMessage(request);
2739
+ } catch (error) {
2740
+ this.failWorker(String(request.layer ?? "visual"), error);
1840
2741
  }
1841
- slot.busy = true;
1842
- slot.inFlightKey = request.key;
1843
- slot.worker.postMessage(request);
1844
2742
  }
1845
2743
  handleWorkerMessage(slot, message) {
1846
2744
  if (this.destroyed || !message || typeof message !== "object")
@@ -1848,14 +2746,14 @@ class ImageVisualLayer {
1848
2746
  const response = message;
1849
2747
  slot.busy = false;
1850
2748
  slot.inFlightKey = "";
1851
- if (response.requestId <= slot.clearedThroughRequestId)
1852
- return;
1853
- if (response.error) {
1854
- this.disableWorker(slot);
1855
- return;
1856
- }
1857
- if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "panel-chrome")) {
1858
- 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
+ }
1859
2757
  }
1860
2758
  const next = slot.queuedRequest;
1861
2759
  slot.queuedRequest = null;
@@ -1863,12 +2761,6 @@ class ImageVisualLayer {
1863
2761
  if (next && !this.isKnownKey(slot, next.key))
1864
2762
  this.dispatchWorkerRender(slot, next);
1865
2763
  }
1866
- renderSync(slot, request) {
1867
- const result = renderImageVisualFrame(request);
1868
- if (!result || this.isKnownKey(slot, result.key) || this.isOutputBackedUp())
1869
- return;
1870
- this.stageFrame(result);
1871
- }
1872
2764
  }
1873
2765
  function pixelSizeForRenderer(renderer) {
1874
2766
  const resolution = renderer.resolution;
@@ -1880,11 +2772,12 @@ function pixelSizeForRenderer(renderer) {
1880
2772
  };
1881
2773
  }
1882
2774
  function forceModernImageVisuals() {
1883
- return isModernImageTerminal(getCapabilities().images);
2775
+ return getRigTerminalCapabilities().supportsKittyImages;
1884
2776
  }
1885
2777
 
1886
2778
  // packages/cli/src/app-opentui/render/text.ts
1887
- 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);
1888
2781
  function selectableMouseHandler(currentLineRef, onLineMouseDown) {
1889
2782
  return (event) => {
1890
2783
  const currentLine = currentLineRef();
@@ -1935,7 +2828,8 @@ function applyTextLine(renderable, line, top, left, width) {
1935
2828
  renderable.width = width;
1936
2829
  renderable.content = line?.styledText ?? line?.text ?? "";
1937
2830
  renderable.fg = line?.fg ?? RIG_UI.ink2;
1938
- 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;
1939
2833
  }
1940
2834
  function applyFlowTextLine(renderable, line, width) {
1941
2835
  renderable.setRigSceneLine?.(line);
@@ -1944,7 +2838,8 @@ function applyFlowTextLine(renderable, line, width) {
1944
2838
  renderable.width = Math.max(1, width);
1945
2839
  renderable.content = line?.styledText ?? line?.text ?? "";
1946
2840
  renderable.fg = line?.fg ?? RIG_UI.ink2;
1947
- 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;
1948
2843
  }
1949
2844
  function clearTextLines(renderables, from = 0) {
1950
2845
  for (let index = from;index < renderables.length; index += 1) {
@@ -1954,26 +2849,27 @@ function clearTextLines(renderables, from = 0) {
1954
2849
  renderable.top = -1;
1955
2850
  renderable.left = 0;
1956
2851
  renderable.width = 1;
2852
+ renderable.bg = TRANSPARENT2;
1957
2853
  }
1958
2854
  }
1959
2855
 
1960
2856
  // packages/cli/src/app-opentui/render/panels.ts
1961
- import { RGBA as RGBA2, ScrollBoxRenderable } from "@opentui/core";
1962
- 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);
1963
2859
  var MAX_PANEL_LINES = 420;
1964
2860
  function hexToRgba(hex, alpha) {
1965
2861
  const clean = hex.replace(/^#/, "");
1966
2862
  const full = clean.length === 3 ? clean.split("").map((part) => `${part}${part}`).join("") : clean;
1967
2863
  const value = Number.parseInt(full, 16);
1968
2864
  if (!Number.isFinite(value))
1969
- return RGBA2.fromInts(14, 15, 17, alpha);
1970
- 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);
1971
2867
  }
1972
2868
  function panelBackground(panel) {
1973
2869
  return hexToRgba(panel.backgroundColor ?? RIG_UI.panel, panel.backgroundAlpha ?? 184);
1974
2870
  }
1975
2871
  function panelBorder(panel) {
1976
- return panel.borderColor ? hexToRgba(panel.borderColor, panel.borderAlpha ?? 54) : TRANSPARENT2;
2872
+ return panel.borderColor ? hexToRgba(panel.borderColor, panel.borderAlpha ?? 54) : TRANSPARENT3;
1977
2873
  }
1978
2874
  function createScrollPanelLayer(renderer, id, onLineMouseDown) {
1979
2875
  const panel = new ScrollBoxRenderable(renderer, {
@@ -1983,7 +2879,7 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
1983
2879
  top: -2,
1984
2880
  width: 1,
1985
2881
  height: 1,
1986
- backgroundColor: TRANSPARENT2,
2882
+ backgroundColor: TRANSPARENT3,
1987
2883
  border: false,
1988
2884
  shouldFill: true,
1989
2885
  opacity: 1,
@@ -1999,7 +2895,7 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
1999
2895
  visible: false,
2000
2896
  width: 1,
2001
2897
  trackOptions: {
2002
- backgroundColor: TRANSPARENT2,
2898
+ backgroundColor: TRANSPARENT3,
2003
2899
  foregroundColor: hexToRgba(RIG_UI.ink4, 84)
2004
2900
  }
2005
2901
  },
@@ -2007,18 +2903,24 @@ function createScrollPanelLayer(renderer, id, onLineMouseDown) {
2007
2903
  height: 0,
2008
2904
  visible: false,
2009
2905
  trackOptions: {
2010
- backgroundColor: TRANSPARENT2,
2011
- foregroundColor: TRANSPARENT2
2906
+ backgroundColor: TRANSPARENT3,
2907
+ foregroundColor: TRANSPARENT3
2012
2908
  }
2013
2909
  }
2014
2910
  });
2015
2911
  panel.__rigTextLines = [];
2016
- for (let index = 0;index < MAX_PANEL_LINES; index += 1) {
2017
- 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);
2018
2921
  panel.__rigTextLines.push(line);
2019
2922
  panel.add(line);
2020
2923
  }
2021
- return panel;
2022
2924
  }
2023
2925
  function hidePanel(panel) {
2024
2926
  panel.visible = false;
@@ -2026,7 +2928,7 @@ function hidePanel(panel) {
2026
2928
  panel.left = 0;
2027
2929
  panel.width = 1;
2028
2930
  panel.height = 1;
2029
- panel.backgroundColor = TRANSPARENT2;
2931
+ panel.backgroundColor = TRANSPARENT3;
2030
2932
  panel.border = false;
2031
2933
  panel.__rigPanelId = undefined;
2032
2934
  panel.__rigTextLines.forEach((line) => applyFlowTextLine(line, undefined, 1));
@@ -2038,19 +2940,12 @@ function applyScrollPanels(panels, layout, scenePanels) {
2038
2940
  hidePanel(renderable);
2039
2941
  return;
2040
2942
  }
2041
- const cardLeft = Math.max(0, Math.min(layout.width - 1, layout.centerLeft + (panel.left ?? 0)));
2042
- const desiredWidth = panel.width ?? layout.centerWidth;
2043
- const cardWidth = Math.max(1, Math.min(layout.width - cardLeft, desiredWidth));
2044
- const cardTop = Math.max(0, Math.min(layout.height - 1, layout.centerTop + panel.top));
2045
- const cardHeight = Math.max(1, Math.min(layout.height - cardTop, panel.height));
2046
- const headerHeight = panel.chrome === "ad-terminal" ? Math.max(3, Math.min(cardHeight - 2, panel.headerHeight ?? 3)) : 0;
2047
- const left = cardLeft + (panel.chrome === "ad-terminal" ? 1 : 0);
2048
- const top = cardTop + headerHeight + (panel.chrome === "ad-terminal" ? 1 : 0);
2049
- const width = Math.max(1, Math.min(layout.width - left, cardWidth - (panel.chrome === "ad-terminal" ? 2 : 0)));
2050
- const height = Math.max(1, Math.min(layout.height - top, cardHeight - headerHeight - (panel.chrome === "ad-terminal" ? 2 : 0)));
2051
- const paddingX = panel.paddingX ?? (panel.chrome === "ad-terminal" ? 5 : 2);
2052
- const paddingY = panel.paddingY ?? (panel.chrome === "ad-terminal" ? 2 : 1);
2053
- 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;
2054
2949
  renderable.visible = true;
2055
2950
  renderable.left = left;
2056
2951
  renderable.top = top;
@@ -2058,7 +2953,7 @@ function applyScrollPanels(panels, layout, scenePanels) {
2058
2953
  renderable.height = height;
2059
2954
  renderable.zIndex = panel.zIndex ?? 6;
2060
2955
  renderable.opacity = panel.opacity ?? 1;
2061
- renderable.backgroundColor = panel.chrome === "ad-terminal" ? TRANSPARENT2 : panelBackground(panel);
2956
+ renderable.backgroundColor = panel.chrome === "ad-terminal" ? TRANSPARENT3 : panelBackground(panel);
2062
2957
  renderable.border = panel.chrome === "ad-terminal" ? false : panel.border ?? false;
2063
2958
  renderable.borderColor = panelBorder(panel);
2064
2959
  renderable.paddingX = paddingX;
@@ -2071,56 +2966,31 @@ function applyScrollPanels(panels, layout, scenePanels) {
2071
2966
  renderable.__rigPanelId = panel.id;
2072
2967
  renderable.scrollTo(0);
2073
2968
  }
2969
+ const lineCount = Math.min(panel.lines.length, MAX_PANEL_LINES);
2970
+ ensurePanelLineCount(renderable, lineCount);
2074
2971
  renderable.__rigTextLines.forEach((line, lineIndex) => {
2075
- applyFlowTextLine(line, panel.lines[lineIndex], contentWidth);
2972
+ applyFlowTextLine(line, lineIndex < lineCount ? panel.lines[lineIndex] : undefined, contentWidth);
2076
2973
  });
2077
2974
  });
2078
2975
  }
2079
2976
 
2080
2977
  // packages/cli/src/app-opentui/render/type-bar.ts
2081
- import { InputRenderable, InputRenderableEvents, TextRenderable as TextRenderable2 } from "@opentui/core";
2082
- function createTypeBar(renderer) {
2083
- const prefix = new TextRenderable2(renderer, {
2084
- id: "rig-typebar-prefix",
2085
- content: " rig \u203A",
2086
- position: "absolute",
2087
- left: 0,
2088
- top: 0,
2089
- fg: RIG_UI.lime,
2090
- selectable: true
2091
- });
2092
- const input = new InputRenderable(renderer, {
2093
- id: "rig-typebar-input",
2094
- position: "absolute",
2095
- left: 7,
2096
- top: 0,
2097
- width: 40,
2098
- placeholder: "fleet \xB7 tasks \xB7 run next \xB7 help",
2099
- textColor: RIG_UI.ink,
2100
- cursorColor: RIG_UI.lime,
2101
- cursorStyle: { style: "block", blinking: false },
2102
- showCursor: true,
2103
- selectable: true
2104
- });
2105
- const footer = new TextRenderable2(renderer, {
2106
- id: "rig-footer",
2107
- content: "",
2108
- position: "absolute",
2109
- left: 0,
2110
- top: 1,
2111
- width: "100%",
2112
- fg: RIG_UI.ink4,
2113
- selectable: true
2114
- });
2115
- input.focus();
2116
- return { input, prefix, footer };
2117
- }
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);
2118
2981
  function positionTypeBar(renderables, input, footer, width, typeBarTop, footerTop) {
2119
- 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;
2120
2990
  renderables.prefix.top = typeBarTop;
2121
2991
  renderables.input.top = typeBarTop;
2122
- renderables.input.left = 7;
2123
- renderables.input.width = Math.max(10, width - 9);
2992
+ renderables.input.left = 5;
2993
+ renderables.input.width = Math.max(10, width - 8);
2124
2994
  renderables.input.placeholder = input.placeholder;
2125
2995
  renderables.input.showCursor = true;
2126
2996
  renderables.footer.top = footerTop;
@@ -2130,6 +3000,8 @@ function positionTypeBar(renderables, input, footer, width, typeBarTop, footerTo
2130
3000
  renderables.input.focus();
2131
3001
  }
2132
3002
  function hideTypeBar(renderables, height) {
3003
+ renderables.background.visible = false;
3004
+ renderables.background.top = height + 1;
2133
3005
  renderables.prefix.content = "";
2134
3006
  renderables.prefix.top = height + 1;
2135
3007
  renderables.input.value = "";
@@ -2141,17 +3013,22 @@ function hideTypeBar(renderables, height) {
2141
3013
  renderables.footer.content = "";
2142
3014
  renderables.footer.top = height + 1;
2143
3015
  }
2144
- function bindTypeBar(renderables, handlers) {
2145
- renderables.input.on(InputRenderableEvents.INPUT, (value) => handlers.onInput(value));
2146
- 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;
2147
3023
  }
2148
3024
  function formatFooter(footer, width) {
3025
+ const safeMessage = safeFooterMessage(footer.message);
2149
3026
  const parts = [
2150
- footer.project ? `project ${footer.project}` : null,
2151
- footer.server ? `server ${footer.server}` : null,
2152
- footer.auth ? `auth ${footer.auth}` : null,
3027
+ footer.project ? "workspace" : null,
3028
+ footer.server ? "server selected" : null,
3029
+ footer.auth ? "auth ready" : null,
2153
3030
  footer.run ? `run ${footer.run}` : null,
2154
- footer.message ?? null
3031
+ safeMessage
2155
3032
  ].filter((part) => Boolean(part));
2156
3033
  return ` ${truncateText(parts.join(" \xB7 "), Math.max(8, width - 2))}`;
2157
3034
  }
@@ -2163,9 +3040,6 @@ function line(text, options = {}) {
2163
3040
  function blank() {
2164
3041
  return { text: "" };
2165
3042
  }
2166
- function center(text, fg = RIG_UI.ink2, bold = false) {
2167
- return { text, fg, bold, align: "center" };
2168
- }
2169
3043
  function deckRow(input) {
2170
3044
  const prefix = input.active ? "\u25B8" : " ";
2171
3045
  const label = input.label.toUpperCase().padEnd(10);
@@ -2176,6 +3050,9 @@ function deckRow(input) {
2176
3050
  activateOnClick: input.activateOnClick
2177
3051
  });
2178
3052
  }
3053
+ function kv(label, value, fg = RIG_UI.ink2) {
3054
+ return line(` ${label.toUpperCase().padEnd(10)} ${value}`, { fg });
3055
+ }
2179
3056
  function fitSceneLine(input, layout) {
2180
3057
  const fitted = truncateText(input.text, layout.centerWidth);
2181
3058
  return {
@@ -2190,9 +3067,52 @@ function makeSceneFrame(input) {
2190
3067
  return input;
2191
3068
  }
2192
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
+
2193
3113
  // packages/cli/src/app-opentui/drone.ts
2194
- import { RGBA as RGBA3, StyledText, TextAttributes as TextAttributes3 } from "@opentui/core";
2195
- var MINI_DRONE = [
3114
+ import { RGBA as RGBA6, StyledText as StyledText2, TextAttributes as TextAttributes4 } from "@opentui/core";
3115
+ var MINI_DRONE2 = [
2196
3116
  "(!!!) (!!!)",
2197
3117
  " \\%==%/ ",
2198
3118
  " %%?%% ",
@@ -2207,13 +3127,13 @@ var LEAD_MARK = [
2207
3127
  var BLADE_FRAMES = ["---", "\\\\\\", "|||", "///"];
2208
3128
  var EYE_FRAMES = ["o", "@", "\u2022", "."];
2209
3129
  var COLOR = {
2210
- body: RGBA3.fromHex(RIG_UI.lime),
2211
- mini: RGBA3.fromHex(RIG_UI.limeDim),
2212
- rotor: RGBA3.fromHex(RIG_UI.cyan),
2213
- path: RGBA3.fromHex(RIG_UI.cyan),
2214
- eye: RGBA3.fromHex(RIG_UI.ink),
2215
- dim: RGBA3.fromHex(RIG_UI.ink4),
2216
- 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)
2217
3137
  };
2218
3138
  function bladeForTick(tick, phase = 0) {
2219
3139
  return BLADE_FRAMES[(Math.floor(tick / 3) + phase) % BLADE_FRAMES.length];
@@ -2223,12 +3143,12 @@ function eyeForTick(tick, phase = 0) {
2223
3143
  return pulse > 0.55 ? EYE_FRAMES[1] : pulse > 0.1 ? EYE_FRAMES[0] : pulse > -0.45 ? EYE_FRAMES[2] : EYE_FRAMES[3];
2224
3144
  }
2225
3145
  function chunk2(text, fg, bold = false, dim = false) {
2226
- let attributes = TextAttributes3.NONE;
3146
+ let attributes = TextAttributes4.NONE;
2227
3147
  if (bold)
2228
- attributes |= TextAttributes3.BOLD;
3148
+ attributes |= TextAttributes4.BOLD;
2229
3149
  if (dim)
2230
- attributes |= TextAttributes3.DIM;
2231
- return { __isChunk: true, text, fg, ...attributes !== TextAttributes3.NONE ? { attributes } : {} };
3150
+ attributes |= TextAttributes4.DIM;
3151
+ return { __isChunk: true, text, fg, ...attributes !== TextAttributes4.NONE ? { attributes } : {} };
2232
3152
  }
2233
3153
  function styledLine(text, colorFor) {
2234
3154
  const chunks = [];
@@ -2237,599 +3157,315 @@ function styledLine(text, colorFor) {
2237
3157
  const flush = () => {
2238
3158
  if (!run || !runColor)
2239
3159
  return;
2240
- chunks.push(chunk2(run, COLOR[runColor], runColor === "body" || runColor === "eye", runColor === "dim"));
2241
- run = "";
2242
- };
2243
- for (const char of text) {
2244
- const next = colorFor(char);
2245
- if (next !== runColor) {
2246
- flush();
2247
- runColor = next;
2248
- }
2249
- run += char;
2250
- }
2251
- flush();
2252
- return new StyledText(chunks.length > 0 ? chunks : [chunk2("", COLOR.ink)]);
2253
- }
2254
- function droneColor(char) {
2255
- if (char === "?" || char === "o" || char === "@" || char === "\u2022")
2256
- return "eye";
2257
- if (char === "%" || char === "=")
2258
- return "mini";
2259
- if (char === "$")
2260
- return "body";
2261
- if (char === "(" || char === ")" || char === "/" || char === "\\" || char === "|" || char === "-")
2262
- return "rotor";
2263
- if (char === "\xB7" || char === "\u2022" || char === "'" || char === ".")
2264
- return "path";
2265
- if (char.trim() === "")
2266
- return "dim";
2267
- return "ink";
2268
- }
2269
- function motionColor(char) {
2270
- if (char === "\u25CF" || char === "\u25C6" || char === "\u25C9")
2271
- return "body";
2272
- if (char === "\u2022" || char === "\xB7" || char === "\u2500" || char === "\u2502" || char === "\u2571" || char === "\u2572" || char === "\u256D" || char === "\u256E" || char === "\u2570" || char === "\u256F")
2273
- return "path";
2274
- if (char === "\u258C" || char === "\u2590" || char === "\u2581" || char === "\u2594")
2275
- return "mini";
2276
- return droneColor(char);
2277
- }
2278
- function materializeDrone(lines, tick, phase = 0) {
2279
- const blade = bladeForTick(tick, phase);
2280
- const eye = eyeForTick(tick, phase);
2281
- return lines.map((line2) => line2.replaceAll("!!!", blade).replaceAll("?", eye));
2282
- }
2283
- function brandFleetLines(tick, align = "center") {
2284
- const left = materializeDrone(MINI_DRONE, tick, 0);
2285
- const right = materializeDrone(MINI_DRONE, tick, 2);
2286
- const lead = materializeDrone(LEAD_MARK, tick, 1);
2287
- const packet = ["\xB7", "\u2022", "\u25CF", "\u2022"][Math.floor(tick / 2) % 4];
2288
- const bridge = [
2289
- ` ${left[0]} ${lead[0]} ${right[0]}`,
2290
- ` ${left[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${lead[1]} \xB7\u2500\u2500${packet}\u2500\u2500\xB7 ${right[1]}`,
2291
- ` ${left[2]} \xB7' '${lead[2]}' '\xB7 ${right[2]}`,
2292
- ` ${left[3]} \u2572 signal bus \u2571 ${right[3]}`,
2293
- ` ${left[4]} \xB7\u2500\u2500\u2500${packet}\u2500\u2500\u2500\u2500\u2500\u2500${packet}\u2500\u2500\u2500\xB7 ${right[4]}`
2294
- ];
2295
- return bridge.map((text) => ({
2296
- text,
2297
- styledText: styledLine(text, motionColor),
2298
- fg: RIG_UI.limeDim,
2299
- align
2300
- }));
2301
- }
2302
- function errorDrone(tick) {
2303
- return brandFleetLines(tick).map((line2, index) => ({
2304
- ...line2,
2305
- fg: index % 2 === 0 ? RIG_UI.red : line2.fg
2306
- }));
2307
- }
2308
-
2309
- // packages/cli/src/app-opentui/scenes/error.ts
2310
- function renderErrorScene(state) {
2311
- const message = state.error?.message ?? "Unknown Rig app error";
2312
- const hint = state.error?.hint ?? "type main, help, or retry after fixing the issue";
2313
- return makeSceneFrame({
2314
- scene: "error",
2315
- title: "Rig error",
2316
- lines: [
2317
- ...errorDrone(state.tick).slice(2, 16),
2318
- blank(),
2319
- center("DRONE SIGNAL DEGRADED", RIG_UI.red, true),
2320
- center(message, RIG_UI.ink2),
2321
- blank(),
2322
- center(hint, RIG_UI.limeDim)
2323
- ],
2324
- typeBarPlaceholder: hint,
2325
- live: true
2326
- });
2327
- }
2328
-
2329
- // packages/cli/src/app-opentui/scenes/help.ts
2330
- var COMMANDS = [
2331
- ["runs", "show runs"],
2332
- ["tasks", "show task source"],
2333
- ["type in Runs", "search loaded runs"],
2334
- ["type in Tasks", "search loaded tasks"],
2335
- ["tasks open", "filter to open tasks"],
2336
- ["tasks all", "show every task returned by the source"],
2337
- ["tasks mine", "show tasks assigned to @me"],
2338
- ["tasks blocked", "show blocked work"],
2339
- ["tasks closed", "show closed work"],
2340
- ["tasks sort priority", "rank by priority"],
2341
- ["tasks sort recency", "rank by update time"],
2342
- ["tasks sort status", "group by status"],
2343
- ["run <task-id>", "dispatch selected task id"],
2344
- ["run next", "dispatch next runnable task"],
2345
- ["attach <run>", "open bundled Pi for a run"],
2346
- ["tab / \u2192", "next screen"],
2347
- ["\u2190", "previous screen"],
2348
- ["\u2191 \u2193", "move selection in Runs/Tasks"],
2349
- ["j k", "move selection outside search-first list screens"],
2350
- ["enter", "activate selected row"],
2351
- ["mouse wheel", "scroll panel under cursor"],
2352
- ["ctrl-]", "detach from embedded Pi"],
2353
- ["ctrl-c / q", "exit"]
2354
- ];
2355
- function panelWidth(layout) {
2356
- return layout?.centerWidth ?? 100;
2357
- }
2358
- function panelHeight(layout, top = 3) {
2359
- return Math.max(4, (layout?.centerHeight ?? 24) - top);
2360
- }
2361
- function clip(value, width) {
2362
- if (width <= 0)
2363
- return "";
2364
- return value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}\u2026`;
2365
- }
2366
- function commandRow(width, label, detail, index) {
2367
- const keyWidth = Math.min(22, Math.max(12, Math.floor(width * 0.28)));
2368
- const key = clip(label, keyWidth).padEnd(keyWidth);
2369
- return line(`${key} ${clip(detail, Math.max(8, width - keyWidth - 4))}`, { fg: index < 3 ? RIG_UI.ink : RIG_UI.ink2 });
2370
- }
2371
- function unsupportedCommand(state) {
2372
- const value = state.intent.action.payload?.unsupported ?? state.intent.action.payload?.unknown;
2373
- return typeof value === "string" && value.trim() ? value.trim() : null;
2374
- }
2375
- function renderHelpScene(state, layout) {
2376
- const width = panelWidth(layout);
2377
- const unsupported = unsupportedCommand(state);
2378
- const panelTop = 0;
2379
- const panelLines = [
2380
- ...unsupported ? [
2381
- line(`${unsupported} uses the plain CLI path.`, { fg: RIG_UI.yellow, bold: true }),
2382
- line("Exit and rerun with RIG_PLAIN=1 if you want legacy text output.", { fg: RIG_UI.ink3 }),
2383
- line("", { fg: RIG_UI.ink3 })
2384
- ] : [],
2385
- line("COMMAND ACTION", { fg: RIG_UI.ink3, bold: true }),
2386
- line("", { fg: RIG_UI.ink3 }),
2387
- ...COMMANDS.map(([label, detail], index) => commandRow(width - 6, label, detail, index))
2388
- ];
2389
- return makeSceneFrame({
2390
- scene: "help",
2391
- title: "Rig help",
2392
- lines: [
2393
- line("", { fg: RIG_UI.ink4 }),
2394
- line(" rig help", { fg: RIG_UI.ink3 })
2395
- ],
2396
- panels: [{
2397
- id: "help-reference",
2398
- top: panelTop,
2399
- width,
2400
- height: panelHeight(layout, panelTop),
2401
- lines: panelLines,
2402
- backgroundColor: RIG_UI.panel,
2403
- backgroundAlpha: 184,
2404
- opacity: 1,
2405
- border: false,
2406
- chrome: "ad-terminal",
2407
- headerText: "rig help",
2408
- headerHeight: 3,
2409
- paddingX: layout?.compact ? 2 : 3,
2410
- paddingY: 1
2411
- }],
2412
- typeBarPlaceholder: "runs \xB7 tasks \xB7 help \xB7 attach <run>",
2413
- footer: { message: "help" },
2414
- live: true
2415
- });
2416
- }
2417
-
2418
- // packages/cli/src/app-opentui/scenes/main.ts
2419
- var CONFIGURED_OPTIONS = [
2420
- ["runs", "run list and attach"],
2421
- ["tasks", "task source and dispatch"],
2422
- ["help", "command reference"]
2423
- ];
2424
- function renderMainScene(state) {
2425
- const configured = state.data.projectConfigured === true;
2426
- const selected = configured ? Math.max(0, Math.min(CONFIGURED_OPTIONS.length - 1, state.selection.index)) : Math.max(0, Math.min(1, state.selection.index));
2427
- const lines = [
2428
- center(configured ? "rig" : "rig setup", RIG_UI.ink, true),
2429
- center(configured ? "runs \xB7 tasks \xB7 help" : "initialize this repo", RIG_UI.ink3),
2430
- blank()
2431
- ];
2432
- if (!configured) {
2433
- 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" }));
2434
- } else {
2435
- CONFIGURED_OPTIONS.forEach(([label, detail], index) => {
2436
- lines.push(deckRow({ label, detail, index, active: index === selected }));
2437
- });
2438
- lines.push(blank(), line(` ${state.actionLabel ?? "ready"}`, { fg: state.status === "action" ? RIG_UI.cyan : RIG_UI.ink4 }));
2439
- }
2440
- return makeSceneFrame({
2441
- scene: "main",
2442
- title: "Rig",
2443
- lines,
2444
- typeBarPlaceholder: configured ? "runs \xB7 tasks \xB7 help" : "init \xB7 help",
2445
- live: true
2446
- });
2447
- }
2448
-
2449
- // packages/cli/src/app-opentui/pi-pty-host.ts
2450
- import { fileURLToPath } from "url";
2451
- import { basename } from "path";
2452
- import { RGBA as RGBA4, StyledText as StyledText2, TextAttributes as TextAttributes4 } from "@opentui/core";
2453
- import { Terminal as XtermTerminal } from "@xterm/headless";
2454
- var MIN_COLS = 40;
2455
- var MIN_ROWS = 12;
2456
- var MAX_ROWS = 300;
2457
- var MAX_SNAPSHOT_LINES = 1200;
2458
- var fallbackChildScriptPath = fileURLToPath(new URL("./pi-host-child.ts", import.meta.url));
2459
- var activeHost = null;
2460
- function clampCols(cols) {
2461
- return Math.max(MIN_COLS, Math.trunc(cols || 100));
2462
- }
2463
- function clampRows(rows) {
2464
- return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.trunc(rows || 35)));
2465
- }
2466
- var XTERM_COLOR_MODE_INDEXED_16 = 16777216;
2467
- var XTERM_COLOR_MODE_INDEXED_256 = 33554432;
2468
- var XTERM_COLOR_MODE_RGB = 50331648;
2469
- function rgbaFromXtermColor(mode, value) {
2470
- if (mode === 1 || mode === 2 || mode === XTERM_COLOR_MODE_INDEXED_16 || mode === XTERM_COLOR_MODE_INDEXED_256) {
2471
- return RGBA4.fromIndex(value);
2472
- }
2473
- if (mode === 3 || mode === XTERM_COLOR_MODE_RGB) {
2474
- return RGBA4.fromInts(value >> 16 & 255, value >> 8 & 255, value & 255);
2475
- }
2476
- return;
2477
- }
2478
- function textAttributesFromCell(cell) {
2479
- let attributes = TextAttributes4.NONE;
2480
- if (cell.isBold())
2481
- attributes |= TextAttributes4.BOLD;
2482
- if (cell.isDim())
2483
- attributes |= TextAttributes4.DIM;
2484
- if (cell.isItalic())
2485
- attributes |= TextAttributes4.ITALIC;
2486
- if (cell.isUnderline())
2487
- attributes |= TextAttributes4.UNDERLINE;
2488
- if (cell.isBlink())
2489
- attributes |= TextAttributes4.BLINK;
2490
- if (cell.isInverse())
2491
- attributes |= TextAttributes4.INVERSE;
2492
- if (cell.isInvisible())
2493
- attributes |= TextAttributes4.HIDDEN;
2494
- if (cell.isStrikethrough())
2495
- attributes |= TextAttributes4.STRIKETHROUGH;
2496
- return attributes;
2497
- }
2498
- function sameRgba(a, b) {
2499
- if (!a && !b)
2500
- return true;
2501
- return Boolean(a && b && a.equals(b));
2502
- }
2503
- function sameStyle(a, b) {
2504
- return sameRgba(a.fg, b.fg) && sameRgba(a.bg, b.bg) && (a.attributes ?? 0) === (b.attributes ?? 0);
2505
- }
2506
- function styleFromCell(cell) {
2507
- return {
2508
- fg: rgbaFromXtermColor(cell.getFgColorMode(), cell.getFgColor()),
2509
- bg: rgbaFromXtermColor(cell.getBgColorMode(), cell.getBgColor()),
2510
- attributes: textAttributesFromCell(cell)
2511
- };
2512
- }
2513
- function lineToStyledText(line2, cols) {
2514
- if (!line2)
2515
- return new StyledText2([{ __isChunk: true, text: "" }]);
2516
- const chunks = [];
2517
- let run = "";
2518
- let runStyle = null;
2519
- const flush = () => {
2520
- if (!run)
2521
- return;
2522
- chunks.push({
2523
- __isChunk: true,
2524
- text: run,
2525
- ...runStyle?.fg ? { fg: runStyle.fg } : {},
2526
- ...runStyle?.bg ? { bg: runStyle.bg } : {},
2527
- ...(runStyle?.attributes ?? 0) !== 0 ? { attributes: runStyle.attributes } : {}
2528
- });
2529
- run = "";
2530
- };
2531
- for (let index = 0;index < cols; index += 1) {
2532
- const cell = line2.getCell(index);
2533
- if (!cell) {
2534
- if (runStyle !== null)
2535
- flush();
2536
- runStyle = null;
2537
- run += " ";
2538
- continue;
2539
- }
2540
- if (cell.getWidth() === 0)
2541
- continue;
2542
- const style = styleFromCell(cell);
2543
- if (!runStyle || !sameStyle(runStyle, style)) {
3160
+ chunks.push(chunk2(run, COLOR[runColor], runColor === "body" || runColor === "eye", runColor === "dim"));
3161
+ run = "";
3162
+ };
3163
+ for (const char of text) {
3164
+ const next = colorFor(char);
3165
+ if (next !== runColor) {
2544
3166
  flush();
2545
- runStyle = style;
3167
+ runColor = next;
2546
3168
  }
2547
- run += cell.getChars() || " ";
3169
+ run += char;
2548
3170
  }
2549
3171
  flush();
2550
- return new StyledText2(chunks.length > 0 ? chunks : [{ __isChunk: true, text: "" }]);
2551
- }
2552
- function childCommandPrefix() {
2553
- const execName = basename(process.execPath).toLowerCase();
2554
- const currentEntry = process.argv[1];
2555
- if ((execName === "bun" || execName === "bun.exe") && currentEntry) {
2556
- return [process.execPath, currentEntry, "__opentui-pi-host"];
2557
- }
2558
- if (currentEntry)
2559
- return [process.execPath, "__opentui-pi-host"];
2560
- return [process.execPath, fallbackChildScriptPath];
3172
+ return new StyledText2(chunks.length > 0 ? chunks : [chunk2("", COLOR.ink)]);
2561
3173
  }
2562
- function withEnv(base) {
2563
- const env = {};
2564
- for (const [key, value] of Object.entries(base ?? process.env)) {
2565
- if (key === "RIG_PLAIN" || key === "RIG_CLI_PLAIN_HELP")
2566
- continue;
2567
- if (typeof value === "string")
2568
- env[key] = value;
2569
- }
2570
- return {
2571
- ...env,
2572
- TERM: "xterm-256color",
2573
- COLORTERM: "truecolor",
2574
- FORCE_COLOR: env.FORCE_COLOR ?? "1",
2575
- RIG_OPENTUI_PI_HOST: "1"
2576
- };
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";
2577
3188
  }
2578
- function getActivePiHost() {
2579
- 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);
2580
3197
  }
2581
- function stopActivePiHost(reason = "detach") {
2582
- activeHost?.dispose(reason);
2583
- 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));
2584
3202
  }
2585
-
2586
- class PiPtyHost {
2587
- runId;
2588
- projectRoot;
2589
- onSnapshot;
2590
- onExit;
2591
- onError;
2592
- terminal;
2593
- disposables = [];
2594
- decoder = new TextDecoder("utf-8");
2595
- proc = null;
2596
- pty = null;
2597
- status = "starting";
2598
- cols;
2599
- rows;
2600
- message = "starting bundled Pi";
2601
- exitCode;
2602
- signal;
2603
- notifyTimer = null;
2604
- _disposed = false;
2605
- constructor(options) {
2606
- this.runId = options.runId;
2607
- this.projectRoot = options.projectRoot;
2608
- this.cols = clampCols(options.cols);
2609
- this.rows = clampRows(options.rows);
2610
- this.onSnapshot = options.onSnapshot;
2611
- this.onExit = options.onExit;
2612
- this.onError = options.onError;
2613
- this.terminal = new XtermTerminal({
2614
- allowProposedApi: true,
2615
- cols: this.cols,
2616
- rows: this.rows,
2617
- scrollback: 5000
2618
- });
2619
- this.registerTerminalResponders();
2620
- }
2621
- get disposed() {
2622
- return this._disposed;
2623
- }
2624
- get snapshot() {
2625
- return this.createSnapshot();
2626
- }
2627
- async start() {
2628
- if (this._disposed)
2629
- throw new Error("Pi PTY host is disposed.");
2630
- if (typeof Bun.Terminal !== "function") {
2631
- throw new Error("Bun native PTY is unavailable in this runtime. Rig OpenTUI Pi host requires Bun.Terminal.");
2632
- }
2633
- const spawnOptions = {
2634
- cwd: this.projectRoot,
2635
- env: withEnv(process.env),
2636
- terminal: {
2637
- cols: this.cols,
2638
- rows: this.rows,
2639
- name: "xterm-256color",
2640
- data: (_terminal, data) => this.handlePtyData(data)
2641
- }
2642
- };
2643
- const proc = Bun.spawn([
2644
- ...childCommandPrefix(),
2645
- "--run-id",
2646
- this.runId,
2647
- "--project-root",
2648
- this.projectRoot
2649
- ], spawnOptions);
2650
- if (!proc.terminal)
2651
- throw new Error("Bun did not attach a terminal to the bundled Pi child process.");
2652
- this.proc = proc;
2653
- this.pty = proc.terminal;
2654
- this.status = "running";
2655
- this.message = "bundled Pi running inside Rig";
2656
- this.emitSnapshotSoon(0);
2657
- proc.exited.then((exitCode) => {
2658
- if (this._disposed)
2659
- return;
2660
- this.status = exitCode === 0 ? "exited" : "failed";
2661
- this.exitCode = exitCode;
2662
- this.signal = null;
2663
- this.message = exitCode === 0 ? "bundled Pi exited" : `bundled Pi exited with code ${exitCode}`;
2664
- const snapshot = this.createSnapshot();
2665
- this.onSnapshot?.(snapshot);
2666
- this.onExit?.(snapshot);
2667
- if (activeHost === this)
2668
- activeHost = null;
2669
- this.dispose("exit", { kill: false, notify: false });
2670
- }).catch((error) => {
2671
- if (this._disposed)
2672
- return;
2673
- this.status = "failed";
2674
- this.message = error instanceof Error ? error.message : String(error);
2675
- const snapshot = this.createSnapshot();
2676
- this.onSnapshot?.(snapshot);
2677
- this.onError?.(error, snapshot);
2678
- if (activeHost === this)
2679
- activeHost = null;
2680
- this.dispose("error", { kill: false, notify: false });
2681
- });
2682
- }
2683
- write(data) {
2684
- if (this._disposed || !this.pty)
2685
- return;
2686
- try {
2687
- this.pty.write(data);
2688
- } catch (error) {
2689
- this.status = "failed";
2690
- this.message = error instanceof Error ? error.message : String(error);
2691
- const snapshot = this.createSnapshot();
2692
- this.onSnapshot?.(snapshot);
2693
- this.onError?.(error, snapshot);
2694
- }
2695
- }
2696
- resize(cols, rows) {
2697
- const nextCols = clampCols(cols);
2698
- const nextRows = clampRows(rows);
2699
- if (nextCols === this.cols && nextRows === this.rows)
2700
- return;
2701
- this.cols = nextCols;
2702
- this.rows = nextRows;
2703
- this.terminal.resize(nextCols, nextRows);
2704
- try {
2705
- this.pty?.resize(nextCols, nextRows);
2706
- } catch {}
2707
- this.emitSnapshotSoon(0);
2708
- }
2709
- detach() {
2710
- this.dispose("detach");
2711
- return this.createSnapshot("detached from bundled Pi");
2712
- }
2713
- dispose(reason = "dispose", options = {}) {
2714
- if (this._disposed)
2715
- return;
2716
- this._disposed = true;
2717
- if (this.notifyTimer)
2718
- clearTimeout(this.notifyTimer);
2719
- this.notifyTimer = null;
2720
- for (const disposable of this.disposables.splice(0)) {
2721
- try {
2722
- disposable.dispose();
2723
- } catch {}
2724
- }
2725
- if (options.kill !== false) {
2726
- try {
2727
- this.proc?.kill("SIGTERM");
2728
- } catch {}
2729
- }
2730
- try {
2731
- this.pty?.close();
2732
- } catch {}
2733
- try {
2734
- this.terminal.dispose();
2735
- } catch {}
2736
- if (activeHost === this)
2737
- activeHost = null;
2738
- if (options.notify) {
2739
- this.message = reason;
2740
- this.onSnapshot?.(this.createSnapshot(reason));
2741
- }
2742
- }
2743
- handlePtyData(data) {
2744
- if (this._disposed)
2745
- return;
2746
- const text = this.decoder.decode(data, { stream: true });
2747
- this.respondToRawTerminalQueries(text);
2748
- this.terminal.write(data, () => this.emitSnapshotSoon());
2749
- }
2750
- emitSnapshotSoon(delayMs = 16) {
2751
- if (this._disposed || this.notifyTimer)
2752
- return;
2753
- this.notifyTimer = setTimeout(() => {
2754
- this.notifyTimer = null;
2755
- if (this._disposed)
2756
- return;
2757
- this.onSnapshot?.(this.createSnapshot());
2758
- }, delayMs);
2759
- }
2760
- createSnapshot(message = this.message) {
2761
- const buffer = this.terminal.buffer.active;
2762
- const end = buffer.length;
2763
- const start = Math.max(0, end - MAX_SNAPSHOT_LINES);
2764
- const lines = [];
2765
- const styledLines = [];
2766
- for (let row = start;row < end; row += 1) {
2767
- const line2 = buffer.getLine(row);
2768
- lines.push((line2?.translateToString(false) ?? "").slice(0, this.cols));
2769
- styledLines.push(lineToStyledText(line2, this.cols));
2770
- }
2771
- while (lines.length < this.rows) {
2772
- lines.push("");
2773
- styledLines.push(new StyledText2([{ __isChunk: true, text: "" }]));
2774
- }
2775
- return {
2776
- runId: this.runId,
2777
- status: this.status,
2778
- cols: this.cols,
2779
- 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),
2780
3438
  lines,
2781
- styledLines,
2782
- message,
2783
- ...this.exitCode !== undefined ? { exitCode: this.exitCode } : {},
2784
- ...this.signal !== undefined ? { signal: this.signal } : {}
2785
- };
2786
- }
2787
- registerTerminalResponders() {
2788
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "c" }, (params) => {
2789
- if (params.length === 0 || params[0] === 0)
2790
- this.write("\x1B[?62;22c");
2791
- return false;
2792
- }));
2793
- this.disposables.push(this.terminal.parser.registerCsiHandler({ prefix: ">", final: "c" }, (params) => {
2794
- if (params.length === 0 || params[0] === 0)
2795
- this.write("\x1B[>0;0;0c");
2796
- return false;
2797
- }));
2798
- this.disposables.push(this.terminal.parser.registerCsiHandler({ final: "n" }, (params) => {
2799
- if (params[0] === 5) {
2800
- this.write("\x1B[0n");
2801
- } else if (params[0] === 6) {
2802
- const row = this.terminal.buffer.active.cursorY + 1;
2803
- const col = this.terminal.buffer.active.cursorX + 1;
2804
- this.write(`\x1B[${row};${col}R`);
2805
- }
2806
- return false;
2807
- }));
2808
- }
2809
- respondToRawTerminalQueries(text) {
2810
- if (!text)
2811
- return;
2812
- const decrqm = /\x1b\[\?(\d+)\$p/g;
2813
- let match;
2814
- while ((match = decrqm.exec(text)) !== null) {
2815
- this.write(`\x1B[?${match[1]};2$y`);
2816
- }
2817
- }
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
+ });
2818
3451
  }
2819
3452
 
2820
3453
  // packages/cli/src/app-opentui/runtime.ts
3454
+ init_pi_pty_host();
2821
3455
  var MAX_SCENE_LINES = 200;
2822
3456
  var MAX_SCENE_PANELS = 5;
2823
- var FRAME_MS = 16;
2824
- var ANIMATION_SLOWDOWN = 3;
3457
+ var ANIMATION_SLOWDOWN = 1;
3458
+ var PRELOADER_BACKDROP = RGBA7.fromHex(RIG_UI.bg);
2825
3459
  var PRIMARY_NAV = [
3460
+ { scene: "main", argv: ["main"], label: "Project menu" },
2826
3461
  { scene: "fleet", argv: ["runs"], label: "Opening runs" },
2827
3462
  { scene: "tasks", argv: ["tasks"], label: "Opening tasks" },
2828
3463
  { scene: "help", argv: ["help"], label: "Opening help" }
2829
3464
  ];
2830
3465
  function primaryNavIntent(scene) {
2831
3466
  const entry = PRIMARY_NAV.find((item) => item.scene === scene) ?? PRIMARY_NAV[0];
2832
- 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 } };
2833
3469
  }
2834
3470
  function nextPrimaryScene(current, delta) {
2835
3471
  const currentIndex = PRIMARY_NAV.findIndex((entry) => entry.scene === current);
@@ -2850,10 +3486,10 @@ function defaultSceneRenderer(state) {
2850
3486
  scene: state.scene,
2851
3487
  title,
2852
3488
  lines: [
2853
- { text: title.toUpperCase(), fg: RIG_UI.ink, bold: true, align: "center" },
2854
- { text: state.actionLabel ?? "adapter scene pending", fg: RIG_UI.limeDim, align: "center" },
2855
- { text: "support state", fg: RIG_UI.ink3, align: "center" },
2856
- { 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 }
2857
3493
  ],
2858
3494
  typeBarPlaceholder: "runs \xB7 tasks \xB7 help",
2859
3495
  live: false
@@ -2879,9 +3515,9 @@ function requestRender(renderer) {
2879
3515
  function recordArray(value) {
2880
3516
  return Array.isArray(value) ? value.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
2881
3517
  }
2882
- function stringField(record, keys) {
3518
+ function stringField2(record2, keys) {
2883
3519
  for (const key of keys) {
2884
- const value = record[key];
3520
+ const value = record2[key];
2885
3521
  if (typeof value === "string" && value.trim())
2886
3522
  return value.trim();
2887
3523
  if (typeof value === "number" && Number.isFinite(value))
@@ -2889,33 +3525,84 @@ function stringField(record, keys) {
2889
3525
  }
2890
3526
  return;
2891
3527
  }
2892
- 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
+ }
2893
3542
  return [
2894
- { id: "runs", label: "runs", intent: { scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }, message: "selected runs" },
3543
+ ...common,
2895
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" },
2896
3548
  { id: "help", label: "help", intent: { scene: "help", argv: ["help"], action: { kind: "none", label: "Opening help" } }, message: "selected help" }
2897
3549
  ];
2898
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
+ }
2899
3559
  function fleetSelectableItems(state) {
2900
3560
  const fleet = state.data.fleet;
2901
3561
  const runs = fleet && typeof fleet === "object" && !Array.isArray(fleet) ? recordArray(fleet.runs) : [];
2902
- return filterRunsForSearch(runs, listSearchQuery(state)).flatMap((run) => {
2903
- 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"]);
2904
3567
  if (!runId)
2905
3568
  return [];
2906
- const title = stringField(run, ["title", "taskTitle", "taskId"]) ?? "run";
3569
+ const title = stringField2(run, ["title", "taskTitle", "taskId"]) ?? "run";
2907
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)}` }];
2908
3571
  });
2909
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
+ }
2910
3582
  function taskSelectableItems(state) {
2911
3583
  const tasks = state.data.tasks;
2912
3584
  const records = tasks && typeof tasks === "object" && !Array.isArray(tasks) ? recordArray(tasks.records) : [];
2913
- return filterTasksForSearch(records, listSearchQuery(state)).flatMap((task) => {
2914
- 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"]);
2915
3590
  if (!taskId)
2916
3591
  return [];
2917
- const title = stringField(task, ["title", "name", "summary"]) ?? "task";
2918
- 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];
2919
3606
  });
2920
3607
  }
2921
3608
  function selectedRunId(state) {
@@ -2936,21 +3623,126 @@ function selectedRunId(state) {
2936
3623
  }
2937
3624
  return;
2938
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
+ }
2939
3634
  function inboxSelectableItems(state) {
2940
3635
  const inbox = state.data.inbox;
2941
3636
  if (!inbox || typeof inbox !== "object" || Array.isArray(inbox))
2942
- return [];
2943
- const record = inbox;
2944
- const approvals = recordArray(record.approvals).map((entry) => ({ ...entry, kind: "approvals" }));
2945
- 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();
2946
3643
  return [...approvals, ...inputs].flatMap((item) => {
2947
- const requestId = stringField(item, ["requestId", "id"]);
2948
- if (!requestId)
3644
+ const requestId = stringField2(item, ["requestId", "id"]);
3645
+ const runId = stringField2(item, ["runId"]);
3646
+ if (!requestId || !runId)
2949
3647
  return [];
2950
- const kind = item.kind === "inputs" ? "answer" : "approve/reject";
2951
- 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
+ };
2952
3718
  });
2953
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
+ }
2954
3746
  function runDetailSelectableItems(state) {
2955
3747
  const runId = selectedRunId(state);
2956
3748
  if (!runId)
@@ -2965,12 +3757,24 @@ function selectableItems(state) {
2965
3757
  switch (state.scene) {
2966
3758
  case "main":
2967
3759
  return mainSelectableItems(state);
3760
+ case "error":
3761
+ return errorSelectableItems();
3762
+ case "help":
3763
+ return helpSelectableItems();
2968
3764
  case "fleet":
2969
3765
  return fleetSelectableItems(state);
2970
3766
  case "tasks":
2971
3767
  return taskSelectableItems(state);
2972
3768
  case "inbox":
2973
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();
2974
3778
  case "run-detail":
2975
3779
  return runDetailSelectableItems(state);
2976
3780
  default:
@@ -3050,13 +3854,18 @@ async function launchRigOpenTuiApp(options) {
3050
3854
  let renderer = null;
3051
3855
  let graphics = null;
3052
3856
  let imageVisuals = null;
3857
+ let preloaderBackdrop = null;
3053
3858
  const scrollPanels = [];
3054
3859
  let typeBar = null;
3860
+ let brandText = null;
3055
3861
  const textLines = [];
3056
- let tickTimer = null;
3862
+ let renderTimer = null;
3057
3863
  let destroyed = false;
3058
3864
  let runnerPromise = null;
3059
3865
  let renderedSelectableItems = [];
3866
+ const preloaderTransition = { scene: null, startedAtMs: 0, minUntilMs: 0 };
3867
+ const resources = new RuntimeResources;
3868
+ const focusManager = new AppFocusManager;
3060
3869
  const emitAsyncError = (label, error) => {
3061
3870
  const normalized = normalizeError(error);
3062
3871
  events.emit({ type: "action.failed", label, message: normalized.message, hint: normalized.hint, cause: normalized.cause });
@@ -3064,6 +3873,23 @@ async function launchRigOpenTuiApp(options) {
3064
3873
  const runAppAction = (label, action) => {
3065
3874
  action().catch((error) => emitAsyncError(label, error));
3066
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
+ };
3067
3893
  const runtime = {
3068
3894
  getState: () => store.getState(),
3069
3895
  patchState: (patch) => store.patch(patch),
@@ -3090,29 +3916,35 @@ async function launchRigOpenTuiApp(options) {
3090
3916
  },
3091
3917
  getRunnerContext: async () => {
3092
3918
  if (!options.initializeRuntime)
3093
- 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.");
3094
3920
  runnerPromise ??= options.initializeRuntime();
3095
3921
  return runnerPromise;
3096
3922
  },
3097
3923
  suspend: () => renderer?.suspend(),
3098
3924
  resume: () => {
3099
3925
  renderer?.resume();
3100
- if (renderer && !destroyed)
3101
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3926
+ renderApp();
3102
3927
  },
3103
3928
  destroy: () => renderer?.destroy()
3104
3929
  };
3105
- events.subscribe((event) => store.patch(reduceAppEvent(store.getState(), event)));
3106
- store.subscribe(() => {
3107
- if (!destroyed && renderer)
3108
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3109
- });
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);
3110
3942
  try {
3111
3943
  renderer = await createCliRenderer({
3112
3944
  screenMode: "alternate-screen",
3113
3945
  exitOnCtrlC: false,
3114
- targetFps: 60,
3115
- maxFps: 60,
3946
+ targetFps: 30,
3947
+ maxFps: 30,
3116
3948
  useMouse: true,
3117
3949
  autoFocus: false,
3118
3950
  useKittyKeyboard: {
@@ -3124,24 +3956,41 @@ async function launchRigOpenTuiApp(options) {
3124
3956
  openConsoleOnError: false,
3125
3957
  backgroundColor: "transparent"
3126
3958
  });
3127
- renderer.setTerminalTitle?.("Rig \u2014 operator cockpit");
3959
+ renderer.setTerminalTitle?.("Operator cockpit");
3128
3960
  const layout = computeStageLayout(renderer.width, renderer.height);
3129
- imageVisuals = forceModernImageVisuals() ? new ImageVisualLayer(process.stdout) : null;
3961
+ imageVisuals = forceModernImageVisuals() ? new ImageVisualLayer(process.stdout, () => scheduleAppRender(0)) : null;
3130
3962
  if (imageVisuals?.enabled) {
3131
- renderer.setFrameCallback(async () => {
3963
+ const flushImageVisuals = async () => {
3132
3964
  imageVisuals?.flush();
3133
- });
3965
+ };
3966
+ renderer.setFrameCallback(flushImageVisuals);
3967
+ resources.add(() => renderer?.removeFrameCallback(flushImageVisuals));
3134
3968
  }
3135
3969
  if (!imageVisuals?.enabled) {
3136
3970
  graphics = createGraphicsLayer(renderer, layout);
3137
3971
  renderer.root.add(graphics);
3138
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);
3139
3986
  for (let index = 0;index < MAX_SCENE_PANELS; index += 1) {
3140
3987
  const panel = createScrollPanelLayer(renderer, `rig-scroll-panel-${index}`, (line2) => {
3141
3988
  const selectableIndex = line2.selectableIndex;
3142
3989
  if (selectableIndex === undefined)
3143
3990
  return;
3144
3991
  selectIndex(store, selectableIndex, renderedSelectableItems);
3992
+ if (line2.activateOnClick)
3993
+ runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3145
3994
  });
3146
3995
  scrollPanels.push(panel);
3147
3996
  renderer.root.add(panel);
@@ -3152,146 +4001,84 @@ async function launchRigOpenTuiApp(options) {
3152
4001
  if (selectableIndex === undefined)
3153
4002
  return;
3154
4003
  selectIndex(store, selectableIndex, renderedSelectableItems);
4004
+ if (line2.activateOnClick)
4005
+ runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3155
4006
  });
3156
4007
  textLines.push(textLine);
3157
4008
  renderer.root.add(textLine);
3158
4009
  }
3159
- typeBar = createTypeBar(renderer);
3160
- renderer.root.add(typeBar.prefix);
3161
- renderer.root.add(typeBar.input);
3162
- renderer.root.add(typeBar.footer);
3163
- bindTypeBar(typeBar, {
3164
- onInput(value) {
3165
- const scene = store.getState().scene;
3166
- events.emit({
3167
- type: "typebar.patch",
3168
- typeBar: {
3169
- value,
3170
- ...isListSearchScene(scene) ? { message: value.trim() ? `search ${JSON.stringify(value.trim())}` : undefined } : {}
3171
- }
3172
- });
3173
- },
3174
- onEnter(value) {
3175
- const scene = store.getState().scene;
3176
- if (isListSearchScene(scene) && value.trim()) {
3177
- if (renderedSelectableItems.length === 0) {
3178
- events.emit({ type: "typebar.patch", typeBar: { message: "no search match" } });
3179
- return;
3180
- }
3181
- const snapshot = renderedSelectableItems;
3182
- typeBar?.input && (typeBar.input.value = "");
3183
- events.emit({ type: "typebar.patch", typeBar: { value: "" } });
3184
- runAppAction("Activate search result", () => activateSelection(runtime, snapshot));
3185
- return;
3186
- }
3187
- typeBar?.input && (typeBar.input.value = "");
3188
- events.emit({ type: "typebar.patch", typeBar: { value: "" } });
3189
- if (value.trim() === "") {
3190
- runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3191
- return;
3192
- }
3193
- runAppAction("Run command", () => runtime.runIntent(intentFromTypeBar(value)));
3194
- }
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
3195
4019
  });
3196
- renderer.keyInput.on("keypress", (key) => {
3197
- const host = getActivePiHost();
3198
- if (host && store.getState().scene === "handoff") {
3199
- if (key.ctrl && key.name === "]") {
3200
- stopActivePiHost("operator detach");
3201
- events.emit({ type: "typebar.patch", typeBar: { value: "", message: "detached from Pi" } });
3202
- runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
3203
- return;
3204
- }
3205
- const sequence = key.raw || key.sequence;
3206
- if (sequence)
3207
- host.write(sequence);
3208
- if (typeBar)
3209
- typeBar.input.value = "";
3210
- return;
3211
- }
3212
- if (key.ctrl && key.name === "c") {
3213
- renderer?.destroy();
3214
- return;
3215
- }
3216
- const currentScene = store.getState().scene;
3217
- const searchScene = isListSearchScene(currentScene);
3218
- const inputValue = typeBar?.input.value ?? "";
3219
- const typeBarEmpty = inputValue.trim() === "";
3220
- if (typeBarEmpty && (key.name === "right" || key.name === "tab")) {
3221
- runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, 1))));
3222
- return;
3223
- }
3224
- if (typeBarEmpty && key.name === "left") {
3225
- runAppAction("Navigate", () => runtime.runIntent(primaryNavIntent(nextPrimaryScene(store.getState().scene, -1))));
3226
- return;
3227
- }
3228
- if (!searchScene && typeBarEmpty && key.name === "r") {
3229
- runAppAction("Opening runs", () => runtime.runIntent(primaryNavIntent("fleet")));
3230
- return;
3231
- }
3232
- if (!searchScene && typeBarEmpty && key.name === "t") {
3233
- runAppAction("Opening tasks", () => runtime.runIntent(primaryNavIntent("tasks")));
3234
- return;
3235
- }
3236
- if (!searchScene && typeBarEmpty && (key.name === "h" || key.sequence === "?")) {
3237
- runAppAction("Opening help", () => runtime.runIntent(primaryNavIntent("help")));
3238
- return;
3239
- }
3240
- if (searchScene && key.name === "up" || !searchScene && typeBarEmpty && (key.name === "up" || key.name === "k")) {
3241
- 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)
3242
4044
  return;
3243
- }
3244
- if (searchScene && key.name === "down" || !searchScene && typeBarEmpty && (key.name === "down" || key.name === "j")) {
3245
- if (moveSelection(store, 1, renderedSelectableItems))
4045
+ host.write(event.bytes);
4046
+ } else if (state.scene === "command") {
4047
+ const host = getActiveCommandHost();
4048
+ if (!host)
3246
4049
  return;
4050
+ host.write(event.bytes);
3247
4051
  }
3248
- if (typeBarEmpty && (key.name === "return" || key.name === "enter")) {
3249
- runAppAction("Activate selection", () => activateSelection(runtime, renderedSelectableItems));
3250
- return;
3251
- }
3252
- if (!searchScene && key.name === "q" && typeBarEmpty) {
3253
- renderer?.destroy();
3254
- return;
3255
- }
3256
- if (key.name === "escape") {
3257
- if (inputValue) {
3258
- if (typeBar)
3259
- typeBar.input.value = "";
3260
- events.emit({ type: "typebar.patch", typeBar: { value: "", message: undefined } });
3261
- } else {
3262
- runAppAction("Opening runs", () => runtime.runIntent({ scene: "fleet", argv: ["runs"], action: { kind: "refresh", label: "Opening runs" } }));
3263
- }
3264
- }
3265
- });
3266
- renderer.keyInput.on("paste", (event) => {
3267
- const host = getActivePiHost();
3268
- if (!host || store.getState().scene !== "handoff")
3269
- return;
3270
- host.write(event.bytes);
3271
- if (typeBar)
3272
- typeBar.input.value = "";
3273
- });
3274
- renderer.on("selection", (selection) => {
4052
+ };
4053
+ renderer.keyInput.on("paste", onPaste);
4054
+ resources.add(() => renderer?.keyInput.off("paste", onPaste));
4055
+ const onSelection = (selection) => {
3275
4056
  const selectedText = selection.getSelectedText();
3276
4057
  if (!selectedText)
3277
4058
  return;
3278
4059
  renderer?.copyToClipboardOSC52?.(selectedText);
3279
4060
  events.emit({ type: "typebar.patch", typeBar: { message: "copied selection" } });
3280
- });
3281
- renderer.on("resize", () => {
3282
- if (renderer)
3283
- renderedSelectableItems = renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, textLines, store.getState(), options.sceneRenderers);
3284
- });
3285
- 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 = () => {
3286
4070
  destroyed = true;
3287
4071
  stopActivePiHost("renderer destroyed");
4072
+ stopActiveCommandHost("renderer destroyed");
3288
4073
  imageVisuals?.destroy();
3289
- if (tickTimer)
3290
- clearInterval(tickTimer);
3291
- tickTimer = null;
3292
- });
3293
- renderer.requestLive?.();
3294
- 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();
3295
4082
  requestRender(renderer);
3296
4083
  queueMicrotask(() => {
3297
4084
  (async () => {
@@ -3309,46 +4096,77 @@ async function launchRigOpenTuiApp(options) {
3309
4096
  }
3310
4097
  })();
3311
4098
  });
3312
- tickTimer = setInterval(() => {
3313
- if (destroyed)
3314
- return;
3315
- const state = store.getState();
3316
- if (state.scene === "handoff" && getActivePiHost())
3317
- return;
3318
- store.patch({ tick: state.tick + 1 });
3319
- }, FRAME_MS);
3320
- 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
+ });
3321
4103
  } finally {
3322
4104
  destroyed = true;
3323
- if (tickTimer)
3324
- clearInterval(tickTimer);
3325
- tickTimer = null;
4105
+ if (renderTimer)
4106
+ clearTimeout(renderTimer);
4107
+ renderTimer = null;
4108
+ resources.disposeAll();
3326
4109
  imageVisuals?.destroy();
3327
4110
  stopActivePiHost("app shutdown");
3328
- try {
3329
- renderer?.dropLive?.();
3330
- } catch {}
4111
+ stopActiveCommandHost("app shutdown");
3331
4112
  if (renderer && !renderer.isDestroyed)
3332
4113
  renderer.destroy();
3333
4114
  }
3334
4115
  }
3335
- 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) {
3336
4117
  const layout = computeStageLayout(renderer.width, renderer.height);
3337
4118
  const animationTick = Math.floor(state.tick / ANIMATION_SLOWDOWN);
3338
4119
  const renderState = animationTick === state.tick ? state : { ...state, tick: animationTick };
3339
- const frame = rendererForScene(renderState.scene, renderers)(renderState, layout);
3340
- 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) {
3341
4128
  getActivePiHost()?.resize(layout.width, Math.max(1, layout.height));
3342
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;
3343
4149
  if (graphics) {
3344
4150
  resizeGraphicsLayer(graphics, layout);
3345
- if (frame.terminalActive || frame.fullScreen)
4151
+ if (targetFrame.terminalActive || targetFrame.fullScreen || preloaderActive)
3346
4152
  clearGraphicsLayer(graphics);
3347
4153
  else
3348
4154
  drawAmbientField(graphics, layout, animationTick, state.scene);
3349
4155
  }
3350
- if (imageVisuals && (frame.terminalActive || frame.fullScreen)) {
3351
- 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);
3352
4170
  }
3353
4171
  const contentLayout = frame.fullScreen ? { ...layout, centerWidth: layout.width, centerLeft: 0, centerTop: 0, centerHeight: Math.min(layout.height, MAX_SCENE_LINES) } : layout;
3354
4172
  const sceneLines = fitSceneLines(frame.lines, contentLayout);
@@ -3374,18 +4192,13 @@ function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, text
3374
4192
  ...panel,
3375
4193
  lines: panel.lines.map(remapSelectableLine)
3376
4194
  }));
3377
- if (imageVisuals && !frame.terminalActive && !frame.fullScreen) {
3378
- imageVisuals.render({
3379
- layout: contentLayout,
3380
- pixelSize: pixelSizeForRenderer(renderer),
3381
- panels: renderedPanels,
3382
- scene: state.scene,
3383
- tick: state.tick
3384
- });
3385
- }
3386
4195
  if (graphics && !frame.terminalActive)
3387
4196
  drawPanelChrome(graphics, contentLayout, renderedPanels);
3388
- 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
+ });
3389
4202
  clearTextLines(textLines, renderedSceneLines.length);
3390
4203
  applyScrollPanels(scrollPanels, contentLayout, renderedPanels);
3391
4204
  if (typeBar) {
@@ -3399,6 +4212,10 @@ function renderNow(renderer, graphics, imageVisuals, scrollPanels, typeBar, text
3399
4212
  }
3400
4213
  }
3401
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
+ }
3402
4219
  return renderedSelectableItems;
3403
4220
  }
3404
4221
  export {