erosolar-cli 1.7.261 → 1.7.262

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 (247) hide show
  1. package/README.md +22 -148
  2. package/dist/core/customCommands.d.ts +1 -0
  3. package/dist/core/customCommands.d.ts.map +1 -1
  4. package/dist/core/customCommands.js +3 -0
  5. package/dist/core/customCommands.js.map +1 -1
  6. package/dist/core/hooks.d.ts +113 -0
  7. package/dist/core/hooks.d.ts.map +1 -0
  8. package/dist/core/hooks.js +267 -0
  9. package/dist/core/hooks.js.map +1 -0
  10. package/dist/core/metricsTracker.d.ts +122 -0
  11. package/dist/core/metricsTracker.d.ts.map +1 -0
  12. package/dist/{alpha-zero → core}/metricsTracker.js +2 -5
  13. package/dist/core/metricsTracker.js.map +1 -0
  14. package/dist/core/toolPreconditions.d.ts.map +1 -1
  15. package/dist/core/toolPreconditions.js +0 -14
  16. package/dist/core/toolPreconditions.js.map +1 -1
  17. package/dist/core/toolRuntime.d.ts.map +1 -1
  18. package/dist/core/toolRuntime.js +0 -5
  19. package/dist/core/toolRuntime.js.map +1 -1
  20. package/dist/core/toolValidation.d.ts.map +1 -1
  21. package/dist/core/toolValidation.js +14 -3
  22. package/dist/core/toolValidation.js.map +1 -1
  23. package/dist/core/validationRunner.d.ts +1 -3
  24. package/dist/core/validationRunner.d.ts.map +1 -1
  25. package/dist/core/validationRunner.js.map +1 -1
  26. package/dist/mcp/sseClient.d.ts.map +1 -1
  27. package/dist/mcp/sseClient.js +9 -18
  28. package/dist/mcp/sseClient.js.map +1 -1
  29. package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
  30. package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
  31. package/dist/plugins/tools/build/buildPlugin.js +4 -10
  32. package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
  33. package/dist/shell/interactiveShell.d.ts +10 -2
  34. package/dist/shell/interactiveShell.d.ts.map +1 -1
  35. package/dist/shell/interactiveShell.js +182 -36
  36. package/dist/shell/interactiveShell.js.map +1 -1
  37. package/dist/shell/terminalInput.d.ts +68 -140
  38. package/dist/shell/terminalInput.d.ts.map +1 -1
  39. package/dist/shell/terminalInput.js +448 -667
  40. package/dist/shell/terminalInput.js.map +1 -1
  41. package/dist/shell/terminalInputAdapter.d.ts +20 -15
  42. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  43. package/dist/shell/terminalInputAdapter.js +14 -22
  44. package/dist/shell/terminalInputAdapter.js.map +1 -1
  45. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  46. package/dist/ui/ShellUIAdapter.js +13 -12
  47. package/dist/ui/ShellUIAdapter.js.map +1 -1
  48. package/dist/ui/display.d.ts +19 -0
  49. package/dist/ui/display.d.ts.map +1 -1
  50. package/dist/ui/display.js +131 -33
  51. package/dist/ui/display.js.map +1 -1
  52. package/dist/ui/theme.d.ts.map +1 -1
  53. package/dist/ui/theme.js +6 -8
  54. package/dist/ui/theme.js.map +1 -1
  55. package/dist/ui/toolDisplay.d.ts +0 -158
  56. package/dist/ui/toolDisplay.d.ts.map +1 -1
  57. package/dist/ui/toolDisplay.js +0 -348
  58. package/dist/ui/toolDisplay.js.map +1 -1
  59. package/dist/ui/unified/layout.d.ts +1 -0
  60. package/dist/ui/unified/layout.d.ts.map +1 -1
  61. package/dist/ui/unified/layout.js +15 -25
  62. package/dist/ui/unified/layout.js.map +1 -1
  63. package/package.json +1 -1
  64. package/dist/alpha-zero/agentWrapper.d.ts +0 -84
  65. package/dist/alpha-zero/agentWrapper.d.ts.map +0 -1
  66. package/dist/alpha-zero/agentWrapper.js +0 -171
  67. package/dist/alpha-zero/agentWrapper.js.map +0 -1
  68. package/dist/alpha-zero/codeEvaluator.d.ts +0 -25
  69. package/dist/alpha-zero/codeEvaluator.d.ts.map +0 -1
  70. package/dist/alpha-zero/codeEvaluator.js +0 -273
  71. package/dist/alpha-zero/codeEvaluator.js.map +0 -1
  72. package/dist/alpha-zero/competitiveRunner.d.ts +0 -66
  73. package/dist/alpha-zero/competitiveRunner.d.ts.map +0 -1
  74. package/dist/alpha-zero/competitiveRunner.js +0 -224
  75. package/dist/alpha-zero/competitiveRunner.js.map +0 -1
  76. package/dist/alpha-zero/index.d.ts +0 -67
  77. package/dist/alpha-zero/index.d.ts.map +0 -1
  78. package/dist/alpha-zero/index.js +0 -99
  79. package/dist/alpha-zero/index.js.map +0 -1
  80. package/dist/alpha-zero/introspection.d.ts +0 -128
  81. package/dist/alpha-zero/introspection.d.ts.map +0 -1
  82. package/dist/alpha-zero/introspection.js +0 -300
  83. package/dist/alpha-zero/introspection.js.map +0 -1
  84. package/dist/alpha-zero/metricsTracker.d.ts +0 -71
  85. package/dist/alpha-zero/metricsTracker.d.ts.map +0 -1
  86. package/dist/alpha-zero/metricsTracker.js.map +0 -1
  87. package/dist/alpha-zero/security/core.d.ts +0 -125
  88. package/dist/alpha-zero/security/core.d.ts.map +0 -1
  89. package/dist/alpha-zero/security/core.js +0 -271
  90. package/dist/alpha-zero/security/core.js.map +0 -1
  91. package/dist/alpha-zero/security/google.d.ts +0 -125
  92. package/dist/alpha-zero/security/google.d.ts.map +0 -1
  93. package/dist/alpha-zero/security/google.js +0 -311
  94. package/dist/alpha-zero/security/google.js.map +0 -1
  95. package/dist/alpha-zero/security/googleLoader.d.ts +0 -17
  96. package/dist/alpha-zero/security/googleLoader.d.ts.map +0 -1
  97. package/dist/alpha-zero/security/googleLoader.js +0 -41
  98. package/dist/alpha-zero/security/googleLoader.js.map +0 -1
  99. package/dist/alpha-zero/security/index.d.ts +0 -29
  100. package/dist/alpha-zero/security/index.d.ts.map +0 -1
  101. package/dist/alpha-zero/security/index.js +0 -32
  102. package/dist/alpha-zero/security/index.js.map +0 -1
  103. package/dist/alpha-zero/security/simulation.d.ts +0 -124
  104. package/dist/alpha-zero/security/simulation.d.ts.map +0 -1
  105. package/dist/alpha-zero/security/simulation.js +0 -277
  106. package/dist/alpha-zero/security/simulation.js.map +0 -1
  107. package/dist/alpha-zero/selfModification.d.ts +0 -109
  108. package/dist/alpha-zero/selfModification.d.ts.map +0 -1
  109. package/dist/alpha-zero/selfModification.js +0 -233
  110. package/dist/alpha-zero/selfModification.js.map +0 -1
  111. package/dist/alpha-zero/types.d.ts +0 -170
  112. package/dist/alpha-zero/types.d.ts.map +0 -1
  113. package/dist/alpha-zero/types.js +0 -31
  114. package/dist/alpha-zero/types.js.map +0 -1
  115. package/dist/core/aiFlowOptimizer.d.ts +0 -26
  116. package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
  117. package/dist/core/aiFlowOptimizer.js +0 -31
  118. package/dist/core/aiFlowOptimizer.js.map +0 -1
  119. package/dist/core/aiOptimizationEngine.d.ts +0 -158
  120. package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
  121. package/dist/core/aiOptimizationEngine.js +0 -428
  122. package/dist/core/aiOptimizationEngine.js.map +0 -1
  123. package/dist/core/aiOptimizationIntegration.d.ts +0 -93
  124. package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
  125. package/dist/core/aiOptimizationIntegration.js +0 -250
  126. package/dist/core/aiOptimizationIntegration.js.map +0 -1
  127. package/dist/core/enhancedErrorRecovery.d.ts +0 -100
  128. package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
  129. package/dist/core/enhancedErrorRecovery.js +0 -345
  130. package/dist/core/enhancedErrorRecovery.js.map +0 -1
  131. package/dist/core/unified/errors.d.ts +0 -189
  132. package/dist/core/unified/errors.d.ts.map +0 -1
  133. package/dist/core/unified/errors.js +0 -497
  134. package/dist/core/unified/errors.js.map +0 -1
  135. package/dist/core/unified/index.d.ts +0 -19
  136. package/dist/core/unified/index.d.ts.map +0 -1
  137. package/dist/core/unified/index.js +0 -68
  138. package/dist/core/unified/index.js.map +0 -1
  139. package/dist/core/unified/schema.d.ts +0 -101
  140. package/dist/core/unified/schema.d.ts.map +0 -1
  141. package/dist/core/unified/schema.js +0 -350
  142. package/dist/core/unified/schema.js.map +0 -1
  143. package/dist/core/unified/toolRuntime.d.ts +0 -179
  144. package/dist/core/unified/toolRuntime.d.ts.map +0 -1
  145. package/dist/core/unified/toolRuntime.js +0 -517
  146. package/dist/core/unified/toolRuntime.js.map +0 -1
  147. package/dist/core/unified/tools.d.ts +0 -127
  148. package/dist/core/unified/tools.d.ts.map +0 -1
  149. package/dist/core/unified/tools.js +0 -1333
  150. package/dist/core/unified/tools.js.map +0 -1
  151. package/dist/core/unified/types.d.ts +0 -352
  152. package/dist/core/unified/types.d.ts.map +0 -1
  153. package/dist/core/unified/types.js +0 -12
  154. package/dist/core/unified/types.js.map +0 -1
  155. package/dist/core/unified/version.d.ts +0 -209
  156. package/dist/core/unified/version.d.ts.map +0 -1
  157. package/dist/core/unified/version.js +0 -454
  158. package/dist/core/unified/version.js.map +0 -1
  159. package/dist/security/active-stack-security.d.ts +0 -112
  160. package/dist/security/active-stack-security.d.ts.map +0 -1
  161. package/dist/security/active-stack-security.js +0 -296
  162. package/dist/security/active-stack-security.js.map +0 -1
  163. package/dist/security/advanced-persistence-research.d.ts +0 -92
  164. package/dist/security/advanced-persistence-research.d.ts.map +0 -1
  165. package/dist/security/advanced-persistence-research.js +0 -195
  166. package/dist/security/advanced-persistence-research.js.map +0 -1
  167. package/dist/security/advanced-targeting.d.ts +0 -119
  168. package/dist/security/advanced-targeting.d.ts.map +0 -1
  169. package/dist/security/advanced-targeting.js +0 -233
  170. package/dist/security/advanced-targeting.js.map +0 -1
  171. package/dist/security/assessment/vulnerabilityAssessment.d.ts +0 -104
  172. package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +0 -1
  173. package/dist/security/assessment/vulnerabilityAssessment.js +0 -315
  174. package/dist/security/assessment/vulnerabilityAssessment.js.map +0 -1
  175. package/dist/security/authorization/securityAuthorization.d.ts +0 -88
  176. package/dist/security/authorization/securityAuthorization.d.ts.map +0 -1
  177. package/dist/security/authorization/securityAuthorization.js +0 -172
  178. package/dist/security/authorization/securityAuthorization.js.map +0 -1
  179. package/dist/security/comprehensive-targeting.d.ts +0 -85
  180. package/dist/security/comprehensive-targeting.d.ts.map +0 -1
  181. package/dist/security/comprehensive-targeting.js +0 -438
  182. package/dist/security/comprehensive-targeting.js.map +0 -1
  183. package/dist/security/global-security-integration.d.ts +0 -91
  184. package/dist/security/global-security-integration.d.ts.map +0 -1
  185. package/dist/security/global-security-integration.js +0 -218
  186. package/dist/security/global-security-integration.js.map +0 -1
  187. package/dist/security/index.d.ts +0 -38
  188. package/dist/security/index.d.ts.map +0 -1
  189. package/dist/security/index.js +0 -47
  190. package/dist/security/index.js.map +0 -1
  191. package/dist/security/persistence-analyzer.d.ts +0 -56
  192. package/dist/security/persistence-analyzer.d.ts.map +0 -1
  193. package/dist/security/persistence-analyzer.js +0 -187
  194. package/dist/security/persistence-analyzer.js.map +0 -1
  195. package/dist/security/persistence-cli.d.ts +0 -36
  196. package/dist/security/persistence-cli.d.ts.map +0 -1
  197. package/dist/security/persistence-cli.js +0 -160
  198. package/dist/security/persistence-cli.js.map +0 -1
  199. package/dist/security/persistence-research.d.ts +0 -92
  200. package/dist/security/persistence-research.d.ts.map +0 -1
  201. package/dist/security/persistence-research.js +0 -364
  202. package/dist/security/persistence-research.js.map +0 -1
  203. package/dist/security/research/persistenceResearch.d.ts +0 -97
  204. package/dist/security/research/persistenceResearch.d.ts.map +0 -1
  205. package/dist/security/research/persistenceResearch.js +0 -282
  206. package/dist/security/research/persistenceResearch.js.map +0 -1
  207. package/dist/security/security-integration.d.ts +0 -74
  208. package/dist/security/security-integration.d.ts.map +0 -1
  209. package/dist/security/security-integration.js +0 -137
  210. package/dist/security/security-integration.js.map +0 -1
  211. package/dist/security/security-testing-framework.d.ts +0 -112
  212. package/dist/security/security-testing-framework.d.ts.map +0 -1
  213. package/dist/security/security-testing-framework.js +0 -364
  214. package/dist/security/security-testing-framework.js.map +0 -1
  215. package/dist/security/simulation/attackSimulation.d.ts +0 -93
  216. package/dist/security/simulation/attackSimulation.d.ts.map +0 -1
  217. package/dist/security/simulation/attackSimulation.js +0 -341
  218. package/dist/security/simulation/attackSimulation.js.map +0 -1
  219. package/dist/security/strategic-operations.d.ts +0 -100
  220. package/dist/security/strategic-operations.d.ts.map +0 -1
  221. package/dist/security/strategic-operations.js +0 -276
  222. package/dist/security/strategic-operations.js.map +0 -1
  223. package/dist/security/tool-security-wrapper.d.ts +0 -58
  224. package/dist/security/tool-security-wrapper.d.ts.map +0 -1
  225. package/dist/security/tool-security-wrapper.js +0 -156
  226. package/dist/security/tool-security-wrapper.js.map +0 -1
  227. package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
  228. package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
  229. package/dist/shell/claudeCodeStreamHandler.js +0 -322
  230. package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
  231. package/dist/shell/inputQueueManager.d.ts +0 -144
  232. package/dist/shell/inputQueueManager.d.ts.map +0 -1
  233. package/dist/shell/inputQueueManager.js +0 -290
  234. package/dist/shell/inputQueueManager.js.map +0 -1
  235. package/dist/shell/streamingOutputManager.d.ts +0 -115
  236. package/dist/shell/streamingOutputManager.d.ts.map +0 -1
  237. package/dist/shell/streamingOutputManager.js +0 -225
  238. package/dist/shell/streamingOutputManager.js.map +0 -1
  239. package/dist/ui/persistentPrompt.d.ts +0 -50
  240. package/dist/ui/persistentPrompt.d.ts.map +0 -1
  241. package/dist/ui/persistentPrompt.js +0 -92
  242. package/dist/ui/persistentPrompt.js.map +0 -1
  243. package/dist/ui/terminalUISchema.d.ts +0 -195
  244. package/dist/ui/terminalUISchema.d.ts.map +0 -1
  245. package/dist/ui/terminalUISchema.js +0 -113
  246. package/dist/ui/terminalUISchema.js.map +0 -1
  247. package/scripts/deploy-security-capabilities.js +0 -178
