@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
@@ -2,7 +2,113 @@
2
2
  // packages/cli/src/app-opentui/render/image-visual-layer.ts
3
3
  import { Worker } from "worker_threads";
4
4
  import { deflateSync } from "zlib";
5
- import { allocateImageId, deleteKittyImage, getCapabilities } from "@earendil-works/pi-tui";
5
+ import { allocateImageId, deleteKittyImage } from "@earendil-works/pi-tui";
6
+
7
+ // packages/cli/src/app-opentui/terminal-capabilities.ts
8
+ import { getCapabilities } from "@earendil-works/pi-tui";
9
+ function getRigTerminalCapabilities() {
10
+ const imageProtocol = getCapabilities().images;
11
+ const supportsKittyImages = imageProtocol === "kitty";
12
+ return {
13
+ imageProtocol,
14
+ supportsKittyImages,
15
+ visualMode: supportsKittyImages ? "kitty-images" : "framebuffer-fallback"
16
+ };
17
+ }
18
+
19
+ // packages/cli/src/app-opentui/render/ascii-fleet.ts
20
+ var LEAD_DRONE = [
21
+ " .-=-. .-=-. ",
22
+ " ( !!! ) ( !!! ) ",
23
+ " '-=-'._ _.'-=-' ",
24
+ " '._ _.' ",
25
+ " '=$$$$$$$=.' ",
26
+ " =$$$$$$$$$$$= ",
27
+ " $$$@@@@@@@@@@$$$ ",
28
+ " $$$@@ @@$$$ ",
29
+ " $$@ ? @$$$ ",
30
+ " $$$@ '-' @$$$ ",
31
+ " $$$@@ @@$$$ ",
32
+ " $$$@@@@@@@@@@$$$ ",
33
+ " =$$$$$$$$$$$= ",
34
+ " '=$$$$$$$=.' ",
35
+ " _.' '._ ",
36
+ " .-=-.' '.-=-. ",
37
+ " ( !!! ) ( !!! ) ",
38
+ " '-=-' '-=-' "
39
+ ];
40
+ var MINI_DRONE = [
41
+ "(!!!) (!!!)",
42
+ " \\%==%/ ",
43
+ " %%?%% ",
44
+ " /%==%\\ ",
45
+ "(!!!) (!!!)"
46
+ ];
47
+ var ROTORS = ["---", "\\\\\\", "|||", "///"];
48
+ var FLEET_GRID_WIDTH = 52;
49
+ var FLEET_GRID_HEIGHT = 27;
50
+ var FLEET = [
51
+ { art: MINI_DRONE, x: 0, y: 2, amp: 1, speed: 0.05, phase: 0, dx: 1, driftSpeed: 0.02 },
52
+ { art: MINI_DRONE, x: 39, y: 2, amp: 1, speed: 0.044, phase: 2.1, dx: 1, driftSpeed: 0.017 },
53
+ { art: MINI_DRONE, x: 1, y: 20, amp: 1, speed: 0.048, phase: 4.2, dx: 1, driftSpeed: 0.023 },
54
+ { art: MINI_DRONE, x: 38, y: 20, amp: 1, speed: 0.041, phase: 1.3, dx: 1, driftSpeed: 0.015 },
55
+ { art: LEAD_DRONE, x: 10, y: 5, amp: 2, speed: 0.04, phase: 0, dx: 0, driftSpeed: 0.011 }
56
+ ];
57
+ function fleetRows(tick, options = {}) {
58
+ const animate = options.animate ?? true;
59
+ const t = animate ? tick : 0;
60
+ const blade = ROTORS[Math.floor(t / 4) % ROTORS.length];
61
+ const pulse = Math.sin(t * 0.07);
62
+ const eye = pulse > 0.45 ? "@" : pulse > -0.1 ? "o" : ".";
63
+ const grid = Array.from({ length: FLEET_GRID_HEIGHT }, () => Array.from({ length: FLEET_GRID_WIDTH }, () => " "));
64
+ for (const drone of FLEET) {
65
+ const yOffset = drone.y + Math.round(drone.amp * Math.sin(t * drone.speed + drone.phase));
66
+ const xOffset = drone.x + Math.round(drone.dx * Math.sin(t * drone.driftSpeed + drone.phase));
67
+ for (let row = 0;row < drone.art.length; row += 1) {
68
+ const source = drone.art[row];
69
+ const targetY = yOffset + row;
70
+ if (targetY < 0 || targetY >= FLEET_GRID_HEIGHT)
71
+ continue;
72
+ for (let col = 0;col < source.length; col += 1) {
73
+ let char = source[col];
74
+ if (char === " ")
75
+ continue;
76
+ const targetX = xOffset + col;
77
+ if (targetX < 0 || targetX >= FLEET_GRID_WIDTH)
78
+ continue;
79
+ if (source.slice(col, col + 3) === "!!!") {
80
+ for (let rotorIndex = 0;rotorIndex < 3; rotorIndex += 1) {
81
+ if (targetX + rotorIndex < FLEET_GRID_WIDTH)
82
+ grid[targetY][targetX + rotorIndex] = blade[rotorIndex];
83
+ }
84
+ col += 2;
85
+ continue;
86
+ }
87
+ if (char === "?")
88
+ char = eye;
89
+ grid[targetY][targetX] = char;
90
+ }
91
+ }
92
+ }
93
+ return grid.map((row) => row.join("").replace(/\s+$/, ""));
94
+ }
95
+
96
+ // packages/cli/src/app-opentui/render/panel-layout.ts
97
+ function panelPixelPlacement(layout, panel, size, minPixels = 8) {
98
+ const cellW = size.width / layout.width;
99
+ const cellH = size.height / layout.height;
100
+ const leftCells = layout.centerLeft + (panel.left ?? 0);
101
+ const topCells = layout.centerTop + panel.top;
102
+ const widthCells = panel.width ?? layout.centerWidth;
103
+ const heightCells = panel.height;
104
+ const left = Math.round(leftCells * cellW);
105
+ const top = Math.round(topCells * cellH);
106
+ const width = Math.round(widthCells * cellW);
107
+ const height = Math.round(heightCells * cellH);
108
+ return width > minPixels && height > minPixels ? { left, top, width, height, leftCells, topCells, widthCells, heightCells } : null;
109
+ }
110
+
111
+ // packages/cli/src/app-opentui/render/image-visual-layer.ts
6
112
  var BG = [7, 8, 9, 255];
7
113
  var PANEL = [16, 17, 21, 184];
8
114
  var PANEL_HEADER = [16, 17, 21, 190];
@@ -16,12 +122,10 @@ var PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
16
122
  var CRC_TABLE = makeCrcTable();
17
123
  var MAX_IMAGE_BYTES = 48 * 1024 * 1024;
18
124
  var KITTY_CHUNK_SIZE = 4096;
19
- var PANEL_BLUR_SCALE = 5;
125
+ var PANEL_BLUR_SCALE = 1;
20
126
  var PANEL_MASK_CACHE_LIMIT = 16;
21
127
  var ZLIB_LEVEL = 1;
