erosolar-cli 1.7.263 → 1.7.265

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