@provos/ironcurtain 0.5.0 → 0.6.0

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 (89) hide show
  1. package/README.md +66 -66
  2. package/dist/cli.js +9 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/types.d.ts +6 -0
  5. package/dist/docker/adapters/claude-code.js +44 -19
  6. package/dist/docker/adapters/claude-code.js.map +1 -1
  7. package/dist/docker/agent-adapter.d.ts +6 -0
  8. package/dist/docker/agent-adapter.js +6 -1
  9. package/dist/docker/agent-adapter.js.map +1 -1
  10. package/dist/docker/docker-agent-session.d.ts +1 -1
  11. package/dist/docker/docker-agent-session.js +3 -2
  12. package/dist/docker/docker-agent-session.js.map +1 -1
  13. package/dist/docker/docker-infrastructure.js +10 -3
  14. package/dist/docker/docker-infrastructure.js.map +1 -1
  15. package/dist/docker/mitm-proxy.d.ts +5 -2
  16. package/dist/docker/mitm-proxy.js +169 -46
  17. package/dist/docker/mitm-proxy.js.map +1 -1
  18. package/dist/docker/oauth-credentials.d.ts +15 -2
  19. package/dist/docker/oauth-credentials.js +92 -8
  20. package/dist/docker/oauth-credentials.js.map +1 -1
  21. package/dist/docker/oauth-token-manager.d.ts +80 -0
  22. package/dist/docker/oauth-token-manager.js +133 -0
  23. package/dist/docker/oauth-token-manager.js.map +1 -0
  24. package/dist/docker/orientation.d.ts +1 -1
  25. package/dist/docker/orientation.js +2 -1
  26. package/dist/docker/orientation.js.map +1 -1
  27. package/dist/docker/pty-session.d.ts +2 -0
  28. package/dist/docker/pty-session.js +105 -9
  29. package/dist/docker/pty-session.js.map +1 -1
  30. package/dist/escalation/escalation-watcher.js +2 -0
  31. package/dist/escalation/escalation-watcher.js.map +1 -1
  32. package/dist/escalation/listener-command.js +9 -58
  33. package/dist/escalation/listener-command.js.map +1 -1
  34. package/dist/escalation/listener-lock.d.ts +20 -0
  35. package/dist/escalation/listener-lock.js +69 -0
  36. package/dist/escalation/listener-lock.js.map +1 -0
  37. package/dist/index.js +21 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/mux/mux-app.d.ts +24 -0
  40. package/dist/mux/mux-app.js +445 -0
  41. package/dist/mux/mux-app.js.map +1 -0
  42. package/dist/mux/mux-command.d.ts +7 -0
  43. package/dist/mux/mux-command.js +93 -0
  44. package/dist/mux/mux-command.js.map +1 -0
  45. package/dist/mux/mux-escalation-manager.d.ts +48 -0
  46. package/dist/mux/mux-escalation-manager.js +162 -0
  47. package/dist/mux/mux-escalation-manager.js.map +1 -0
  48. package/dist/mux/mux-input-handler.d.ts +49 -0
  49. package/dist/mux/mux-input-handler.js +457 -0
  50. package/dist/mux/mux-input-handler.js.map +1 -0
  51. package/dist/mux/mux-renderer.d.ts +86 -0
  52. package/dist/mux/mux-renderer.js +758 -0
  53. package/dist/mux/mux-renderer.js.map +1 -0
  54. package/dist/mux/mux-splash.d.ts +17 -0
  55. package/dist/mux/mux-splash.js +322 -0
  56. package/dist/mux/mux-splash.js.map +1 -0
  57. package/dist/mux/pty-bridge.d.ts +79 -0
  58. package/dist/mux/pty-bridge.js +151 -0
  59. package/dist/mux/pty-bridge.js.map +1 -0
  60. package/dist/mux/trusted-input.d.ts +27 -0
  61. package/dist/mux/trusted-input.js +27 -0
  62. package/dist/mux/trusted-input.js.map +1 -0
  63. package/dist/mux/types.d.ts +92 -0
  64. package/dist/mux/types.js +45 -0
  65. package/dist/mux/types.js.map +1 -0
  66. package/dist/sandbox/index.js +6 -0
  67. package/dist/sandbox/index.js.map +1 -1
  68. package/dist/session/agent-session.js +3 -0
  69. package/dist/session/agent-session.js.map +1 -1
  70. package/dist/session/index.js +14 -7
  71. package/dist/session/index.js.map +1 -1
  72. package/dist/session/types.d.ts +6 -0
  73. package/dist/session/workspace-validation.d.ts +17 -0
  74. package/dist/session/workspace-validation.js +65 -0
  75. package/dist/session/workspace-validation.js.map +1 -0
  76. package/dist/trusted-process/auto-approver.d.ts +13 -4
  77. package/dist/trusted-process/auto-approver.js +12 -5
  78. package/dist/trusted-process/auto-approver.js.map +1 -1
  79. package/dist/trusted-process/escalation.js +5 -0
  80. package/dist/trusted-process/escalation.js.map +1 -1
  81. package/dist/trusted-process/index.js +23 -0
  82. package/dist/trusted-process/index.js.map +1 -1
  83. package/dist/trusted-process/mcp-proxy-server.d.ts +0 -2
  84. package/dist/trusted-process/mcp-proxy-server.js +82 -26
  85. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  86. package/dist/trusted-process/path-utils.d.ts +15 -0
  87. package/dist/trusted-process/path-utils.js +38 -0
  88. package/dist/trusted-process/path-utils.js.map +1 -1
  89. package/package.json +6 -1