22
128
  var DEFAULT_IMAGE_RESOLUTION_SCALE = 1;
23
- var DEFAULT_DRONE_LAYER_SCALE = 0.18;
24
- var DEFAULT_IMAGE_FPS = 24;
25
129
  var panelMaskCache = new Map;
26
130
  var BG_WORD = rgbaWord(BG);
27
131
  function imageTransport() {
@@ -36,24 +140,6 @@ function imageResolutionScale() {
36
140
  return Math.max(0.25, Math.min(1, raw));
37
141
  return DEFAULT_IMAGE_RESOLUTION_SCALE;
38
142
  }
39
- function droneLayerScale() {
40
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_DRONE_SCALE ?? "");
41
- if (Number.isFinite(raw) && raw > 0)
42
- return Math.max(0.15, Math.min(1, raw));
43
- return DEFAULT_DRONE_LAYER_SCALE;
44
- }
45
- function scaledPixelSize(size, scale, layout) {
46
- return {
47
- width: Math.max(layout.width, Math.floor(size.width * scale)),
48
- height: Math.max(layout.height, Math.floor(size.height * scale))
49
- };
50
- }
51
- function imageFps() {
52
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_IMAGE_FPS ?? "");
53
- if (Number.isFinite(raw) && raw > 0)
54
- return Math.max(10, Math.min(60, raw));
55
- return DEFAULT_IMAGE_FPS;
56
- }
57
143
  function rgbaWord(color) {
58
144
  return (color[3] << 24 | color[2] << 16 | color[1] << 8 | color[0]) >>> 0;
59
145
  }
@@ -69,6 +155,109 @@ function fillRgba(data, color, word = rgbaWord(color)) {
69
155
  data[i + 3] = color[3];
70
156
  }
71
157
  }
