@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.
Files changed (259) hide show
  1. package/README.md +29 -100
  2. package/dist/auth/auth-command.d.ts +15 -0
  3. package/dist/auth/auth-command.js +424 -0
  4. package/dist/auth/auth-command.js.map +1 -0
  5. package/dist/auth/oauth-flow.d.ts +46 -0
  6. package/dist/auth/oauth-flow.js +279 -0
  7. package/dist/auth/oauth-flow.js.map +1 -0
  8. package/dist/auth/oauth-provider.d.ts +55 -0
  9. package/dist/auth/oauth-provider.js +73 -0
  10. package/dist/auth/oauth-provider.js.map +1 -0
  11. package/dist/auth/oauth-registry.d.ts +34 -0
  12. package/dist/auth/oauth-registry.js +69 -0
  13. package/dist/auth/oauth-registry.js.map +1 -0
  14. package/dist/auth/oauth-token-provider.d.ts +52 -0
  15. package/dist/auth/oauth-token-provider.js +159 -0
  16. package/dist/auth/oauth-token-provider.js.map +1 -0
  17. package/dist/auth/oauth-token-store.d.ts +30 -0
  18. package/dist/auth/oauth-token-store.js +93 -0
  19. package/dist/auth/oauth-token-store.js.map +1 -0
  20. package/dist/auth/providers/google-scopes.d.ts +35 -0
  21. package/dist/auth/providers/google-scopes.js +183 -0
  22. package/dist/auth/providers/google-scopes.js.map +1 -0
  23. package/dist/auth/providers/google.d.ts +2 -0
  24. package/dist/auth/providers/google.js +28 -0
  25. package/dist/auth/providers/google.js.map +1 -0
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config/config-command.js +39 -1
  29. package/dist/config/config-command.js.map +1 -1
  30. package/dist/config/constitution-readonly.md +14 -11
  31. package/dist/config/constitution-user-base.md +15 -0
  32. package/dist/config/first-start.js +4 -1
  33. package/dist/config/first-start.js.map +1 -1
  34. package/dist/config/generated/test-scenarios.json +1 -1
  35. package/dist/config/generated/tool-annotations.json +10617 -296
  36. package/dist/config/generated-readonly/compiled-policy.json +787 -244
  37. package/dist/config/index.d.ts +2 -2
  38. package/dist/config/index.js +8 -0
  39. package/dist/config/index.js.map +1 -1
  40. package/dist/config/mcp-servers.json +31 -2
  41. package/dist/config/paths.d.ts +20 -0
  42. package/dist/config/paths.js +43 -0
  43. package/dist/config/paths.js.map +1 -1
  44. package/dist/config/types.d.ts +8 -0
  45. package/dist/config/user-config.d.ts +29 -0
  46. package/dist/config/user-config.js +30 -0
  47. package/dist/config/user-config.js.map +1 -1
  48. package/dist/cron/constitution-generator.d.ts +2 -1
  49. package/dist/cron/constitution-generator.js +35 -36
  50. package/dist/cron/constitution-generator.js.map +1 -1
  51. package/dist/cron/headless-transport.d.ts +4 -0
  52. package/dist/cron/headless-transport.js +1 -1
  53. package/dist/cron/headless-transport.js.map +1 -1
  54. package/dist/daemon/ironcurtain-daemon.js +7 -1
  55. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  56. package/dist/docker/adapters/claude-code.js +32 -5
  57. package/dist/docker/adapters/claude-code.js.map +1 -1
  58. package/dist/docker/agent-adapter.d.ts +40 -0
  59. package/dist/docker/claude-md-seed.d.ts +27 -0
  60. package/dist/docker/claude-md-seed.js +42 -0
  61. package/dist/docker/claude-md-seed.js.map +1 -0
  62. package/dist/docker/docker-agent-session.d.ts +3 -1
  63. package/dist/docker/docker-agent-session.js +51 -28
  64. package/dist/docker/docker-agent-session.js.map +1 -1
  65. package/dist/docker/docker-infrastructure.d.ts +15 -1
  66. package/dist/docker/docker-infrastructure.js +105 -9
  67. package/dist/docker/docker-infrastructure.js.map +1 -1
  68. package/dist/docker/docker-manager.d.ts +2 -0
  69. package/dist/docker/docker-manager.js +9 -1
  70. package/dist/docker/docker-manager.js.map +1 -1
  71. package/dist/docker/mitm-proxy.d.ts +65 -0
  72. package/dist/docker/mitm-proxy.js +288 -31
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/package-types.d.ts +127 -0
  75. package/dist/docker/package-types.js +8 -0
  76. package/dist/docker/package-types.js.map +1 -0
  77. package/dist/docker/package-validator.d.ts +41 -0
  78. package/dist/docker/package-validator.js +90 -0
  79. package/dist/docker/package-validator.js.map +1 -0
  80. package/dist/docker/platform.d.ts +6 -6
  81. package/dist/docker/platform.js +8 -6
  82. package/dist/docker/platform.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +80 -0
  84. package/dist/docker/proxy-tools.js +248 -0
  85. package/dist/docker/proxy-tools.js.map +1 -0
  86. package/dist/docker/pty-session.d.ts +15 -0
  87. package/dist/docker/pty-session.js +253 -73
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +28 -0
  90. package/dist/docker/pty-types.js +2 -0
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/registry-proxy.d.ts +142 -0
  93. package/dist/docker/registry-proxy.js +856 -0
  94. package/dist/docker/registry-proxy.js.map +1 -0
  95. package/dist/escalation/escalation-watcher.d.ts +3 -1
  96. package/dist/escalation/escalation-watcher.js +6 -2
  97. package/dist/escalation/escalation-watcher.js.map +1 -1
  98. package/dist/escalation/listener-command.js +24 -10
  99. package/dist/escalation/listener-command.js.map +1 -1
  100. package/dist/escalation/listener-lock.d.ts +5 -0
  101. package/dist/escalation/listener-lock.js +14 -0
  102. package/dist/escalation/listener-lock.js.map +1 -1
  103. package/dist/index.js +16 -3
  104. package/dist/index.js.map +1 -1
  105. package/dist/memory/auto-save.d.ts +25 -0
  106. package/dist/memory/auto-save.js +89 -0
  107. package/dist/memory/auto-save.js.map +1 -0
  108. package/dist/memory/memory-annotations.d.ts +47 -0
  109. package/dist/memory/memory-annotations.js +87 -0
  110. package/dist/memory/memory-annotations.js.map +1 -0
  111. package/dist/memory/memory-prompt.d.ts +12 -0
  112. package/dist/memory/memory-prompt.js +20 -0
  113. package/dist/memory/memory-prompt.js.map +1 -0
  114. package/dist/memory/resolve-memory-path.d.ts +20 -0
  115. package/dist/memory/resolve-memory-path.js +30 -0
  116. package/dist/memory/resolve-memory-path.js.map +1 -0
  117. package/dist/mux/mux-app.d.ts +4 -0
  118. package/dist/mux/mux-app.js +246 -28
  119. package/dist/mux/mux-app.js.map +1 -1
  120. package/dist/mux/mux-command.d.ts +2 -2
  121. package/dist/mux/mux-command.js +14 -22
  122. package/dist/mux/mux-command.js.map +1 -1
  123. package/dist/mux/mux-escalation-manager.d.ts +9 -3
  124. package/dist/mux/mux-escalation-manager.js +69 -19
  125. package/dist/mux/mux-escalation-manager.js.map +1 -1
  126. package/dist/mux/mux-input-handler.d.ts +59 -3
  127. package/dist/mux/mux-input-handler.js +270 -32
  128. package/dist/mux/mux-input-handler.js.map +1 -1
  129. package/dist/mux/mux-renderer.d.ts +19 -1
  130. package/dist/mux/mux-renderer.js +466 -11
  131. package/dist/mux/mux-renderer.js.map +1 -1
  132. package/dist/mux/persona-scanner.d.ts +19 -0
  133. package/dist/mux/persona-scanner.js +53 -0
  134. package/dist/mux/persona-scanner.js.map +1 -0
  135. package/dist/mux/pty-bridge.d.ts +8 -0
  136. package/dist/mux/pty-bridge.js +19 -1
  137. package/dist/mux/pty-bridge.js.map +1 -1
  138. package/dist/mux/session-scanner.d.ts +18 -0
  139. package/dist/mux/session-scanner.js +65 -0
  140. package/dist/mux/session-scanner.js.map +1 -0
  141. package/dist/mux/types.d.ts +36 -1
  142. package/dist/mux/types.js +14 -1
  143. package/dist/mux/types.js.map +1 -1
  144. package/dist/persona/persona-command.js +130 -93
  145. package/dist/persona/persona-command.js.map +1 -1
  146. package/dist/persona/persona-prompt.d.ts +7 -7
  147. package/dist/persona/persona-prompt.js +14 -29
  148. package/dist/persona/persona-prompt.js.map +1 -1
  149. package/dist/persona/resolve.js +5 -2
  150. package/dist/persona/resolve.js.map +1 -1
  151. package/dist/pipeline/annotate.js +1 -7
  152. package/dist/pipeline/annotate.js.map +1 -1
  153. package/dist/pipeline/compile.d.ts +5 -2
  154. package/dist/pipeline/compile.js +23 -11
  155. package/dist/pipeline/compile.js.map +1 -1
  156. package/dist/pipeline/constitution-compiler.d.ts +302 -3
  157. package/dist/pipeline/constitution-compiler.js +285 -23
  158. package/dist/pipeline/constitution-compiler.js.map +1 -1
  159. package/dist/pipeline/generate-with-repair.d.ts +2 -2
  160. package/dist/pipeline/generate-with-repair.js +2 -2
  161. package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
  162. package/dist/pipeline/handwritten-scenarios.js +20 -41
  163. package/dist/pipeline/handwritten-scenarios.js.map +1 -1
  164. package/dist/pipeline/list-resolver.d.ts +18 -20
  165. package/dist/pipeline/list-resolver.js +28 -36
  166. package/dist/pipeline/list-resolver.js.map +1 -1
  167. package/dist/pipeline/pipeline-runner.d.ts +102 -13
  168. package/dist/pipeline/pipeline-runner.js +497 -237
  169. package/dist/pipeline/pipeline-runner.js.map +1 -1
  170. package/dist/pipeline/pipeline-shared.d.ts +7 -9
  171. package/dist/pipeline/pipeline-shared.js +8 -18
  172. package/dist/pipeline/pipeline-shared.js.map +1 -1
  173. package/dist/pipeline/policy-verifier.d.ts +32 -5
  174. package/dist/pipeline/policy-verifier.js +137 -8
  175. package/dist/pipeline/policy-verifier.js.map +1 -1
  176. package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
  177. package/dist/pipeline/proxy-mcp-connections.js +121 -0
  178. package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
  179. package/dist/pipeline/refresh-lists.js +21 -13
  180. package/dist/pipeline/refresh-lists.js.map +1 -1
  181. package/dist/pipeline/scenario-generator.d.ts +29 -49
  182. package/dist/pipeline/scenario-generator.js +180 -143
  183. package/dist/pipeline/scenario-generator.js.map +1 -1
  184. package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
  185. package/dist/pipeline/scenario-schema-validator.js +132 -0
  186. package/dist/pipeline/scenario-schema-validator.js.map +1 -0
  187. package/dist/pipeline/tool-annotator.d.ts +9 -8
  188. package/dist/pipeline/tool-annotator.js +48 -95
  189. package/dist/pipeline/tool-annotator.js.map +1 -1
  190. package/dist/pipeline/types.d.ts +31 -24
  191. package/dist/sandbox/index.d.ts +26 -10
  192. package/dist/sandbox/index.js +151 -40
  193. package/dist/sandbox/index.js.map +1 -1
  194. package/dist/servers/fetch-server.js +10 -5
  195. package/dist/servers/fetch-server.js.map +1 -1
  196. package/dist/session/agent-session.d.ts +8 -20
  197. package/dist/session/agent-session.js +22 -70
  198. package/dist/session/agent-session.js.map +1 -1
  199. package/dist/session/base-transport.d.ts +17 -1
  200. package/dist/session/base-transport.js +25 -1
  201. package/dist/session/base-transport.js.map +1 -1
  202. package/dist/session/cli-transport.d.ts +5 -0
  203. package/dist/session/cli-transport.js +48 -25
  204. package/dist/session/cli-transport.js.map +1 -1
  205. package/dist/session/index.d.ts +25 -1
  206. package/dist/session/index.js +57 -8
  207. package/dist/session/index.js.map +1 -1
  208. package/dist/session/prompts.js +8 -9
  209. package/dist/session/prompts.js.map +1 -1
  210. package/dist/session/session-manager.d.ts +6 -0
  211. package/dist/session/session-manager.js +12 -0
  212. package/dist/session/session-manager.js.map +1 -1
  213. package/dist/session/types.d.ts +11 -1
  214. package/dist/session/types.js.map +1 -1
  215. package/dist/session/workspace-validation.js +37 -6
  216. package/dist/session/workspace-validation.js.map +1 -1
  217. package/dist/signal/signal-bot-daemon.js +24 -8
  218. package/dist/signal/signal-bot-daemon.js.map +1 -1
  219. package/dist/signal/signal-transport.d.ts +5 -2
  220. package/dist/signal/signal-transport.js +3 -3
  221. package/dist/signal/signal-transport.js.map +1 -1
  222. package/dist/trusted-process/approval-whitelist.d.ts +117 -0
  223. package/dist/trusted-process/approval-whitelist.js +237 -0
  224. package/dist/trusted-process/approval-whitelist.js.map +1 -0
  225. package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
  226. package/dist/trusted-process/gworkspace-credentials.js +45 -0
  227. package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
  228. package/dist/trusted-process/index.d.ts +11 -1
  229. package/dist/trusted-process/index.js +83 -53
  230. package/dist/trusted-process/index.js.map +1 -1
  231. package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
  232. package/dist/trusted-process/mcp-proxy-server.js +409 -94
  233. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  234. package/dist/trusted-process/policy-engine.d.ts +12 -3
  235. package/dist/trusted-process/policy-engine.js +33 -23
  236. package/dist/trusted-process/policy-engine.js.map +1 -1
  237. package/dist/trusted-process/policy-types.d.ts +17 -0
  238. package/dist/trusted-process/sandbox-integration.d.ts +23 -0
  239. package/dist/trusted-process/sandbox-integration.js +115 -3
  240. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  241. package/dist/trusted-process/token-file-refresher.d.ts +69 -0
  242. package/dist/trusted-process/token-file-refresher.js +120 -0
  243. package/dist/trusted-process/token-file-refresher.js.map +1 -0
  244. package/dist/types/argument-roles.d.ts +7 -1
  245. package/dist/types/argument-roles.js +50 -0
  246. package/dist/types/argument-roles.js.map +1 -1
  247. package/dist/types/audit.d.ts +11 -0
  248. package/docker/Dockerfile.base +22 -0
  249. package/docker/Dockerfile.base.arm64 +44 -4
  250. package/docker/Dockerfile.claude-code +2 -2
  251. package/docker/entrypoint-claude-code.sh +24 -6
  252. package/package.json +8 -2
  253. package/src/config/constitution-user-base.md +15 -0
  254. package/src/config/generated/test-scenarios.json +1 -1
  255. package/src/config/generated/tool-annotations.json +10617 -296
  256. package/src/config/mcp-servers.json +31 -2
  257. package/dist/pipeline/mcp-connections.d.ts +0 -16
  258. package/dist/pipeline/mcp-connections.js +0 -61
  259. package/dist/pipeline/mcp-connections.js.map +0 -1
@@ -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
- if (mode === 'picker')
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 === 'picker')
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 /approve or /deny`);
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 menuHeight = 4;
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
- clearLine(y0 + 3);
469
- moveTo(2, y0 + 3);
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 === 'picker') {
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