@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.
- package/README.md +66 -66
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/types.d.ts +6 -0
- package/dist/docker/adapters/claude-code.js +44 -19
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +6 -0
- package/dist/docker/agent-adapter.js +6 -1
- package/dist/docker/agent-adapter.js.map +1 -1
- package/dist/docker/docker-agent-session.d.ts +1 -1
- package/dist/docker/docker-agent-session.js +3 -2
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.js +10 -3
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +5 -2
- package/dist/docker/mitm-proxy.js +169 -46
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +15 -2
- package/dist/docker/oauth-credentials.js +92 -8
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/oauth-token-manager.d.ts +80 -0
- package/dist/docker/oauth-token-manager.js +133 -0
- package/dist/docker/oauth-token-manager.js.map +1 -0
- package/dist/docker/orientation.d.ts +1 -1
- package/dist/docker/orientation.js +2 -1
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/pty-session.d.ts +2 -0
- package/dist/docker/pty-session.js +105 -9
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/escalation/escalation-watcher.js +2 -0
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +9 -58
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +20 -0
- package/dist/escalation/listener-lock.js +69 -0
- package/dist/escalation/listener-lock.js.map +1 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.d.ts +24 -0
- package/dist/mux/mux-app.js +445 -0
- package/dist/mux/mux-app.js.map +1 -0
- package/dist/mux/mux-command.d.ts +7 -0
- package/dist/mux/mux-command.js +93 -0
- package/dist/mux/mux-command.js.map +1 -0
- package/dist/mux/mux-escalation-manager.d.ts +48 -0
- package/dist/mux/mux-escalation-manager.js +162 -0
- package/dist/mux/mux-escalation-manager.js.map +1 -0
- package/dist/mux/mux-input-handler.d.ts +49 -0
- package/dist/mux/mux-input-handler.js +457 -0
- package/dist/mux/mux-input-handler.js.map +1 -0
- package/dist/mux/mux-renderer.d.ts +86 -0
- package/dist/mux/mux-renderer.js +758 -0
- package/dist/mux/mux-renderer.js.map +1 -0
- package/dist/mux/mux-splash.d.ts +17 -0
- package/dist/mux/mux-splash.js +322 -0
- package/dist/mux/mux-splash.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +79 -0
- package/dist/mux/pty-bridge.js +151 -0
- package/dist/mux/pty-bridge.js.map +1 -0
- package/dist/mux/trusted-input.d.ts +27 -0
- package/dist/mux/trusted-input.js +27 -0
- package/dist/mux/trusted-input.js.map +1 -0
- package/dist/mux/types.d.ts +92 -0
- package/dist/mux/types.js +45 -0
- package/dist/mux/types.js.map +1 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session/agent-session.js +3 -0
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/index.js +14 -7
- package/dist/session/index.js.map +1 -1
- package/dist/session/types.d.ts +6 -0
- package/dist/session/workspace-validation.d.ts +17 -0
- package/dist/session/workspace-validation.js +65 -0
- package/dist/session/workspace-validation.js.map +1 -0
- package/dist/trusted-process/auto-approver.d.ts +13 -4
- package/dist/trusted-process/auto-approver.js +12 -5
- package/dist/trusted-process/auto-approver.js.map +1 -1
- package/dist/trusted-process/escalation.js +5 -0
- package/dist/trusted-process/escalation.js.map +1 -1
- package/dist/trusted-process/index.js +23 -0
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +0 -2
- package/dist/trusted-process/mcp-proxy-server.js +82 -26
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/path-utils.d.ts +15 -0
- package/dist/trusted-process/path-utils.js +38 -0
- package/dist/trusted-process/path-utils.js.map +1 -1
- 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
|