erosolar-cli 1.7.309 → 1.7.310

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 (323) hide show
  1. package/README.md +24 -148
  2. package/dist/bin/erosolar.js +3 -1
  3. package/dist/bin/erosolar.js.map +1 -1
  4. package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
  5. package/dist/capabilities/agentSpawningCapability.js +56 -31
  6. package/dist/capabilities/agentSpawningCapability.js.map +1 -1
  7. package/dist/contracts/agent-schemas.json +0 -15
  8. package/dist/contracts/tools.schema.json +0 -9
  9. package/dist/core/agent.d.ts +2 -2
  10. package/dist/core/agent.d.ts.map +1 -1
  11. package/dist/core/agent.js.map +1 -1
  12. package/dist/core/customCommands.d.ts +1 -0
  13. package/dist/core/customCommands.d.ts.map +1 -1
  14. package/dist/core/customCommands.js +3 -0
  15. package/dist/core/customCommands.js.map +1 -1
  16. package/dist/core/hooks.d.ts +113 -0
  17. package/dist/core/hooks.d.ts.map +1 -0
  18. package/dist/core/hooks.js +267 -0
  19. package/dist/core/hooks.js.map +1 -0
  20. package/dist/core/metricsTracker.d.ts +122 -0
  21. package/dist/core/metricsTracker.d.ts.map +1 -0
  22. package/dist/{alpha-zero → core}/metricsTracker.js +2 -5
  23. package/dist/core/metricsTracker.js.map +1 -0
  24. package/dist/core/securityAssessment.d.ts +91 -0
  25. package/dist/core/securityAssessment.d.ts.map +1 -0
  26. package/dist/core/securityAssessment.js +580 -0
  27. package/dist/core/securityAssessment.js.map +1 -0
  28. package/dist/core/toolPreconditions.d.ts.map +1 -1
  29. package/dist/core/toolPreconditions.js +0 -14
  30. package/dist/core/toolPreconditions.js.map +1 -1
  31. package/dist/core/toolRuntime.d.ts +22 -1
  32. package/dist/core/toolRuntime.d.ts.map +1 -1
  33. package/dist/core/toolRuntime.js +0 -5
  34. package/dist/core/toolRuntime.js.map +1 -1
  35. package/dist/core/toolValidation.d.ts.map +1 -1
  36. package/dist/core/toolValidation.js +14 -3
  37. package/dist/core/toolValidation.js.map +1 -1
  38. package/dist/core/validationRunner.d.ts +1 -3
  39. package/dist/core/validationRunner.d.ts.map +1 -1
  40. package/dist/core/validationRunner.js.map +1 -1
  41. package/dist/core/verification.d.ts +137 -0
  42. package/dist/core/verification.d.ts.map +1 -0
  43. package/dist/core/verification.js +323 -0
  44. package/dist/core/verification.js.map +1 -0
  45. package/dist/headless/headlessApp.d.ts.map +1 -1
  46. package/dist/headless/headlessApp.js +21 -0
  47. package/dist/headless/headlessApp.js.map +1 -1
  48. package/dist/mcp/sseClient.d.ts.map +1 -1
  49. package/dist/mcp/sseClient.js +9 -18
  50. package/dist/mcp/sseClient.js.map +1 -1
  51. package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
  52. package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
  53. package/dist/plugins/tools/build/buildPlugin.js +4 -10
  54. package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
  55. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  56. package/dist/plugins/tools/nodeDefaults.js +0 -2
  57. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  58. package/dist/runtime/agentSession.d.ts +2 -2
  59. package/dist/runtime/agentSession.d.ts.map +1 -1
  60. package/dist/runtime/agentSession.js +2 -2
  61. package/dist/runtime/agentSession.js.map +1 -1
  62. package/dist/shell/interactiveShell.d.ts +16 -7
  63. package/dist/shell/interactiveShell.d.ts.map +1 -1
  64. package/dist/shell/interactiveShell.js +223 -163
  65. package/dist/shell/interactiveShell.js.map +1 -1
  66. package/dist/shell/shellApp.d.ts +2 -0
  67. package/dist/shell/shellApp.d.ts.map +1 -1
  68. package/dist/shell/shellApp.js +40 -9
  69. package/dist/shell/shellApp.js.map +1 -1
  70. package/dist/shell/systemPrompt.d.ts.map +1 -1
  71. package/dist/shell/systemPrompt.js +1 -4
  72. package/dist/shell/systemPrompt.js.map +1 -1
  73. package/dist/shell/terminalInput.d.ts +126 -113
  74. package/dist/shell/terminalInput.d.ts.map +1 -1
  75. package/dist/shell/terminalInput.js +553 -521
  76. package/dist/shell/terminalInput.js.map +1 -1
  77. package/dist/shell/terminalInputAdapter.d.ts +56 -20
  78. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  79. package/dist/shell/terminalInputAdapter.js +66 -30
  80. package/dist/shell/terminalInputAdapter.js.map +1 -1
  81. package/dist/subagents/agentConfig.d.ts +27 -0
  82. package/dist/subagents/agentConfig.d.ts.map +1 -0
  83. package/dist/subagents/agentConfig.js +89 -0
  84. package/dist/subagents/agentConfig.js.map +1 -0
  85. package/dist/subagents/agentRegistry.d.ts +33 -0
  86. package/dist/subagents/agentRegistry.d.ts.map +1 -0
  87. package/dist/subagents/agentRegistry.js +162 -0
  88. package/dist/subagents/agentRegistry.js.map +1 -0
  89. package/dist/subagents/taskRunner.d.ts +7 -1
  90. package/dist/subagents/taskRunner.d.ts.map +1 -1
  91. package/dist/subagents/taskRunner.js +180 -47
  92. package/dist/subagents/taskRunner.js.map +1 -1
  93. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  94. package/dist/ui/ShellUIAdapter.js +13 -12
  95. package/dist/ui/ShellUIAdapter.js.map +1 -1
  96. package/dist/ui/display.d.ts +24 -45
  97. package/dist/ui/display.d.ts.map +1 -1
  98. package/dist/ui/display.js +140 -259
  99. package/dist/ui/display.js.map +1 -1
  100. package/dist/ui/theme.d.ts.map +1 -1
  101. package/dist/ui/theme.js +6 -8
  102. package/dist/ui/theme.js.map +1 -1
  103. package/dist/ui/toolDisplay.d.ts +0 -158
  104. package/dist/ui/toolDisplay.d.ts.map +1 -1
  105. package/dist/ui/toolDisplay.js +0 -348
  106. package/dist/ui/toolDisplay.js.map +1 -1
  107. package/dist/ui/unified/layout.d.ts +1 -0
  108. package/dist/ui/unified/layout.d.ts.map +1 -1
  109. package/dist/ui/unified/layout.js +15 -25
  110. package/dist/ui/unified/layout.js.map +1 -1
  111. package/dist/utils/frontmatter.d.ts +10 -0
  112. package/dist/utils/frontmatter.d.ts.map +1 -0
  113. package/dist/utils/frontmatter.js +78 -0
  114. package/dist/utils/frontmatter.js.map +1 -0
  115. package/package.json +4 -4
  116. package/dist/alpha-zero/agentWrapper.d.ts +0 -84
  117. package/dist/alpha-zero/agentWrapper.d.ts.map +0 -1
  118. package/dist/alpha-zero/agentWrapper.js +0 -171
  119. package/dist/alpha-zero/agentWrapper.js.map +0 -1
  120. package/dist/alpha-zero/codeEvaluator.d.ts +0 -25
  121. package/dist/alpha-zero/codeEvaluator.d.ts.map +0 -1
  122. package/dist/alpha-zero/codeEvaluator.js +0 -273
  123. package/dist/alpha-zero/codeEvaluator.js.map +0 -1
  124. package/dist/alpha-zero/competitiveRunner.d.ts +0 -66
  125. package/dist/alpha-zero/competitiveRunner.d.ts.map +0 -1
  126. package/dist/alpha-zero/competitiveRunner.js +0 -224
  127. package/dist/alpha-zero/competitiveRunner.js.map +0 -1
  128. package/dist/alpha-zero/index.d.ts +0 -67
  129. package/dist/alpha-zero/index.d.ts.map +0 -1
  130. package/dist/alpha-zero/index.js +0 -99
  131. package/dist/alpha-zero/index.js.map +0 -1
  132. package/dist/alpha-zero/introspection.d.ts +0 -128
  133. package/dist/alpha-zero/introspection.d.ts.map +0 -1
  134. package/dist/alpha-zero/introspection.js +0 -300
  135. package/dist/alpha-zero/introspection.js.map +0 -1
  136. package/dist/alpha-zero/metricsTracker.d.ts +0 -71
  137. package/dist/alpha-zero/metricsTracker.d.ts.map +0 -1
  138. package/dist/alpha-zero/metricsTracker.js.map +0 -1
  139. package/dist/alpha-zero/security/core.d.ts +0 -125
  140. package/dist/alpha-zero/security/core.d.ts.map +0 -1
  141. package/dist/alpha-zero/security/core.js +0 -271
  142. package/dist/alpha-zero/security/core.js.map +0 -1
  143. package/dist/alpha-zero/security/google.d.ts +0 -125
  144. package/dist/alpha-zero/security/google.d.ts.map +0 -1
  145. package/dist/alpha-zero/security/google.js +0 -311
  146. package/dist/alpha-zero/security/google.js.map +0 -1
  147. package/dist/alpha-zero/security/googleLoader.d.ts +0 -17
  148. package/dist/alpha-zero/security/googleLoader.d.ts.map +0 -1
  149. package/dist/alpha-zero/security/googleLoader.js +0 -41
  150. package/dist/alpha-zero/security/googleLoader.js.map +0 -1
  151. package/dist/alpha-zero/security/index.d.ts +0 -29
  152. package/dist/alpha-zero/security/index.d.ts.map +0 -1
  153. package/dist/alpha-zero/security/index.js +0 -32
  154. package/dist/alpha-zero/security/index.js.map +0 -1
  155. package/dist/alpha-zero/security/simulation.d.ts +0 -124
  156. package/dist/alpha-zero/security/simulation.d.ts.map +0 -1
  157. package/dist/alpha-zero/security/simulation.js +0 -277
  158. package/dist/alpha-zero/security/simulation.js.map +0 -1
  159. package/dist/alpha-zero/selfModification.d.ts +0 -109
  160. package/dist/alpha-zero/selfModification.d.ts.map +0 -1
  161. package/dist/alpha-zero/selfModification.js +0 -233
  162. package/dist/alpha-zero/selfModification.js.map +0 -1
  163. package/dist/alpha-zero/types.d.ts +0 -170
  164. package/dist/alpha-zero/types.d.ts.map +0 -1
  165. package/dist/alpha-zero/types.js +0 -31
  166. package/dist/alpha-zero/types.js.map +0 -1
  167. package/dist/capabilities/securityTestingCapability.d.ts +0 -13
  168. package/dist/capabilities/securityTestingCapability.d.ts.map +0 -1
  169. package/dist/capabilities/securityTestingCapability.js +0 -25
  170. package/dist/capabilities/securityTestingCapability.js.map +0 -1
  171. package/dist/core/aiFlowOptimizer.d.ts +0 -26
  172. package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
  173. package/dist/core/aiFlowOptimizer.js +0 -31
  174. package/dist/core/aiFlowOptimizer.js.map +0 -1
  175. package/dist/core/aiOptimizationEngine.d.ts +0 -158
  176. package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
  177. package/dist/core/aiOptimizationEngine.js +0 -428
  178. package/dist/core/aiOptimizationEngine.js.map +0 -1
  179. package/dist/core/aiOptimizationIntegration.d.ts +0 -93
  180. package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
  181. package/dist/core/aiOptimizationIntegration.js +0 -250
  182. package/dist/core/aiOptimizationIntegration.js.map +0 -1
  183. package/dist/core/enhancedErrorRecovery.d.ts +0 -100
  184. package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
  185. package/dist/core/enhancedErrorRecovery.js +0 -345
  186. package/dist/core/enhancedErrorRecovery.js.map +0 -1
  187. package/dist/core/hooksSystem.d.ts +0 -65
  188. package/dist/core/hooksSystem.d.ts.map +0 -1
  189. package/dist/core/hooksSystem.js +0 -273
  190. package/dist/core/hooksSystem.js.map +0 -1
  191. package/dist/core/memorySystem.d.ts +0 -48
  192. package/dist/core/memorySystem.d.ts.map +0 -1
  193. package/dist/core/memorySystem.js +0 -271
  194. package/dist/core/memorySystem.js.map +0 -1
  195. package/dist/core/unified/errors.d.ts +0 -189
  196. package/dist/core/unified/errors.d.ts.map +0 -1
  197. package/dist/core/unified/errors.js +0 -497
  198. package/dist/core/unified/errors.js.map +0 -1
  199. package/dist/core/unified/index.d.ts +0 -19
  200. package/dist/core/unified/index.d.ts.map +0 -1
  201. package/dist/core/unified/index.js +0 -68
  202. package/dist/core/unified/index.js.map +0 -1
  203. package/dist/core/unified/schema.d.ts +0 -101
  204. package/dist/core/unified/schema.d.ts.map +0 -1
  205. package/dist/core/unified/schema.js +0 -350
  206. package/dist/core/unified/schema.js.map +0 -1
  207. package/dist/core/unified/toolRuntime.d.ts +0 -179
  208. package/dist/core/unified/toolRuntime.d.ts.map +0 -1
  209. package/dist/core/unified/toolRuntime.js +0 -517
  210. package/dist/core/unified/toolRuntime.js.map +0 -1
  211. package/dist/core/unified/tools.d.ts +0 -127
  212. package/dist/core/unified/tools.d.ts.map +0 -1
  213. package/dist/core/unified/tools.js +0 -1333
  214. package/dist/core/unified/tools.js.map +0 -1
  215. package/dist/core/unified/types.d.ts +0 -352
  216. package/dist/core/unified/types.d.ts.map +0 -1
  217. package/dist/core/unified/types.js +0 -12
  218. package/dist/core/unified/types.js.map +0 -1
  219. package/dist/core/unified/version.d.ts +0 -209
  220. package/dist/core/unified/version.d.ts.map +0 -1
  221. package/dist/core/unified/version.js +0 -454
  222. package/dist/core/unified/version.js.map +0 -1
  223. package/dist/plugins/tools/security/securityPlugin.d.ts +0 -3
  224. package/dist/plugins/tools/security/securityPlugin.d.ts.map +0 -1
  225. package/dist/plugins/tools/security/securityPlugin.js +0 -12
  226. package/dist/plugins/tools/security/securityPlugin.js.map +0 -1
  227. package/dist/security/active-stack-security.d.ts +0 -112
  228. package/dist/security/active-stack-security.d.ts.map +0 -1
  229. package/dist/security/active-stack-security.js +0 -296
  230. package/dist/security/active-stack-security.js.map +0 -1
  231. package/dist/security/advanced-persistence-research.d.ts +0 -92
  232. package/dist/security/advanced-persistence-research.d.ts.map +0 -1
  233. package/dist/security/advanced-persistence-research.js +0 -195
  234. package/dist/security/advanced-persistence-research.js.map +0 -1
  235. package/dist/security/advanced-targeting.d.ts +0 -119
  236. package/dist/security/advanced-targeting.d.ts.map +0 -1
  237. package/dist/security/advanced-targeting.js +0 -233
  238. package/dist/security/advanced-targeting.js.map +0 -1
  239. package/dist/security/assessment/vulnerabilityAssessment.d.ts +0 -104
  240. package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +0 -1
  241. package/dist/security/assessment/vulnerabilityAssessment.js +0 -315
  242. package/dist/security/assessment/vulnerabilityAssessment.js.map +0 -1
  243. package/dist/security/authorization/securityAuthorization.d.ts +0 -88
  244. package/dist/security/authorization/securityAuthorization.d.ts.map +0 -1
  245. package/dist/security/authorization/securityAuthorization.js +0 -172
  246. package/dist/security/authorization/securityAuthorization.js.map +0 -1
  247. package/dist/security/comprehensive-targeting.d.ts +0 -85
  248. package/dist/security/comprehensive-targeting.d.ts.map +0 -1
  249. package/dist/security/comprehensive-targeting.js +0 -438
  250. package/dist/security/comprehensive-targeting.js.map +0 -1
  251. package/dist/security/global-security-integration.d.ts +0 -91
  252. package/dist/security/global-security-integration.d.ts.map +0 -1
  253. package/dist/security/global-security-integration.js +0 -218
  254. package/dist/security/global-security-integration.js.map +0 -1
  255. package/dist/security/index.d.ts +0 -38
  256. package/dist/security/index.d.ts.map +0 -1
  257. package/dist/security/index.js +0 -47
  258. package/dist/security/index.js.map +0 -1
  259. package/dist/security/persistence-analyzer.d.ts +0 -56
  260. package/dist/security/persistence-analyzer.d.ts.map +0 -1
  261. package/dist/security/persistence-analyzer.js +0 -187
  262. package/dist/security/persistence-analyzer.js.map +0 -1
  263. package/dist/security/persistence-cli.d.ts +0 -36
  264. package/dist/security/persistence-cli.d.ts.map +0 -1
  265. package/dist/security/persistence-cli.js +0 -160
  266. package/dist/security/persistence-cli.js.map +0 -1
  267. package/dist/security/persistence-research.d.ts +0 -92
  268. package/dist/security/persistence-research.d.ts.map +0 -1
  269. package/dist/security/persistence-research.js +0 -364
  270. package/dist/security/persistence-research.js.map +0 -1
  271. package/dist/security/research/persistenceResearch.d.ts +0 -97
  272. package/dist/security/research/persistenceResearch.d.ts.map +0 -1
  273. package/dist/security/research/persistenceResearch.js +0 -282
  274. package/dist/security/research/persistenceResearch.js.map +0 -1
  275. package/dist/security/security-integration.d.ts +0 -74
  276. package/dist/security/security-integration.d.ts.map +0 -1
  277. package/dist/security/security-integration.js +0 -137
  278. package/dist/security/security-integration.js.map +0 -1
  279. package/dist/security/security-testing-framework.d.ts +0 -112
  280. package/dist/security/security-testing-framework.d.ts.map +0 -1
  281. package/dist/security/security-testing-framework.js +0 -364
  282. package/dist/security/security-testing-framework.js.map +0 -1
  283. package/dist/security/simulation/attackSimulation.d.ts +0 -93
  284. package/dist/security/simulation/attackSimulation.d.ts.map +0 -1
  285. package/dist/security/simulation/attackSimulation.js +0 -341
  286. package/dist/security/simulation/attackSimulation.js.map +0 -1
  287. package/dist/security/strategic-operations.d.ts +0 -100
  288. package/dist/security/strategic-operations.d.ts.map +0 -1
  289. package/dist/security/strategic-operations.js +0 -276
  290. package/dist/security/strategic-operations.js.map +0 -1
  291. package/dist/security/tool-security-wrapper.d.ts +0 -58
  292. package/dist/security/tool-security-wrapper.d.ts.map +0 -1
  293. package/dist/security/tool-security-wrapper.js +0 -156
  294. package/dist/security/tool-security-wrapper.js.map +0 -1
  295. package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
  296. package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
  297. package/dist/shell/claudeCodeStreamHandler.js +0 -322
  298. package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
  299. package/dist/shell/inputQueueManager.d.ts +0 -144
  300. package/dist/shell/inputQueueManager.d.ts.map +0 -1
  301. package/dist/shell/inputQueueManager.js +0 -290
  302. package/dist/shell/inputQueueManager.js.map +0 -1
  303. package/dist/shell/metricsTracker.d.ts +0 -60
  304. package/dist/shell/metricsTracker.d.ts.map +0 -1
  305. package/dist/shell/metricsTracker.js +0 -119
  306. package/dist/shell/metricsTracker.js.map +0 -1
  307. package/dist/shell/streamingOutputManager.d.ts +0 -115
  308. package/dist/shell/streamingOutputManager.d.ts.map +0 -1
  309. package/dist/shell/streamingOutputManager.js +0 -225
  310. package/dist/shell/streamingOutputManager.js.map +0 -1
  311. package/dist/tools/securityTools.d.ts +0 -22
  312. package/dist/tools/securityTools.d.ts.map +0 -1
  313. package/dist/tools/securityTools.js +0 -448
  314. package/dist/tools/securityTools.js.map +0 -1
  315. package/dist/ui/persistentPrompt.d.ts +0 -50
  316. package/dist/ui/persistentPrompt.d.ts.map +0 -1
  317. package/dist/ui/persistentPrompt.js +0 -92
  318. package/dist/ui/persistentPrompt.js.map +0 -1
  319. package/dist/ui/terminalUISchema.d.ts +0 -195
  320. package/dist/ui/terminalUISchema.d.ts.map +0 -1
  321. package/dist/ui/terminalUISchema.js +0 -113
  322. package/dist/ui/terminalUISchema.js.map +0 -1
  323. package/scripts/deploy-security-capabilities.js +0 -178