@@ -3,16 +3,18 @@
3
3
  *
4
4
  * Design principles:
5
5
  * - Single source of truth for input state
6
+ * - One bottom-pinned chat box for the entire session (no inline anchors)
6
7
  * - Native bracketed paste support (no heuristics)
7
8
  * - Clean cursor model with render-time wrapping
8
9
  * - State machine for different input modes
9
10
  * - No readline dependency for display
10
11
  */
11
12
  import { EventEmitter } from 'node:events';
12
- import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
13
+ import { isMultilinePaste } from '../core/multilinePasteHandler.js';
13
14
  import { writeLock } from '../ui/writeLock.js';
14
- import { renderDivider } from '../ui/unified/layout.js';
15
- import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
15
+ import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
16
+ import { isStreamingMode } from '../ui/globalWriteLock.js';
17
+ import { formatThinking } from '../ui/toolDisplay.js';
16
18
  // ANSI escape codes
17
19
  const ESC = {
18
20
  // Cursor control
@@ -67,6 +69,11 @@ export class TerminalInput extends EventEmitter {
67
69
  statusMessage = null;
68
70
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
69
71
  streamingLabel = null; // Streaming progress indicator
72
+ metaElapsedSeconds = null; // Optional elapsed time for header line
73
+ metaTokensUsed = null; // Optional token usage
74
+ metaTokenLimit = null; // Optional token window
75
+ metaThinkingMs = null; // Optional thinking duration
76
+ metaThinkingHasContent = false; // Whether collapsed thinking content exists
70
77
  reservedLines = 2;
71
78
  scrollRegionActive = false;
72
79
  lastRenderContent = '';
@@ -74,45 +81,35 @@ export class TerminalInput extends EventEmitter {
74
81
  renderDirty = false;
75
82
  isRendering = false;
76
83
  pinnedTopRows = 0;
77
- inlineAnchorRow = null;
78
- inlineLayout = false;
79
- anchorProvider = null;
80
- // Flow mode: when true, renders inline after content (no absolute positioning)
81
- flowMode = true;
82
- flowModeRenderedLines = 0; // Track lines rendered for clearing
83
- // Command suggestions (Claude Code style auto-complete)
84
- commandSuggestions = [];
85
- filteredSuggestions = [];
86
- selectedSuggestionIndex = 0;
87
- showSuggestions = false;
88
- maxVisibleSuggestions = 10;
89
84
  // Lifecycle
90
85
  disposed = false;
91
86
  enabled = true;
92
87
  contextUsage = null;
88
+ contextAutoCompactThreshold = 90;
89
+ thinkingModeLabel = null;
93
90
  editMode = 'display-edits';
94
91
  verificationEnabled = true;
95
92
  autoContinueEnabled = false;
96
93
  verificationHotkey = 'alt+v';
97
94
  autoContinueHotkey = 'alt+c';
95
+ thinkingHotkey = '/thinking';
96
+ modelLabel = null;
97
+ providerLabel = null;
98
98
  // Output interceptor cleanup
99
99
  outputInterceptorCleanup;
100
- // Metrics tracking for status bar
101
- streamingStartTime = null;
102
- tokensUsed = 0;
103
- thinkingEnabled = true;
104
- // Streaming input area render timer (updates elapsed time display)
100
+ // Streaming render throttle
101
+ lastStreamingRender = 0;
102
+ streamingRenderInterval = 250; // ms between renders during streaming
105
103
  streamingRenderTimer = null;
106
104
  constructor(writeStream = process.stdout, config = {}) {
107
105
  super();
108
106
  this.out = writeStream;
109
- // Use schema defaults for configuration consistency
110
107
  this.config = {
111
- maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
112
- maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
108
+ maxLines: config.maxLines ?? 1000,
109
+ maxLength: config.maxLength ?? 10000,
113
110
  maxQueueSize: config.maxQueueSize ?? 100,
114
- promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
115
- continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
111
+ promptChar: config.promptChar ?? '> ',
112
+ continuationChar: config.continuationChar ?? '│ ',
116
113
  };
117
114
  }
118
115
  // ===========================================================================
@@ -191,11 +188,6 @@ export class TerminalInput extends EventEmitter {
191
188
  if (handled)
192
189
  return;
193
190
  }
194
- // Handle '?' for help hint (if buffer is empty)
195
- if (str === '?' && this.buffer.length === 0) {
196
- this.emit('showHelp');
197
- return;
198
- }
199
191
  // Insert printable characters
200
192
  if (str && !key?.ctrl && !key?.meta) {
201
193
  this.insertText(str);
@@ -204,343 +196,38 @@ export class TerminalInput extends EventEmitter {
204
196
  /**
205
197
  * Set the input mode
206
198
  *
207
- * Streaming mode disables scroll region and lets content flow naturally.
208
- * The input area will be re-rendered after streaming ends at wherever
209
- * the cursor is (below the streamed content).
199
+ * Streaming keeps the scroll region active so the prompt/status stay pinned
200
+ * below the streaming output. When streaming ends, we refresh the input area.
210
201
  */
211
202
  setMode(mode) {
212
203
  const prevMode = this.mode;
213
204
  this.mode = mode;
214
205
  if (mode === 'streaming' && prevMode !== 'streaming') {
215
- // Track streaming start time for elapsed display
216
- this.streamingStartTime = Date.now();
217
- // NO scroll regions - content flows naturally to terminal scrollback
218
- // Input area renders at absolute bottom using cursor save/restore
219
- this.pinnedTopRows = 0;
220
- this.reservedLines = 5; // Reserve space for input area at bottom
221
- // Disable any existing scroll region
222
- this.disableScrollRegion();
223
- // Initial render of input area at bottom
224
- this.renderStreamingInputArea();
225
- // Start timer to update streaming status and re-render input area
226
- this.streamingRenderTimer = setInterval(() => {
227
- if (this.mode === 'streaming') {
228
- this.updateStreamingStatus();
229
- this.renderStreamingInputArea();
230
- }
231
- }, 1000);
206
+ // Keep scroll region active so status/prompt stay pinned while streaming
207
+ this.resetStreamingRenderThrottle();
208
+ this.enableScrollRegion();
232
209
  this.renderDirty = true;
210
+ this.render();
233
211
  }
234
212
  else if (mode !== 'streaming' && prevMode === 'streaming') {
235
- // Stop streaming render timer
236
- if (this.streamingRenderTimer) {
237
- clearInterval(this.streamingRenderTimer);
238
- this.streamingRenderTimer = null;
239
- }
240
- // Reset streaming time
241
- this.streamingStartTime = null;
242
- this.pinnedTopRows = 0;
243
- // Ensure no scroll region is active
244
- this.disableScrollRegion();
245
- // Reset flow mode tracking
246
- this.flowModeRenderedLines = 0;
247
- // Render input area using unified method (same as streaming, but normal mode)
248
- this.renderPinnedInputArea();
213
+ // Streaming ended - render the input area
214
+ this.resetStreamingRenderThrottle();
215
+ this.enableScrollRegion();
216
+ this.forceRender();
249
217
  }
250
218
  }
251
- /**
252
- * Update streaming status label (called by timer)
253
- */
254
- updateStreamingStatus() {
255
- if (this.mode !== 'streaming' || !this.streamingStartTime)
256
- return;
257
- // Calculate elapsed time
258
- const elapsed = Date.now() - this.streamingStartTime;
259
- const seconds = Math.floor(elapsed / 1000);
260
- const minutes = Math.floor(seconds / 60);
261
- const secs = seconds % 60;
262
- // Format elapsed time
263
- let elapsedStr;
264
- if (minutes > 0) {
265
- elapsedStr = `${minutes}m ${secs}s`;
266
- }
267
- else {
268
- elapsedStr = `${secs}s`;
269
- }
270
- // Update streaming label
271
- this.streamingLabel = `Streaming ${elapsedStr}`;
272
- }
273
- /**
274
- * Render input area at absolute bottom - unified for streaming and normal modes.
275
- * Supports multi-line input, background styling, and cursor positioning.
276
- * Uses cursor save/restore during streaming so content flow is not disrupted.
277
- */
278
- renderPinnedInputArea() {
279
- const { rows, cols } = this.getSize();
280
- const maxWidth = Math.max(8, cols - 4);
281
- const divider = renderDivider(cols - 2);
282
- const isStreaming = this.mode === 'streaming';
283
- // Wrap buffer into display lines (multi-line support)
284
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
285
- const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
286
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
287
- const displayLines = Math.min(lines.length, maxVisible);
288
- // Calculate display window (keep cursor visible)
289
- let startLine = 0;
290
- if (lines.length > displayLines) {
291
- startLine = Math.max(0, cursorLine - displayLines + 1);
292
- startLine = Math.min(startLine, lines.length - displayLines);
293
- }
294
- const visibleLines = lines.slice(startLine, startLine + displayLines);
295
- const adjustedCursorLine = cursorLine - startLine;
296
- // Calculate total height: status + topDiv + input lines + bottomDiv + controls
297
- const totalHeight = 4 + visibleLines.length;
298
- // Save cursor position during streaming (so content flow resumes correctly)
299
- if (isStreaming) {
300
- this.write(ESC.SAVE);
301
- }
302
- this.write(ESC.HIDE);
303
- this.write(ESC.RESET);
304
- // Position from absolute bottom
305
- let currentRow = Math.max(1, rows - totalHeight + 1);
306
- let finalRow = currentRow;
307
- let finalCol = 3;
308
- // Status bar
309
- this.write(ESC.TO(currentRow, 1));
310
- this.write(ESC.CLEAR_LINE);
311
- this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
312
- currentRow++;
313
- // Top divider
314
- this.write(ESC.TO(currentRow, 1));
315
- this.write(ESC.CLEAR_LINE);
316
- this.write(divider);
317
- currentRow++;
318
- // Input lines with background styling
319
- for (let i = 0; i < visibleLines.length; i++) {
320
- this.write(ESC.TO(currentRow, 1));
321
- this.write(ESC.CLEAR_LINE);
322
- const line = visibleLines[i] ?? '';
323
- const absoluteLineIdx = startLine + i;
324
- const isFirstLine = absoluteLineIdx === 0;
325
- const isCursorLine = i === adjustedCursorLine;
326
- // Background
327
- this.write(ESC.BG_DARK);
328
- // Prompt prefix
329
- this.write(ESC.DIM);
330
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
331
- this.write(ESC.RESET);
332
- this.write(ESC.BG_DARK);
333
- if (isCursorLine) {
334
- const col = Math.min(cursorCol, line.length);
335
- const before = line.slice(0, col);
336
- const at = col < line.length ? line[col] : ' ';
337
- const after = col < line.length ? line.slice(col + 1) : '';
338
- this.write(before);
339
- this.write(ESC.REVERSE + ESC.BOLD);
340
- this.write(at);
341
- this.write(ESC.RESET + ESC.BG_DARK);
342
- this.write(after);
343
- finalRow = currentRow;
344
- finalCol = this.config.promptChar.length + col + 1;
345
- }
346
- else {
347
- this.write(line);
348
- }
349
- // Pad to edge
350
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
351
- const padding = Math.max(0, cols - lineLen - 1);
352
- if (padding > 0)
353
- this.write(' '.repeat(padding));
354
- this.write(ESC.RESET);
355
- currentRow++;
356
- }
357
- // Bottom divider
358
- this.write(ESC.TO(currentRow, 1));
359
- this.write(ESC.CLEAR_LINE);
360
- this.write(divider);
361
- currentRow++;
362
- // Mode controls line
363
- this.write(ESC.TO(currentRow, 1));
364
- this.write(ESC.CLEAR_LINE);
365
- this.write(this.buildModeControls(cols));
366
- // Restore cursor position during streaming, or show cursor in normal mode
367
- if (isStreaming) {
368
- this.write(ESC.RESTORE);
369
- }
370
- else {
371
- // Position cursor in input area
372
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
373
- this.write(ESC.SHOW);
374
- }
375
- // Update reserved lines for scroll region calculations
376
- this.updateReservedLines(totalHeight);
377
- }
378
- /**
379
- * Render input area during streaming (alias for unified method)
380
- */
381
- renderStreamingInputArea() {
382
- this.renderPinnedInputArea();
383
- }
384
- /**
385
- * Enable or disable flow mode.
386
- * In flow mode, the input renders immediately after content (wherever cursor is).
387
- * When disabled, input renders at the absolute bottom of terminal.
388
- */
389
- setFlowMode(enabled) {
390
- if (this.flowMode === enabled)
391
- return;
392
- this.flowMode = enabled;
393
- this.renderDirty = true;
394
- this.scheduleRender();
395
- }
396
- /**
397
- * Check if flow mode is enabled.
398
- */
399
- isFlowMode() {
400
- return this.flowMode;
401
- }
402
- /**
403
- * Set available slash commands for auto-complete suggestions.
404
- */
405
- setCommands(commands) {
406
- this.commandSuggestions = commands;
407
- this.updateSuggestions();
408
- }
409
- /**
410
- * Update filtered suggestions based on current input.
411
- */
412
- updateSuggestions() {
413
- const input = this.buffer.trim();
414
- // Only show suggestions when input starts with "/"
415
- if (!input.startsWith('/')) {
416
- this.showSuggestions = false;
417
- this.filteredSuggestions = [];
418
- this.selectedSuggestionIndex = 0;
419
- return;
420
- }
421
- const query = input.toLowerCase();
422
- this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
423
- cmd.command.toLowerCase().includes(query.slice(1)));
424
- // Show suggestions if we have matches
425
- this.showSuggestions = this.filteredSuggestions.length > 0;
426
- // Keep selection in bounds
427
- if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
428
- this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
429
- }
430
- }
431
- /**
432
- * Select next suggestion (arrow down / tab).
433
- */
434
- selectNextSuggestion() {
435
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
436
- return;
437
- this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
438
- this.renderDirty = true;
439
- this.scheduleRender();
440
- }
441
- /**
442
- * Select previous suggestion (arrow up / shift+tab).
443
- */
444
- selectPrevSuggestion() {
445
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
446
- return;
447
- this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
448
- ? this.filteredSuggestions.length - 1
449
- : this.selectedSuggestionIndex - 1;
450
- this.renderDirty = true;
451
- this.scheduleRender();
452
- }
453
- /**
454
- * Accept current suggestion and insert into buffer.
455
- */
456
- acceptSuggestion() {
457
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
458
- return false;
459
- const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
460
- if (!selected)
461
- return false;
462
- // Replace buffer with selected command
463
- this.buffer = selected.command + ' ';
464
- this.cursor = this.buffer.length;
465
- this.showSuggestions = false;
466
- this.renderDirty = true;
467
- this.scheduleRender();
468
- return true;
469
- }
470
- /**
471
- * Check if suggestions are visible.
472
- */
473
- areSuggestionsVisible() {
474
- return this.showSuggestions && this.filteredSuggestions.length > 0;
475
- }
476
- /**
477
- * Update token count for metrics display
478
- */
479
- setTokensUsed(tokens) {
480
- this.tokensUsed = tokens;
481
- }
482
- /**
483
- * Toggle thinking/reasoning mode
484
- */
485
- toggleThinking() {
486
- this.thinkingEnabled = !this.thinkingEnabled;
487
- this.emit('thinkingToggle', this.thinkingEnabled);
488
- this.scheduleRender();
489
- }
490
- /**
491
- * Get thinking enabled state
492
- */
493
- isThinkingEnabled() {
494
- return this.thinkingEnabled;
495
- }
496
219
  /**
497
220
  * Keep the top N rows pinned outside the scroll region (used for the launch banner).
498
221
  */
499
222
  setPinnedHeaderLines(count) {
500
- // Set pinned header rows (banner area that scroll region excludes)
501
- if (this.pinnedTopRows !== count) {
502
- this.pinnedTopRows = count;
223
+ // No pinned header rows anymore; keep everything in the scroll region.
224
+ if (this.pinnedTopRows !== 0) {
225
+ this.pinnedTopRows = 0;
503
226
  if (this.scrollRegionActive) {
504
227
  this.applyScrollRegion();
505
228
  }
506
229
  }
507
230
  }
508
- /**
509
- * Anchor prompt rendering near a specific row (inline layout). Pass null to
510
- * restore the default bottom-aligned layout.
511
- */
512
- setInlineAnchor(row) {
513
- if (row === null || row === undefined) {
514
- this.inlineAnchorRow = null;
515
- this.inlineLayout = false;
516
- this.renderDirty = true;
517
- this.render();
518
- return;
519
- }
520
- const { rows } = this.getSize();
521
- const clamped = Math.max(1, Math.min(Math.floor(row), rows));
522
- this.inlineAnchorRow = clamped;
523
- this.inlineLayout = true;
524
- this.renderDirty = true;
525
- this.render();
526
- }
527
- /**
528
- * Provide a dynamic anchor callback. When set, the prompt will follow the
529
- * output by re-evaluating the anchor before each render.
530
- */
531
- setInlineAnchorProvider(provider) {
532
- this.anchorProvider = provider;
533
- if (!provider) {
534
- this.inlineLayout = false;
535
- this.inlineAnchorRow = null;
536
- this.renderDirty = true;
537
- this.render();
538
- return;
539
- }
540
- this.inlineLayout = true;
541
- this.renderDirty = true;
542
- this.render();
543
- }
544
231
  /**
545
232
  * Get current mode
546
233
  */
@@ -650,6 +337,37 @@ export class TerminalInput extends EventEmitter {
650
337
  this.streamingLabel = next;
651
338
  this.scheduleRender();
652
339
  }
340
+ /**
341
+ * Surface meta status just above the divider (e.g., elapsed time or token usage).
342
+ */
343
+ setMetaStatus(meta) {
344
+ const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
345
+ ? Math.floor(meta.elapsedSeconds)
346
+ : null;
347
+ const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
348
+ ? Math.floor(meta.tokensUsed)
349
+ : null;
350
+ const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
351
+ ? Math.floor(meta.tokenLimit)
352
+ : null;
353
+ const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
354
+ ? Math.floor(meta.thinkingMs)
355
+ : null;
356
+ const nextThinkingHasContent = !!meta.thinkingHasContent;
357
+ if (this.metaElapsedSeconds === nextElapsed &&
358
+ this.metaTokensUsed === nextTokens &&
359
+ this.metaTokenLimit === nextLimit &&
360
+ this.metaThinkingMs === nextThinking &&
361
+ this.metaThinkingHasContent === nextThinkingHasContent) {
362
+ return;
363
+ }
364
+ this.metaElapsedSeconds = nextElapsed;
365
+ this.metaTokensUsed = nextTokens;
366
+ this.metaTokenLimit = nextLimit;
367
+ this.metaThinkingMs = nextThinking;
368
+ this.metaThinkingHasContent = nextThinkingHasContent;
369
+ this.scheduleRender();
370
+ }
653
371
  /**
654
372
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
655
373
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -659,16 +377,22 @@ export class TerminalInput extends EventEmitter {
659
377
  const nextAutoContinue = !!options.autoContinueEnabled;
660
378
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
661
379
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
380
+ const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
381
+ const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
662
382
  if (this.verificationEnabled === nextVerification &&
663
383
  this.autoContinueEnabled === nextAutoContinue &&
664
384
  this.verificationHotkey === nextVerifyHotkey &&
665
- this.autoContinueHotkey === nextAutoHotkey) {
385
+ this.autoContinueHotkey === nextAutoHotkey &&
386
+ this.thinkingHotkey === nextThinkingHotkey &&
387
+ this.thinkingModeLabel === nextThinkingLabel) {
666
388
  return;
667
389
  }
668
390
  this.verificationEnabled = nextVerification;
669
391
  this.autoContinueEnabled = nextAutoContinue;
670
392
  this.verificationHotkey = nextVerifyHotkey;
671
393
  this.autoContinueHotkey = nextAutoHotkey;
394
+ this.thinkingHotkey = nextThinkingHotkey;
395
+ this.thinkingModeLabel = nextThinkingLabel;
672
396
  this.scheduleRender();
673
397
  }
674
398
  /**
@@ -680,298 +404,386 @@ export class TerminalInput extends EventEmitter {
680
404
  this.streamingLabel = null;
681
405
  this.scheduleRender();
682
406
  }
407
+ /**
408
+ * Surface model/provider context in the controls bar.
409
+ */
410
+ setModelContext(options) {
411
+ const nextModel = options.model?.trim() || null;
412
+ const nextProvider = options.provider?.trim() || null;
413
+ if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
414
+ return;
415
+ }
416
+ this.modelLabel = nextModel;
417
+ this.providerLabel = nextProvider;
418
+ this.scheduleRender();
419
+ }
683
420
  /**
684
421
  * Render the input area - Claude Code style with mode controls
685
422
  *
686
- * Same rendering for both normal and streaming modes - just different status bar.
687
- * During streaming, uses cursor save/restore to preserve streaming position.
423
+ * During streaming we keep the scroll region active and repaint only the
424
+ * pinned status/input block (throttled) so streamed content can scroll
425
+ * naturally above while elapsed time and status stay fresh.
688
426
  */
689
427
  render() {
690
428
  if (!this.canRender())
691
429
  return;
692
430
  if (this.isRendering)
693
431
  return;
432
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
433
+ // During streaming we still render the pinned input/status region, but throttle
434
+ // to avoid fighting with the streamed content flow.
435
+ if (streamingActive && this.lastStreamingRender > 0) {
436
+ const elapsed = Date.now() - this.lastStreamingRender;
437
+ const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
438
+ if (waitMs > 0) {
439
+ this.renderDirty = true;
440
+ this.scheduleStreamingRender(waitMs);
441
+ return;
442
+ }
443
+ }
694
444
  const shouldSkip = !this.renderDirty &&
695
445
  this.buffer === this.lastRenderContent &&
696
446
  this.cursor === this.lastRenderCursor;
697
447
  this.renderDirty = false;
698
- // Skip if nothing changed (unless explicitly forced)
448
+ // Skip if nothing changed and no explicit refresh requested
699
449
  if (shouldSkip) {
700
450
  return;
701
451
  }
702
- // If write lock is held, defer render
452
+ // If write lock is held, defer render to avoid race conditions
703
453
  if (writeLock.isLocked()) {
704
454
  writeLock.safeWrite(() => this.render());
705
455
  return;
706
456
  }
707
- this.isRendering = true;
708
- writeLock.lock('terminalInput.render');
709
- try {
710
- // Render input area at bottom (outside scroll region)
711
- this.renderBottomPinned();
712
- }
713
- finally {
714
- writeLock.unlock();
715
- this.isRendering = false;
716
- }
717
- }
718
- /**
719
- * Render in flow mode - delegates to bottom-pinned for stability.
720
- *
721
- * Flow mode attempted inline rendering but caused duplicate renders
722
- * due to unreliable cursor position tracking. Bottom-pinned is reliable.
723
- */
724
- renderFlowMode() {
725
- // Use stable bottom-pinned approach
726
- this.renderBottomPinned();
727
- }
728
- /**
729
- * Render in bottom-pinned mode - Claude Code style with suggestions
730
- *
731
- * Works for both normal and streaming modes:
732
- * - During streaming: saves/restores cursor position
733
- * - Status bar shows streaming info or "Type a message"
734
- *
735
- * Layout when suggestions visible:
736
- * - Top divider
737
- * - Input line(s)
738
- * - Bottom divider
739
- * - Suggestions (command list)
740
- *
741
- * Layout when suggestions hidden:
742
- * - Status bar (Ready/Streaming)
743
- * - Top divider
744
- * - Input line(s)
745
- * - Bottom divider
746
- * - Mode controls
747
- */
748
- renderBottomPinned() {
749
- const { rows, cols } = this.getSize();
750
- const maxWidth = Math.max(8, cols - 4);
751
- const isStreaming = this.mode === 'streaming';
752
- // Use unified pinned input area (works for both streaming and normal)
753
- // Only use complex rendering when suggestions are visible
754
- const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
755
- if (!hasSuggestions) {
756
- this.renderPinnedInputArea();
757
- return;
758
- }
759
- // Wrap buffer into display lines
760
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
761
- const availableForContent = Math.max(1, rows - 3);
762
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
763
- const displayLines = Math.min(lines.length, maxVisible);
764
- // Calculate display window (keep cursor visible)
765
- let startLine = 0;
766
- if (lines.length > displayLines) {
767
- startLine = Math.max(0, cursorLine - displayLines + 1);
768
- startLine = Math.min(startLine, lines.length - displayLines);
769
- }
770
- const visibleLines = lines.slice(startLine, startLine + displayLines);
771
- const adjustedCursorLine = cursorLine - startLine;
772
- // Calculate suggestion display (not during streaming)
773
- const suggestionsToShow = (!isStreaming && this.showSuggestions)
774
- ? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
775
- : [];
776
- const suggestionLines = suggestionsToShow.length;
777
- this.write(ESC.HIDE);
778
- this.write(ESC.RESET);
779
- const divider = renderDivider(cols - 2);
780
- // Calculate positions from absolute bottom
781
- let currentRow;
782
- if (suggestionLines > 0) {
783
- // With suggestions: input area + dividers + suggestions
784
- // Layout: [topDiv] [input] [bottomDiv] [suggestions...]
785
- const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
786
- currentRow = Math.max(1, rows - totalHeight + 1);
787
- this.updateReservedLines(totalHeight);
788
- // Top divider
457
+ const performRender = () => {
458
+ if (!this.scrollRegionActive) {
459
+ this.enableScrollRegion();
460
+ }
461
+ const { rows, cols } = this.getSize();
462
+ const maxWidth = Math.max(8, cols - 4);
463
+ // Wrap buffer into display lines
464
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
465
+ const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
466
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
467
+ const displayLines = Math.min(lines.length, maxVisible);
468
+ const metaLines = this.buildMetaLines(cols - 2);
469
+ // Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
470
+ this.updateReservedLines(displayLines + 2 + metaLines.length);
471
+ // Calculate display window (keep cursor visible)
472
+ let startLine = 0;
473
+ if (lines.length > displayLines) {
474
+ startLine = Math.max(0, cursorLine - displayLines + 1);
475
+ startLine = Math.min(startLine, lines.length - displayLines);
476
+ }
477
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
478
+ const adjustedCursorLine = cursorLine - startLine;
479
+ // Render
480
+ this.write(ESC.HIDE);
481
+ this.write(ESC.RESET);
482
+ const startRow = Math.max(1, rows - this.reservedLines + 1);
483
+ let currentRow = startRow;
484
+ // Clear the reserved block to avoid stale meta/status lines
485
+ this.clearReservedArea(startRow, this.reservedLines, cols);
486
+ // Meta/status header (elapsed, tokens/context)
487
+ for (const metaLine of metaLines) {
488
+ this.write(ESC.TO(currentRow, 1));
489
+ this.write(ESC.CLEAR_LINE);
490
+ this.write(metaLine);
491
+ currentRow += 1;
492
+ }
493
+ // Separator line
789
494
  this.write(ESC.TO(currentRow, 1));
790
495
  this.write(ESC.CLEAR_LINE);
496
+ const divider = renderDivider(cols - 2);
791
497
  this.write(divider);
792
- currentRow++;
793
- // Input lines
498
+ currentRow += 1;
499
+ // Render input lines
794
500
  let finalRow = currentRow;
795
501
  let finalCol = 3;
796
502
  for (let i = 0; i < visibleLines.length; i++) {
797
- this.write(ESC.TO(currentRow, 1));
503
+ const rowNum = currentRow + i;
504
+ this.write(ESC.TO(rowNum, 1));
798
505
  this.write(ESC.CLEAR_LINE);
799
506
  const line = visibleLines[i] ?? '';
800
507
  const absoluteLineIdx = startLine + i;
801
508
  const isFirstLine = absoluteLineIdx === 0;
802
509
  const isCursorLine = i === adjustedCursorLine;
510
+ // Background
511
+ this.write(ESC.BG_DARK);
512
+ // Prompt prefix
513
+ this.write(ESC.DIM);
803
514
  this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
515
+ this.write(ESC.RESET);
516
+ this.write(ESC.BG_DARK);
804
517
  if (isCursorLine) {
518
+ // Render with block cursor
805
519
  const col = Math.min(cursorCol, line.length);
806
- this.write(line.slice(0, col));
807
- this.write(ESC.REVERSE);
808
- this.write(col < line.length ? line[col] : ' ');
809
- this.write(ESC.RESET);
810
- this.write(line.slice(col + 1));
811
- finalRow = currentRow;
520
+ const before = line.slice(0, col);
521
+ const at = col < line.length ? line[col] : ' ';
522
+ const after = col < line.length ? line.slice(col + 1) : '';
523
+ this.write(before);
524
+ this.write(ESC.REVERSE + ESC.BOLD);
525
+ this.write(at);
526
+ this.write(ESC.RESET + ESC.BG_DARK);
527
+ this.write(after);
528
+ finalRow = rowNum;
812
529
  finalCol = this.config.promptChar.length + col + 1;
813
530
  }
814
531
  else {
815
532
  this.write(line);
816
533
  }
817
- currentRow++;
534
+ // Pad to edge for clean look
535
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
536
+ const padding = Math.max(0, cols - lineLen - 1);
537
+ if (padding > 0)
538
+ this.write(' '.repeat(padding));
539
+ this.write(ESC.RESET);
818
540
  }
819
- // Bottom divider
820
- this.write(ESC.TO(currentRow, 1));
541
+ // Mode controls line (Claude Code style)
542
+ const controlRow = currentRow + visibleLines.length;
543
+ this.write(ESC.TO(controlRow, 1));
821
544
  this.write(ESC.CLEAR_LINE);
822
- this.write(divider);
823
- currentRow++;
824
- // Suggestions (Claude Code style)
825
- for (let i = 0; i < suggestionsToShow.length; i++) {
826
- this.write(ESC.TO(currentRow, 1));
827
- this.write(ESC.CLEAR_LINE);
828
- const suggestion = suggestionsToShow[i];
829
- const isSelected = i === this.selectedSuggestionIndex;
830
- // Indent and highlight selected
831
- this.write(' ');
832
- if (isSelected) {
833
- this.write(ESC.REVERSE);
834
- this.write(ESC.BOLD);
835
- }
836
- this.write(suggestion.command);
837
- if (isSelected) {
838
- this.write(ESC.RESET);
839
- }
840
- // Description (dimmed)
841
- const descSpace = cols - suggestion.command.length - 8;
842
- if (descSpace > 10 && suggestion.description) {
843
- const desc = suggestion.description.slice(0, descSpace);
844
- this.write(ESC.RESET);
845
- this.write(ESC.DIM);
846
- this.write(' ');
847
- this.write(desc);
848
- this.write(ESC.RESET);
849
- }
850
- currentRow++;
851
- }
852
- // Position cursor in input area
545
+ this.write(this.buildModeControls(cols));
546
+ // Position cursor
853
547
  this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
548
+ this.write(ESC.SHOW);
549
+ // Update state
550
+ this.lastRenderContent = this.buffer;
551
+ this.lastRenderCursor = this.cursor;
552
+ this.lastStreamingRender = streamingActive ? Date.now() : 0;
553
+ if (this.streamingRenderTimer) {
554
+ clearTimeout(this.streamingRenderTimer);
555
+ this.streamingRenderTimer = null;
556
+ }
557
+ };
558
+ // Use write lock during render to prevent interleaved output
559
+ writeLock.lock('terminalInput.render');
560
+ this.isRendering = true;
561
+ try {
562
+ performRender();
563
+ }
564
+ finally {
565
+ writeLock.unlock();
566
+ this.isRendering = false;
854
567
  }
855
- this.write(ESC.SHOW);
856
- // Update state
857
- this.lastRenderContent = this.buffer;
858
- this.lastRenderCursor = this.cursor;
859
568
  }
860
569
  /**
861
- * Build status bar for streaming mode (shows elapsed time, queue count).
570
+ * Build one or more compact meta lines above the divider (thinking, status, usage).
571
+ * During streaming, consolidates into a single line to minimize cursor repositioning
572
+ * and prevent escape code interleaving with streamed content.
862
573
  */
863
- buildStreamingStatusBar(cols) {
864
- const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
865
- // Streaming status with elapsed time
866
- let elapsed = '0s';
867
- if (this.streamingStartTime) {
868
- const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
869
- const mins = Math.floor(secs / 60);
870
- elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
871
- }
872
- let status = `${GREEN}● Streaming${R} ${elapsed}`;
873
- // Queue indicator
574
+ buildMetaLines(width) {
575
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
576
+ // During streaming, consolidate everything into a single line to reduce escape codes
577
+ if (streamingActive) {
578
+ const parts = [];
579
+ // Essential streaming info only
580
+ if (this.metaThinkingMs !== null) {
581
+ parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
582
+ }
583
+ if (this.metaElapsedSeconds !== null) {
584
+ parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
585
+ }
586
+ parts.push({ text: 'esc to stop', tone: 'warn' });
587
+ return parts.length ? [renderStatusLine(parts, width)] : [];
588
+ }
589
+ // Non-streaming: show full status info
590
+ const lines = [];
591
+ if (this.metaThinkingMs !== null) {
592
+ const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
593
+ lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
594
+ }
595
+ if (this.modelLabel) {
596
+ const modelText = this.providerLabel
597
+ ? `${this.modelLabel} @ ${this.providerLabel}`
598
+ : this.modelLabel;
599
+ lines.push(renderStatusLine([{ text: `model ${modelText}`, tone: 'muted' }], width));
600
+ }
601
+ const statusParts = [];
602
+ const statusLabel = this.statusMessage ?? this.streamingLabel;
603
+ if (statusLabel) {
604
+ statusParts.push({ text: statusLabel, tone: 'info' });
605
+ }
606
+ if (this.metaElapsedSeconds !== null) {
607
+ statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
608
+ }
609
+ const tokensRemaining = this.computeTokensRemaining();
610
+ if (tokensRemaining !== null) {
611
+ statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
612
+ }
613
+ if (statusParts.length) {
614
+ lines.push(renderStatusLine(statusParts, width));
615
+ }
616
+ const usageParts = [];
617
+ if (this.metaTokensUsed !== null) {
618
+ const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
619
+ const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
620
+ usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
621
+ }
622
+ if (this.contextUsage !== null) {
623
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
624
+ const left = Math.max(0, 100 - this.contextUsage);
625
+ usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
626
+ }
874
627
  if (this.queue.length > 0) {
875
- status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
628
+ usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
629
+ }
630
+ if (usageParts.length) {
631
+ lines.push(renderStatusLine(usageParts, width));
876
632
  }
877
- // Hint for typing
878
- status += ` ${DIM}· type to queue message${R}`;
879
- return status;
633
+ return lines;
880
634
  }
881
635
  /**
882
- * Build status bar showing streaming/ready status and key info.
883
- * This is the TOP line above the input area - minimal Claude Code style.
636
+ * Clear the reserved bottom block (meta + divider + input + controls) before repainting.
884
637
  */
885
- buildStatusBar(cols) {
886
- const maxWidth = cols - 2;
887
- const parts = [];
888
- // Streaming status with elapsed time (left side)
889
- if (this.mode === 'streaming') {
890
- let statusText = ' Streaming';
891
- if (this.streamingStartTime) {
892
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
893
- const mins = Math.floor(elapsed / 60);
894
- const secs = elapsed % 60;
895
- statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
896
- }
897
- parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
898
- }
899
- // Queue indicator during streaming
900
- if (this.mode === 'streaming' && this.queue.length > 0) {
901
- parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
638
+ clearReservedArea(startRow, reservedLines, cols) {
639
+ const width = Math.max(1, cols);
640
+ for (let i = 0; i < reservedLines; i++) {
641
+ const row = startRow + i;
642
+ this.write(ESC.TO(row, 1));
643
+ this.write(' '.repeat(width));
902
644
  }
903
- // Paste indicator
904
- if (this.pastePlaceholders.length > 0) {
905
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
906
- parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
645
+ }
646
+ /**
647
+ * Build Claude Code style mode controls line.
648
+ * Combines streaming label + override status + main status for simultaneous display.
649
+ */
650
+ buildModeControls(cols) {
651
+ const width = Math.max(8, cols - 2);
652
+ const leftParts = [];
653
+ const rightParts = [];
654
+ if (this.streamingLabel) {
655
+ leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
907
656
  }
908
- // Override/warning status
909
657
  if (this.overrideStatusMessage) {
910
- parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
911
- }
912
- // If idle with empty buffer, show quick shortcuts
913
- if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
914
- return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
658
+ leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
659
+ }
660
+ if (this.statusMessage) {
661
+ leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
662
+ }
663
+ const editHotkey = this.formatHotkey('shift+tab');
664
+ const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
665
+ const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
666
+ leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
667
+ const verifyHotkey = this.formatHotkey(this.verificationHotkey);
668
+ const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
669
+ leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
670
+ const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
671
+ const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
672
+ leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
673
+ if (this.queue.length > 0 && this.mode !== 'streaming') {
674
+ leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
915
675
  }
916
- // Multi-line indicator
917
676
  if (this.buffer.includes('\n')) {
918
- parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
677
+ const lineCount = this.buffer.split('\n').length;
678
+ leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
679
+ }
680
+ if (this.pastePlaceholders.length > 0) {
681
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
682
+ leftParts.push({
683
+ text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
684
+ tone: 'info',
685
+ });
686
+ }
687
+ const contextRemaining = this.computeContextRemaining();
688
+ if (this.thinkingModeLabel) {
689
+ const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
690
+ rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
691
+ }
692
+ if (this.modelLabel) {
693
+ const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
694
+ rightParts.push({ text: modelText, tone: 'muted' });
695
+ }
696
+ if (contextRemaining !== null) {
697
+ const tone = contextRemaining <= 10 ? 'warn' : 'muted';
698
+ const label = contextRemaining === 0 && this.contextUsage !== null
699
+ ? 'Context auto-compact imminent'
700
+ : `Context left until auto-compact: ${contextRemaining}%`;
701
+ rightParts.push({ text: label, tone });
702
+ }
703
+ if (!rightParts.length || width < 60) {
704
+ const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
705
+ return renderStatusLine(merged, width);
706
+ }
707
+ const leftWidth = Math.max(12, Math.floor(width * 0.6));
708
+ const rightWidth = Math.max(14, width - leftWidth - 1);
709
+ const leftText = renderStatusLine(leftParts, leftWidth);
710
+ const rightText = renderStatusLine(rightParts, rightWidth);
711
+ const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
712
+ return `${leftText}${' '.repeat(spacing)}${rightText}`;
713
+ }
714
+ formatHotkey(hotkey) {
715
+ const normalized = hotkey.trim().toLowerCase();
716
+ if (!normalized)
717
+ return hotkey;
718
+ const parts = normalized.split('+').filter(Boolean);
719
+ const map = {
720
+ shift: '⇧',
721
+ sh: '⇧',
722
+ alt: '⌥',
723
+ option: '⌥',
724
+ opt: '⌥',
725
+ ctrl: '⌃',
726
+ control: '⌃',
727
+ cmd: '⌘',
728
+ meta: '⌘',
729
+ };
730
+ const formatted = parts
731
+ .map((part) => {
732
+ const symbol = map[part];
733
+ if (symbol)
734
+ return symbol;
735
+ return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
736
+ })
737
+ .join('');
738
+ return formatted || hotkey;
739
+ }
740
+ computeContextRemaining() {
741
+ if (this.contextUsage === null) {
742
+ return null;
743
+ }
744
+ return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
745
+ }
746
+ computeTokensRemaining() {
747
+ if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
748
+ return null;
749
+ }
750
+ const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
751
+ return this.formatTokenCount(remaining);
752
+ }
753
+ formatElapsedLabel(seconds) {
754
+ if (seconds < 60) {
755
+ return `${seconds}s`;
756
+ }
757
+ const mins = Math.floor(seconds / 60);
758
+ const secs = seconds % 60;
759
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
760
+ }
761
+ formatTokenCount(value) {
762
+ if (!Number.isFinite(value)) {
763
+ return `${value}`;
919
764
  }
920
- if (parts.length === 0) {
921
- return ''; // Empty status bar when idle
765
+ if (value >= 1_000_000) {
766
+ return `${(value / 1_000_000).toFixed(1)}M`;
922
767
  }
923
- const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
924
- return joined.slice(0, maxWidth);
768
+ if (value >= 1_000) {
769
+ return `${(value / 1_000).toFixed(1)}k`;
770
+ }
771
+ return `${Math.round(value)}`;
772
+ }
773
+ visibleLength(value) {
774
+ const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
775
+ return value.replace(ansiPattern, '').length;
925
776
  }
926
777
  /**
927
- * Build mode controls line showing toggles and context info.
928
- * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
929
- *
930
- * Layout: [toggles on left] ... [context info on right]
778
+ * Debug-only snapshot used by tests to assert rendered strings without
779
+ * needing a TTY. Not used by production code.
931
780
  */
932
- buildModeControls(cols) {
933
- const maxWidth = cols - 2;
934
- // Use schema-defined colors for consistency
935
- const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
936
- // Mode toggles with colors (following ModeControlsSchema)
937
- const toggles = [];
938
- // Edit mode (green=auto, yellow=ask) - per schema.editMode
939
- if (this.editMode === 'display-edits') {
940
- toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
941
- }
942
- else {
943
- toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
944
- }
945
- // Thinking mode (cyan when on) - per schema.thinkingMode
946
- toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
947
- // Verification (green when on) - per schema.verificationMode
948
- toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
949
- // Auto-continue (magenta when on) - per schema.autoContinueMode
950
- toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
951
- const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
952
- // Context usage with color - per schema.contextUsage thresholds
953
- let rightPart = '';
954
- if (this.contextUsage !== null) {
955
- const rem = Math.max(0, 100 - this.contextUsage);
956
- // Thresholds: critical < 10%, warning < 25%
957
- if (rem < 10)
958
- rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
959
- else if (rem < 25)
960
- rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
961
- else
962
- rightPart = `${DIM}ctx: ${rem}%${R}`;
963
- }
964
- // Calculate visible lengths (strip ANSI)
965
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
966
- const leftLen = strip(leftPart).length;
967
- const rightLen = strip(rightPart).length;
968
- if (leftLen + rightLen < maxWidth - 4) {
969
- return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
970
- }
971
- if (rightLen > 0 && leftLen + 8 < maxWidth) {
972
- return `${leftPart} ${rightPart}`;
973
- }
974
- return leftPart;
781
+ getDebugUiSnapshot(width) {
782
+ const cols = Math.max(8, width ?? this.getSize().cols);
783
+ return {
784
+ meta: this.buildMetaLines(cols - 2),
785
+ controls: this.buildModeControls(cols),
786
+ };
975
787
  }
976
788
  /**
977
789
  * Force a re-render
@@ -994,17 +806,19 @@ export class TerminalInput extends EventEmitter {
994
806
  handleResize() {
995
807
  this.lastRenderContent = '';
996
808
  this.lastRenderCursor = -1;
809
+ this.resetStreamingRenderThrottle();
997
810
  // Re-clamp pinned header rows to the new terminal height
998
811
  this.setPinnedHeaderLines(this.pinnedTopRows);
812
+ if (this.scrollRegionActive) {
813
+ this.disableScrollRegion();
814
+ this.enableScrollRegion();
815
+ }
999
816
  this.scheduleRender();
1000
817
  }
1001
818
  /**
1002
819
  * Register with display's output interceptor to position cursor correctly.
1003
820
  * When scroll region is active, output needs to go to the scroll region,
1004
821
  * not the protected bottom area where the input is rendered.
1005
- *
1006
- * NOTE: With scroll region properly set, content naturally stays within
1007
- * the region boundaries - no cursor manipulation needed per-write.
1008
822
  */
1009
823
  registerOutputInterceptor(display) {
1010
824
  if (this.outputInterceptorCleanup) {
@@ -1012,11 +826,20 @@ export class TerminalInput extends EventEmitter {
1012
826
  }
1013
827
  this.outputInterceptorCleanup = display.registerOutputInterceptor({
1014
828
  beforeWrite: () => {
1015
- // Scroll region handles content containment automatically
1016
- // No per-write cursor manipulation needed
829
+ // When the scroll region is active, temporarily move the cursor into
830
+ // the scrollable area so streamed output lands above the pinned prompt.
831
+ if (this.scrollRegionActive) {
832
+ const { rows } = this.getSize();
833
+ const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
834
+ this.write(ESC.SAVE);
835
+ this.write(ESC.TO(scrollBottom, 1));
836
+ }
1017
837
  },
1018
838
  afterWrite: () => {
1019
- // No cursor manipulation needed
839
+ // Restore cursor back to the pinned prompt after output completes.
840
+ if (this.scrollRegionActive) {
841
+ this.write(ESC.RESTORE);
842
+ }
1020
843
  },
1021
844
  });
1022
845
  }
@@ -1026,11 +849,6 @@ export class TerminalInput extends EventEmitter {
1026
849
  dispose() {
1027
850
  if (this.disposed)
1028
851
  return;
1029
- // Clean up streaming render timer
1030
- if (this.streamingRenderTimer) {
1031
- clearInterval(this.streamingRenderTimer);
1032
- this.streamingRenderTimer = null;
1033
- }
1034
852
  // Clean up output interceptor
1035
853
  if (this.outputInterceptorCleanup) {
1036
854
  this.outputInterceptorCleanup();
@@ -1038,6 +856,7 @@ export class TerminalInput extends EventEmitter {
1038
856
  }
1039
857
  this.disposed = true;
1040
858
  this.enabled = false;
859
+ this.resetStreamingRenderThrottle();
1041
860
  this.disableScrollRegion();
1042
861
  this.disableBracketedPaste();
1043
862
  this.buffer = '';
@@ -1143,22 +962,7 @@ export class TerminalInput extends EventEmitter {
1143
962
  this.toggleEditMode();
1144
963
  return true;
1145
964
  }
1146
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
1147
- if (this.findPlaceholderAt(this.cursor)) {
1148
- this.togglePasteExpansion();
1149
- }
1150
- else {
1151
- this.toggleThinking();
1152
- }
1153
- return true;
1154
- case 'escape':
1155
- // Esc: interrupt if streaming, otherwise clear buffer
1156
- if (this.mode === 'streaming') {
1157
- this.emit('interrupt');
1158
- }
1159
- else if (this.buffer.length > 0) {
1160
- this.clear();
1161
- }
965
+ this.insertText(' ');
1162
966
  return true;
1163
967
  }
1164
968
  return false;
@@ -1176,7 +980,6 @@ export class TerminalInput extends EventEmitter {
1176
980
  this.insertPlainText(chunk, insertPos);
1177
981
  this.cursor = insertPos + chunk.length;
1178
982
  this.emit('change', this.buffer);
1179
- this.updateSuggestions();
1180
983
  this.scheduleRender();
1181
984
  }
1182
985
  insertNewline() {
@@ -1201,7 +1004,6 @@ export class TerminalInput extends EventEmitter {
1201
1004
  this.cursor = Math.max(0, this.cursor - 1);
1202
1005
  }
1203
1006
  this.emit('change', this.buffer);
1204
- this.updateSuggestions();
1205
1007
  this.scheduleRender();
1206
1008
  }
1207
1009
  deleteForward() {
@@ -1451,7 +1253,9 @@ export class TerminalInput extends EventEmitter {
1451
1253
  if (available <= 0)
1452
1254
  return;
1453
1255
  const chunk = clean.slice(0, available);
1454
- if (isMultilinePaste(chunk)) {
1256
+ const isMultiline = isMultilinePaste(chunk);
1257
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1258
+ if (isMultiline && !isShortMultiline) {
1455
1259
  this.insertPastePlaceholder(chunk);
1456
1260
  }
1457
1261
  else {
@@ -1471,6 +1275,7 @@ export class TerminalInput extends EventEmitter {
1471
1275
  return;
1472
1276
  this.applyScrollRegion();
1473
1277
  this.scrollRegionActive = true;
1278
+ this.forceRender();
1474
1279
  }
1475
1280
  disableScrollRegion() {
1476
1281
  if (!this.scrollRegionActive)
@@ -1621,17 +1426,19 @@ export class TerminalInput extends EventEmitter {
1621
1426
  this.shiftPlaceholders(position, text.length);
1622
1427
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1623
1428
  }
1429
+ shouldInlineMultiline(content) {
1430
+ const lines = content.split('\n').length;
1431
+ const maxInlineLines = 4;
1432
+ const maxInlineChars = 240;
1433
+ return lines <= maxInlineLines && content.length <= maxInlineChars;
1434
+ }
1624
1435
  findPlaceholderAt(position) {
1625
1436
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1626
1437
  }
1627
- buildPlaceholder(summary) {
1438
+ buildPlaceholder(lineCount) {
1628
1439
  const id = ++this.pasteCounter;
1629
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1630
- // Show first line preview (truncated)
1631
- const preview = summary.preview.length > 30
1632
- ? `${summary.preview.slice(0, 30)}...`
1633
- : summary.preview;
1634
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1440
+ const plural = lineCount === 1 ? '' : 's';
1441
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1635
1442
  return { id, placeholder };
1636
1443
  }
1637
1444
  insertPastePlaceholder(content) {
@@ -1639,67 +1446,21 @@ export class TerminalInput extends EventEmitter {
1639
1446
  if (available <= 0)
1640
1447
  return;
1641
1448
  const cleanContent = content.slice(0, available);
1642
- const summary = generatePasteSummary(cleanContent);
1643
- // For short pastes (< 5 lines), show full content instead of placeholder
1644
- if (summary.lineCount < 5) {
1645
- const placeholder = this.findPlaceholderAt(this.cursor);
1646
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1647
- this.insertPlainText(cleanContent, insertPos);
1648
- this.cursor = insertPos + cleanContent.length;
1649
- return;
1650
- }
1651
- const { id, placeholder } = this.buildPlaceholder(summary);
1449
+ const lineCount = cleanContent.split('\n').length;
1450
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1652
1451
  const insertPos = this.cursor;
1653
1452
  this.shiftPlaceholders(insertPos, placeholder.length);
1654
1453
  this.pastePlaceholders.push({
1655
1454
  id,
1656
1455
  content: cleanContent,
1657
- lineCount: summary.lineCount,
1456
+ lineCount,
1658
1457
  placeholder,
1659
1458
  start: insertPos,
1660
1459
  end: insertPos + placeholder.length,
1661
- summary,
1662
- expanded: false,
1663
1460
  });
1664
1461
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1665
1462
  this.cursor = insertPos + placeholder.length;
1666
1463
  }
1667
- /**
1668
- * Toggle expansion of a paste placeholder at the current cursor position.
1669
- * When expanded, shows first 3 and last 2 lines of the content.
1670
- */
1671
- togglePasteExpansion() {
1672
- const placeholder = this.findPlaceholderAt(this.cursor);
1673
- if (!placeholder)
1674
- return false;
1675
- placeholder.expanded = !placeholder.expanded;
1676
- // Update the placeholder text in buffer
1677
- const newPlaceholder = placeholder.expanded
1678
- ? this.buildExpandedPlaceholder(placeholder)
1679
- : this.buildPlaceholder(placeholder.summary).placeholder;
1680
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1681
- // Update buffer
1682
- this.buffer =
1683
- this.buffer.slice(0, placeholder.start) +
1684
- newPlaceholder +
1685
- this.buffer.slice(placeholder.end);
1686
- // Update placeholder tracking
1687
- placeholder.placeholder = newPlaceholder;
1688
- placeholder.end = placeholder.start + newPlaceholder.length;
1689
- // Shift other placeholders
1690
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1691
- this.scheduleRender();
1692
- return true;
1693
- }
1694
- buildExpandedPlaceholder(ph) {
1695
- const lines = ph.content.split('\n');
1696
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1697
- const lastLines = lines.length > 5
1698
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1699
- : '';
1700
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1701
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1702
- }
1703
1464
  deletePlaceholder(placeholder) {
1704
1465
  const length = placeholder.end - placeholder.start;
1705
1466
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1707,7 +1468,11 @@ export class TerminalInput extends EventEmitter {
1707
1468
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1708
1469
  this.cursor = placeholder.start;
1709
1470
  }
1710
- updateContextUsage(value) {
1471
+ updateContextUsage(value, autoCompactThreshold) {
1472
+ if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1473
+ const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1474
+ this.contextAutoCompactThreshold = boundedThreshold;
1475
+ }
1711
1476
  if (value === null || !Number.isFinite(value)) {
1712
1477
  this.contextUsage = null;
1713
1478
  }
@@ -1734,6 +1499,22 @@ export class TerminalInput extends EventEmitter {
1734
1499
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1735
1500
  this.setEditMode(next);
1736
1501
  }
1502
+ scheduleStreamingRender(delayMs) {
1503
+ if (this.streamingRenderTimer)
1504
+ return;
1505
+ const wait = Math.max(16, delayMs);
1506
+ this.streamingRenderTimer = setTimeout(() => {
1507
+ this.streamingRenderTimer = null;
1508
+ this.render();
1509
+ }, wait);
1510
+ }
1511
+ resetStreamingRenderThrottle() {
1512
+ if (this.streamingRenderTimer) {
1513
+ clearTimeout(this.streamingRenderTimer);
1514
+ this.streamingRenderTimer = null;
1515
+ }
1516
+ this.lastStreamingRender = 0;
1517
+ }
1737
1518
  scheduleRender() {
1738
1519
  if (!this.canRender())
1739
1520
  return;