158
+ var ASCII_GLYPHS = {
159
+ ".": ["00000", "00000", "00000", "00000", "00000", "00100", "00100"],
160
+ "'": ["00100", "00100", "00000", "00000", "00000", "00000", "00000"],
161
+ "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
162
+ _: ["00000", "00000", "00000", "00000", "00000", "00000", "11111"],
163
+ "=": ["00000", "11111", "00000", "00000", "11111", "00000", "00000"],
164
+ "|": ["00100", "00100", "00100", "00100", "00100", "00100", "00100"],
165
+ "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"],
166
+ "\\": ["10000", "01000", "01000", "00100", "00010", "00010", "00001"],
167
+ "(": ["00010", "00100", "01000", "01000", "01000", "00100", "00010"],
168
+ ")": ["01000", "00100", "00010", "00010", "00010", "00100", "01000"],
169
+ "!": ["00100", "00100", "00100", "00100", "00100", "00000", "00100"],
170
+ "%": ["11001", "11010", "00100", "01000", "10110", "00110", "00000"],
171
+ $: ["00100", "11110", "10100", "11110", "00101", "11110", "00100"],
172
+ "@": ["01110", "10001", "10111", "10101", "10111", "10000", "01111"],
173
+ o: ["00000", "01110", "10001", "10001", "10001", "01110", "00000"],
174
+ "*": ["00100", "10101", "01110", "11111", "01110", "10101", "00100"]
175
+ };
176
+ function sceneStaticTick(scene) {
177
+ let hash = 0;
178
+ for (let index = 0;index < scene.length; index += 1)
179
+ hash = hash * 33 + scene.charCodeAt(index) >>> 0;
180
+ return 11 + hash % 97;
181
+ }
182
+ function charColor(char, row, totalRows) {
183
+ if (char === "@" || char === "$" || char === "o")
184
+ return LIME;
185
+ if (char === "%" || char === "!" || char === "/" || char === "\\" || char === "|")
186
+ return CYAN;
187
+ if (char === "." || char === "'" || char === "_" || row < 3 || row > totalRows - 4)
188
+ return INK_DIM;
189
+ if (char === "-" || char === "=" || char === "(" || char === ")")
190
+ return LIME_DIM;
191
+ return MAGENTA;
192
+ }
193
+ function fillRectAlpha(data, width, height, left, top, rectWidth, rectHeight, color, alpha) {
194
+ const x0 = Math.max(0, Math.floor(left));
195
+ const y0 = Math.max(0, Math.floor(top));
196
+ const x1 = Math.min(width, Math.ceil(left + rectWidth));
197
+ const y1 = Math.min(height, Math.ceil(top + rectHeight));
198
+ for (let y = y0;y < y1; y += 1) {
199
+ for (let x = x0;x < x1; x += 1)
200
+ blendOver(data, (y * width + x) * 4, color, alpha);
201
+ }
202
+ }
203
+ function drawAsciiGlyph(data, width, height, x, y, cellWidth, cellHeight, char, color, alpha) {
204
+ const pattern = ASCII_GLYPHS[char] ?? ASCII_GLYPHS["*"];
205
+ const glyphWidth = Math.max(1, cellWidth * 0.56);
206
+ const glyphHeight = Math.max(1, cellHeight * 0.7);
207
+ const originX = x + (cellWidth - glyphWidth) * 0.5;
208
+ const originY = y + (cellHeight - glyphHeight) * 0.5;
209
+ const pixelWidth = Math.max(1, glyphWidth / pattern[0].length);
210
+ const pixelHeight = Math.max(1, glyphHeight / pattern.length);
211
+ for (let row = 0;row < pattern.length; row += 1) {
212
+ const bits = pattern[row];
213
+ for (let col = 0;col < bits.length; col += 1) {
214
+ if (bits[col] !== "1")
215
+ continue;
216
+ fillRectAlpha(data, width, height, originX + col * pixelWidth, originY + row * pixelHeight, pixelWidth * 0.72, pixelHeight * 0.72, color, alpha);
217
+ }
218
+ }
219
+ }
220
+ function drawStaticAsciiFleetBackground(data, width, height, scene, layout) {
221
+ const rows = fleetRows(sceneStaticTick(scene), { animate: true });
222
+ const cellWidth = width / Math.max(1, layout.width);
223
+ const cellHeight = height / Math.max(1, layout.height);
224
+ const leftCells = Math.floor((layout.width - FLEET_GRID_WIDTH) / 2);
225
+ const topCells = Math.floor((layout.height - FLEET_GRID_HEIGHT) / 2);
226
+ const fleetLeft = leftCells * cellWidth;
227
+ const fleetTop = topCells * cellHeight;
228
+ drawGlow(data, width, height, width * 0.5, height * 0.5, Math.min(width, height) * 0.4, LIME, 0.03);
229
+ drawGlow(data, width, height, width * 0.18, height * 0.22, Math.min(width, height) * 0.28, CYAN, 0.022);
230
+ drawGlow(data, width, height, width * 0.82, height * 0.78, Math.min(width, height) * 0.22, MAGENTA, 0.014);
231
+ for (let row = 0;row < rows.length; row += 1) {
232
+ const line = rows[row];
233
+ for (let col = 0;col < line.length; col += 1) {
234
+ const char = line[col];
235
+ if (char === " ")
236
+ continue;
237
+ const x = fleetLeft + col * cellWidth;
238
+ const y = fleetTop + row * cellHeight;
239
+ const color = charColor(char, row, rows.length);
240
+ drawAsciiGlyph(data, width, height, x + cellWidth * 0.07, y + cellHeight * 0.05, cellWidth, cellHeight, char, color, 0.52);
241
+ if (char === "@" || char === "$" || char === "%") {
242
+ drawGlow(data, width, height, x + cellWidth * 0.5, y + cellHeight * 0.5, Math.max(cellWidth, cellHeight) * 0.95, color, 0.02);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ function applyStaticAtmosphere(data, width, height) {
248
+ const cx = width * 0.5;
249
+ const cy = height * 0.42;
250
+ const radius = Math.max(width, height) * 0.78;
251
+ for (let y = 0;y < height; y += 1) {
252
+ for (let x = 0;x < width; x += 1) {
253
+ const distance = Math.hypot(x - cx, y - cy) / radius;
254
+ const alpha = Math.max(0.1, Math.min(0.82, 0.12 + Math.pow(distance, 1.65) * 0.62));
255
+ blendOver(data, (y * width + x) * 4, BG, alpha);
256
+ }
257
+ }
258
+ drawGlow(data, width, height, width * 0.82, height * 0.04, Math.min(width, height) * 0.2, LIME, 0.03);
259
+ drawGlow(data, width, height, width * 0.12, height * 0.18, Math.min(width, height) * 0.16, CYAN, 0.022);
260
+ }
72
261
  function makeCrcTable() {
73
262
  const table = new Uint32Array(256);
74
263
  for (let n = 0;n < 256; n += 1) {
@@ -139,9 +328,6 @@ function kittyImageSequence(base64, options) {
139
328
  }
140
329
  return chunks.join("");
141
330
  }
142
- function isModernImageTerminal(protocol) {
143
- return protocol === "kitty";
144
- }
145
331
  function clampByte(value) {
146
332
  return Math.max(0, Math.min(255, Math.round(value)));
147
333
  }
@@ -153,23 +339,6 @@ function blendOver(data, index, color, alphaScale = 1) {
153
339
  data[index + 2] = clampByte(color[2] * alpha + data[index + 2] * inv);
154
340
  data[index + 3] = 255;
155
341
  }
156
- function drawDisc(data, width, height, cx, cy, radius, color, alpha = 1) {
157
- const minX = Math.max(0, Math.floor(cx - radius));
158
- const maxX = Math.min(width - 1, Math.ceil(cx + radius));
159
- const minY = Math.max(0, Math.floor(cy - radius));
160
- const maxY = Math.min(height - 1, Math.ceil(cy + radius));
161
- for (let y = minY;y <= maxY; y += 1) {
162
- for (let x = minX;x <= maxX; x += 1) {
163
- const dx = x + 0.5 - cx;
164
- const dy = y + 0.5 - cy;
165
- const d = Math.sqrt(dx * dx + dy * dy);
166
- if (d > radius)
167
- continue;
168
- const edge = Math.max(0, Math.min(1, radius - d));
169
- blendOver(data, (y * width + x) * 4, color, alpha * Math.min(1, 0.35 + edge));
170
- }
171
- }
172
- }
173
342
  function drawGlow(data, width, height, cx, cy, radius, color, alpha = 0.18) {
174
343
  const minX = Math.max(0, Math.floor(cx - radius));
175
344
  const maxX = Math.min(width - 1, Math.ceil(cx + radius));
@@ -187,30 +356,6 @@ function drawGlow(data, width, height, cx, cy, radius, color, alpha = 0.18) {
187
356
  }
188
357
  }
189
358
  }
190
- function drawLine(data, width, height, x0, y0, x1, y1, color, alpha = 0.5, thickness = 1.4) {
191
- const dx = x1 - x0;
192
- const dy = y1 - y0;
193
- const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) * 1.2));
194
- for (let i = 0;i <= steps; i += 1) {
195
- const t = i / steps;
196
- drawDisc(data, width, height, x0 + dx * t, y0 + dy * t, thickness, color, alpha);
197
- }
198
- }
199
- function drawDrone(data, width, height, x, y, scale, tick, color) {
200
- const wobble = Math.sin(tick * 0.055) * scale * 0.35;
201
- const cy = y + wobble;
202
- drawGlow(data, width, height, x, cy, scale * 7, color, 0.18);
203
- drawDisc(data, width, height, x, cy, scale * 1.45, color, 0.92);
204
- drawDisc(data, width, height, x, cy, scale * 0.55, [235, 255, 176, 255], 0.85);
205
- const arm = scale * 4.1;
206
- const rotor = scale * 1.6;
207
- const arms = [[-arm, -arm * 0.72], [arm, -arm * 0.72], [-arm, arm * 0.72], [arm, arm * 0.72]];
208
- for (const [dx, dy] of arms) {
209
- drawLine(data, width, height, x, cy, x + dx, cy + dy, color, 0.42, Math.max(1, scale * 0.22));
210
- drawGlow(data, width, height, x + dx, cy + dy, rotor * 2.6, color, 0.1);
211
- drawDisc(data, width, height, x + dx, cy + dy, rotor, color, 0.34 + 0.12 * Math.sin(tick * 0.15));
212
- }
213
- }
214
359
  function roundRectAlpha(px, py, x, y, w, h, r) {
215
360
  const rx = Math.max(x + r, Math.min(px, x + w - r));
216
361
  const ry = Math.max(y + r, Math.min(py, y + h - r));
@@ -220,17 +365,7 @@ function roundRectAlpha(px, py, x, y, w, h, r) {
220
365
  return Math.max(0, Math.min(1, r + 0.75 - dist));
221
366
  }
222
367
  function panelRect(layout, panel, size) {
223
- const cellW = size.width / layout.width;
224
- const cellH = size.height / layout.height;
225
- const leftCells = layout.centerLeft + (panel.left ?? 0);
226
- const topCells = layout.centerTop + panel.top;
227
- const widthCells = panel.width ?? layout.centerWidth;
228
- const heightCells = panel.height;
229
- const left = Math.round(leftCells * cellW);
230
- const top = Math.round(topCells * cellH);
231
- const width = Math.round(widthCells * cellW);
232
- const height = Math.round(heightCells * cellH);
233
- return width > 8 && height > 8 ? { left, top, width, height } : null;
368
+ return panelPixelPlacement(layout, panel, size);
234
369
  }
235
370
  function downsampleRegion(data, canvasW, canvasH, rect, scale) {
236
371
  const w = Math.max(1, Math.ceil(rect.width / scale));
@@ -379,7 +514,8 @@ function getPanelMask(width, height, rect, headerPx, blurScale) {
379
514
  function compositePanel(data, width, height, rect, headerPx) {
380
515
  const blurScale = PANEL_BLUR_SCALE;
381
516
  const sampled = downsampleRegion(data, width, height, rect, blurScale);
382
- const blurred = blurRgb(sampled.pixels, sampled.width, sampled.height, 4);
517
+ const blurRadius = Math.max(8, Math.min(28, Math.round(Math.min(rect.width, rect.height) * 0.018)));
518
+ const blurred = blurRgb(sampled.pixels, sampled.width, sampled.height, blurRadius);
383
519
  const mask = getPanelMask(width, height, rect, headerPx, blurScale);
384
520
  for (let i = 0;i < mask.indices.length; i += 1) {
385
521
  const idx = mask.indices[i];
@@ -397,161 +533,10 @@ function compositePanel(data, width, height, rect, headerPx) {
397
533
  blendOver(data, mask.dividerIndices[i], PANEL_LINE, 0.28);
398
534
  }
399
535
  }
400
- function visualSceneKind(scene) {
401
- if (scene === "tasks")
402
- return "loop";
403
- if (scene === "help")
404
- return "carrier";
405
- return "dispatch";
406
- }
407
- function seededUnit(seed) {
408
- let value = seed >>> 0;
409
- value = value * 1664525 + 1013904223 >>> 0;
410
- return value / 4294967295;
411
- }
412
- function sceneCore(width, height, kind) {
413
- if (kind === "dispatch")
414
- return { x: width * 0.3, y: height * 0.45 };
415
- if (kind === "carrier")
416
- return { x: width * 0.5, y: height * 0.54 };
417
- return { x: width * 0.09, y: height * 0.56 };
418
- }
419
- function dispatchLane(width, index, count) {
420
- const a = seededUnit(28672 + index * 101);
421
- const b = seededUnit(36864 + index * 131);
422
- const c = seededUnit(45056 + index * 151);
423
- const ang = (-0.9 + 1.8 * (index / Math.max(1, count - 1))) * (Math.PI * 0.5);
424
- return { ang, len: width * (0.32 + a * 0.42), curve: (b - 0.5) * 0.7, phase: c * Math.PI * 2 };
425
- }
426
- function lanePoint(width, height, lane, p, kind = "dispatch") {
427
- const core = sceneCore(width, height, kind);
428
- const dist = lane.len * p;
429
- const bend = Math.sin(p * Math.PI) * lane.curve * Math.min(width, height) * 0.035;
430
- const ux = Math.cos(lane.ang);
431
- const uy = Math.sin(lane.ang);
432
- return { x: core.x + ux * dist + -uy * bend, y: core.y + uy * dist + ux * bend };
433
- }
434
- function drawMarketingRails(data, width, height, kind, tick) {
435
- const ys = kind === "loop" ? [0.18, 0.5, 0.82] : kind === "carrier" ? [0.24, 0.54, 0.82] : [0.18, 0.58, 0.82];
436
- ys.forEach((ratio, index) => {
437
- const y = height * ratio + Math.sin(tick * 0.018 + index) * Math.max(2, height * 0.0025);
438
- const color = index === 2 ? CYAN : index === 1 ? LIME : LIME_DIM;
439
- drawLine(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));
440
- for (let p = (tick * 0.004 + index * 0.21) % 1;p < 1; p += 0.18 + index * 0.02) {
441
- const x = width * (0.06 + 0.88 * p);
442
- drawDisc(data, width, height, x, y, Math.max(1.2, height * (0.0017 + index * 0.0004)), color, 0.24);
443
- drawGlow(data, width, height, x, y, Math.max(5, height * 0.006), color, 0.035);
444
- }
445
- });
446
- }
447
- function drawDispatchScene(data, width, height, tick) {
448
- const core = sceneCore(width, height, "dispatch");
449
- const min = Math.min(width, height);
450
- drawGlow(data, width, height, core.x, core.y, min * 0.085, LIME, 0.12);
451
- drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.85);
452
- const laneCount = width < 1000 ? 7 : 12;
453
- const lanes = Array.from({ length: laneCount }, (_, index) => dispatchLane(width, index, laneCount));
454
- lanes.forEach((lane) => {
455
- let prev = lanePoint(width, height, lane, 0);
456
- for (let step = 1;step <= 32; step += 1) {
457
- const next = lanePoint(width, height, lane, step / 32);
458
- drawLine(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));
459
- prev = next;
460
- }
461
- });
462
- const packets = width < 1000 ? 70 : 150;
463
- for (let i = 0;i < packets; i += 1) {
464
- const lane = lanes[i % lanes.length];
465
- const speed = 0.003 + seededUnit(12288 + i * 17) * 0.009;
466
- const p = (seededUnit(16384 + i * 19) + tick * speed) % 1;
467
- const pt = lanePoint(width, height, lane, p);
468
- const wobble = (seededUnit(20480 + i * 23) - 0.5) * min * 0.012;
469
- const fade = p < 0.1 ? p / 0.1 : p > 0.88 ? (1 - p) / 0.12 : 1;
470
- const color = seededUnit(24576 + i * 29) < 0.22 ? CYAN : LIME_DIM;
471
- const y = pt.y + wobble * Math.sin(p * Math.PI * 2);
472
- drawLine(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));
473
- drawDisc(data, width, height, pt.x, y, Math.max(1.3, min * 0.0022), color, 0.5 * fade);
474
- drawGlow(data, width, height, pt.x, y, Math.max(4, min * 0.008), color, 0.045 * fade);
475
- }
476
- for (let i = 0;i < (width < 1000 ? 4 : 8); i += 1) {
477
- const lane = lanes[i % lanes.length];
478
- const p = (tick * 0.002 + i * 0.13) % 1;
479
- const pt = lanePoint(width, height, lane, p);
480
- 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);
481
- }
482
- }
483
- function drawLoopScene(data, width, height, tick) {
484
- const min = Math.min(width, height);
485
- const y = height * 0.56;
486
- const x0 = width * 0.09;
487
- const x1 = width * 0.91;
488
- drawLine(data, width, height, x0, y, x1, y, LIME_DIM, 0.12, Math.max(0.8, min * 0.001));
489
- const gates = width < 1000 ? 4 : 5;
490
- const active = Math.floor(tick / 16) % gates;
491
- for (let i = 0;i < gates; i += 1) {
492
- const x = x0 + (x1 - x0) * (i / Math.max(1, gates - 1));
493
- const color = i === active ? LIME : i <= active ? LIME_DIM : INK_DIM;
494
- drawLine(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));
495
- drawDisc(data, width, height, x, y, i === active ? min * 0.006 : min * 0.0035, color, i === active ? 0.72 : 0.25);
496
- drawGlow(data, width, height, x, y, i === active ? min * 0.018 : min * 0.01, color, i === active ? 0.1 : 0.035);
497
- }
498
- const packets = width < 1000 ? 36 : 84;
499
- for (let i = 0;i < packets; i += 1) {
500
- const p = (seededUnit(33024 + i * 13) + tick * (0.0022 + seededUnit(33280 + i * 17) * 0.004)) % 1;
501
- const x = x0 + (x1 - x0) * p;
502
- const segment = Math.min(gates - 1, Math.floor(p * gates));
503
- const color = segment < active ? LIME_DIM : segment === active ? CYAN : INK_DIM;
504
- const py = y + (seededUnit(33536 + i * 19) - 0.5) * min * 0.018 * Math.sin(p * Math.PI * 2);
505
- drawLine(data, width, height, x - min * 0.014, py, x, py, color, 0.1, Math.max(0.7, min * 0.0008));
506
- drawDisc(data, width, height, x, py, Math.max(1.2, min * 0.002), color, color === INK_DIM ? 0.22 : 0.58);
507
- }
508
- drawDrone(data, width, height, x0 + (x1 - x0) * (tick * 0.003 % 1), y - min * 0.04, min * 0.0065, tick, LIME_DIM);
509
- drawDrone(data, width, height, x0 + (x1 - x0) * ((tick * 0.002 + 0.5) % 1), y + min * 0.036, min * 0.0055, tick + 40, MAGENTA);
510
- }
511
- function drawCarrierScene(data, width, height, tick) {
512
- const min = Math.min(width, height);
513
- const core = sceneCore(width, height, "carrier");
514
- drawGlow(data, width, height, core.x, core.y, min * 0.075, LIME, 0.12);
515
- drawDisc(data, width, height, core.x, core.y, min * 0.0065, LIME, 0.92);
516
- const nodes = width < 1000 ? 5 : 7;
517
- const rx = width * 0.36;
518
- const ry = height * 0.22;
519
- for (let i = 0;i < nodes; i += 1) {
520
- const a = i / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
521
- const x = core.x + Math.cos(a) * rx;
522
- const y = core.y + Math.sin(a) * ry;
523
- const color = i % 2 ? CYAN : LIME_DIM;
524
- drawLine(data, width, height, core.x, core.y, x, y, color, 0.055, Math.max(0.7, min * 0.0008));
525
- drawGlow(data, width, height, x, y, min * 0.018, color, 0.055);
526
- drawDisc(data, width, height, x, y, min * 0.0048, color, 0.62);
527
- drawDrone(data, width, height, x, y - min * 0.018, min * 0.005, tick + i * 9, color);
528
- }
529
- const packets = width < 1000 ? 30 : 72;
530
- for (let i = 0;i < packets; i += 1) {
531
- const lane = i % nodes;
532
- const a = lane / nodes * Math.PI * 2 - Math.PI / 2 + tick * 0.006;
533
- const nx = core.x + Math.cos(a) * rx;
534
- const ny = core.y + Math.sin(a) * ry;
535
- const p = (seededUnit(37120 + i * 37) + tick * (0.003 + seededUnit(37376 + i * 43) * 0.009)) % 1;
536
- const x = core.x + (nx - core.x) * p;
537
- const y = core.y + (ny - core.y) * p;
538
- const color = seededUnit(37632 + i * 47) < 0.35 ? CYAN : LIME_DIM;
539
- drawDisc(data, width, height, x, y, Math.max(1.2, min * 0.002), color, 0.48);
540
- drawGlow(data, width, height, x, y, min * 0.008, color, 0.035);
541
- }
542
- }
543
- function drawBackground(data, width, height, tick, scene) {
536
+ function drawBackground(data, width, height, _tick, scene, layout) {
544
537
  fillRgba(data, BG, BG_WORD);
545
- const kind = visualSceneKind(scene);
546
- drawGlow(data, width, height, width * 0.82, height * 0.04, Math.min(width, height) * 0.22, LIME, 0.035);
547
- drawGlow(data, width, height, width * 0.12, height * 0.18, Math.min(width, height) * 0.17, CYAN, 0.025);
548
- drawMarketingRails(data, width, height, kind, tick);
549
- if (kind === "loop")
550
- drawLoopScene(data, width, height, tick);
551
- else if (kind === "carrier")
552
- drawCarrierScene(data, width, height, tick);
553
- else
554
- drawDispatchScene(data, width, height, tick);
538
+ drawStaticAsciiFleetBackground(data, width, height, scene, layout);
539
+ applyStaticAtmosphere(data, width, height);
555
540
  }
556
541
  function imageVisualFrameKey(input) {
557
542
  const width = Math.max(1, Math.floor(input.pixelSize.width));
@@ -576,14 +561,76 @@ function encodeKittyFrame(width, height, data, input) {
576
561
  compressed: true
577
562
  });
578
563
  }