@@ -3,15 +3,20 @@
3
3
  *
4
4
  * Design principles:
5
5
  * - Single source of truth for input state
6
+ * - Hybrid floating/scroll approach:
7
+ * - Initially: chat box floats below content
8
+ * - When terminal fills: scroll region activates, chat box pins to bottom
6
9
  * - Native bracketed paste support (no heuristics)
7
10
  * - Clean cursor model with render-time wrapping
8
11
  * - State machine for different input modes
9
12
  * - No readline dependency for display
10
13
  */
11
14
  import { EventEmitter } from 'node:events';
12
- import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
15
+ import { isMultilinePaste } from '../core/multilinePasteHandler.js';
13
16
  import { writeLock } from '../ui/writeLock.js';
14
- import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
17
+ import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
18
+ import { isStreamingMode } from '../ui/globalWriteLock.js';
19
+ import { formatThinking } from '../ui/toolDisplay.js';
15
20
  // ANSI escape codes
16
21
  const ESC = {
17
22
  // Cursor control
@@ -21,12 +26,12 @@ const ESC = {
21
26
  SHOW: '\x1b[?25h',
22
27
  TO: (row, col) => `\x1b[${row};${col}H`,
23
28
  TO_COL: (col) => `\x1b[${col}G`,
24
- // Screen control
25
- CLEAR_SCREEN: '\x1b[2J',
26
- HOME: '\x1b[H',
27
29
  // Line control
28
30
  CLEAR_LINE: '\x1b[2K',
29
31
  CLEAR_TO_END: '\x1b[0J',
32
+ // Scroll region
33
+ SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
34
+ RESET_SCROLL: '\x1b[r',
30
35
  // Style
31
36
  RESET: '\x1b[0m',
32
37
  DIM: '\x1b[2m',
@@ -66,47 +71,46 @@ export class TerminalInput extends EventEmitter {
66
71
  statusMessage = null;
67
72
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
68
73
  streamingLabel = null; // Streaming progress indicator
74
+ metaElapsedSeconds = null; // Optional elapsed time for header line
75
+ metaTokensUsed = null; // Optional token usage
76
+ metaTokenLimit = null; // Optional token window
77
+ metaThinkingMs = null; // Optional thinking duration
78
+ metaThinkingHasContent = false; // Whether collapsed thinking content exists
69
79
  lastRenderContent = '';
70
80
  lastRenderCursor = -1;
71
81
  renderDirty = false;
72
82
  isRendering = false;
73
- flowModeRenderedLines = 0; // Track lines rendered for clearing
74
- inputAreaStartRow = 0; // Track absolute row position of input area
75
- contentEndRow = 0; // Row where content ends (chat box renders below this)
76
- // Command suggestions (Claude Code style auto-complete)
77
- commandSuggestions = [];
78
- filteredSuggestions = [];
79
- selectedSuggestionIndex = 0;
80
- showSuggestions = false;
81
83
  // Lifecycle
82
84
  disposed = false;
83
85
  enabled = true;
84
86
  contextUsage = null;
87
+ contextAutoCompactThreshold = 90;
88
+ // Track current content row (starts at top, moves down)
89
+ contentRow = 1;
90
+ // Track if scroll region is currently active
91
+ scrollRegionActive = false;
92
+ thinkingModeLabel = null;
85
93
  editMode = 'display-edits';
86
94
  verificationEnabled = true;
87
95
  autoContinueEnabled = false;
88
96
  verificationHotkey = 'alt+v';
89
97
  autoContinueHotkey = 'alt+c';
90
- // Output interceptor cleanup
91
- outputInterceptorCleanup;
92
- // Metrics tracking for status bar
93
- streamingStartTime = null;
94
- thinkingEnabled = true;
95
- modelInfo = null; // Provider · Model info
96
- // Streaming input area render timer (updates elapsed time display)
98
+ thinkingHotkey = '/thinking';
99
+ modelLabel = null;
100
+ providerLabel = null;
101
+ // Streaming render throttle
102
+ lastStreamingRender = 0;
103
+ streamingRenderInterval = 250; // ms between renders during streaming
97
104
  streamingRenderTimer = null;
98
- // Unified UI initialization flag
99
- unifiedUIInitialized = false;
100
105
  constructor(writeStream = process.stdout, config = {}) {
101
106
  super();
102
107
  this.out = writeStream;
103
- // Use schema defaults for configuration consistency
104
108
  this.config = {
105
- maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
106
- maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
109
+ maxLines: config.maxLines ?? 1000,
110
+ maxLength: config.maxLength ?? 10000,
107
111
  maxQueueSize: config.maxQueueSize ?? 100,
108
- promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
109
- continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
112
+ promptChar: config.promptChar ?? '> ',
113
+ continuationChar: config.continuationChar ?? '│ ',
110
114
  };
111
115
  }
112
116
  // ===========================================================================
@@ -185,302 +189,36 @@ export class TerminalInput extends EventEmitter {
185
189
  if (handled)
186
190
  return;
187
191
  }
188
- // Handle '?' for help hint (if buffer is empty)
189
- if (str === '?' && this.buffer.length === 0) {
190
- this.emit('showHelp');
191
- return;
192
- }
193
192
  // Insert printable characters
194
193
  if (str && !key?.ctrl && !key?.meta) {
195
194
  this.insertText(str);
196
195
  }
197
196
  }
198
- // Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
199
- bannerContent = null;
200
- /**
201
- * Set banner content to be written when unified UI initializes.
202
- */
203
- setBannerContent(content) {
204
- this.bannerContent = content;
205
- }
206
- /**
207
- * Initialize the unified UI system.
208
- *
209
- * Layout:
210
- * 1. Clear screen
211
- * 2. Write banner at top
212
- * 3. Track content end position
213
- * 4. Render floating input area below banner
214
- */
215
- initializeUnifiedUI() {
216
- if (this.unifiedUIInitialized) {
217
- return;
218
- }
219
- // Hide cursor during setup
220
- this.write(ESC.HIDE);
221
- // Clear screen and go home
222
- this.write(ESC.HOME);
223
- this.write(ESC.CLEAR_SCREEN);
224
- // Write banner at top and track where it ends
225
- let bannerLines = 0;
226
- if (this.bannerContent) {
227
- const lines = this.bannerContent.split('\n');
228
- bannerLines = lines.length + 2; // +2 for the trailing \n\n
229
- process.stdout.write(this.bannerContent + '\n\n');
230
- }
231
- // Set content end row so input renders right after banner
232
- this.contentEndRow = bannerLines > 0 ? bannerLines : 1;
233
- // Mark initialized
234
- this.unifiedUIInitialized = true;
235
- // Render floating input area below the banner
236
- this.renderFloatingInputArea();
237
- }
238
- /**
239
- * Clear the input area at its tracked position.
240
- * Returns true if something was cleared.
241
- */
242
- clearInputArea() {
243
- if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
244
- for (let i = 0; i < this.flowModeRenderedLines; i++) {
245
- this.write(ESC.TO(this.inputAreaStartRow + i, 1));
246
- this.write(ESC.CLEAR_LINE);
247
- }
248
- return true;
249
- }
250
- return false;
251
- }
252
- /**
253
- * Reset input area tracking state.
254
- */
255
- resetInputAreaTracking() {
256
- this.inputAreaStartRow = 0;
257
- this.flowModeRenderedLines = 0;
258
- }
259
- /**
260
- * Render chat box - UNIFIED floating approach.
261
- * Both during and after streaming: chat box floats right below content.
262
- * Uses scroll region during streaming to protect chat box from content overwrites.
263
- */
264
- renderFloatingInputArea() {
265
- const { rows, cols } = this.getSize();
266
- const divider = '─'.repeat(cols - 1);
267
- const { dim: DIM, reset: R } = UI_COLORS;
268
- // Calculate lines needed for chat box
269
- const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
270
- // FIRST: Clear any previously rendered chat box
271
- this.clearInputArea();
272
- // Hide cursor during render
273
- this.write(ESC.HIDE);
274
- // Calculate where to render - ALWAYS float right below content
275
- let startRow;
276
- if (this.contentEndRow > 0) {
277
- // Float right below content (no wasted space)
278
- startRow = this.contentEndRow + 1;
279
- }
280
- else {
281
- // Default: start at row 1 (top of screen)
282
- startRow = 1;
283
- }
284
- // Clamp to ensure chat box fits in terminal
285
- // (never start so low that chat box would extend past terminal bottom)
286
- const maxStartRow = rows - linesNeeded + 1;
287
- startRow = Math.min(startRow, maxStartRow);
288
- startRow = Math.max(1, startRow);
289
- // During streaming: set scroll region to protect chat box area
290
- // Content writes in rows 1 to (startRow - 1), chat box at startRow onwards
291
- if (this.mode === 'streaming' && startRow > 1) {
292
- this.write(`\x1b[1;${startRow - 1}r`); // Set scroll region
293
- }
294
- else if (this.mode !== 'streaming') {
295
- this.write('\x1b[r'); // Reset scroll region when not streaming
296
- }
297
- // Track this position
298
- this.inputAreaStartRow = startRow;
299
- let currentRow = startRow;
300
- // Status bar
301
- this.write(ESC.TO(currentRow, 1));
302
- this.write(this.buildStatusBar(cols));
303
- currentRow++;
304
- // Model info line (if set)
305
- if (this.modelInfo) {
306
- this.write(ESC.TO(currentRow, 1));
307
- let modelLine = `${DIM}${this.modelInfo}${R}`;
308
- if (this.contextUsage !== null) {
309
- const rem = Math.max(0, 100 - this.contextUsage);
310
- if (rem < 10)
311
- modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
312
- else if (rem < 25)
313
- modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
314
- else
315
- modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
316
- }
317
- this.write(modelLine);
318
- currentRow++;
319
- }
320
- // Top divider
321
- this.write(ESC.TO(currentRow, 1));
322
- this.write(divider);
323
- currentRow++;
324
- // Input line with prompt and buffer content
325
- const { lines, cursorCol } = this.wrapBuffer(cols - 4);
326
- const displayLine = lines[0] ?? '';
327
- const inputRow = currentRow;
328
- this.write(ESC.TO(currentRow, 1));
329
- this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
330
- this.write(ESC.BG_DARK + displayLine);
331
- const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
332
- if (padding > 0)
333
- this.write(' '.repeat(padding));
334
- this.write(ESC.RESET);
335
- currentRow++;
336
- // Bottom divider
337
- this.write(ESC.TO(currentRow, 1));
338
- this.write(divider);
339
- currentRow++;
340
- // Mode controls
341
- this.write(ESC.TO(currentRow, 1));
342
- this.write(this.buildModeControls(cols));
343
- // Track lines rendered
344
- this.flowModeRenderedLines = currentRow - startRow + 1;
345
- // Position cursor in input line for typing
346
- this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
347
- // Show cursor
348
- this.write(ESC.SHOW);
349
- // Update tracking
350
- this.lastRenderContent = this.buffer;
351
- this.lastRenderCursor = this.cursor;
352
- }
353
197
  /**
354
198
  * Set the input mode
355
199
  *
356
- * UNIFIED FLOATING: Chat box always floats right below content.
357
- * During streaming: scroll region protects chat box area.
358
- * After streaming: scroll region reset, chat box floats below final content.
200
+ * Content flows naturally - no scroll region pinning.
359
201
  */
360
202
  setMode(mode) {
361
203
  const prevMode = this.mode;
362
204
  this.mode = mode;
363
205
  if (mode === 'streaming' && prevMode !== 'streaming') {
364
- // Track streaming start time for elapsed display
365
- this.streamingStartTime = Date.now();
366
- // Ensure unified UI is initialized
367
- if (!this.unifiedUIInitialized) {
368
- this.initializeUnifiedUI();
369
- }
206
+ this.resetStreamingRenderThrottle();
370
207
  this.renderDirty = true;
371
- this.scheduleRender();
208
+ this.render();
372
209
  }
373
210
  else if (mode !== 'streaming' && prevMode === 'streaming') {
374
- // Stop streaming render timer (if any)
375
- if (this.streamingRenderTimer) {
376
- clearInterval(this.streamingRenderTimer);
377
- this.streamingRenderTimer = null;
378
- }
379
- // Reset streaming time
380
- this.streamingStartTime = null;
381
- // CRITICAL: Reset scroll region when leaving streaming mode
382
- this.write('\x1b[r');
383
- // Re-render floating input area below content
384
- this.renderDirty = true;
385
- this.scheduleRender();
386
- }
387
- }
388
- /**
389
- * Set the row where content ends (for idle mode positioning).
390
- * Input area will render starting from this row + 1.
391
- */
392
- setContentEndRow(row) {
393
- this.contentEndRow = Math.max(0, row);
394
- this.renderDirty = true;
395
- this.scheduleRender();
396
- }
397
- /**
398
- * Set available slash commands for auto-complete suggestions.
399
- */
400
- setCommands(commands) {
401
- this.commandSuggestions = commands;
402
- this.updateSuggestions();
403
- }
404
- /**
405
- * Update filtered suggestions based on current input.
406
- */
407
- updateSuggestions() {
408
- const input = this.buffer.trim();
409
- // Only show suggestions when input starts with "/"
410
- if (!input.startsWith('/')) {
411
- this.showSuggestions = false;
412
- this.filteredSuggestions = [];
413
- this.selectedSuggestionIndex = 0;
414
- return;
211
+ // Streaming ended - render the input area
212
+ this.resetStreamingRenderThrottle();
213
+ this.forceRender();
415
214
  }
416
- const query = input.toLowerCase();
417
- this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
418
- cmd.command.toLowerCase().includes(query.slice(1)));
419
- // Show suggestions if we have matches
420
- this.showSuggestions = this.filteredSuggestions.length > 0;
421
- // Keep selection in bounds
422
- if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
423
- this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
424
- }
425
- }
426
- /**
427
- * Select next suggestion (arrow down / tab).
428
- */
429
- selectNextSuggestion() {
430
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
431
- return;
432
- this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
433
- this.renderDirty = true;
434
- this.scheduleRender();
435
- }
436
- /**
437
- * Select previous suggestion (arrow up / shift+tab).
438
- */
439
- selectPrevSuggestion() {
440
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
441
- return;
442
- this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
443
- ? this.filteredSuggestions.length - 1
444
- : this.selectedSuggestionIndex - 1;
445
- this.renderDirty = true;
446
- this.scheduleRender();
447
- }
448
- /**
449
- * Accept current suggestion and insert into buffer.
450
- */
451
- acceptSuggestion() {
452
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
453
- return false;
454
- const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
455
- if (!selected)
456
- return false;
457
- // Replace buffer with selected command
458
- this.buffer = selected.command + ' ';
459
- this.cursor = this.buffer.length;
460
- this.showSuggestions = false;
461
- this.renderDirty = true;
462
- this.scheduleRender();
463
- return true;
464
- }
465
- /**
466
- * Check if suggestions are visible.
467
- */
468
- areSuggestionsVisible() {
469
- return this.showSuggestions && this.filteredSuggestions.length > 0;
470
215
  }
