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