@@ -0,0 +1,758 @@
1
+ /**
2
+ * MuxRenderer -- composes all UI regions into terminal output
3
+ * and draws to the real terminal with throttled rendering.
4
+ *
5
+ * UI regions (top to bottom):
6
+ * [tab bar] 1 row
7
+ * [pty viewport] variable height (fills remaining space)
8
+ * [escalation panel] 0 or N rows (only in command mode with pending escalations)
9
+ * [hint bar] 1 row
10
+ * [input line] 0 or 1 row (only in command mode)
11
+ *
12
+ * The PTY viewport size is constant regardless of input mode.
13
+ * Command-mode UI overlays the bottom of the viewport.
14
+ */
15
+ import { calculateLayout } from './types.js';
16
+ import { createSplashScreen } from './mux-splash.js';
17
+ // -- xterm.js color mode constants (from IBufferCell.getFgColorMode/getBgColorMode) --
18
+ const CM_DEFAULT = 0;
19
+ const CM_P16 = 16777216; // 0x01000000
20
+ const CM_P256 = 33554432; // 0x02000000
21
+ const CM_RGB = 50331648; // 0x03000000
22
+ /** Minimum render interval (~60 FPS). */
23
+ const MIN_RENDER_INTERVAL_MS = 16;
24
+ /**
25
+ * Creates a MuxRenderer.
26
+ */
27
+ export function createMuxRenderer(term, cols, rows, deps) {
28
+ let _cols = cols;
29
+ let _rows = rows;
30
+ let _layout = calculateLayout(_rows, deps.getMode(), deps.getPendingCount());
31
+ // Render throttling
32
+ let renderScheduled = false;
33
+ let lastRenderTime = 0;
34
+ let renderTimeout = null;
35
+ // Splash screen (shown when no tabs exist)
36
+ let _splash = null;
37
+ // Transient flash message
38
+ let _flashMessage = null;
39
+ let _flashTimeout = null;
40
+ const FLASH_DURATION_MS = 3000;
41
+ function recalcLayout() {
42
+ _layout = calculateLayout(_rows, deps.getMode(), deps.getPendingCount());
43
+ }
44
+ function moveTo(x, y) {
45
+ term.moveTo(x + 1, y + 1); // terminal-kit is 1-indexed
46
+ }
47
+ function clearLine(y) {
48
+ moveTo(0, y);
49
+ term.eraseLine();
50
+ }
51
+ function drawTabBar() {
52
+ clearLine(_layout.tabBarY);
53
+ moveTo(0, _layout.tabBarY);
54
+ const tabs = deps.getTabs();
55
+ const activeIdx = deps.getActiveTabIndex();
56
+ for (let i = 0; i < tabs.length; i++) {
57
+ const tab = tabs[i];
58
+ const isActive = i === activeIdx;
59
+ const statusSuffix = tab.status === 'exited' ? ' exited' : '';
60
+ const label = ` #${tab.number} ${tab.label}${statusSuffix} `;
61
+ if (isActive) {
62
+ term.bgWhite.black(label);
63
+ }
64
+ else {
65
+ term.bgDefaultColor.dim(label);
66
+ }
67
+ }
68
+ // Escalation badge
69
+ const pendingCount = deps.getPendingCount();
70
+ if (pendingCount > 0) {
71
+ const badge = ` [!${pendingCount} pending] `;
72
+ const badgeX = Math.max(0, _cols - badge.length);
73
+ moveTo(badgeX, _layout.tabBarY);
74
+ term.bgYellow.black(badge);
75
+ }
76
+ term.styleReset();
77
+ term.eraseLineAfter();
78
+ }
79
+ function tearDownSplash() {
80
+ if (_splash) {
81
+ _splash.stop();
82
+ _splash = null;
83
+ }
84
+ }
85
+ /** Returns the number of viewport rows reserved by the current overlay. */
86
+ function activeOverlayRows() {
87
+ const mode = deps.getMode();
88
+ if (mode === 'command')
89
+ return _layout.overlayRows;
90
+ if (mode === 'picker')
91
+ return _layout.pickerRows;
92
+ return 0;
93
+ }
94
+ function drawPtyViewport() {
95
+ const activeTab = deps.getActiveTab();
96
+ if (!activeTab) {
97
+ if (deps.getTabs().length === 0) {
98
+ const reserved = activeOverlayRows();
99
+ // Lazily create and start the splash screen
100
+ if (!_splash) {
101
+ _splash = createSplashScreen(term, _cols, _layout.ptyViewportRows, _layout.ptyViewportY, reserved);
102
+ _splash.start();
103
+ }
104
+ else {
105
+ // Update reserved rows in case mode changed (e.g. command -> picker)
106
+ _splash.resize(_cols, _layout.ptyViewportRows, _layout.ptyViewportY, reserved);
107
+ }
108
+ _splash.draw();
109
+ }
110
+ else {
111
+ tearDownSplash();
112
+ for (let y = _layout.ptyViewportY; y < _layout.ptyViewportY + _layout.ptyViewportRows; y++) {
113
+ clearLine(y);
114
+ }
115
+ }
116
+ return;
117
+ }
118
+ tearDownSplash();
119
+ const xtermTerminal = activeTab.bridge.terminal;
120
+ const baseY = xtermTerminal.buffer.active.baseY;
121
+ const scrollOffset = deps.getScrollOffset();
122
+ const readFrom = scrollOffset ?? baseY;
123
+ const cells = readTerminalBuffer(xtermTerminal, readFrom, _layout.ptyViewportRows, _cols);
124
+ // Determine how many rows to render (skip overlay in command/picker mode)
125
+ const mode = deps.getMode();
126
+ let visibleRows = _layout.ptyViewportRows;
127
+ if (mode === 'command')
128
+ visibleRows -= _layout.overlayRows;
129
+ else if (mode === 'picker')
130
+ visibleRows -= _layout.pickerRows;
131
+ let lastStyle = null;
132
+ for (let y = 0; y < visibleRows; y++) {
133
+ moveTo(0, _layout.ptyViewportY + y);
134
+ const row = cells[y];
135
+ if (row.length === 0) {
136
+ term.eraseLineAfter();
137
+ lastStyle = null;
138
+ continue;
139
+ }
140
+ renderRow(term, row, lastStyle);
141
+ term.noFormat('\x1b[0m'); // reset before eraseLineAfter
142
+ term.eraseLineAfter();
143
+ lastStyle = null;
144
+ }
145
+ // Position cursor where xterm.js says it should be (only when at live viewport)
146
+ if (mode === 'pty' && scrollOffset === null) {
147
+ const buffer = xtermTerminal.buffer.active;
148
+ moveTo(buffer.cursorX, _layout.ptyViewportY + buffer.cursorY);
149
+ }
150
+ }
151
+ function drawFooter() {
152
+ const row1 = _layout.footerY;
153
+ const row2 = _layout.footerY + 1;
154
+ clearLine(row1);
155
+ clearLine(row2);
156
+ const activeTab = deps.getActiveTab();
157
+ const mode = deps.getMode();
158
+ const pendingCount = deps.getPendingCount();
159
+ if (mode === 'pty') {
160
+ // Row 1: status — tab label left, scroll indicator + escalation badge right
161
+ moveTo(0, row1);
162
+ const tabLabel = activeTab ? `PTY #${activeTab.number} ${activeTab.label}` : 'No session';
163
+ term.dim(` [${tabLabel}]`);
164
+ // Right-aligned badges: scroll indicator + escalation badge
165
+ const scrollOffset = deps.getScrollOffset();
166
+ const scrollBadge = scrollOffset !== null && activeTab
167
+ ? ` [\u2191 ${activeTab.bridge.terminal.buffer.active.baseY - scrollOffset} lines] `
168
+ : '';
169
+ const escalationBadge = pendingCount > 0 ? ` [!${pendingCount} pending] ` : '';
170
+ const rightContent = scrollBadge + escalationBadge;
171
+ if (rightContent.length > 0) {
172
+ moveTo(Math.max(0, _cols - rightContent.length), row1);
173
+ if (scrollBadge)
174
+ term.cyan(scrollBadge);
175
+ if (escalationBadge) {
176
+ term.bgYellow.black(escalationBadge);
177
+ term.styleReset();
178
+ }
179
+ }
180
+ term.eraseLineAfter();
181
+ // Row 2: flash message or guidance
182
+ moveTo(0, row2);
183
+ if (_flashMessage) {
184
+ term.yellow(` ${_flashMessage}`);
185
+ }
186
+ else {
187
+ term(' ');
188
+ term.bgWhite.black(' ^^A ');
189
+ term.styleReset();
190
+ if (pendingCount > 0) {
191
+ term.dim(` command mode \u00b7 ${pendingCount} escalation${pendingCount !== 1 ? 's' : ''} pending \u2014 /approve or /deny`);
192
+ }
193
+ else {
194
+ term.dim(' command mode \u00b7 type a message to enable auto-approver');
195
+ }
196
+ }
197
+ term.styleReset();
198
+ term.eraseLineAfter();
199
+ }
200
+ else {
201
+ // Row 1: command mode — badge-style keys
202
+ moveTo(0, row1);
203
+ term(' ');
204
+ term.bgWhite.black(' CMD ');
205
+ term(' ');
206
+ term.bgWhite.black(' ^^A ');
207
+ term.styleReset();
208
+ term.dim(' pty ');
209
+ term.bgWhite.black(' Esc ');
210
+ term.styleReset();
211
+ term.dim(' back');
212
+ term.eraseLineAfter();
213
+ // Row 2: flash message or trusted input hint
214
+ moveTo(0, row2);
215
+ if (_flashMessage) {
216
+ term.yellow(` ${_flashMessage}`);
217
+ }
218
+ else {
219
+ term.dim(' type a message to send as trusted input to the agent');
220
+ }
221
+ term.styleReset();
222
+ term.eraseLineAfter();
223
+ }
224
+ }
225
+ function drawCommandOverlay() {
226
+ if (deps.getMode() !== 'command')
227
+ return;
228
+ const startY = _layout.overlayY;
229
+ let currentY = startY;
230
+ // Escalation panel
231
+ if (_layout.escalationPanelRows > 0) {
232
+ const pendingEscalations = [...deps.getEscalationState().pendingEscalations.values()].sort((a, b) => a.displayNumber - b.displayNumber);
233
+ let rowsUsed = 0;
234
+ let shownCount = 0;
235
+ for (const esc of pendingEscalations) {
236
+ if (rowsUsed >= _layout.escalationPanelRows)
237
+ break;
238
+ // Row 1: header — [N] Session #M server/tool
239
+ clearLine(currentY);
240
+ moveTo(2, currentY);
241
+ term.yellow(`[${esc.displayNumber}]`);
242
+ term(` Session #${esc.sessionDisplayNumber} `);
243
+ term.cyan(`${esc.request.serverName}/${esc.request.toolName}`);
244
+ currentY++;
245
+ rowsUsed++;
246
+ shownCount++;
247
+ // Row 2+: arguments — packed key: value pairs
248
+ if (rowsUsed < _layout.escalationPanelRows) {
249
+ const argLines = formatArgLines(esc.request.arguments, _cols - 6);
250
+ for (const line of argLines) {
251
+ if (rowsUsed >= _layout.escalationPanelRows)
252
+ break;
253
+ clearLine(currentY);
254
+ moveTo(6, currentY);
255
+ term(line);
256
+ currentY++;
257
+ rowsUsed++;
258
+ }
259
+ }
260
+ // Reason line
261
+ if (rowsUsed < _layout.escalationPanelRows) {
262
+ clearLine(currentY);
263
+ moveTo(6, currentY);
264
+ term.dim(`Reason: ${esc.request.reason}`);
265
+ currentY++;
266
+ rowsUsed++;
267
+ }
268
+ }
269
+ // Overflow indicator when not all escalations fit
270
+ const remaining = pendingEscalations.length - shownCount;
271
+ if (remaining > 0 && rowsUsed >= _layout.escalationPanelRows) {
272
+ const lastY = startY + _layout.escalationPanelRows - 1;
273
+ clearLine(lastY);
274
+ moveTo(6, lastY);
275
+ term.dim(`[+${remaining} more \u2014 /approve all or /deny all]`);
276
+ }
277
+ }
278
+ // Hint bar
279
+ clearLine(currentY);
280
+ moveTo(2, currentY);
281
+ const pendingCount = deps.getPendingCount();
282
+ if (pendingCount > 0) {
283
+ term.cyan('/approve');
284
+ term.dim(' N ');
285
+ term.cyan('/deny');
286
+ term.dim(' N ');
287
+ term.cyan('/approve all');
288
+ term.dim(' \u2502 ');
289
+ term.cyan('/new');
290
+ term.dim(' ');
291
+ term.cyan('/quit');
292
+ }
293
+ else {
294
+ term.cyan('/new');
295
+ term.dim(' ');
296
+ term.cyan('/tab');
297
+ term.dim(' N ');
298
+ term.cyan('/close');
299
+ term.dim(' ');
300
+ term.cyan('/sessions');
301
+ term.dim(' ');
302
+ term.cyan('/quit');
303
+ }
304
+ term.styleReset();
305
+ currentY++;
306
+ // Input line with visible block cursor
307
+ clearLine(currentY);
308
+ moveTo(2, currentY);
309
+ term('> ');
310
+ const buf = deps.getInputBuffer();
311
+ const cp = deps.getCursorPos();
312
+ term(buf.slice(0, cp));
313
+ term.bgWhite.black(cp < buf.length ? buf[cp] : ' ');
314
+ term.styleReset();
315
+ term(buf.slice(cp + 1));
316
+ term.eraseLineAfter();
317
+ // Keep the real terminal cursor aligned with the logical input cursor for accessibility
318
+ moveTo(2 + 2 + cp, currentY);
319
+ }
320
+ function drawActiveOverlay() {
321
+ const mode = deps.getMode();
322
+ if (mode === 'command') {
323
+ drawCommandOverlay();
324
+ }
325
+ else if (mode === 'picker') {
326
+ drawPickerOverlay();
327
+ }
328
+ }
329
+ function drawPickerOverlay() {
330
+ const ps = deps.getPickerState();
331
+ if (!ps)
332
+ return;
333
+ const startY = _layout.pickerY;
334
+ const totalRows = _layout.pickerRows;
335
+ if (totalRows < 4)
336
+ return; // not enough space
337
+ if (ps.phase === 'menu') {
338
+ drawPickerMenu(ps, startY, totalRows);
339
+ }
340
+ else {
341
+ drawPickerBrowse(ps, startY, totalRows);
342
+ }
343
+ }
344
+ function drawMenuOption(y, label, selected, boxWidth) {
345
+ clearLine(y);
346
+ moveTo(2, y);
347
+ term.cyan('\u2502');
348
+ term(' ');
349
+ if (selected) {
350
+ term.bgCyan.black('>' + label);
351
+ term.styleReset();
352
+ }
353
+ else {
354
+ term(' ' + label);
355
+ }
356
+ const pad = Math.max(0, boxWidth - label.length - 2);
357
+ term(' '.repeat(pad));
358
+ term.cyan('\u2502');
359
+ }
360
+ function drawPickerMenu(ps, startY, totalRows) {
361
+ const menuHeight = 4;
362
+ const topPad = Math.max(0, Math.floor((totalRows - menuHeight) / 2));
363
+ for (let y = startY; y < startY + totalRows; y++) {
364
+ clearLine(y);
365
+ }
366
+ const y0 = startY + topPad;
367
+ const boxWidth = Math.min(34, _cols - 4);
368
+ // Top border
369
+ moveTo(2, y0);
370
+ term.cyan('\u250c /new ' + '\u2500'.repeat(Math.max(0, boxWidth - 6)) + '\u2510');
371
+ drawMenuOption(y0 + 1, ' New sandbox', ps.menuSelection === 0, boxWidth);
372
+ drawMenuOption(y0 + 2, ' Existing directory', ps.menuSelection === 1, boxWidth);
373
+ // Bottom border
374
+ clearLine(y0 + 3);
375
+ moveTo(2, y0 + 3);
376
+ term.cyan('\u2514' + '\u2500'.repeat(boxWidth) + '\u2518');
377
+ term.styleReset();
378
+ }
379
+ function drawPickerBrowse(ps, startY, totalRows) {
380
+ // Row 0: path input line with cursor
381
+ // Row 1: separator
382
+ // Rows 2..N-2: entry list
383
+ // Row N-1: hint bar (or error)
384
+ let currentY = startY;
385
+ // Path input line — render with visible cursor block
386
+ clearLine(currentY);
387
+ moveTo(2, currentY);
388
+ term.cyan('Path: ');
389
+ const pathPrefix = 'Path: ';
390
+ const beforeCursor = ps.inputPath.slice(0, ps.cursorPos);
391
+ const cursorChar = ps.cursorPos < ps.inputPath.length ? ps.inputPath[ps.cursorPos] : ' ';
392
+ const afterCursor = ps.cursorPos < ps.inputPath.length ? ps.inputPath.slice(ps.cursorPos + 1) : '';
393
+ term(beforeCursor);
394
+ if (!ps.inList) {
395
+ // Show block cursor when input field has focus
396
+ term.bgWhite.black(cursorChar);
397
+ term.styleReset();
398
+ }
399
+ else {
400
+ term(cursorChar);
401
+ }
402
+ term(afterCursor);
403
+ term.eraseLineAfter();
404
+ const cursorX = 2 + pathPrefix.length + ps.cursorPos;
405
+ currentY++;
406
+ // Separator
407
+ clearLine(currentY);
408
+ moveTo(2, currentY);
409
+ term.dim('\u2500'.repeat(Math.max(0, _cols - 4)));
410
+ term.styleReset();
411
+ currentY++;
412
+ // Entry list
413
+ const listRows = Math.max(0, totalRows - 3); // subtract input, separator, hint bar
414
+ // Only adjust scroll when focus is in the list
415
+ if (ps.inList) {
416
+ if (ps.selectedIndex < ps.scrollOffset) {
417
+ ps.scrollOffset = ps.selectedIndex;
418
+ }
419
+ else if (ps.selectedIndex >= ps.scrollOffset + listRows) {
420
+ ps.scrollOffset = ps.selectedIndex - listRows + 1;
421
+ }
422
+ }
423
+ for (let i = 0; i < listRows; i++) {
424
+ clearLine(currentY);
425
+ const entryIdx = ps.scrollOffset + i;
426
+ if (entryIdx < ps.entries.length) {
427
+ moveTo(2, currentY);
428
+ const entry = ps.entries[entryIdx];
429
+ const isHighlighted = ps.inList && entryIdx === ps.selectedIndex;
430
+ const isDir = entry.endsWith('/');
431
+ if (isHighlighted) {
432
+ term.bgCyan.black('> ' + entry);
433
+ term.styleReset();
434
+ }
435
+ else {
436
+ term(' ');
437
+ if (isDir) {
438
+ term.cyan(entry);
439
+ }
440
+ else {
441
+ term(entry);
442
+ }
443
+ }
444
+ }
445
+ term.eraseLineAfter();
446
+ currentY++;
447
+ }
448
+ // Hint bar / error
449
+ clearLine(currentY);
450
+ moveTo(2, currentY);
451
+ if (ps.error) {
452
+ term.red(truncate(ps.error, _cols - 4));
453
+ }
454
+ else if (ps.inList) {
455
+ term.bgWhite.black(' Enter ');
456
+ term.styleReset();
457
+ term.dim(' pick ');
458
+ term.bgWhite.black(' Esc ');
459
+ term.styleReset();
460
+ term.dim(' back to input');
461
+ }
462
+ else {
463
+ term.bgWhite.black(' Enter ');
464
+ term.styleReset();
465
+ term.dim(' submit ');
466
+ term.bgWhite.black(' Tab ');
467
+ term.styleReset();
468
+ term.dim(' complete ');
469
+ term.bgWhite.black(' \u2193 ');
470
+ term.styleReset();
471
+ term.dim(' browse ');
472
+ term.bgWhite.black(' Esc ');
473
+ term.styleReset();
474
+ term.dim(' back');
475
+ }
476
+ term.styleReset();
477
+ term.eraseLineAfter();
478
+ // Position terminal cursor on the input line (for accessibility)
479
+ moveTo(cursorX, startY);
480
+ }
481
+ return {
482
+ get layout() {
483
+ return _layout;
484
+ },
485
+ fullRedraw() {
486
+ recalcLayout();
487
+ term.clear();
488
+ drawTabBar();
489
+ drawPtyViewport();
490
+ drawFooter();
491
+ drawActiveOverlay();
492
+ },
493
+ redrawPty() {
494
+ recalcLayout();
495
+ drawPtyViewport();
496
+ drawActiveOverlay();
497
+ },
498
+ redrawTabBar() {
499
+ drawTabBar();
500
+ },
501
+ redrawCommandArea() {
502
+ recalcLayout();
503
+ const mode = deps.getMode();
504
+ if (mode === 'command' || mode === 'picker') {
505
+ // Repaint viewport first to clear stale overlay rows from a
506
+ // previously larger overlay (e.g., after resolving an escalation).
507
+ drawPtyViewport();
508
+ drawActiveOverlay();
509
+ }
510
+ else {
511
+ drawFooter();
512
+ }
513
+ },
514
+ resize(newCols, newRows) {
515
+ _cols = newCols;
516
+ _rows = newRows;
517
+ recalcLayout();
518
+ _splash?.resize(_cols, _layout.ptyViewportRows, _layout.ptyViewportY, activeOverlayRows());
519
+ },
520
+ destroy() {
521
+ tearDownSplash();
522
+ if (renderTimeout) {
523
+ clearTimeout(renderTimeout);
524
+ renderTimeout = null;
525
+ }
526
+ if (_flashTimeout) {
527
+ clearTimeout(_flashTimeout);
528
+ _flashTimeout = null;
529
+ }
530
+ },
531
+ showMessage(message) {
532
+ _flashMessage = message;
533
+ if (_flashTimeout)
534
+ clearTimeout(_flashTimeout);
535
+ _flashTimeout = setTimeout(() => {
536
+ _flashMessage = null;
537
+ _flashTimeout = null;
538
+ drawFooter();
539
+ }, FLASH_DURATION_MS);
540
+ drawFooter();
541
+ },
542
+ scheduleRedraw() {
543
+ if (renderScheduled)
544
+ return;
545
+ renderScheduled = true;
546
+ const elapsed = Date.now() - lastRenderTime;
547
+ const delay = Math.max(0, MIN_RENDER_INTERVAL_MS - elapsed);
548
+ renderTimeout = setTimeout(() => {
549
+ renderScheduled = false;
550
+ lastRenderTime = Date.now();
551
+ recalcLayout();
552
+ drawPtyViewport();
553
+ drawFooter();
554
+ drawActiveOverlay();
555
+ }, delay);
556
+ },
557
+ };
558
+ }
559
+ // -- Cell translation --
560
+ function translateColor(mode, colorValue) {
561
+ switch (mode) {
562
+ case CM_DEFAULT:
563
+ return 'default';
564
+ case CM_P16:
565
+ case CM_P256:
566
+ return colorValue;
567
+ case CM_RGB:
568
+ return {
569
+ r: (colorValue >> 16) & 0xff,
570
+ g: (colorValue >> 8) & 0xff,
571
+ b: colorValue & 0xff,
572
+ };
573
+ default:
574
+ return 'default';
575
+ }
576
+ }
577
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
578
+ function translateCell(cell) {
579
+ const width = cell.getWidth();
580
+ if (width === 0)
581
+ return null; // Wide char placeholder
582
+ const char = cell.getChars() || ' ';
583
+ const fg = translateColor(cell.getFgColorMode(), cell.getFgColor());
584
+ const bg = translateColor(cell.getBgColorMode(), cell.getBgColor());
585
+ return {
586
+ char,
587
+ width,
588
+ fg,
589
+ bg,
590
+ bold: cell.isBold() !== 0,
591
+ italic: cell.isItalic() !== 0,
592
+ underline: cell.isUnderline() !== 0,
593
+ dim: cell.isDim() !== 0,
594
+ inverse: cell.isInverse() !== 0,
595
+ strikethrough: cell.isStrikethrough() !== 0,
596
+ };
597
+ }
598
+ /**
599
+ * Reads the headless terminal buffer and translates each cell.
600
+ */
601
+ export function readTerminalBuffer(terminal, startRow, rows, cols) {
602
+ const buffer = terminal.buffer.active;
603
+ const result = [];
604
+ for (let y = 0; y < rows; y++) {
605
+ const lineIndex = startRow + y;
606
+ const line = buffer.getLine(lineIndex);
607
+ if (!line) {
608
+ result.push([]);
609
+ continue;
610
+ }
611
+ const row = [];
612
+ let reusableCell;
613
+ for (let x = 0; x < cols; x++) {
614
+ const cell = line.getCell(x, reusableCell);
615
+ if (!cell)
616
+ continue;
617
+ reusableCell = cell;
618
+ const translated = translateCell(cell);
619
+ if (translated)
620
+ row.push(translated);
621
+ }
622
+ result.push(row);
623
+ }
624
+ return result;
625
+ }
626
+ /**
627
+ * Builds the SGR parameter string for a cell's style attributes.
628
+ */
629
+ export function buildSgrSequence(cell) {
630
+ const sgr = [0];
631
+ if (cell.bold)
632
+ sgr.push(1);
633
+ if (cell.dim)
634
+ sgr.push(2);
635
+ if (cell.italic)
636
+ sgr.push(3);
637
+ if (cell.underline)
638
+ sgr.push(4);
639
+ if (cell.inverse)
640
+ sgr.push(7);
641
+ if (cell.strikethrough)
642
+ sgr.push(9);
643
+ // Foreground
644
+ if (cell.fg === 'default') {
645
+ sgr.push(39);
646
+ }
647
+ else if (typeof cell.fg === 'number') {
648
+ sgr.push(38, 5, cell.fg);
649
+ }
650
+ else {
651
+ sgr.push(38, 2, cell.fg.r, cell.fg.g, cell.fg.b);
652
+ }
653
+ // Background
654
+ if (cell.bg === 'default') {
655
+ sgr.push(49);
656
+ }
657
+ else if (typeof cell.bg === 'number') {
658
+ sgr.push(48, 5, cell.bg);
659
+ }
660
+ else {
661
+ sgr.push(48, 2, cell.bg.r, cell.bg.g, cell.bg.b);
662
+ }
663
+ return sgr.join(';');
664
+ }
665
+ export function colorEquals(a, b) {
666
+ if (a === b)
667
+ return true;
668
+ if (typeof a === 'object' && typeof b === 'object') {
669
+ return a.r === b.r && a.g === b.g && a.b === b.b;
670
+ }
671
+ return false;
672
+ }
673
+ export function cellStyleEquals(a, b) {
674
+ return (a.bold === b.bold &&
675
+ a.dim === b.dim &&
676
+ a.italic === b.italic &&
677
+ a.underline === b.underline &&
678
+ a.inverse === b.inverse &&
679
+ a.strikethrough === b.strikethrough &&
680
+ colorEquals(a.fg, b.fg) &&
681
+ colorEquals(a.bg, b.bg));
682
+ }
683
+ /**
684
+ * Renders a row by batching consecutive same-styled cells into runs.
685
+ * Skips SGR output when the run's style matches the last emitted style
686
+ * (Option C). Returns the style of the last emitted run for cross-row
687
+ * carry-over.
688
+ */
689
+ function renderRow(term, row, lastStyle) {
690
+ let prevStyle = lastStyle;
691
+ let i = 0;
692
+ while (i < row.length) {
693
+ // Find the end of the current same-style run
694
+ let j = i + 1;
695
+ while (j < row.length && cellStyleEquals(row[i], row[j])) {
696
+ j++;
697
+ }
698
+ // Collect characters for this run
699
+ let chars = '';
700
+ for (let k = i; k < j; k++) {
701
+ chars += row[k].char;
702
+ }
703
+ // Only emit SGR if the style changed from the last emitted style
704
+ if (prevStyle && cellStyleEquals(prevStyle, row[i])) {
705
+ term.noFormat(chars);
706
+ }
707
+ else {
708
+ term.noFormat(`\x1b[${buildSgrSequence(row[i])}m${chars}`);
709
+ }
710
+ prevStyle = row[i];
711
+ i = j;
712
+ }
713
+ }
714
+ /**
715
+ * Formats tool call arguments into packed display lines.
716
+ * Each line contains as many `key: value` pairs as fit within maxWidth,
717
+ * separated by double spaces.
718
+ */
719
+ function formatArgLines(args, maxWidth) {
720
+ const entries = Object.entries(args);
721
+ if (entries.length === 0)
722
+ return [];
723
+ const lines = [];
724
+ let currentLine = '';
725
+ for (const [key, value] of entries) {
726
+ const formatted = formatArgValue(value);
727
+ const pair = `${key}: ${formatted}`;
728
+ if (currentLine.length === 0) {
729
+ currentLine = pair;
730
+ }
731
+ else if (currentLine.length + 2 + pair.length <= maxWidth) {
732
+ currentLine += ' ' + pair;
733
+ }
734
+ else {
735
+ lines.push(truncate(currentLine, maxWidth));
736
+ currentLine = pair;
737
+ }
738
+ }
739
+ if (currentLine.length > 0) {
740
+ lines.push(truncate(currentLine, maxWidth));
741
+ }
742
+ return lines;
743
+ }
744
+ function formatArgValue(value) {
745
+ if (typeof value === 'string')
746
+ return value;
747
+ if (typeof value === 'number' || typeof value === 'boolean')
748
+ return String(value);
749
+ if (value == null)
750
+ return String(value);
751
+ return JSON.stringify(value);
752
+ }
753
+ function truncate(str, maxLen) {
754
+ if (str.length <= maxLen)
755
+ return str;
756
+ return str.slice(0, maxLen - 1) + '\u2026';
757
+ }
758
+ //# sourceMappingURL=mux-renderer.js.map