471
216
  /**
472
- * Toggle thinking/reasoning mode
217
+ * Legacy method - no longer used (content flows naturally).
218
+ * @deprecated Use setContentRow instead
473
219
  */
474
- toggleThinking() {
475
- this.thinkingEnabled = !this.thinkingEnabled;
476
- this.emit('thinkingToggle', this.thinkingEnabled);
477
- this.scheduleRender();
478
- }
479
- /**
480
- * Get thinking enabled state
481
- */
482
- isThinkingEnabled() {
483
- return this.thinkingEnabled;
220
+ setPinnedHeaderLines(_count) {
221
+ // No-op: scroll region pinning removed
484
222
  }
485
223
  /**
486
224
  * Get current mode
@@ -513,17 +251,14 @@ export class TerminalInput extends EventEmitter {
513
251
  }
514
252
  /**
515
253
  * Clear the buffer
516
- * @param skipRender - If true, don't trigger a re-render (used during submit flow)
517
254
  */
518
- clear(skipRender = false) {
255
+ clear() {
519
256
  this.buffer = '';
520
257
  this.cursor = 0;
521
258
  this.historyIndex = -1;
522
259
  this.tempInput = '';
523
260
  this.pastePlaceholders = [];
524
- if (!skipRender) {
525
- this.scheduleRender();
526
- }
261
+ this.scheduleRender();
527
262
  }
528
263
  /**
529
264
  * Get queued inputs
@@ -594,6 +329,37 @@ export class TerminalInput extends EventEmitter {
594
329
  this.streamingLabel = next;
595
330
  this.scheduleRender();
596
331
  }
332
+ /**
333
+ * Surface meta status just above the divider (e.g., elapsed time or token usage).
334
+ */
335
+ setMetaStatus(meta) {
336
+ const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
337
+ ? Math.floor(meta.elapsedSeconds)
338
+ : null;
339
+ const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
340
+ ? Math.floor(meta.tokensUsed)
341
+ : null;
342
+ const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
343
+ ? Math.floor(meta.tokenLimit)
344
+ : null;
345
+ const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
346
+ ? Math.floor(meta.thinkingMs)
347
+ : null;
348
+ const nextThinkingHasContent = !!meta.thinkingHasContent;
349
+ if (this.metaElapsedSeconds === nextElapsed &&
350
+ this.metaTokensUsed === nextTokens &&
351
+ this.metaTokenLimit === nextLimit &&
352
+ this.metaThinkingMs === nextThinking &&
353
+ this.metaThinkingHasContent === nextThinkingHasContent) {
354
+ return;
355
+ }
356
+ this.metaElapsedSeconds = nextElapsed;
357
+ this.metaTokensUsed = nextTokens;
358
+ this.metaTokenLimit = nextLimit;
359
+ this.metaThinkingMs = nextThinking;
360
+ this.metaThinkingHasContent = nextThinkingHasContent;
361
+ this.scheduleRender();
362
+ }
597
363
  /**
598
364
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
599
365
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -603,26 +369,22 @@ export class TerminalInput extends EventEmitter {
603
369
  const nextAutoContinue = !!options.autoContinueEnabled;
604
370
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
605
371
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
372
+ const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
373
+ const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
606
374
  if (this.verificationEnabled === nextVerification &&
607
375
  this.autoContinueEnabled === nextAutoContinue &&
608
376
  this.verificationHotkey === nextVerifyHotkey &&
609
- this.autoContinueHotkey === nextAutoHotkey) {
377
+ this.autoContinueHotkey === nextAutoHotkey &&
378
+ this.thinkingHotkey === nextThinkingHotkey &&
379
+ this.thinkingModeLabel === nextThinkingLabel) {
610
380
  return;
611
381
  }
612
382
  this.verificationEnabled = nextVerification;
613
383
  this.autoContinueEnabled = nextAutoContinue;
614
384
  this.verificationHotkey = nextVerifyHotkey;
615
385
  this.autoContinueHotkey = nextAutoHotkey;
616
- this.scheduleRender();
617
- }
618
- /**
619
- * Set the model info string (e.g., "OpenAI · gpt-4")
620
- * This is displayed persistently above the input area.
621
- */
622
- setModelInfo(info) {
623
- if (this.modelInfo === info)
624
- return;
625
- this.modelInfo = info;
386
+ this.thinkingHotkey = nextThinkingHotkey;
387
+ this.thinkingModeLabel = nextThinkingLabel;
626
388
  this.scheduleRender();
627
389
  }
628
390
  /**
@@ -635,33 +397,159 @@ export class TerminalInput extends EventEmitter {
635
397
  this.scheduleRender();
636
398
  }
637
399
  /**
638
- * Render the input area.
639
- * During streaming: renders at terminal bottom (with scroll region)
640
- * After streaming: renders floating below content
400
+ * Surface model/provider context in the controls bar.
401
+ */
402
+ setModelContext(options) {
403
+ const nextModel = options.model?.trim() || null;
404
+ const nextProvider = options.provider?.trim() || null;
405
+ if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
406
+ return;
407
+ }
408
+ this.modelLabel = nextModel;
409
+ this.providerLabel = nextProvider;
410
+ this.scheduleRender();
411
+ }
412
+ /**
413
+ * Render the floating input area at contentRow.
414
+ *
415
+ * The chat box "floats" - it renders right below the last streamed content.
416
+ * As content is added, contentRow advances, and the chat box moves down.
417
+ * No scroll regions - pure floating behavior.
641
418
  */
642
419
  render() {
643
420
  if (!this.canRender())
644
421
  return;
645
422
  if (this.isRendering)
646
423
  return;
424
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
425
+ // During streaming, throttle re-renders
426
+ if (streamingActive && this.lastStreamingRender > 0) {
427
+ const elapsed = Date.now() - this.lastStreamingRender;
428
+ const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
429
+ if (waitMs > 0) {
430
+ this.renderDirty = true;
431
+ this.scheduleStreamingRender(waitMs);
432
+ return;
433
+ }
434
+ }
647
435
  const shouldSkip = !this.renderDirty &&
648
436
  this.buffer === this.lastRenderContent &&
649
437
  this.cursor === this.lastRenderCursor;
650
438
  this.renderDirty = false;
651
- // Skip if nothing changed (unless explicitly forced)
652
439
  if (shouldSkip) {
653
440
  return;
654
441
  }
655
- // If write lock is held, defer render
656
442
  if (writeLock.isLocked()) {
657
443
  writeLock.safeWrite(() => this.render());
658
444
  return;
659
445
  }
446
+ this.renderFloatingInputArea();
447
+ }
448
+ /**
449
+ * Core floating input area renderer.
450
+ * Chat box always floats at contentRow (below streamed content).
451
+ * This creates "persistent bottom floating" behavior.
452
+ */
453
+ renderFloatingInputArea() {
454
+ const { rows, cols } = this.getSize();
455
+ const maxWidth = Math.max(8, cols - 4);
456
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
457
+ // Wrap buffer into display lines
458
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
459
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
460
+ const displayLines = Math.min(lines.length, maxVisible);
461
+ const metaLines = this.buildMetaLines(cols - 2);
462
+ // Calculate display window (keep cursor visible)
463
+ let startLine = 0;
464
+ if (lines.length > displayLines) {
465
+ startLine = Math.max(0, cursorLine - displayLines + 1);
466
+ startLine = Math.min(startLine, lines.length - displayLines);
467
+ }
468
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
469
+ const adjustedCursorLine = cursorLine - startLine;
470
+ // Chat box height (must match getChatBoxHeight calculation)
471
+ const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
472
+ // Unified floating: chat box always at contentRow + 1
473
+ // When scroll region is active, contentRow is capped at maxContentRow
474
+ // so chat box ends up at the bottom but still "floats" below content
475
+ const chatBoxStartRow = this.contentRow + 1;
476
+ writeLock.lock('terminalInput.renderFloating');
660
477
  this.isRendering = true;
661
- writeLock.lock('terminalInput.render');
662
478
  try {
663
- // Always render floating right after content (no wasted space)
664
- this.renderFloatingInputArea();
479
+ // Hide cursor during render
480
+ this.write(ESC.HIDE);
481
+ this.write(ESC.RESET);
482
+ // Clear the chat box area
483
+ for (let i = 0; i < chatBoxHeight; i++) {
484
+ const row = chatBoxStartRow + i;
485
+ if (row <= rows) {
486
+ this.write(ESC.TO(row, 1));
487
+ this.write(ESC.CLEAR_LINE);
488
+ }
489
+ }
490
+ let currentRow = chatBoxStartRow;
491
+ // Meta/status header
492
+ for (const metaLine of metaLines) {
493
+ this.write(ESC.TO(currentRow, 1));
494
+ this.write(metaLine);
495
+ currentRow += 1;
496
+ }
497
+ // Separator line
498
+ this.write(ESC.TO(currentRow, 1));
499
+ this.write(renderDivider(cols - 2));
500
+ currentRow += 1;
501
+ // Render input lines
502
+ let finalRow = currentRow;
503
+ let finalCol = 3;
504
+ for (let i = 0; i < visibleLines.length; i++) {
505
+ const rowNum = currentRow + i;
506
+ this.write(ESC.TO(rowNum, 1));
507
+ const line = visibleLines[i] ?? '';
508
+ const isFirstLine = (startLine + i) === 0;
509
+ const isCursorLine = i === adjustedCursorLine;
510
+ this.write(ESC.BG_DARK);
511
+ this.write(ESC.DIM);
512
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
513
+ this.write(ESC.RESET);
514
+ this.write(ESC.BG_DARK);
515
+ if (isCursorLine) {
516
+ const col = Math.min(cursorCol, line.length);
517
+ const before = line.slice(0, col);
518
+ const at = col < line.length ? line[col] : ' ';
519
+ const after = col < line.length ? line.slice(col + 1) : '';
520
+ this.write(before);
521
+ this.write(ESC.REVERSE + ESC.BOLD);
522
+ this.write(at);
523
+ this.write(ESC.RESET + ESC.BG_DARK);
524
+ this.write(after);
525
+ finalRow = rowNum;
526
+ finalCol = this.config.promptChar.length + col + 1;
527
+ }
528
+ else {
529
+ this.write(line);
530
+ }
531
+ // Pad to edge
532
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
533
+ const padding = Math.max(0, cols - lineLen - 1);
534
+ if (padding > 0)
535
+ this.write(' '.repeat(padding));
536
+ this.write(ESC.RESET);
537
+ }
538
+ // Mode controls line
539
+ const controlRow = currentRow + visibleLines.length;
540
+ this.write(ESC.TO(controlRow, 1));
541
+ this.write(this.buildModeControls(cols));
542
+ // Position cursor in input box
543
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
544
+ this.write(ESC.SHOW);
545
+ // Update state
546
+ this.lastRenderContent = this.buffer;
547
+ this.lastRenderCursor = this.cursor;
548
+ this.lastStreamingRender = streamingActive ? Date.now() : 0;
549
+ if (this.streamingRenderTimer) {
550
+ clearTimeout(this.streamingRenderTimer);
551
+ this.streamingRenderTimer = null;
552
+ }
665
553
  }
666
554
  finally {
667
555
  writeLock.unlock();
@@ -669,99 +557,217 @@ export class TerminalInput extends EventEmitter {
669
557
  }
670
558
  }
671
559
  /**
672
- * Build status bar showing streaming/ready status and key info.
673
- * This is the TOP line above the input area - minimal Claude Code style.
560
+ * Build one or more compact meta lines above the divider (thinking, status, usage).
561
+ * During streaming, shows model line pinned above streaming info.
674
562
  */
675
- buildStatusBar(cols) {
676
- const maxWidth = cols - 2;
677
- const parts = [];
678
- // Streaming status with elapsed time (left side)
679
- if (this.mode === 'streaming') {
680
- let statusText = '● Streaming';
681
- if (this.streamingStartTime) {
682
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
683
- const mins = Math.floor(elapsed / 60);
684
- const secs = elapsed % 60;
685
- statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
563
+ buildMetaLines(width) {
564
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
565
+ const lines = [];
566
+ // Model line should ALWAYS be shown (pinned above streaming content)
567
+ if (this.modelLabel) {
568
+ const modelText = this.providerLabel
569
+ ? `model ${this.modelLabel} @ ${this.providerLabel}`
570
+ : `model ${this.modelLabel}`;
571
+ lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
572
+ }
573
+ // During streaming, add a compact status line with essential info
574
+ if (streamingActive) {
575
+ const parts = [];
576
+ // Essential streaming info
577
+ if (this.metaThinkingMs !== null) {
578
+ parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
686
579
  }
687
- parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
580
+ if (this.metaElapsedSeconds !== null) {
581
+ parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
582
+ }
583
+ parts.push({ text: 'esc to stop', tone: 'warn' });
584
+ if (parts.length) {
585
+ lines.push(renderStatusLine(parts, width));
586
+ }
587
+ return lines;
688
588
  }
689
- // Queue indicator during streaming
690
- if (this.mode === 'streaming' && this.queue.length > 0) {
691
- parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
589
+ // Non-streaming: show full status info (model line already added above)
590
+ if (this.metaThinkingMs !== null) {
591
+ const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
592
+ lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
692
593
  }
693
- // Paste indicator
694
- if (this.pastePlaceholders.length > 0) {
695
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
696
- parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
594
+ const statusParts = [];
595
+ const statusLabel = this.statusMessage ?? this.streamingLabel;
596
+ if (statusLabel) {
597
+ statusParts.push({ text: statusLabel, tone: 'info' });
697
598
  }
698
- // Override/warning status
699
- if (this.overrideStatusMessage) {
700
- parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
599
+ if (this.metaElapsedSeconds !== null) {
600
+ statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
701
601
  }
702
- // If idle with empty buffer, show quick shortcuts
703
- if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
704
- return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
602
+ const tokensRemaining = this.computeTokensRemaining();
603
+ if (tokensRemaining !== null) {
604
+ statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
705
605
  }
706
- // Multi-line indicator
707
- if (this.buffer.includes('\n')) {
708
- parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
606
+ if (statusParts.length) {
607
+ lines.push(renderStatusLine(statusParts, width));
608
+ }
609
+ const usageParts = [];
610
+ if (this.metaTokensUsed !== null) {
611
+ const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
612
+ const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
613
+ usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
614
+ }
615
+ if (this.contextUsage !== null) {
616
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
617
+ const left = Math.max(0, 100 - this.contextUsage);
618
+ usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
619
+ }
620
+ if (this.queue.length > 0) {
621
+ usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
709
622
  }
710
- if (parts.length === 0) {
711
- return ''; // Empty status bar when idle
623
+ if (usageParts.length) {
624
+ lines.push(renderStatusLine(usageParts, width));
712
625
  }
713
- const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
714
- return joined.slice(0, maxWidth);
626
+ return lines;
715
627
  }
716
628
  /**
717
- * Build mode controls line showing toggles and context info.
718
- * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
719
- *
720
- * Layout: [toggles on left] ... [context info on right]
629
+ * Build Claude Code style mode controls line.
630
+ * Combines streaming label + override status + main status for simultaneous display.
721
631
  */
722
632
  buildModeControls(cols) {
723
- const maxWidth = cols - 2;
724
- // Use schema-defined colors for consistency
725
- const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
726
- // Mode toggles with colors (following ModeControlsSchema)
727
- const toggles = [];
728
- // Edit mode (green=auto, yellow=ask) - per schema.editMode
729
- if (this.editMode === 'display-edits') {
730
- toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
633
+ const width = Math.max(8, cols - 2);
634
+ const leftParts = [];
635
+ const rightParts = [];
636
+ if (this.streamingLabel) {
637
+ leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
731
638
  }
732
- else {
733
- toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
734
- }
735
- // Thinking mode (cyan when on) - per schema.thinkingMode
736
- toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
737
- // Verification (green when on) - per schema.verificationMode
738
- toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
739
- // Auto-continue (magenta when on) - per schema.autoContinueMode
740
- toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
741
- const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
742
- // Context usage with color - per schema.contextUsage thresholds
743
- let rightPart = '';
744
- if (this.contextUsage !== null) {
745
- const rem = Math.max(0, 100 - this.contextUsage);
746
- // Thresholds: critical < 10%, warning < 25%
747
- if (rem < 10)
748
- rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
749
- else if (rem < 25)
750
- rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
751
- else
752
- rightPart = `${DIM}ctx: ${rem}%${R}`;
753
- }
754
- // Calculate visible lengths (strip ANSI)
755
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
756
- const leftLen = strip(leftPart).length;
757
- const rightLen = strip(rightPart).length;
758
- if (leftLen + rightLen < maxWidth - 4) {
759
- return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
760
- }
761
- if (rightLen > 0 && leftLen + 8 < maxWidth) {
762
- return `${leftPart} ${rightPart}`;
763
- }
764
- return leftPart;
639
+ if (this.overrideStatusMessage) {
640
+ leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
641
+ }
642
+ if (this.statusMessage) {
643
+ leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
644
+ }
645
+ const editHotkey = this.formatHotkey('shift+tab');
646
+ const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
647
+ const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
648
+ leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
649
+ const verifyHotkey = this.formatHotkey(this.verificationHotkey);
650
+ const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
651
+ leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
652
+ const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
653
+ const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
654
+ leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
655
+ if (this.queue.length > 0 && this.mode !== 'streaming') {
656
+ leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
657
+ }
658
+ if (this.buffer.includes('\n')) {
659
+ const lineCount = this.buffer.split('\n').length;
660
+ leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
661
+ }
662
+ if (this.pastePlaceholders.length > 0) {
663
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
664
+ leftParts.push({
665
+ text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
666
+ tone: 'info',
667
+ });
668
+ }
669
+ const contextRemaining = this.computeContextRemaining();
670
+ if (this.thinkingModeLabel) {
671
+ const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
672
+ rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
673
+ }
674
+ // Show model in controls only when NOT streaming (during streaming it's in meta lines)
675
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
676
+ if (this.modelLabel && !streamingActive) {
677
+ const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
678
+ rightParts.push({ text: modelText, tone: 'muted' });
679
+ }
680
+ if (contextRemaining !== null) {
681
+ const tone = contextRemaining <= 10 ? 'warn' : 'muted';
682
+ const label = contextRemaining === 0 && this.contextUsage !== null
683
+ ? 'Context auto-compact imminent'
684
+ : `Context left until auto-compact: ${contextRemaining}%`;
685
+ rightParts.push({ text: label, tone });
686
+ }
687
+ if (!rightParts.length || width < 60) {
688
+ const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
689
+ return renderStatusLine(merged, width);
690
+ }
691
+ const leftWidth = Math.max(12, Math.floor(width * 0.6));
692
+ const rightWidth = Math.max(14, width - leftWidth - 1);
693
+ const leftText = renderStatusLine(leftParts, leftWidth);
694
+ const rightText = renderStatusLine(rightParts, rightWidth);
695
+ const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
696
+ return `${leftText}${' '.repeat(spacing)}${rightText}`;
697
+ }
698
+ formatHotkey(hotkey) {
699
+ const normalized = hotkey.trim().toLowerCase();
700
+ if (!normalized)
701
+ return hotkey;
702
+ const parts = normalized.split('+').filter(Boolean);
703
+ const map = {
704
+ shift: '⇧',
705
+ sh: '⇧',
706
+ alt: '⌥',
707
+ option: '⌥',
708
+ opt: '⌥',
709
+ ctrl: '⌃',
710
+ control: '⌃',
711
+ cmd: '⌘',
712
+ meta: '⌘',
713
+ };
714
+ const formatted = parts
715
+ .map((part) => {
716
+ const symbol = map[part];
717
+ if (symbol)
718
+ return symbol;
719
+ return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
720
+ })
721
+ .join('');
722
+ return formatted || hotkey;
723
+ }
724
+ computeContextRemaining() {
725
+ if (this.contextUsage === null) {
726
+ return null;
727
+ }
728
+ return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
729
+ }
730
+ computeTokensRemaining() {
731
+ if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
732
+ return null;
733
+ }
734
+ const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
735
+ return this.formatTokenCount(remaining);
736
+ }
737
+ formatElapsedLabel(seconds) {
738
+ if (seconds < 60) {
739
+ return `${seconds}s`;
740
+ }
741
+ const mins = Math.floor(seconds / 60);
742
+ const secs = seconds % 60;
743
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
744
+ }
745
+ formatTokenCount(value) {
746
+ if (!Number.isFinite(value)) {
747
+ return `${value}`;
748
+ }
749
+ if (value >= 1_000_000) {
750
+ return `${(value / 1_000_000).toFixed(1)}M`;
751
+ }
752
+ if (value >= 1_000) {
753
+ return `${(value / 1_000).toFixed(1)}k`;
754
+ }
755
+ return `${Math.round(value)}`;
756
+ }
757
+ visibleLength(value) {
758
+ const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
759
+ return value.replace(ansiPattern, '').length;
760
+ }
761
+ /**
762
+ * Debug-only snapshot used by tests to assert rendered strings without
763
+ * needing a TTY. Not used by production code.
764
+ */
765
+ getDebugUiSnapshot(width) {
766
+ const cols = Math.max(8, width ?? this.getSize().cols);
767
+ return {
768
+ meta: this.buildMetaLines(cols - 2),
769
+ controls: this.buildModeControls(cols),
770
+ };
765
771
  }
766
772
  /**
767
773
  * Force a re-render
@@ -784,32 +790,108 @@ export class TerminalInput extends EventEmitter {
784
790
  handleResize() {
785
791
  this.lastRenderContent = '';
786
792
  this.lastRenderCursor = -1;
793
+ this.resetStreamingRenderThrottle();
787
794
  this.scheduleRender();
788
795
  }
789
796
  /**
790
- * Register with display's output interceptor.
791
- * During streaming: ensures scroll region protects chat box before writes.
792
- * After streaming: no-op (chat box floats naturally).
797
+ * Stream content with floating chat box.
798
+ *
799
+ * Clean approach - no scroll regions, just cursor positioning:
800
+ * 1. Save cursor state
801
+ * 2. Clear chat box area (it will be re-rendered)
802
+ * 3. Position at contentRow
803
+ * 4. Write content
804
+ * 5. Advance contentRow
805
+ * 6. Re-render chat box
793
806
  */
794
- registerOutputInterceptor(display) {
795
- if (this.outputInterceptorCleanup) {
796
- this.outputInterceptorCleanup();
797
- }
798
- this.outputInterceptorCleanup = display.registerOutputInterceptor({
799
- beforeWrite: () => {
800
- // During streaming, ensure scroll region is set to protect chat box
801
- if (this.mode === 'streaming' && this.inputAreaStartRow > 1) {
802
- this.write(`\x1b[1;${this.inputAreaStartRow - 1}r`);
803
- }
804
- },
805
- afterWrite: () => {
806
- // Re-render chat box after content writes during streaming
807
- if (this.mode === 'streaming') {
808
- this.renderDirty = true;
809
- this.scheduleRender();
810
- }
811
- },
812
- });
807
+ streamContent(content) {
808
+ if (!content)
809
+ return;
810
+ writeLock.lock('streamContent');
811
+ try {
812
+ // Save cursor and hide it
813
+ this.write(ESC.SAVE);
814
+ this.write(ESC.HIDE);
815
+ // Clear the chat box area first (it will be re-rendered after)
816
+ const { rows, cols } = this.getSize();
817
+ const chatBoxHeight = 6; // Approximate
818
+ const chatBoxStart = this.contentRow + 1;
819
+ for (let i = 0; i < chatBoxHeight && chatBoxStart + i <= rows; i++) {
820
+ this.write(ESC.TO(chatBoxStart + i, 1));
821
+ this.write(ESC.CLEAR_LINE);
822
+ }
823
+ // Position at contentRow and write content
824
+ this.write(ESC.TO(this.contentRow, 1));
825
+ this.write(content);
826
+ // Count newlines and advance contentRow
827
+ const newlines = (content.match(/\n/g) || []).length;
828
+ this.contentRow += newlines;
829
+ // Cap contentRow to leave room for chat box
830
+ const maxContentRow = Math.max(1, rows - chatBoxHeight);
831
+ if (this.contentRow > maxContentRow) {
832
+ this.contentRow = maxContentRow;
833
+ }
834
+ // Restore cursor
835
+ this.write(ESC.RESTORE);
836
+ this.write(ESC.SHOW);
837
+ }
838
+ finally {
839
+ writeLock.unlock();
840
+ }
841
+ // Re-render chat box at new position
842
+ this.forceRender();
843
+ }
844
+ /**
845
+ * Enable scroll region (no-op in floating mode).
846
+ */
847
+ enableScrollRegion() {
848
+ // No-op: using pure floating approach
849
+ }
850
+ /**
851
+ * Disable scroll region (no-op in floating mode).
852
+ */
853
+ disableScrollRegion() {
854
+ // No-op: using pure floating approach
855
+ }
856
+ /**
857
+ * Calculate chat box height.
858
+ */
859
+ getChatBoxHeight() {
860
+ return 6; // Fixed: meta + divider + input + controls + buffer
861
+ }
862
+ /**
863
+ * @deprecated Use streamContent() instead
864
+ * Register with display's output interceptor - kept for backwards compatibility
865
+ */
866
+ registerOutputInterceptor(_display) {
867
+ // No-op: Use streamContent() for cleaner floating chat box behavior
868
+ }
869
+ /**
870
+ * @deprecated Use streamContent() instead
871
+ * Write content above the floating chat box.
872
+ */
873
+ writeToScrollRegion(content) {
874
+ this.streamContent(content);
875
+ }
876
+ /**
877
+ * Reset content position to row 1.
878
+ * Does NOT clear the terminal - content starts from current position.
879
+ */
880
+ resetContentPosition() {
881
+ this.contentRow = 1;
882
+ }
883
+ /**
884
+ * Set the content row explicitly (used after banner is written).
885
+ * This tells the input where content should start flowing from.
886
+ */
887
+ setContentRow(row) {
888
+ this.contentRow = Math.max(1, row);
889
+ }
890
+ /**
891
+ * Get the current content row position.
892
+ */
893
+ getContentRow() {
894
+ return this.contentRow;
813
895
  }
814
896
  /**
815
897
  * Dispose and clean up
@@ -817,20 +899,10 @@ export class TerminalInput extends EventEmitter {
817
899
  dispose() {
818
900
  if (this.disposed)
819
901
  return;
820
- // Clean up streaming render timer
821
- if (this.streamingRenderTimer) {
822
- clearInterval(this.streamingRenderTimer);
823
- this.streamingRenderTimer = null;
824
- }
825
- // Clean up output interceptor
826
- if (this.outputInterceptorCleanup) {
827
- this.outputInterceptorCleanup();
828
- this.outputInterceptorCleanup = undefined;
829
- }
830
- // Reset scroll region before disposing
831
- this.write('\x1b[r');
832
902
  this.disposed = true;
833
903
  this.enabled = false;
904
+ this.disableScrollRegion();
905
+ this.resetStreamingRenderThrottle();
834
906
  this.disableBracketedPaste();
835
907
  this.buffer = '';
836
908
  this.queue = [];
@@ -935,22 +1007,7 @@ export class TerminalInput extends EventEmitter {
935
1007
  this.toggleEditMode();
936
1008
  return true;
937
1009
  }
938
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
939
- if (this.findPlaceholderAt(this.cursor)) {
940
- this.togglePasteExpansion();
941
- }
942
- else {
943
- this.toggleThinking();
944
- }
945
- return true;
946
- case 'escape':
947
- // Esc: interrupt if streaming, otherwise clear buffer
948
- if (this.mode === 'streaming') {
949
- this.emit('interrupt');
950
- }
951
- else if (this.buffer.length > 0) {
952
- this.clear();
953
- }
1010
+ this.insertText(' ');
954
1011
  return true;
955
1012
  }
956
1013
  return false;
@@ -968,7 +1025,6 @@ export class TerminalInput extends EventEmitter {
968
1025
  this.insertPlainText(chunk, insertPos);
969
1026
  this.cursor = insertPos + chunk.length;
970
1027
  this.emit('change', this.buffer);
971
- this.updateSuggestions();
972
1028
  this.scheduleRender();
973
1029
  }
974
1030
  insertNewline() {
@@ -993,7 +1049,6 @@ export class TerminalInput extends EventEmitter {
993
1049
  this.cursor = Math.max(0, this.cursor - 1);
994
1050
  }
995
1051
  this.emit('change', this.buffer);
996
- this.updateSuggestions();
997
1052
  this.scheduleRender();
998
1053
  }
999
1054
  deleteForward() {
@@ -1221,13 +1276,12 @@ export class TerminalInput extends EventEmitter {
1221
1276
  timestamp: Date.now(),
1222
1277
  });
1223
1278
  this.emit('queue', text);
1224
- this.clear(); // Clear immediately for queued input, re-render to update queue display
1279
+ this.clear(); // Clear immediately for queued input
1225
1280
  }
1226
1281
  else {
1227
- // In idle mode, clear the input WITHOUT rendering.
1228
- // The caller will display the user message and start streaming.
1229
- // We'll render the input area again after streaming ends.
1230
- this.clear(true); // Skip render - streaming will handle display
1282
+ // In idle mode, clear the input first, then emit submit.
1283
+ // The prompt will be logged as a visible message by the caller.
1284
+ this.clear();
1231
1285
  this.emit('submit', text);
1232
1286
  }
1233
1287
  }
@@ -1244,7 +1298,9 @@ export class TerminalInput extends EventEmitter {
1244
1298
  if (available <= 0)
1245
1299
  return;
1246
1300
  const chunk = clean.slice(0, available);
1247
- if (isMultilinePaste(chunk)) {
1301
+ const isMultiline = isMultilinePaste(chunk);
1302
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1303
+ if (isMultiline && !isShortMultiline) {
1248
1304
  this.insertPastePlaceholder(chunk);
1249
1305
  }
1250
1306
  else {
@@ -1380,17 +1436,19 @@ export class TerminalInput extends EventEmitter {
1380
1436
  this.shiftPlaceholders(position, text.length);
1381
1437
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1382
1438
  }
1439
+ shouldInlineMultiline(content) {
1440
+ const lines = content.split('\n').length;
1441
+ const maxInlineLines = 4;
1442
+ const maxInlineChars = 240;
1443
+ return lines <= maxInlineLines && content.length <= maxInlineChars;
1444
+ }
1383
1445
  findPlaceholderAt(position) {
1384
1446
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1385
1447
  }
1386
- buildPlaceholder(summary) {
1448
+ buildPlaceholder(lineCount) {
1387
1449
  const id = ++this.pasteCounter;
1388
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1389
- // Show first line preview (truncated)
1390
- const preview = summary.preview.length > 30
1391
- ? `${summary.preview.slice(0, 30)}...`
1392
- : summary.preview;
1393
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1450
+ const plural = lineCount === 1 ? '' : 's';
1451
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1394
1452
  return { id, placeholder };
1395
1453
  }
1396
1454
  insertPastePlaceholder(content) {
@@ -1398,67 +1456,21 @@ export class TerminalInput extends EventEmitter {
1398
1456
  if (available <= 0)
1399
1457
  return;
1400
1458
  const cleanContent = content.slice(0, available);
1401
- const summary = generatePasteSummary(cleanContent);
1402
- // For short pastes (< 5 lines), show full content instead of placeholder
1403
- if (summary.lineCount < 5) {
1404
- const placeholder = this.findPlaceholderAt(this.cursor);
1405
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1406
- this.insertPlainText(cleanContent, insertPos);
1407
- this.cursor = insertPos + cleanContent.length;
1408
- return;
1409
- }
1410
- const { id, placeholder } = this.buildPlaceholder(summary);
1459
+ const lineCount = cleanContent.split('\n').length;
1460
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1411
1461
  const insertPos = this.cursor;
1412
1462
  this.shiftPlaceholders(insertPos, placeholder.length);
1413
1463
  this.pastePlaceholders.push({
1414
1464
  id,
1415
1465
  content: cleanContent,
1416
- lineCount: summary.lineCount,
1466
+ lineCount,
1417
1467
  placeholder,
1418
1468
  start: insertPos,
1419
1469
  end: insertPos + placeholder.length,
1420
- summary,
1421
- expanded: false,
1422
1470
  });
1423
1471
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1424
1472
  this.cursor = insertPos + placeholder.length;
1425
1473
  }
1426
- /**
1427
- * Toggle expansion of a paste placeholder at the current cursor position.
1428
- * When expanded, shows first 3 and last 2 lines of the content.
1429
- */
1430
- togglePasteExpansion() {
1431
- const placeholder = this.findPlaceholderAt(this.cursor);
1432
- if (!placeholder)
1433
- return false;
1434
- placeholder.expanded = !placeholder.expanded;
1435
- // Update the placeholder text in buffer
1436
- const newPlaceholder = placeholder.expanded
1437
- ? this.buildExpandedPlaceholder(placeholder)
1438
- : this.buildPlaceholder(placeholder.summary).placeholder;
1439
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1440
- // Update buffer
1441
- this.buffer =
1442
- this.buffer.slice(0, placeholder.start) +
1443
- newPlaceholder +
1444
- this.buffer.slice(placeholder.end);
1445
- // Update placeholder tracking
1446
- placeholder.placeholder = newPlaceholder;
1447
- placeholder.end = placeholder.start + newPlaceholder.length;
1448
- // Shift other placeholders
1449
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1450
- this.scheduleRender();
1451
- return true;
1452
- }
1453
- buildExpandedPlaceholder(ph) {
1454
- const lines = ph.content.split('\n');
1455
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1456
- const lastLines = lines.length > 5
1457
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1458
- : '';
1459
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1460
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1461
- }
1462
1474
  deletePlaceholder(placeholder) {
1463
1475
  const length = placeholder.end - placeholder.start;
1464
1476
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1466,7 +1478,11 @@ export class TerminalInput extends EventEmitter {
1466
1478
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1467
1479
  this.cursor = placeholder.start;
1468
1480
  }
1469
- updateContextUsage(value) {
1481
+ updateContextUsage(value, autoCompactThreshold) {
1482
+ if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1483
+ const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1484
+ this.contextAutoCompactThreshold = boundedThreshold;
1485
+ }
1470
1486
  if (value === null || !Number.isFinite(value)) {
1471
1487
  this.contextUsage = null;
1472
1488
  }
@@ -1493,6 +1509,22 @@ export class TerminalInput extends EventEmitter {
1493
1509
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1494
1510
  this.setEditMode(next);
1495
1511
  }
1512
+ scheduleStreamingRender(delayMs) {
1513
+ if (this.streamingRenderTimer)
1514
+ return;
1515
+ const wait = Math.max(16, delayMs);
1516
+ this.streamingRenderTimer = setTimeout(() => {
1517
+ this.streamingRenderTimer = null;
1518
+ this.render();
1519
+ }, wait);
1520
+ }
1521
+ resetStreamingRenderThrottle() {
1522
+ if (this.streamingRenderTimer) {
1523
+ clearTimeout(this.streamingRenderTimer);
1524
+ this.streamingRenderTimer = null;
1525
+ }
1526
+ this.lastStreamingRender = 0;
1527
+ }
1496
1528
  scheduleRender() {
1497
1529
  if (!this.canRender())
1498
1530
  return;