@provos/ironcurtain 0.7.2 → 0.9.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 +29 -100
- package/dist/auth/auth-command.d.ts +15 -0
- package/dist/auth/auth-command.js +424 -0
- package/dist/auth/auth-command.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +46 -0
- package/dist/auth/oauth-flow.js +279 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +55 -0
- package/dist/auth/oauth-provider.js +73 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/oauth-registry.d.ts +34 -0
- package/dist/auth/oauth-registry.js +69 -0
- package/dist/auth/oauth-registry.js.map +1 -0
- package/dist/auth/oauth-token-provider.d.ts +52 -0
- package/dist/auth/oauth-token-provider.js +159 -0
- package/dist/auth/oauth-token-provider.js.map +1 -0
- package/dist/auth/oauth-token-store.d.ts +30 -0
- package/dist/auth/oauth-token-store.js +93 -0
- package/dist/auth/oauth-token-store.js.map +1 -0
- package/dist/auth/providers/google-scopes.d.ts +35 -0
- package/dist/auth/providers/google-scopes.js +183 -0
- package/dist/auth/providers/google-scopes.js.map +1 -0
- package/dist/auth/providers/google.d.ts +2 -0
- package/dist/auth/providers/google.js +28 -0
- package/dist/auth/providers/google.js.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +39 -1
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/constitution-readonly.md +14 -11
- package/dist/config/constitution-user-base.md +15 -0
- package/dist/config/first-start.js +4 -1
- package/dist/config/first-start.js.map +1 -1
- package/dist/config/generated/test-scenarios.json +1 -1
- package/dist/config/generated/tool-annotations.json +10617 -296
- package/dist/config/generated-readonly/compiled-policy.json +787 -244
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +31 -2
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +43 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/user-config.d.ts +29 -0
- package/dist/config/user-config.js +30 -0
- package/dist/config/user-config.js.map +1 -1
- package/dist/cron/constitution-generator.d.ts +2 -1
- package/dist/cron/constitution-generator.js +35 -36
- package/dist/cron/constitution-generator.js.map +1 -1
- package/dist/cron/headless-transport.d.ts +4 -0
- package/dist/cron/headless-transport.js +1 -1
- package/dist/cron/headless-transport.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.js +7 -1
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/docker/adapters/claude-code.js +32 -5
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +40 -0
- package/dist/docker/claude-md-seed.d.ts +27 -0
- package/dist/docker/claude-md-seed.js +42 -0
- package/dist/docker/claude-md-seed.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +3 -1
- package/dist/docker/docker-agent-session.js +51 -28
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +15 -1
- package/dist/docker/docker-infrastructure.js +105 -9
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +2 -0
- package/dist/docker/docker-manager.js +9 -1
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +65 -0
- package/dist/docker/mitm-proxy.js +288 -31
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/package-types.d.ts +127 -0
- package/dist/docker/package-types.js +8 -0
- package/dist/docker/package-types.js.map +1 -0
- package/dist/docker/package-validator.d.ts +41 -0
- package/dist/docker/package-validator.js +90 -0
- package/dist/docker/package-validator.js.map +1 -0
- package/dist/docker/platform.d.ts +6 -6
- package/dist/docker/platform.js +8 -6
- package/dist/docker/platform.js.map +1 -1
- package/dist/docker/proxy-tools.d.ts +80 -0
- package/dist/docker/proxy-tools.js +248 -0
- package/dist/docker/proxy-tools.js.map +1 -0
- package/dist/docker/pty-session.d.ts +15 -0
- package/dist/docker/pty-session.js +253 -73
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/pty-types.d.ts +28 -0
- package/dist/docker/pty-types.js +2 -0
- package/dist/docker/pty-types.js.map +1 -1
- package/dist/docker/registry-proxy.d.ts +142 -0
- package/dist/docker/registry-proxy.js +856 -0
- package/dist/docker/registry-proxy.js.map +1 -0
- package/dist/escalation/escalation-watcher.d.ts +3 -1
- package/dist/escalation/escalation-watcher.js +6 -2
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +24 -10
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +5 -0
- package/dist/escalation/listener-lock.js +14 -0
- package/dist/escalation/listener-lock.js.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/memory/auto-save.d.ts +25 -0
- package/dist/memory/auto-save.js +89 -0
- package/dist/memory/auto-save.js.map +1 -0
- package/dist/memory/memory-annotations.d.ts +47 -0
- package/dist/memory/memory-annotations.js +87 -0
- package/dist/memory/memory-annotations.js.map +1 -0
- package/dist/memory/memory-prompt.d.ts +12 -0
- package/dist/memory/memory-prompt.js +20 -0
- package/dist/memory/memory-prompt.js.map +1 -0
- package/dist/memory/resolve-memory-path.d.ts +20 -0
- package/dist/memory/resolve-memory-path.js +30 -0
- package/dist/memory/resolve-memory-path.js.map +1 -0
- package/dist/mux/mux-app.d.ts +4 -0
- package/dist/mux/mux-app.js +246 -28
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.d.ts +2 -2
- package/dist/mux/mux-command.js +14 -22
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-escalation-manager.d.ts +9 -3
- package/dist/mux/mux-escalation-manager.js +69 -19
- package/dist/mux/mux-escalation-manager.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +59 -3
- package/dist/mux/mux-input-handler.js +270 -32
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.d.ts +19 -1
- package/dist/mux/mux-renderer.js +466 -11
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/persona-scanner.d.ts +19 -0
- package/dist/mux/persona-scanner.js +53 -0
- package/dist/mux/persona-scanner.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +8 -0
- package/dist/mux/pty-bridge.js +19 -1
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/session-scanner.d.ts +18 -0
- package/dist/mux/session-scanner.js +65 -0
- package/dist/mux/session-scanner.js.map +1 -0
- package/dist/mux/types.d.ts +36 -1
- package/dist/mux/types.js +14 -1
- package/dist/mux/types.js.map +1 -1
- package/dist/persona/persona-command.js +130 -93
- package/dist/persona/persona-command.js.map +1 -1
- package/dist/persona/persona-prompt.d.ts +7 -7
- package/dist/persona/persona-prompt.js +14 -29
- package/dist/persona/persona-prompt.js.map +1 -1
- package/dist/persona/resolve.js +5 -2
- package/dist/persona/resolve.js.map +1 -1
- package/dist/pipeline/annotate.js +1 -7
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.d.ts +5 -2
- package/dist/pipeline/compile.js +23 -11
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.d.ts +302 -3
- package/dist/pipeline/constitution-compiler.js +285 -23
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/generate-with-repair.d.ts +2 -2
- package/dist/pipeline/generate-with-repair.js +2 -2
- package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
- package/dist/pipeline/handwritten-scenarios.js +20 -41
- package/dist/pipeline/handwritten-scenarios.js.map +1 -1
- package/dist/pipeline/list-resolver.d.ts +18 -20
- package/dist/pipeline/list-resolver.js +28 -36
- package/dist/pipeline/list-resolver.js.map +1 -1
- package/dist/pipeline/pipeline-runner.d.ts +102 -13
- package/dist/pipeline/pipeline-runner.js +497 -237
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +7 -9
- package/dist/pipeline/pipeline-shared.js +8 -18
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/policy-verifier.d.ts +32 -5
- package/dist/pipeline/policy-verifier.js +137 -8
- package/dist/pipeline/policy-verifier.js.map +1 -1
- package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
- package/dist/pipeline/proxy-mcp-connections.js +121 -0
- package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
- package/dist/pipeline/refresh-lists.js +21 -13
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-generator.d.ts +29 -49
- package/dist/pipeline/scenario-generator.js +180 -143
- package/dist/pipeline/scenario-generator.js.map +1 -1
- package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
- package/dist/pipeline/scenario-schema-validator.js +132 -0
- package/dist/pipeline/scenario-schema-validator.js.map +1 -0
- package/dist/pipeline/tool-annotator.d.ts +9 -8
- package/dist/pipeline/tool-annotator.js +48 -95
- package/dist/pipeline/tool-annotator.js.map +1 -1
- package/dist/pipeline/types.d.ts +31 -24
- package/dist/sandbox/index.d.ts +26 -10
- package/dist/sandbox/index.js +151 -40
- package/dist/sandbox/index.js.map +1 -1
- package/dist/servers/fetch-server.js +10 -5
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/agent-session.d.ts +8 -20
- package/dist/session/agent-session.js +22 -70
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/base-transport.d.ts +17 -1
- package/dist/session/base-transport.js +25 -1
- package/dist/session/base-transport.js.map +1 -1
- package/dist/session/cli-transport.d.ts +5 -0
- package/dist/session/cli-transport.js +48 -25
- package/dist/session/cli-transport.js.map +1 -1
- package/dist/session/index.d.ts +25 -1
- package/dist/session/index.js +57 -8
- package/dist/session/index.js.map +1 -1
- package/dist/session/prompts.js +8 -9
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +12 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/types.d.ts +11 -1
- package/dist/session/types.js.map +1 -1
- package/dist/session/workspace-validation.js +37 -6
- package/dist/session/workspace-validation.js.map +1 -1
- package/dist/signal/signal-bot-daemon.js +24 -8
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-transport.d.ts +5 -2
- package/dist/signal/signal-transport.js +3 -3
- package/dist/signal/signal-transport.js.map +1 -1
- package/dist/trusted-process/approval-whitelist.d.ts +117 -0
- package/dist/trusted-process/approval-whitelist.js +237 -0
- package/dist/trusted-process/approval-whitelist.js.map +1 -0
- package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
- package/dist/trusted-process/gworkspace-credentials.js +45 -0
- package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
- package/dist/trusted-process/index.d.ts +11 -1
- package/dist/trusted-process/index.js +83 -53
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
- package/dist/trusted-process/mcp-proxy-server.js +409 -94
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +12 -3
- package/dist/trusted-process/policy-engine.js +33 -23
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/policy-types.d.ts +17 -0
- package/dist/trusted-process/sandbox-integration.d.ts +23 -0
- package/dist/trusted-process/sandbox-integration.js +115 -3
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/token-file-refresher.d.ts +69 -0
- package/dist/trusted-process/token-file-refresher.js +120 -0
- package/dist/trusted-process/token-file-refresher.js.map +1 -0
- package/dist/types/argument-roles.d.ts +7 -1
- package/dist/types/argument-roles.js +50 -0
- package/dist/types/argument-roles.js.map +1 -1
- package/dist/types/audit.d.ts +11 -0
- package/docker/Dockerfile.base +22 -0
- package/docker/Dockerfile.base.arm64 +44 -4
- package/docker/Dockerfile.claude-code +2 -2
- package/docker/entrypoint-claude-code.sh +24 -6
- package/package.json +8 -2
- package/src/config/constitution-user-base.md +15 -0
- package/src/config/generated/test-scenarios.json +1 -1
- package/src/config/generated/tool-annotations.json +10617 -296
- package/src/config/mcp-servers.json +31 -2
- package/dist/pipeline/mcp-connections.d.ts +0 -16
- package/dist/pipeline/mcp-connections.js +0 -61
- package/dist/pipeline/mcp-connections.js.map +0 -1
package/dist/mux/mux-renderer.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* The PTY viewport size is constant regardless of input mode.
|
|
13
13
|
* Command-mode UI overlays the bottom of the viewport.
|
|
14
14
|
*/
|
|
15
|
-
import { calculateLayout } from './types.js';
|
|
15
|
+
import { calculateLayout, isPickerMode, isBottomPanelPicker, } from './types.js';
|
|
16
16
|
import { createSplashScreen } from './mux-splash.js';
|
|
17
|
+
import { formatRelativeTime } from './session-scanner.js';
|
|
17
18
|
// -- xterm.js color mode constants (from IBufferCell.getFgColorMode/getBgColorMode) --
|
|
18
19
|
const CM_DEFAULT = 0;
|
|
19
20
|
const CM_P16 = 16777216; // 0x01000000
|
|
@@ -76,6 +77,7 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
76
77
|
let renderScheduled = false;
|
|
77
78
|
let lastRenderTime = 0;
|
|
78
79
|
let renderTimeout = null;
|
|
80
|
+
let _destroyed = false;
|
|
79
81
|
// Splash screen (shown when no tabs exist)
|
|
80
82
|
let _splash = null;
|
|
81
83
|
// Multiline input scroll offset (first visible visual line index)
|
|
@@ -146,7 +148,8 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
146
148
|
const mode = deps.getMode();
|
|
147
149
|
if (mode === 'command')
|
|
148
150
|
return _layout.overlayRows;
|
|
149
|
-
|
|
151
|
+
// Escalation picker floats over the full viewport -- no rows reserved.
|
|
152
|
+
if (isBottomPanelPicker(mode))
|
|
150
153
|
return _layout.pickerRows;
|
|
151
154
|
return 0;
|
|
152
155
|
}
|
|
@@ -180,12 +183,13 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
180
183
|
const scrollOffset = deps.getScrollOffset();
|
|
181
184
|
const readFrom = scrollOffset ?? baseY;
|
|
182
185
|
const cells = readTerminalBuffer(xtermTerminal, readFrom, _layout.ptyViewportRows, _cols);
|
|
183
|
-
// Determine how many rows to render (skip overlay in command/picker mode)
|
|
186
|
+
// Determine how many rows to render (skip overlay in command/bottom-panel picker mode).
|
|
187
|
+
// Escalation picker floats over the full viewport -- no rows subtracted.
|
|
184
188
|
const mode = deps.getMode();
|
|
185
189
|
let visibleRows = _layout.ptyViewportRows;
|
|
186
190
|
if (mode === 'command')
|
|
187
191
|
visibleRows -= _layout.overlayRows;
|
|
188
|
-
else if (mode
|
|
192
|
+
else if (isBottomPanelPicker(mode))
|
|
189
193
|
visibleRows -= _layout.pickerRows;
|
|
190
194
|
let lastStyle = null;
|
|
191
195
|
for (let y = 0; y < visibleRows; y++) {
|
|
@@ -247,7 +251,7 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
247
251
|
term.bgWhite.black(' ^^A ');
|
|
248
252
|
term.styleReset();
|
|
249
253
|
if (pendingCount > 0) {
|
|
250
|
-
term.dim(` command mode \u00b7 ${pendingCount} escalation${pendingCount !== 1 ? 's' : ''} pending \u2014
|
|
254
|
+
term.dim(` command mode \u00b7 ${pendingCount} escalation${pendingCount !== 1 ? 's' : ''} pending \u2014 Ctrl-E to review`);
|
|
251
255
|
}
|
|
252
256
|
else {
|
|
253
257
|
term.dim(' command mode \u00b7 type a message to enable auto-approver \u00b7 Shift+drag to select');
|
|
@@ -324,6 +328,21 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
324
328
|
currentY++;
|
|
325
329
|
rowsUsed++;
|
|
326
330
|
}
|
|
331
|
+
// Whitelist candidate line (shows what /approve+ would whitelist)
|
|
332
|
+
if (rowsUsed < _layout.escalationPanelRows &&
|
|
333
|
+
esc.request.whitelistCandidates &&
|
|
334
|
+
esc.request.whitelistCandidates.length > 0) {
|
|
335
|
+
const candidate = esc.request.whitelistCandidates[0];
|
|
336
|
+
clearLine(currentY);
|
|
337
|
+
moveTo(6, currentY);
|
|
338
|
+
term.dim('/approve+ ');
|
|
339
|
+
term.cyan(candidate.description);
|
|
340
|
+
if (candidate.warning) {
|
|
341
|
+
term.yellow(` (${candidate.warning})`);
|
|
342
|
+
}
|
|
343
|
+
currentY++;
|
|
344
|
+
rowsUsed++;
|
|
345
|
+
}
|
|
327
346
|
}
|
|
328
347
|
// Overflow indicator when not all escalations fit
|
|
329
348
|
const remaining = pendingEscalations.length - shownCount;
|
|
@@ -341,6 +360,8 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
341
360
|
if (pendingCount > 0) {
|
|
342
361
|
term.cyan('/approve');
|
|
343
362
|
term.dim(' N ');
|
|
363
|
+
term.cyan('/approve+');
|
|
364
|
+
term.dim(' N ');
|
|
344
365
|
term.cyan('/deny');
|
|
345
366
|
term.dim(' N ');
|
|
346
367
|
term.cyan('/approve all');
|
|
@@ -352,6 +373,8 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
352
373
|
else {
|
|
353
374
|
term.cyan('/new');
|
|
354
375
|
term.dim(' ');
|
|
376
|
+
term.cyan('/resume');
|
|
377
|
+
term.dim(' ');
|
|
355
378
|
term.cyan('/tab');
|
|
356
379
|
term.dim(' N ');
|
|
357
380
|
term.cyan('/close');
|
|
@@ -411,6 +434,236 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
411
434
|
const cursorScreenRow = startY + _layout.escalationPanelRows + 1 + (cursorVis.row - _inputScrollOffset);
|
|
412
435
|
moveTo(2 + 2 + cursorVis.col, cursorScreenRow);
|
|
413
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Draws a single row of the escalation dialog box at the given screen
|
|
439
|
+
* position, wrapped in box-drawing side borders. Content is rendered
|
|
440
|
+
* by the `renderContent` callback which receives the inner width.
|
|
441
|
+
*/
|
|
442
|
+
function drawBoxRow(y, boxX, innerWidth, renderContent) {
|
|
443
|
+
moveTo(boxX, y);
|
|
444
|
+
term.brightCyan('\u2502');
|
|
445
|
+
term(' ');
|
|
446
|
+
// Clear the full inner area with spaces first to avoid stale characters
|
|
447
|
+
// from previous redraws. We cannot use eraseLineAfter because that would
|
|
448
|
+
// wipe PTY content to the right of the floating overlay.
|
|
449
|
+
term(' '.repeat(innerWidth));
|
|
450
|
+
// Move back and render content over the cleared area
|
|
451
|
+
moveTo(boxX + 2, y);
|
|
452
|
+
renderContent(innerWidth);
|
|
453
|
+
moveTo(boxX + innerWidth + 2, y);
|
|
454
|
+
term(' ');
|
|
455
|
+
term.brightCyan('\u2502');
|
|
456
|
+
}
|
|
457
|
+
function drawEscalationPickerOverlay() {
|
|
458
|
+
const eps = deps.getEscalationPickerState();
|
|
459
|
+
if (!eps)
|
|
460
|
+
return;
|
|
461
|
+
const pending = deps.getEscalationState().pendingEscalations;
|
|
462
|
+
const sortedEscalations = [...pending.values()].sort((a, b) => a.displayNumber - b.displayNumber);
|
|
463
|
+
if (sortedEscalations.length === 0)
|
|
464
|
+
return;
|
|
465
|
+
const focused = pending.get(eps.focusedDisplayNumber);
|
|
466
|
+
if (!focused)
|
|
467
|
+
return;
|
|
468
|
+
// Compute dialog dimensions -- centered in the PTY viewport
|
|
469
|
+
const viewportRows = _layout.ptyViewportRows;
|
|
470
|
+
const viewportY = _layout.ptyViewportY;
|
|
471
|
+
// Horizontal sizing: leave 3-column margin on each side, max 80 inner width.
|
|
472
|
+
// Clamp to available columns so we never draw past the right edge.
|
|
473
|
+
const boxMargin = 3;
|
|
474
|
+
const maxInnerWidth = 80;
|
|
475
|
+
const minBoxWidth = 12; // absolute minimum to show anything useful
|
|
476
|
+
const innerWidth = Math.min(maxInnerWidth, Math.max(minBoxWidth - 4, _cols - boxMargin * 2 - 4));
|
|
477
|
+
const boxWidth = innerWidth + 4; // 4 = border + space on each side
|
|
478
|
+
if (boxWidth > _cols)
|
|
479
|
+
return; // terminal too narrow for overlay
|
|
480
|
+
const boxX = Math.max(0, Math.floor((_cols - boxWidth) / 2));
|
|
481
|
+
// Content rows: tab bar + separator + header + separator + detail area + separator + hint bar
|
|
482
|
+
const minContentRows = 7; // minimum: tab + sep + header + sep + 1 detail + sep + hints
|
|
483
|
+
const maxContentRows = Math.max(minContentRows, viewportRows - 4); // leave 2 rows margin top+bottom
|
|
484
|
+
const detailBudget = maxContentRows - 6; // 6 = tab bar + 2 separators + header + separator + hint bar
|
|
485
|
+
// Collect detail lines — use innerWidth (minus arg indent) so line
|
|
486
|
+
// packing matches the actual box geometry.
|
|
487
|
+
const argLines = formatArgLines(focused.request.arguments, innerWidth - 2);
|
|
488
|
+
const detailLines = [];
|
|
489
|
+
if (argLines.length > 0) {
|
|
490
|
+
detailLines.push({ kind: 'label', text: 'Arguments:' });
|
|
491
|
+
for (const line of argLines) {
|
|
492
|
+
detailLines.push({ kind: 'arg', text: line });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
detailLines.push({ kind: 'reason', text: `Reason: ${focused.request.reason}` });
|
|
496
|
+
if (focused.request.whitelistCandidates && focused.request.whitelistCandidates.length > 0) {
|
|
497
|
+
const candidate = focused.request.whitelistCandidates[0];
|
|
498
|
+
const wlText = candidate.warning ? `${candidate.description} (${candidate.warning})` : candidate.description;
|
|
499
|
+
detailLines.push({ kind: 'whitelist', text: `/approve+ ${wlText}` });
|
|
500
|
+
}
|
|
501
|
+
const actualDetailRows = Math.min(detailLines.length, detailBudget);
|
|
502
|
+
const totalBoxRows = 6 + actualDetailRows; // includes top+bottom border
|
|
503
|
+
const totalBoxHeight = totalBoxRows + 2; // +2 for top and bottom border lines
|
|
504
|
+
if (totalBoxHeight > viewportRows)
|
|
505
|
+
return; // terminal too small
|
|
506
|
+
// Vertical centering within the viewport
|
|
507
|
+
const startY = viewportY + Math.max(0, Math.floor((viewportRows - totalBoxHeight) / 2));
|
|
508
|
+
let currentY = startY;
|
|
509
|
+
// Top border with title
|
|
510
|
+
moveTo(boxX, currentY);
|
|
511
|
+
const title = ' Escalation ';
|
|
512
|
+
const borderAfterTitle = Math.max(0, boxWidth - 2 - title.length);
|
|
513
|
+
term.brightCyan('\u250c' + title);
|
|
514
|
+
term.brightCyan('\u2500'.repeat(borderAfterTitle) + '\u2510');
|
|
515
|
+
term.styleReset();
|
|
516
|
+
currentY++;
|
|
517
|
+
// Escalation tab bar
|
|
518
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
519
|
+
const tabs = sortedEscalations.map((esc) => ({
|
|
520
|
+
displayNumber: esc.displayNumber,
|
|
521
|
+
label: `[${esc.displayNumber}] ${esc.request.serverName}/${esc.request.toolName}`,
|
|
522
|
+
}));
|
|
523
|
+
const fitted = fitTabLabels(tabs, eps.focusedDisplayNumber, w);
|
|
524
|
+
let written = 0;
|
|
525
|
+
for (const tab of fitted) {
|
|
526
|
+
if (tab.isFocused) {
|
|
527
|
+
term.bgCyan.black(' ' + tab.label + ' ');
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
term.dim(' ' + tab.label + ' ');
|
|
531
|
+
}
|
|
532
|
+
term.styleReset();
|
|
533
|
+
written += tab.label.length + 2;
|
|
534
|
+
}
|
|
535
|
+
const pad = Math.max(0, w - written);
|
|
536
|
+
if (pad > 0)
|
|
537
|
+
term(' '.repeat(pad));
|
|
538
|
+
});
|
|
539
|
+
currentY++;
|
|
540
|
+
// Separator
|
|
541
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
542
|
+
term.dim('\u2500'.repeat(w));
|
|
543
|
+
term.styleReset();
|
|
544
|
+
});
|
|
545
|
+
currentY++;
|
|
546
|
+
// Tool header (truncated to fit within inner width)
|
|
547
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
548
|
+
const timeAgo = formatRelativeTime(focused.receivedAt.toISOString());
|
|
549
|
+
const sessionPrefix = `Session #${focused.sessionDisplayNumber} `;
|
|
550
|
+
const toolName = `${focused.request.serverName}/${focused.request.toolName}`;
|
|
551
|
+
const timeSuffix = ` ${timeAgo}`;
|
|
552
|
+
const totalLen = sessionPrefix.length + toolName.length + timeSuffix.length;
|
|
553
|
+
if (totalLen <= w) {
|
|
554
|
+
// Everything fits
|
|
555
|
+
term(sessionPrefix);
|
|
556
|
+
term.cyan(toolName);
|
|
557
|
+
term.dim(timeSuffix);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Truncate the tool name to fit; drop time suffix if still too long
|
|
561
|
+
const budgetForTool = w - sessionPrefix.length - timeSuffix.length;
|
|
562
|
+
if (budgetForTool >= 4) {
|
|
563
|
+
term(sessionPrefix);
|
|
564
|
+
term.cyan(truncate(toolName, budgetForTool));
|
|
565
|
+
term.dim(timeSuffix);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Not enough room for time suffix -- drop it entirely
|
|
569
|
+
const toolBudget = w - sessionPrefix.length;
|
|
570
|
+
if (toolBudget > 0) {
|
|
571
|
+
term(sessionPrefix);
|
|
572
|
+
term.cyan(truncate(toolName, toolBudget));
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
term(truncate(sessionPrefix, w));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
term.styleReset();
|
|
580
|
+
});
|
|
581
|
+
currentY++;
|
|
582
|
+
// Separator
|
|
583
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
584
|
+
term.dim('\u2500'.repeat(w));
|
|
585
|
+
term.styleReset();
|
|
586
|
+
});
|
|
587
|
+
currentY++;
|
|
588
|
+
// Detail rows — drawBoxRow pre-clears the inner area with spaces and
|
|
589
|
+
// handles right-border positioning, so callbacks only need to write
|
|
590
|
+
// content (no manual padding required).
|
|
591
|
+
for (let i = 0; i < actualDetailRows; i++) {
|
|
592
|
+
const detail = detailLines[i];
|
|
593
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
594
|
+
const indent = detail.kind === 'arg' ? 2 : 0;
|
|
595
|
+
const maxTextWidth = w - indent;
|
|
596
|
+
if (indent > 0)
|
|
597
|
+
term(' '.repeat(indent));
|
|
598
|
+
if (detail.kind === 'label') {
|
|
599
|
+
term.dim(truncate(detail.text, maxTextWidth));
|
|
600
|
+
}
|
|
601
|
+
else if (detail.kind === 'reason') {
|
|
602
|
+
term.dim('Reason: ');
|
|
603
|
+
term.styleReset();
|
|
604
|
+
term(truncate(focused.request.reason, maxTextWidth - 8));
|
|
605
|
+
}
|
|
606
|
+
else if (detail.kind === 'whitelist') {
|
|
607
|
+
term.dim('/approve+ ');
|
|
608
|
+
term.styleReset();
|
|
609
|
+
const candidate = focused.request.whitelistCandidates?.[0];
|
|
610
|
+
if (candidate) {
|
|
611
|
+
const descBudget = maxTextWidth - 10;
|
|
612
|
+
if (candidate.warning) {
|
|
613
|
+
const warnText = ` (${candidate.warning})`;
|
|
614
|
+
term.cyan(truncate(candidate.description, Math.max(1, descBudget - warnText.length)));
|
|
615
|
+
term.yellow(truncate(warnText, descBudget));
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
term.cyan(truncate(candidate.description, descBudget));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
term(truncate(detail.text, maxTextWidth));
|
|
624
|
+
}
|
|
625
|
+
term.styleReset();
|
|
626
|
+
});
|
|
627
|
+
currentY++;
|
|
628
|
+
}
|
|
629
|
+
// Separator before hint bar
|
|
630
|
+
drawBoxRow(currentY, boxX, innerWidth, (w) => {
|
|
631
|
+
term.dim('\u2500'.repeat(w));
|
|
632
|
+
term.styleReset();
|
|
633
|
+
});
|
|
634
|
+
currentY++;
|
|
635
|
+
// Hint bar
|
|
636
|
+
drawBoxRow(currentY, boxX, innerWidth, () => {
|
|
637
|
+
term.bgWhite.black(' a ');
|
|
638
|
+
term.styleReset();
|
|
639
|
+
term.dim(' approve ');
|
|
640
|
+
term.bgWhite.black(' d ');
|
|
641
|
+
term.styleReset();
|
|
642
|
+
term.dim(' deny ');
|
|
643
|
+
term.bgWhite.black(' w ');
|
|
644
|
+
term.styleReset();
|
|
645
|
+
term.dim(' approve+ ');
|
|
646
|
+
if (sortedEscalations.length > 1) {
|
|
647
|
+
term.bgWhite.black(' A ');
|
|
648
|
+
term.styleReset();
|
|
649
|
+
term.dim(' all ');
|
|
650
|
+
term.bgWhite.black(' D ');
|
|
651
|
+
term.styleReset();
|
|
652
|
+
term.dim(' deny all ');
|
|
653
|
+
term.bgWhite.black(' \u2190\u2192 ');
|
|
654
|
+
term.styleReset();
|
|
655
|
+
term.dim(' switch ');
|
|
656
|
+
}
|
|
657
|
+
term.bgWhite.black(' Esc ');
|
|
658
|
+
term.styleReset();
|
|
659
|
+
term.dim(' dismiss');
|
|
660
|
+
});
|
|
661
|
+
currentY++;
|
|
662
|
+
// Bottom border
|
|
663
|
+
moveTo(boxX, currentY);
|
|
664
|
+
term.brightCyan('\u2514' + '\u2500'.repeat(boxWidth - 2) + '\u2518');
|
|
665
|
+
term.styleReset();
|
|
666
|
+
}
|
|
414
667
|
function drawActiveOverlay() {
|
|
415
668
|
const mode = deps.getMode();
|
|
416
669
|
if (mode === 'command') {
|
|
@@ -419,6 +672,15 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
419
672
|
else if (mode === 'picker') {
|
|
420
673
|
drawPickerOverlay();
|
|
421
674
|
}
|
|
675
|
+
else if (mode === 'resume-picker') {
|
|
676
|
+
drawResumePickerOverlay();
|
|
677
|
+
}
|
|
678
|
+
else if (mode === 'persona-picker') {
|
|
679
|
+
drawPersonaPickerOverlay();
|
|
680
|
+
}
|
|
681
|
+
else if (mode === 'escalation-picker') {
|
|
682
|
+
drawEscalationPickerOverlay();
|
|
683
|
+
}
|
|
422
684
|
}
|
|
423
685
|
function drawPickerOverlay() {
|
|
424
686
|
const ps = deps.getPickerState();
|
|
@@ -452,7 +714,8 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
452
714
|
term.cyan('\u2502');
|
|
453
715
|
}
|
|
454
716
|
function drawPickerMenu(ps, startY, totalRows) {
|
|
455
|
-
const
|
|
717
|
+
const hasPersonas = ps.menuItemCount >= 3;
|
|
718
|
+
const menuHeight = hasPersonas ? 5 : 4;
|
|
456
719
|
const topPad = Math.max(0, Math.floor((totalRows - menuHeight) / 2));
|
|
457
720
|
for (let y = startY; y < startY + totalRows; y++) {
|
|
458
721
|
clearLine(y);
|
|
@@ -464,9 +727,13 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
464
727
|
term.cyan('\u250c /new ' + '\u2500'.repeat(Math.max(0, boxWidth - 6)) + '\u2510');
|
|
465
728
|
drawMenuOption(y0 + 1, ' New sandbox', ps.menuSelection === 0, boxWidth);
|
|
466
729
|
drawMenuOption(y0 + 2, ' Existing directory', ps.menuSelection === 1, boxWidth);
|
|
730
|
+
if (hasPersonas) {
|
|
731
|
+
drawMenuOption(y0 + 3, ' Use a persona', ps.menuSelection === 2, boxWidth);
|
|
732
|
+
}
|
|
467
733
|
// Bottom border
|
|
468
|
-
|
|
469
|
-
|
|
734
|
+
const bottomY = hasPersonas ? y0 + 4 : y0 + 3;
|
|
735
|
+
clearLine(bottomY);
|
|
736
|
+
moveTo(2, bottomY);
|
|
470
737
|
term.cyan('\u2514' + '\u2500'.repeat(boxWidth) + '\u2518');
|
|
471
738
|
term.styleReset();
|
|
472
739
|
}
|
|
@@ -572,6 +839,166 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
572
839
|
// Position terminal cursor on the input line (for accessibility)
|
|
573
840
|
moveTo(cursorX, startY);
|
|
574
841
|
}
|
|
842
|
+
function drawResumePickerOverlay() {
|
|
843
|
+
const rps = deps.getResumePickerState();
|
|
844
|
+
if (!rps)
|
|
845
|
+
return;
|
|
846
|
+
const startY = _layout.pickerY;
|
|
847
|
+
const totalRows = _layout.pickerRows;
|
|
848
|
+
if (totalRows < 3)
|
|
849
|
+
return;
|
|
850
|
+
let currentY = startY;
|
|
851
|
+
// Title
|
|
852
|
+
clearLine(currentY);
|
|
853
|
+
moveTo(2, currentY);
|
|
854
|
+
term.cyan('/resume');
|
|
855
|
+
term.dim(' \u2014 select a session to resume');
|
|
856
|
+
term.eraseLineAfter();
|
|
857
|
+
currentY++;
|
|
858
|
+
if (rps.sessions.length === 0) {
|
|
859
|
+
clearLine(currentY);
|
|
860
|
+
moveTo(4, currentY);
|
|
861
|
+
term.dim('No resumable sessions found');
|
|
862
|
+
term.eraseLineAfter();
|
|
863
|
+
currentY++;
|
|
864
|
+
for (let y = currentY; y < startY + totalRows; y++) {
|
|
865
|
+
clearLine(y);
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
// Session list
|
|
870
|
+
const listRows = Math.max(0, totalRows - 2); // title + hint bar
|
|
871
|
+
// Scroll adjustment (render-time only; do not mutate rps)
|
|
872
|
+
let scrollOffset = rps.scrollOffset;
|
|
873
|
+
if (rps.selectedIndex < scrollOffset) {
|
|
874
|
+
scrollOffset = rps.selectedIndex;
|
|
875
|
+
}
|
|
876
|
+
else if (rps.selectedIndex >= scrollOffset + listRows) {
|
|
877
|
+
scrollOffset = rps.selectedIndex - listRows + 1;
|
|
878
|
+
}
|
|
879
|
+
for (let i = 0; i < listRows; i++) {
|
|
880
|
+
clearLine(currentY);
|
|
881
|
+
const idx = scrollOffset + i;
|
|
882
|
+
if (idx < rps.sessions.length) {
|
|
883
|
+
const s = rps.sessions[idx];
|
|
884
|
+
const isSelected = idx === rps.selectedIndex;
|
|
885
|
+
const shortId = s.sessionId.substring(0, 8);
|
|
886
|
+
const timeAgo = formatRelativeTime(s.lastActivity);
|
|
887
|
+
const line = `${shortId} ${s.agent} ${s.label} ${timeAgo} [${s.status}]`;
|
|
888
|
+
moveTo(2, currentY);
|
|
889
|
+
if (isSelected) {
|
|
890
|
+
term.bgCyan.black('> ' + truncate(line, Math.max(1, _cols - 6)));
|
|
891
|
+
term.styleReset();
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
term(' ');
|
|
895
|
+
term.dim(shortId);
|
|
896
|
+
term(' ');
|
|
897
|
+
term(s.agent);
|
|
898
|
+
term(' ');
|
|
899
|
+
term.cyan(truncate(s.label, Math.max(10, _cols - 50)));
|
|
900
|
+
term(' ');
|
|
901
|
+
term.dim(timeAgo);
|
|
902
|
+
term(' ');
|
|
903
|
+
term.dim(`[${s.status}]`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
term.eraseLineAfter();
|
|
907
|
+
currentY++;
|
|
908
|
+
}
|
|
909
|
+
// Hint bar
|
|
910
|
+
clearLine(currentY);
|
|
911
|
+
moveTo(2, currentY);
|
|
912
|
+
term.bgWhite.black(' Enter ');
|
|
913
|
+
term.styleReset();
|
|
914
|
+
term.dim(' resume ');
|
|
915
|
+
term.bgWhite.black(' Esc ');
|
|
916
|
+
term.styleReset();
|
|
917
|
+
term.dim(' cancel');
|
|
918
|
+
term.styleReset();
|
|
919
|
+
term.eraseLineAfter();
|
|
920
|
+
}
|
|
921
|
+
function drawPersonaPickerOverlay() {
|
|
922
|
+
const pps = deps.getPersonaPickerState();
|
|
923
|
+
if (!pps)
|
|
924
|
+
return;
|
|
925
|
+
const startY = _layout.pickerY;
|
|
926
|
+
const totalRows = _layout.pickerRows;
|
|
927
|
+
if (totalRows < 3)
|
|
928
|
+
return;
|
|
929
|
+
let currentY = startY;
|
|
930
|
+
// Title
|
|
931
|
+
clearLine(currentY);
|
|
932
|
+
moveTo(2, currentY);
|
|
933
|
+
term.cyan('/new');
|
|
934
|
+
term.dim(' \u2014 select a persona');
|
|
935
|
+
term.eraseLineAfter();
|
|
936
|
+
currentY++;
|
|
937
|
+
if (pps.personas.length === 0) {
|
|
938
|
+
clearLine(currentY);
|
|
939
|
+
moveTo(4, currentY);
|
|
940
|
+
term.dim('No personas found');
|
|
941
|
+
term.eraseLineAfter();
|
|
942
|
+
currentY++;
|
|
943
|
+
for (let y = currentY; y < startY + totalRows; y++) {
|
|
944
|
+
clearLine(y);
|
|
945
|
+
}
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
// Persona list
|
|
949
|
+
const listRows = Math.max(0, totalRows - 2); // title + hint bar
|
|
950
|
+
// Scroll adjustment
|
|
951
|
+
let scrollOffset = pps.scrollOffset;
|
|
952
|
+
if (pps.selectedIndex < scrollOffset) {
|
|
953
|
+
scrollOffset = pps.selectedIndex;
|
|
954
|
+
}
|
|
955
|
+
else if (pps.selectedIndex >= scrollOffset + listRows) {
|
|
956
|
+
scrollOffset = pps.selectedIndex - listRows + 1;
|
|
957
|
+
}
|
|
958
|
+
for (let i = 0; i < listRows; i++) {
|
|
959
|
+
clearLine(currentY);
|
|
960
|
+
const idx = scrollOffset + i;
|
|
961
|
+
if (idx < pps.personas.length) {
|
|
962
|
+
const p = pps.personas[idx];
|
|
963
|
+
const isSelected = idx === pps.selectedIndex;
|
|
964
|
+
const status = p.compiled ? '' : ' [not compiled]';
|
|
965
|
+
const desc = truncate(p.description, Math.max(10, _cols - p.name.length - status.length - 10));
|
|
966
|
+
const line = `${p.name} ${desc}${status}`;
|
|
967
|
+
moveTo(2, currentY);
|
|
968
|
+
if (isSelected) {
|
|
969
|
+
term.bgCyan.black('> ' + truncate(line, Math.max(1, _cols - 6)));
|
|
970
|
+
term.styleReset();
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
term(' ');
|
|
974
|
+
if (p.compiled) {
|
|
975
|
+
term.cyan(p.name);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
term.dim(p.name);
|
|
979
|
+
}
|
|
980
|
+
term(' ');
|
|
981
|
+
term.dim(desc);
|
|
982
|
+
if (!p.compiled) {
|
|
983
|
+
term.yellow(status);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
term.eraseLineAfter();
|
|
988
|
+
currentY++;
|
|
989
|
+
}
|
|
990
|
+
// Hint bar
|
|
991
|
+
clearLine(currentY);
|
|
992
|
+
moveTo(2, currentY);
|
|
993
|
+
term.bgWhite.black(' Enter ');
|
|
994
|
+
term.styleReset();
|
|
995
|
+
term.dim(' spawn ');
|
|
996
|
+
term.bgWhite.black(' Esc ');
|
|
997
|
+
term.styleReset();
|
|
998
|
+
term.dim(' cancel');
|
|
999
|
+
term.styleReset();
|
|
1000
|
+
term.eraseLineAfter();
|
|
1001
|
+
}
|
|
575
1002
|
return {
|
|
576
1003
|
get layout() {
|
|
577
1004
|
return _layout;
|
|
@@ -595,7 +1022,7 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
595
1022
|
redrawCommandArea() {
|
|
596
1023
|
recalcLayout();
|
|
597
1024
|
const mode = deps.getMode();
|
|
598
|
-
if (mode === 'command' || mode
|
|
1025
|
+
if (mode === 'command' || isPickerMode(mode)) {
|
|
599
1026
|
// Repaint viewport first to clear stale overlay rows from a
|
|
600
1027
|
// previously larger overlay (e.g., after resolving an escalation).
|
|
601
1028
|
drawPtyViewport();
|
|
@@ -612,6 +1039,7 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
612
1039
|
_splash?.resize(_cols, _layout.ptyViewportRows, _layout.ptyViewportY, activeOverlayRows());
|
|
613
1040
|
},
|
|
614
1041
|
destroy() {
|
|
1042
|
+
_destroyed = true;
|
|
615
1043
|
tearDownSplash();
|
|
616
1044
|
if (renderTimeout) {
|
|
617
1045
|
clearTimeout(renderTimeout);
|
|
@@ -634,7 +1062,7 @@ export function createMuxRenderer(term, cols, rows, deps) {
|
|
|
634
1062
|
drawFooter();
|
|
635
1063
|
},
|
|
636
1064
|
scheduleRedraw() {
|
|
637
|
-
if (renderScheduled)
|
|
1065
|
+
if (_destroyed || renderScheduled)
|
|
638
1066
|
return;
|
|
639
1067
|
renderScheduled = true;
|
|
640
1068
|
const elapsed = Date.now() - lastRenderTime;
|
|
@@ -842,9 +1270,36 @@ function formatArgValue(value) {
|
|
|
842
1270
|
return String(value);
|
|
843
1271
|
return JSON.stringify(value);
|
|
844
1272
|
}
|
|
845
|
-
function truncate(str, maxLen) {
|
|
1273
|
+
export function truncate(str, maxLen) {
|
|
846
1274
|
if (str.length <= maxLen)
|
|
847
1275
|
return str;
|
|
848
1276
|
return str.slice(0, maxLen - 1) + '\u2026';
|
|
849
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Computes which tab labels to render in the escalation tab bar,
|
|
1280
|
+
* truncating the focused tab if needed. Returns an array of
|
|
1281
|
+
* { label, isFocused } entries that fit within `width`.
|
|
1282
|
+
*
|
|
1283
|
+
* Exported for testing.
|
|
1284
|
+
*/
|
|
1285
|
+
export function fitTabLabels(tabs, focusedDisplayNumber, width) {
|
|
1286
|
+
const result = [];
|
|
1287
|
+
let written = 0;
|
|
1288
|
+
for (const tab of tabs) {
|
|
1289
|
+
const isFocused = tab.displayNumber === focusedDisplayNumber;
|
|
1290
|
+
const cellWidth = tab.label.length + 2; // +2 for surrounding spaces
|
|
1291
|
+
if (written + cellWidth > width) {
|
|
1292
|
+
// Truncate the focused tab to fit; need at least 3 cols: space + 1 char + space
|
|
1293
|
+
const remaining = width - written;
|
|
1294
|
+
if (isFocused && remaining >= 3) {
|
|
1295
|
+
const truncatedLabel = truncate(tab.label, remaining - 2);
|
|
1296
|
+
result.push({ label: truncatedLabel, isFocused: true });
|
|
1297
|
+
}
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
result.push({ label: tab.label, isFocused });
|
|
1301
|
+
written += cellWidth;
|
|
1302
|
+
}
|
|
1303
|
+
return result;
|
|
1304
|
+
}
|
|
850
1305
|
//# sourceMappingURL=mux-renderer.js.map
|