564
+ function cropRgba(data, sourceWidth, rect) {
565
+ const out = new Uint8ClampedArray(rect.width * rect.height * 4);
566
+ for (let y = 0;y < rect.height; y += 1) {
567
+ const sourceStart = ((rect.top + y) * sourceWidth + rect.left) * 4;
568
+ out.set(data.subarray(sourceStart, sourceStart + rect.width * 4), y * rect.width * 4);
569
+ }
570
+ return out;
571
+ }
572
+ function encodePanelPatch(data, sourceWidth, placement, imageId, zIndex) {
573
+ const patch = cropRgba(data, sourceWidth, placement);
574
+ const png = encodePng(placement.width, placement.height, patch);
575
+ const sequence = kittyImageSequence(png.toString("base64"), {
576
+ columns: Math.max(1, Math.round(placement.widthCells)),
577
+ rows: Math.max(1, Math.round(placement.heightCells)),
578
+ imageId,
579
+ zIndex,
580
+ format: 100
581
+ });
582
+ return `\x1B[${Math.max(1, Math.round(placement.topCells) + 1)};${Math.max(1, Math.round(placement.leftCells) + 1)}H${sequence}`;
583
+ }
579
584
  function renderImageVisualFrame(input) {
580
585
  const width = Math.max(1, Math.floor(input.pixelSize.width));
581
586
  const height = Math.max(1, Math.floor(input.pixelSize.height));
582
587
  const byteCount = width * height * 4;
583
588
  if (byteCount > MAX_IMAGE_BYTES)
584
589
  return null;
590
+ const layer = input.layer ?? "full";
585
591
  const data = new Uint8ClampedArray(byteCount);
586
- drawBackground(data, width, height, input.tick, input.scene);
592
+ drawBackground(data, width, height, 0, input.scene, input.layout);
593
+ if (layer === "split") {
594
+ const sequences = [encodeKittyFrame(width, height, data, input)];
595
+ let panelIndex = 0;
596
+ for (const panel of input.panels) {
597
+ if (panel.chrome !== "ad-terminal")
598
+ continue;
599
+ const placement = panelPixelPlacement(input.layout, panel, { width, height });
600
+ if (!placement)
601
+ continue;
602
+ const cellH = height / input.layout.height;
603
+ const headerPx = Math.max(cellH * 3, (panel.headerHeight ?? 3) * cellH);
604
+ compositePanel(data, width, height, placement, headerPx);
605
+ const imageId = input.imageIds?.[panelIndex] ?? (input.imageId + 1000 + panelIndex >>> 0 || panelIndex + 1);
606
+ sequences.push(encodePanelPatch(data, width, placement, imageId, -12));
607
+ panelIndex += 1;
608
+ }
609
+ return { key: imageVisualFrameKey(input), sequence: sequences.join("") };
610
+ }
611
+ if (layer === "panel-chrome")
612
+ return { key: imageVisualFrameKey(input), sequence: "" };
613
+ if (layer === "background") {
614
+ return { key: imageVisualFrameKey(input), sequence: encodeKittyFrame(width, height, data, input) };
615
+ }
616
+ if (layer === "panel-patches") {
617
+ const sequences = [];
618
+ let panelIndex = 0;
619
+ for (const panel of input.panels) {
620
+ if (panel.chrome !== "ad-terminal")
621
+ continue;
622
+ const placement = panelPixelPlacement(input.layout, panel, { width, height });
623
+ if (!placement)
624
+ continue;
625
+ const cellH = height / input.layout.height;
626
+ const headerPx = Math.max(cellH * 3, (panel.headerHeight ?? 3) * cellH);
627
+ compositePanel(data, width, height, placement, headerPx);
628
+ const imageId = input.imageIds?.[panelIndex] ?? (input.imageId + 1000 + panelIndex >>> 0 || panelIndex + 1);
629
+ sequences.push(encodePanelPatch(data, width, placement, imageId, input.zIndex));
630
+ panelIndex += 1;
631
+ }
632
+ return { key: imageVisualFrameKey(input), sequence: sequences.join("") };
633
+ }
587
634
  for (const panel of input.panels) {
588
635
  if (panel.chrome !== "ad-terminal")
589
636
  continue;
@@ -596,8 +643,11 @@ function renderImageVisualFrame(input) {
596
643
  }
597
644
  return { key: imageVisualFrameKey(input), sequence: encodeKittyFrame(width, height, data, input) };
598
645
  }
646
+ function shouldRenderImagesInline() {
647
+ return process.env.RIG_OPENTUI_IMAGE_WORKER === "0" || import.meta.url.includes("$bunfs");
648
+ }
599
649
  function createImageWorker() {
600
- if (process.env.RIG_OPENTUI_IMAGE_WORKER === "0")
650
+ if (shouldRenderImagesInline())
601
651
  return null;
602
652
  const isTypeScriptRuntime = import.meta.url.endsWith(".ts");
603
653
  const workerUrl = new URL(isTypeScriptRuntime ? "./image-visual-layer-worker.ts" : "./image-visual-layer-worker.js", import.meta.url);
@@ -613,24 +663,8 @@ function createImageWorker() {
613
663
  }
614
664
  }
615
665
  var RECENT_WRITTEN_LIMIT = 24;
616
- var DEFAULT_PANEL_FPS = 24;
617
- var DEFAULT_PANEL_LAYER_SCALE = 0.22;
618
666
  var DEFAULT_IMAGE_OUTPUT_BACKLOG_BYTES = 256 * 1024;
619
667
  var DEFAULT_IMAGE_OUTPUT_BYTES_PER_SECOND = 850 * 1024;
620
- var MIN_BACKGROUND_BUDGET_BYTES = 24 * 1024;
621
- var MIN_PANEL_BUDGET_BYTES = 56 * 1024;
622
- function panelFps() {
623
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_FPS ?? "");
624
- if (Number.isFinite(raw) && raw > 0)
625
- return Math.max(4, Math.min(60, raw));
626
- return DEFAULT_PANEL_FPS;
627
- }
628
- function panelLayerScale() {
629
- const raw = Number.parseFloat(process.env.RIG_OPENTUI_PANEL_SCALE ?? "");
630
- if (Number.isFinite(raw) && raw > 0)
631
- return Math.max(0.18, Math.min(1, raw));
632
- return DEFAULT_PANEL_LAYER_SCALE;
633
- }
634
668
  function imageOutputBacklogBytes() {
635
669
  const raw = Number.parseInt(process.env.RIG_OPENTUI_IMAGE_BACKLOG_BYTES ?? "", 10);
636
670
  if (Number.isFinite(raw) && raw > 0)
@@ -652,52 +686,43 @@ class ImageVisualLayer {
652
686
  panelImageIds = Array.from({ length: 8 }, () => allocateImageId());
653
687
  panelChromeImageIds = Array.from({ length: 8 }, () => allocateImageId());
654
688
  stdout;
689
+ onFrameReady;
655
690
  protocol;
656
691
  transport = imageTransport();
657
- frameIntervalMs = 1000 / imageFps();
658
- panelIntervalMs = 1000 / panelFps();
659
692
  maxOutputBacklogBytes = imageOutputBacklogBytes();
660
693
  outputBytesPerSecond = imageOutputBytesPerSecond();
661
694
  outputBudgetBytes = imageOutputBytesPerSecond();
662
695
  lastBudgetAtMs = Date.now();
663
696
  motionSlot = this.createSlot();
664
- panelSlot = this.createSlot();
665
- chromeSlot = this.createSlot();
697
+ workersStarted = false;
666
698
  pendingFrames = new Map;
667
699
  recentWrittenKeys = [];
700
+ imagesVisible = false;
701
+ flushPaused = false;
668
702
  requestId = 0;
669
- lastMotionFrameIndex = -1;
670
- lastPanelFrameIndex = -1;
671
- lastChromeKey = "";
703
+ lastBackgroundKey = "";
704
+ fatalWorkerError = null;
672
705
  destroyed = false;
673
- constructor(stdout = process.stdout) {
706
+ constructor(stdout = process.stdout, onFrameReady) {
674
707
  this.stdout = stdout;
675
- this.protocol = getCapabilities().images;
676
- if (this.enabled) {
677
- this.startWorker(this.motionSlot);
678
- this.startWorker(this.panelSlot);
679
- this.startWorker(this.chromeSlot);
680
- }
708
+ this.onFrameReady = onFrameReady;
709
+ this.protocol = getRigTerminalCapabilities().imageProtocol;
681
710
  }
682
711
  get enabled() {
683
- return isModernImageTerminal(this.protocol);
712
+ return this.protocol === "kitty";
684
713
  }
685
714
  clear() {
686
715
  if (this.enabled) {
687
- this.stdout.write([this.imageId, ...this.panelImageIds, ...this.panelChromeImageIds].map((id) => deleteKittyImage(id)).join("") + "\x1B[?25h");
716
+ if (this.imagesVisible)
717
+ this.stdout.write([this.imageId, ...this.panelImageIds, ...this.panelChromeImageIds].map((id) => deleteKittyImage(id)).join("") + "\x1B[?25h");
718
+ this.imagesVisible = false;
688
719
  this.pendingFrames.clear();
689
720
  this.recentWrittenKeys.length = 0;
690
721
  this.resetSlot(this.motionSlot);
691
- this.resetSlot(this.panelSlot);
692
- this.resetSlot(this.chromeSlot);
693
- this.lastMotionFrameIndex = -1;
694
- this.lastPanelFrameIndex = -1;
695
- this.lastChromeKey = "";
722
+ this.lastBackgroundKey = "";
696
723
  this.outputBudgetBytes = this.outputBytesPerSecond;
697
724
  this.lastBudgetAtMs = Date.now();
698
725
  this.motionSlot.clearedThroughRequestId = this.requestId;
699
- this.panelSlot.clearedThroughRequestId = this.requestId;
700
- this.chromeSlot.clearedThroughRequestId = this.requestId;
701
726
  }
702
727
  }
703
728
  destroy() {
@@ -705,19 +730,31 @@ class ImageVisualLayer {
705
730
  return;
706
731
  this.clear();
707
732
  this.destroyed = true;
708
- const workers = [this.motionSlot.worker, this.panelSlot.worker, this.chromeSlot.worker];
733
+ const workers = [this.motionSlot.worker];
709
734
  this.motionSlot.worker = null;
710
- this.panelSlot.worker = null;
711
- this.chromeSlot.worker = null;
712
735
  for (const worker of workers)
713
736
  if (worker)
714
737
  worker.terminate().catch(() => {
715
738
  return;
716
739
  });
717
740
  }
741
+ setFlushPaused(paused) {
742
+ this.flushPaused = paused;
743
+ }
744
+ isReady(input) {
745
+ if (!this.enabled || this.destroyed)
746
+ return true;
747
+ this.throwIfFatal();
748
+ const splitKey = imageVisualFrameKey({ ...input, tick: 0, layer: "split" });
749
+ return this.recentWrittenKeys.includes(splitKey) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === splitKey);
750
+ }
718
751
  render(input) {
719
752
  if (!this.enabled || this.destroyed)
720
753
  return;
754
+ this.throwIfFatal();
755
+ if (this.transport !== "png")
756
+ this.failWorker("renderer", new Error("OpenTUI image visuals require PNG transport for static-background rendering."));
757
+ this.ensureWorkersStarted();
721
758
  const width = Math.max(1, Math.floor(input.pixelSize.width));
722
759
  const height = Math.max(1, Math.floor(input.pixelSize.height));
723
760
  if (width * height * 4 > MAX_IMAGE_BYTES)
@@ -725,54 +762,32 @@ class ImageVisualLayer {
725
762
  if (this.isOutputBackedUp())
726
763
  return;
727
764
  this.refillOutputBudget();
728
- const now = Date.now();
729
- const visualTick = Math.floor(now / (1000 / 60));
730
- const useDynamicSplit = Boolean(this.motionSlot.worker && this.panelSlot.worker && this.chromeSlot.worker && this.transport === "png" && process.env.RIG_OPENTUI_DYNAMIC_SPLIT !== "0");
731
- if (!useDynamicSplit) {
732
- const frameIndex = Math.floor(now / this.frameIntervalMs);
733
- if (!input.force && frameIndex === this.lastMotionFrameIndex)
734
- return;
735
- this.scheduleRender(this.motionSlot, { ...input, tick: visualTick, layer: "full" }, this.imageId, -10, input.force);
736
- this.lastMotionFrameIndex = frameIndex;
737
- return;
738
- }
739
- const motionFrameIndex = Math.floor(now / this.frameIntervalMs);
740
- if ((input.force || motionFrameIndex !== this.lastMotionFrameIndex) && this.outputBudgetBytes >= MIN_BACKGROUND_BUDGET_BYTES) {
741
- this.scheduleRender(this.motionSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, droneLayerScale(), input.layout), panels: [], tick: visualTick, layer: "background" }, this.imageId, -30, input.force);
742
- this.lastMotionFrameIndex = motionFrameIndex;
743
- }
744
- const panelFrameIndex = Math.floor(now / this.panelIntervalMs);
745
- if ((input.force || panelFrameIndex !== this.lastPanelFrameIndex) && this.outputBudgetBytes >= MIN_PANEL_BUDGET_BYTES) {
746
- this.scheduleRender(this.panelSlot, { ...input, pixelSize: scaledPixelSize(input.pixelSize, panelLayerScale(), input.layout), tick: visualTick, layer: "panel-patches" }, this.imageId, -12, input.force, this.panelImageIds);
747
- this.lastPanelFrameIndex = panelFrameIndex;
748
- }
749
- const chromeInput = { ...input, tick: 0, layer: "panel-chrome" };
750
- const chromeKey = imageVisualFrameKey(chromeInput);
751
- if (input.force || chromeKey !== this.lastChromeKey) {
752
- this.scheduleRender(this.chromeSlot, chromeInput, this.imageId, -8, input.force, this.panelChromeImageIds);
753
- this.lastChromeKey = chromeKey;
765
+ const splitInput = { ...input, tick: 0, layer: "split" };
766
+ const splitKey = imageVisualFrameKey(splitInput);
767
+ if (input.force || splitKey !== this.lastBackgroundKey) {
768
+ this.scheduleRender(this.motionSlot, splitInput, this.imageId, -30, input.force, this.panelImageIds);
769
+ this.lastBackgroundKey = splitKey;
754
770
  }
755
771
  }
756
772
  flush() {
773
+ this.throwIfFatal();
774
+ if (this.flushPaused)
775
+ return false;
757
776
  if (this.pendingFrames.size === 0)
758
777
  return false;
759
778
  this.refillOutputBudget();
760
779
  if (this.isOutputBackedUp()) {
761
- const chrome = this.pendingFrames.get("panel-chrome");
780
+ const retainedSplit = this.pendingFrames.get("split");
762
781
  this.pendingFrames.clear();
763
- if (chrome)
764
- this.pendingFrames.set("panel-chrome", chrome);
782
+ if (retainedSplit)
783
+ this.pendingFrames.set("split", retainedSplit);
765
784
  return false;
766
785
  }
767
786
  const framePriority = (frame) => {
768
787
  const layer = pendingLayerKey(frame.key);
769
- if (layer === "panel-chrome")
770
- return 0;
771
- if (layer === "background")
788
+ if (layer === "split")
772
789
  return 1;
773
- if (layer === "panel-patches")
774
- return 2;
775
- return 2;
790
+ return 4;
776
791
  };
777
792
  const frames = Array.from(this.pendingFrames.values()).sort((a, b) => framePriority(a) - framePriority(b));
778
793
  const prefix = "\x1B[?25l\x1B7\x1B[s\x1B[H";
@@ -784,12 +799,14 @@ class ImageVisualLayer {
784
799
  for (const frame of frames) {
785
800
  const candidate = sequence + frame.sequence;
786
801
  const candidateBytes = Buffer.byteLength(prefix + candidate + suffix);
787
- if (candidateBytes <= this.outputBudgetBytes) {
802
+ const layer = pendingLayerKey(frame.key);
803
+ const allowOneStaticFrame = selected.length === 0 && layer === "split";
804
+ if (candidateBytes <= this.outputBudgetBytes || allowOneStaticFrame) {
788
805
  selected.push(frame);
789
806
  sequence = candidate;
790
807
  byteLength = candidateBytes;
791
- } else if (pendingLayerKey(frame.key) === "panel-chrome") {
792
- retained.set(pendingLayerKey(frame.key), frame);
808
+ } else if (layer === "split") {
809
+ retained.set(layer, frame);
793
810
  }
794
811
  }
795
812
  this.pendingFrames.clear();
@@ -804,10 +821,11 @@ class ImageVisualLayer {
804
821
  this.stdout.cork?.();
805
822
  const accepted = this.stdout.write(payload);
806
823
  this.stdout.uncork?.();
824
+ this.imagesVisible = true;
807
825
  return accepted;
808
826
  }
809
827
  createSlot() {
810
- return { worker: null, busy: false, queuedRequest: null, inFlightKey: "", queuedKey: "", clearedThroughRequestId: 0 };
828
+ return { worker: null, busy: false, queuedRequest: null, inFlightKey: "", queuedKey: "", clearedThroughRequestId: 0, latestRequestId: 0 };
811
829
  }
812
830
  resetSlot(slot) {
813
831
  slot.busy = false;
@@ -835,7 +853,23 @@ class ImageVisualLayer {
835
853
  isKnownKey(slot, key) {
836
854
  return this.recentWrittenKeys.includes(key) || Array.from(this.pendingFrames.values()).some((frame) => frame.key === key) || key === slot.inFlightKey || key === slot.queuedKey;
837
855
  }
856
+ ensureWorkersStarted() {
857
+ if (this.workersStarted)
858
+ return;
859
+ this.workersStarted = true;
860
+ this.startWorker(this.motionSlot, "static-visuals");
861
+ }
862
+ failWorker(name, error) {
863
+ const detail = error instanceof Error ? error.message : String(error);
864
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} failed: ${detail}`);
865
+ throw this.fatalWorkerError;
866
+ }
867
+ throwIfFatal() {
868
+ if (this.fatalWorkerError)
869
+ throw this.fatalWorkerError;
870
+ }
838
871
  scheduleRender(slot, input, imageId, zIndex, force, imageIds) {
872
+ this.throwIfFatal();
839
873
  const key = imageVisualFrameKey(input);
840
874
  if (!force && this.isKnownKey(slot, key))
841
875
  return;
@@ -848,38 +882,41 @@ class ImageVisualLayer {
848
882
  requestId: ++this.requestId,
849
883
  key
850
884
  };
851
- if (slot.worker) {
852
- this.enqueueWorkerRender(slot, request);
885
+ slot.latestRequestId = request.requestId;
886
+ if (!slot.worker) {
887
+ this.renderInline(slot, request);
853
888
  return;
854
889
  }
855
- this.renderSync(slot, request);
890
+ this.enqueueWorkerRender(slot, request);
856
891
  }
857
- startWorker(slot) {
892
+ startWorker(slot, name) {
858
893
  const worker = createImageWorker();
859
894
  if (!worker)
860
895
  return;
861
896
  slot.worker = worker;
862
897
  worker.on("message", (message) => this.handleWorkerMessage(slot, message));
863
- worker.once("error", () => this.disableWorker(slot));
864
- worker.once("exit", () => this.disableWorker(slot));
865
- }
866
- disableWorker(slot) {
867
- if (this.destroyed)
868
- return;
869
- slot.worker = null;
870
- slot.busy = false;
871
- slot.inFlightKey = "";
872
- const queued = slot.queuedRequest;
873
- slot.queuedRequest = null;
874
- slot.queuedKey = "";
875
- if (queued?.layer === "full")
876
- this.renderSync(slot, queued);
898
+ worker.once("error", (error) => {
899
+ if (!this.destroyed)
900
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} failed: ${error instanceof Error ? error.message : String(error)}`);
901
+ });
902
+ worker.once("exit", (code) => {
903
+ if (!this.destroyed)
904
+ this.fatalWorkerError ??= new Error(`OpenTUI image worker ${name} exited unexpectedly with code ${code}.`);
905
+ });
906
+ }
907
+ renderInline(slot, request) {
908
+ try {
909
+ const result = renderImageVisualFrame(request);
910
+ if (result?.sequence && !this.isKnownKey(slot, result.key) && (!this.isOutputBackedUp() || pendingLayerKey(result.key) === "split")) {
911
+ this.stageFrame({ key: result.key, sequence: result.sequence });
912
+ this.onFrameReady?.();
913
+ }
914
+ } catch (error) {
915
+ this.failWorker(String(request.layer ?? "visual"), error);
916
+ }
877
917
  }
878
918
  enqueueWorkerRender(slot, request) {
879
- if (!slot.worker) {
880
- this.renderSync(slot, request);
881
- return;
882
- }
919
+ this.throwIfFatal();
883
920
  if (slot.busy) {
884
921
  slot.queuedRequest = request;
885
922
  slot.queuedKey = request.key;
@@ -888,13 +925,16 @@ class ImageVisualLayer {
888
925
  this.dispatchWorkerRender(slot, request);
889
926
  }
890
927
  dispatchWorkerRender(slot, request) {
891
- if (!slot.worker) {
892
- this.renderSync(slot, request);
893
- return;
894
- }
928
+ this.throwIfFatal();
929
+ if (!slot.worker)
930
+ this.failWorker(String(request.layer ?? "visual"), new Error("worker is unavailable"));
895
931
  slot.busy = true;
896
932
  slot.inFlightKey = request.key;
897
- slot.worker.postMessage(request);
933
+ try {
934
+ slot.worker.postMessage(request);
935
+ } catch (error) {
936
+ this.failWorker(String(request.layer ?? "visual"), error);
937
+ }
898
938
  }
899
939
  handleWorkerMessage(slot, message) {
900
940
  if (this.destroyed || !message || typeof message !== "object")
@@ -902,14 +942,14 @@ class ImageVisualLayer {
902
942
  const response = message;
903
943
  slot.busy = false;
904
944
  slot.inFlightKey = "";
905
- if (response.requestId <= slot.clearedThroughRequestId)
906
- return;
907
- if (response.error) {
908
- this.disableWorker(slot);
909
- return;
910
- }
911
- if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "panel-chrome")) {
912
- this.stageFrame({ key: response.key, sequence: response.sequence });
945
+ const isStale = response.requestId <= slot.clearedThroughRequestId || response.requestId < slot.latestRequestId;
946
+ if (!isStale) {
947
+ if (response.error)
948
+ this.failWorker(pendingLayerKey(response.key), new Error(response.error));
949
+ if (response.sequence && !this.isKnownKey(slot, response.key) && (!this.isOutputBackedUp() || pendingLayerKey(response.key) === "split")) {
950
+ this.stageFrame({ key: response.key, sequence: response.sequence });
951
+ this.onFrameReady?.();
952
+ }
913
953
  }
914
954
  const next = slot.queuedRequest;
915
955
  slot.queuedRequest = null;
@@ -917,12 +957,6 @@ class ImageVisualLayer {
917
957
  if (next && !this.isKnownKey(slot, next.key))
918
958
  this.dispatchWorkerRender(slot, next);
919
959
  }
920
- renderSync(slot, request) {
921
- const result = renderImageVisualFrame(request);
922
- if (!result || this.isKnownKey(slot, result.key) || this.isOutputBackedUp())
923
- return;
924
- this.stageFrame(result);
925
- }
926
960
  }
927
961
  function pixelSizeForRenderer(renderer) {
928
962
  const resolution = renderer.resolution;
@@ -934,7 +968,7 @@ function pixelSizeForRenderer(renderer) {
934
968
  };
935
969
  }
936
970
  function forceModernImageVisuals() {
937
- return isModernImageTerminal(getCapabilities().images);
971
+ return getRigTerminalCapabilities().supportsKittyImages;
938
972
  }
939
973
  export {
940
974
  renderImageVisualFrame,