erosolar-cli 1.7.314 → 1.7.315

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 +5 -21
  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 +229 -164
  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 +131 -116
  74. package/dist/shell/terminalInput.d.ts.map +1 -1
  75. package/dist/shell/terminalInput.js +571 -531
  76. package/dist/shell/terminalInput.js.map +1 -1
  77. package/dist/shell/terminalInputAdapter.d.ts +61 -20
  78. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  79. package/dist/shell/terminalInputAdapter.js +73 -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,15 @@ 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
+ // Screen control
33
+ HOME: '\x1b[H',
34
+ CLEAR_SCREEN: '\x1b[2J',
35
+ // Scroll region
36
+ SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
37
+ RESET_SCROLL: '\x1b[r',
30
38
  // Style
31
39
  RESET: '\x1b[0m',
32
40
  DIM: '\x1b[2m',
@@ -66,47 +74,46 @@ export class TerminalInput extends EventEmitter {
66
74
  statusMessage = null;
67
75
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
68
76
  streamingLabel = null; // Streaming progress indicator
77
+ metaElapsedSeconds = null; // Optional elapsed time for header line
78
+ metaTokensUsed = null; // Optional token usage
79
+ metaTokenLimit = null; // Optional token window
80
+ metaThinkingMs = null; // Optional thinking duration
81
+ metaThinkingHasContent = false; // Whether collapsed thinking content exists
69
82
  lastRenderContent = '';
70
83
  lastRenderCursor = -1;
71
84
  renderDirty = false;
72
85
  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
86
  // Lifecycle
82
87
  disposed = false;
83
88
  enabled = true;
84
89
  contextUsage = null;
90
+ contextAutoCompactThreshold = 90;
91
+ // Track current content row (starts at top, moves down)
92
+ contentRow = 1;
93
+ // Track if scroll region is currently active
94
+ scrollRegionActive = false;
95
+ thinkingModeLabel = null;
85
96
  editMode = 'display-edits';
86
97
  verificationEnabled = true;
87
98
  autoContinueEnabled = false;
88
99
  verificationHotkey = 'alt+v';
89
100
  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)
101
+ thinkingHotkey = '/thinking';
102
+ modelLabel = null;
103
+ providerLabel = null;
104
+ // Streaming render throttle
105
+ lastStreamingRender = 0;
106
+ streamingRenderInterval = 250; // ms between renders during streaming
97
107
  streamingRenderTimer = null;
98
- // Unified UI initialization flag
99
- unifiedUIInitialized = false;
100
108
  constructor(writeStream = process.stdout, config = {}) {
101
109
  super();
102
110
  this.out = writeStream;
103
- // Use schema defaults for configuration consistency
104
111
  this.config = {
105
- maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
106
- maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
112
+ maxLines: config.maxLines ?? 1000,
113
+ maxLength: config.maxLength ?? 10000,
107
114
  maxQueueSize: config.maxQueueSize ?? 100,
108
- promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
109
- continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
115
+ promptChar: config.promptChar ?? '> ',
116
+ continuationChar: config.continuationChar ?? '│ ',
110
117
  };
111
118
  }
112
119
  // ===========================================================================
@@ -185,314 +192,36 @@ export class TerminalInput extends EventEmitter {
185
192
  if (handled)
186
193
  return;
187
194
  }
188
- // Handle '?' for help hint (if buffer is empty)
189
- if (str === '?' && this.buffer.length === 0) {
190
- this.emit('showHelp');
191
- return;
192
- }
193
195
  // Insert printable characters
194
196
  if (str && !key?.ctrl && !key?.meta) {
195
197
  this.insertText(str);
196
198
  }
197
199
  }
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 - Claude Code style floating input.
261
- * Clean, minimal design with dividers and mode controls.
262
- */
263
- renderFloatingInputArea() {
264
- const { rows, cols } = this.getSize();
265
- const divider = '─'.repeat(cols);
266
- const { dim: DIM, reset: R } = UI_COLORS;
267
- // Calculate lines needed: status (optional) + divider + input + divider + controls
268
- const hasStatus = this.mode === 'streaming' || this.statusMessage;
269
- const linesNeeded = (hasStatus ? 1 : 0) + 4; // status + divider + input + divider + controls
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
- startRow = this.contentEndRow + 1;
278
- }
279
- else {
280
- startRow = 1;
281
- }
282
- // Clamp to terminal bounds
283
- const maxStartRow = rows - linesNeeded + 1;
284
- startRow = Math.min(startRow, maxStartRow);
285
- startRow = Math.max(1, startRow);
286
- // Track this position
287
- this.inputAreaStartRow = startRow;
288
- let currentRow = startRow;
289
- // Streaming status line (above input area)
290
- if (this.mode === 'streaming') {
291
- this.write(ESC.TO(currentRow, 1));
292
- let statusText = '✻ Streaming';
293
- if (this.streamingStartTime) {
294
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
295
- const mins = Math.floor(elapsed / 60);
296
- const secs = elapsed % 60;
297
- statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
298
- }
299
- statusText += '… (esc to interrupt)';
300
- this.write(`${UI_COLORS.cyan}${statusText}${R}`);
301
- currentRow++;
302
- }
303
- else if (this.statusMessage) {
304
- this.write(ESC.TO(currentRow, 1));
305
- this.write(`${DIM}${this.statusMessage}${R}`);
306
- currentRow++;
307
- }
308
- // Top divider
309
- this.write(ESC.TO(currentRow, 1));
310
- this.write(`${DIM}${divider}${R}`);
311
- currentRow++;
312
- // Input line with > prompt
313
- const { lines, cursorCol } = this.wrapBuffer(cols - 3);
314
- const displayLine = lines[0] ?? '';
315
- const inputRow = currentRow;
316
- this.write(ESC.TO(currentRow, 1));
317
- this.write(`${DIM}>${R} ${displayLine}`);
318
- currentRow++;
319
- // Bottom divider
320
- this.write(ESC.TO(currentRow, 1));
321
- this.write(`${DIM}${divider}${R}`);
322
- currentRow++;
323
- // Mode controls line - Claude Code style
324
- this.write(ESC.TO(currentRow, 1));
325
- this.write(this.buildClaudeStyleControls(cols));
326
- // Track lines rendered
327
- this.flowModeRenderedLines = currentRow - startRow + 1;
328
- // Position cursor in input line
329
- this.write(ESC.TO(inputRow, 3 + cursorCol)); // "> " = 2 chars + 1 for position
330
- // Show cursor
331
- this.write(ESC.SHOW);
332
- // Update tracking
333
- this.lastRenderContent = this.buffer;
334
- this.lastRenderCursor = this.cursor;
335
- }
336
- /**
337
- * Build Claude Code style controls line.
338
- * Shows: edit mode indicator (shift+tab to cycle)
339
- */
340
- buildClaudeStyleControls(cols) {
341
- const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
342
- // Edit mode indicator
343
- let editModeText;
344
- if (this.editMode === 'display-edits') {
345
- editModeText = `${GREEN}⏵⏵${R} accept edits on`;
346
- }
347
- else {
348
- editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
349
- }
350
- // Build controls line
351
- const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
352
- // Add thinking mode if enabled
353
- if (this.thinkingEnabled) {
354
- parts.push(`${CYAN}💭${R}`);
355
- }
356
- // Add context usage if available
357
- if (this.contextUsage !== null) {
358
- const rem = Math.max(0, 100 - this.contextUsage);
359
- if (rem < 10) {
360
- parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
361
- }
362
- else if (rem < 25) {
363
- parts.push(`${YELLOW}ctx ${rem}%${R}`);
364
- }
365
- }
366
- return parts.join(` ${DIM}·${R} `);
367
- }
368
200
  /**
369
201
  * Set the input mode
370
202
  *
371
- * UNIFIED FLOATING: Chat box always floats right below content.
372
- * No scroll regions - pure floating with clear and re-render.
203
+ * Content flows naturally - no scroll region pinning.
373
204
  */
374
205
  setMode(mode) {
375
206
  const prevMode = this.mode;
376
207
  this.mode = mode;
377
208
  if (mode === 'streaming' && prevMode !== 'streaming') {
378
- // Track streaming start time for elapsed display
379
- this.streamingStartTime = Date.now();
380
- // Ensure unified UI is initialized
381
- if (!this.unifiedUIInitialized) {
382
- this.initializeUnifiedUI();
383
- }
209
+ this.resetStreamingRenderThrottle();
384
210
  this.renderDirty = true;
385
- this.scheduleRender();
211
+ this.render();
386
212
  }
387
213
  else if (mode !== 'streaming' && prevMode === 'streaming') {
388
- // Stop streaming render timer (if any)
389
- if (this.streamingRenderTimer) {
390
- clearInterval(this.streamingRenderTimer);
391
- this.streamingRenderTimer = null;
392
- }
393
- // Reset streaming time
394
- this.streamingStartTime = null;
395
- // Re-render floating input area below content
396
- this.renderDirty = true;
397
- this.scheduleRender();
214
+ // Streaming ended - render the input area
215
+ this.resetStreamingRenderThrottle();
216
+ this.forceRender();
398
217
  }
399
218
  }
400
219
  /**
401
- * Set the row where content ends (for idle mode positioning).
402
- * Input area will render starting from this row + 1.
403
- */
404
- setContentEndRow(row) {
405
- this.contentEndRow = Math.max(0, row);
406
- this.renderDirty = true;
407
- this.scheduleRender();
408
- }
409
- /**
410
- * Set available slash commands for auto-complete suggestions.
220
+ * Legacy method - no longer used (content flows naturally).
221
+ * @deprecated Use setContentRow instead
411
222
  */
412
- setCommands(commands) {
413
- this.commandSuggestions = commands;
414
- this.updateSuggestions();
415
- }
416
- /**
417
- * Update filtered suggestions based on current input.
418
- */
419
- updateSuggestions() {
420
- const input = this.buffer.trim();
421
- // Only show suggestions when input starts with "/"
422
- if (!input.startsWith('/')) {
423
- this.showSuggestions = false;
424
- this.filteredSuggestions = [];
425
- this.selectedSuggestionIndex = 0;
426
- return;
427
- }
428
- const query = input.toLowerCase();
429
- this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
430
- cmd.command.toLowerCase().includes(query.slice(1)));
431
- // Show suggestions if we have matches
432
- this.showSuggestions = this.filteredSuggestions.length > 0;
433
- // Keep selection in bounds
434
- if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
435
- this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
436
- }
437
- }
438
- /**
439
- * Select next suggestion (arrow down / tab).
440
- */
441
- selectNextSuggestion() {
442
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
443
- return;
444
- this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
445
- this.renderDirty = true;
446
- this.scheduleRender();
447
- }
448
- /**
449
- * Select previous suggestion (arrow up / shift+tab).
450
- */
451
- selectPrevSuggestion() {
452
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
453
- return;
454
- this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
455
- ? this.filteredSuggestions.length - 1
456
- : this.selectedSuggestionIndex - 1;
457
- this.renderDirty = true;
458
- this.scheduleRender();
459
- }
460
- /**
461
- * Accept current suggestion and insert into buffer.
462
- */
463
- acceptSuggestion() {
464
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
465
- return false;
466
- const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
467
- if (!selected)
468
- return false;
469
- // Replace buffer with selected command
470
- this.buffer = selected.command + ' ';
471
- this.cursor = this.buffer.length;
472
- this.showSuggestions = false;
473
- this.renderDirty = true;
474
- this.scheduleRender();
475
- return true;
476
- }
477
- /**
478
- * Check if suggestions are visible.
479
- */
480
- areSuggestionsVisible() {
481
- return this.showSuggestions && this.filteredSuggestions.length > 0;
482
- }
483
- /**
484
- * Toggle thinking/reasoning mode
485
- */
486
- toggleThinking() {
487
- this.thinkingEnabled = !this.thinkingEnabled;
488
- this.emit('thinkingToggle', this.thinkingEnabled);
489
- this.scheduleRender();
490
- }
491
- /**
492
- * Get thinking enabled state
493
- */
494
- isThinkingEnabled() {
495
- return this.thinkingEnabled;
223
+ setPinnedHeaderLines(_count) {
224
+ // No-op: scroll region pinning removed
496
225
  }
497
226
  /**
498
227
  * Get current mode
@@ -525,17 +254,14 @@ export class TerminalInput extends EventEmitter {
525
254
  }
526
255
  /**
527
256
  * Clear the buffer
528
- * @param skipRender - If true, don't trigger a re-render (used during submit flow)
529
257
  */
530
- clear(skipRender = false) {
258
+ clear() {
531
259
  this.buffer = '';
532
260
  this.cursor = 0;
533
261
  this.historyIndex = -1;
534
262
  this.tempInput = '';
535
263
  this.pastePlaceholders = [];
536
- if (!skipRender) {
537
- this.scheduleRender();
538
- }
264
+ this.scheduleRender();
539
265
  }
540
266
  /**
541
267
  * Get queued inputs
@@ -606,6 +332,37 @@ export class TerminalInput extends EventEmitter {
606
332
  this.streamingLabel = next;
607
333
  this.scheduleRender();
608
334
  }
335
+ /**
336
+ * Surface meta status just above the divider (e.g., elapsed time or token usage).
337
+ */
338
+ setMetaStatus(meta) {
339
+ const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
340
+ ? Math.floor(meta.elapsedSeconds)
341
+ : null;
342
+ const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
343
+ ? Math.floor(meta.tokensUsed)
344
+ : null;
345
+ const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
346
+ ? Math.floor(meta.tokenLimit)
347
+ : null;
348
+ const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
349
+ ? Math.floor(meta.thinkingMs)
350
+ : null;
351
+ const nextThinkingHasContent = !!meta.thinkingHasContent;
352
+ if (this.metaElapsedSeconds === nextElapsed &&
353
+ this.metaTokensUsed === nextTokens &&
354
+ this.metaTokenLimit === nextLimit &&
355
+ this.metaThinkingMs === nextThinking &&
356
+ this.metaThinkingHasContent === nextThinkingHasContent) {
357
+ return;
358
+ }
359
+ this.metaElapsedSeconds = nextElapsed;
360
+ this.metaTokensUsed = nextTokens;
361
+ this.metaTokenLimit = nextLimit;
362
+ this.metaThinkingMs = nextThinking;
363
+ this.metaThinkingHasContent = nextThinkingHasContent;
364
+ this.scheduleRender();
365
+ }
609
366
  /**
610
367
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
611
368
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -615,26 +372,22 @@ export class TerminalInput extends EventEmitter {
615
372
  const nextAutoContinue = !!options.autoContinueEnabled;
616
373
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
617
374
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
375
+ const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
376
+ const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
618
377
  if (this.verificationEnabled === nextVerification &&
619
378
  this.autoContinueEnabled === nextAutoContinue &&
620
379
  this.verificationHotkey === nextVerifyHotkey &&
621
- this.autoContinueHotkey === nextAutoHotkey) {
380
+ this.autoContinueHotkey === nextAutoHotkey &&
381
+ this.thinkingHotkey === nextThinkingHotkey &&
382
+ this.thinkingModeLabel === nextThinkingLabel) {
622
383
  return;
623
384
  }
624
385
  this.verificationEnabled = nextVerification;
625
386
  this.autoContinueEnabled = nextAutoContinue;
626
387
  this.verificationHotkey = nextVerifyHotkey;
627
388
  this.autoContinueHotkey = nextAutoHotkey;
628
- this.scheduleRender();
629
- }
630
- /**
631
- * Set the model info string (e.g., "OpenAI · gpt-4")
632
- * This is displayed persistently above the input area.
633
- */
634
- setModelInfo(info) {
635
- if (this.modelInfo === info)
636
- return;
637
- this.modelInfo = info;
389
+ this.thinkingHotkey = nextThinkingHotkey;
390
+ this.thinkingModeLabel = nextThinkingLabel;
638
391
  this.scheduleRender();
639
392
  }
640
393
  /**
@@ -647,33 +400,159 @@ export class TerminalInput extends EventEmitter {
647
400
  this.scheduleRender();
648
401
  }
649
402
  /**
650
- * Render the input area.
651
- * During streaming: renders at terminal bottom (with scroll region)
652
- * After streaming: renders floating below content
403
+ * Surface model/provider context in the controls bar.
404
+ */
405
+ setModelContext(options) {
406
+ const nextModel = options.model?.trim() || null;
407
+ const nextProvider = options.provider?.trim() || null;
408
+ if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
409
+ return;
410
+ }
411
+ this.modelLabel = nextModel;
412
+ this.providerLabel = nextProvider;
413
+ this.scheduleRender();
414
+ }
415
+ /**
416
+ * Render the floating input area at contentRow.
417
+ *
418
+ * The chat box "floats" - it renders right below the last streamed content.
419
+ * As content is added, contentRow advances, and the chat box moves down.
420
+ * No scroll regions - pure floating behavior.
653
421
  */
654
422
  render() {
655
423
  if (!this.canRender())
656
424
  return;
657
425
  if (this.isRendering)
658
426
  return;
427
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
428
+ // During streaming, throttle re-renders
429
+ if (streamingActive && this.lastStreamingRender > 0) {
430
+ const elapsed = Date.now() - this.lastStreamingRender;
431
+ const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
432
+ if (waitMs > 0) {
433
+ this.renderDirty = true;
434
+ this.scheduleStreamingRender(waitMs);
435
+ return;
436
+ }
437
+ }
659
438
  const shouldSkip = !this.renderDirty &&
660
439
  this.buffer === this.lastRenderContent &&
661
440
  this.cursor === this.lastRenderCursor;
662
441
  this.renderDirty = false;
663
- // Skip if nothing changed (unless explicitly forced)
664
442
  if (shouldSkip) {
665
443
  return;
666
444
  }
667
- // If write lock is held, defer render
668
445
  if (writeLock.isLocked()) {
669
446
  writeLock.safeWrite(() => this.render());
670
447
  return;
671
448
  }
449
+ this.renderFloatingInputArea();
450
+ }
451
+ /**
452
+ * Core floating input area renderer.
453
+ * Chat box always floats at contentRow (below streamed content).
454
+ * This creates "persistent bottom floating" behavior.
455
+ */
456
+ renderFloatingInputArea() {
457
+ const { rows, cols } = this.getSize();
458
+ const maxWidth = Math.max(8, cols - 4);
459
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
460
+ // Wrap buffer into display lines
461
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
462
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
463
+ const displayLines = Math.min(lines.length, maxVisible);
464
+ const metaLines = this.buildMetaLines(cols - 2);
465
+ // Calculate display window (keep cursor visible)
466
+ let startLine = 0;
467
+ if (lines.length > displayLines) {
468
+ startLine = Math.max(0, cursorLine - displayLines + 1);
469
+ startLine = Math.min(startLine, lines.length - displayLines);
470
+ }
471
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
472
+ const adjustedCursorLine = cursorLine - startLine;
473
+ // Chat box height (must match getChatBoxHeight calculation)
474
+ const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
475
+ // Unified floating: chat box always at contentRow + 1
476
+ // When scroll region is active, contentRow is capped at maxContentRow
477
+ // so chat box ends up at the bottom but still "floats" below content
478
+ const chatBoxStartRow = this.contentRow + 1;
479
+ writeLock.lock('terminalInput.renderFloating');
672
480
  this.isRendering = true;
673
- writeLock.lock('terminalInput.render');
674
481
  try {
675
- // Always render floating right after content (no wasted space)
676
- this.renderFloatingInputArea();
482
+ // Hide cursor during render
483
+ this.write(ESC.HIDE);
484
+ this.write(ESC.RESET);
485
+ // Clear the chat box area
486
+ for (let i = 0; i < chatBoxHeight; i++) {
487
+ const row = chatBoxStartRow + i;
488
+ if (row <= rows) {
489
+ this.write(ESC.TO(row, 1));
490
+ this.write(ESC.CLEAR_LINE);
491
+ }
492
+ }
493
+ let currentRow = chatBoxStartRow;
494
+ // Meta/status header
495
+ for (const metaLine of metaLines) {
496
+ this.write(ESC.TO(currentRow, 1));
497
+ this.write(metaLine);
498
+ currentRow += 1;
499
+ }
500
+ // Separator line
501
+ this.write(ESC.TO(currentRow, 1));
502
+ this.write(renderDivider(cols - 2));
503
+ currentRow += 1;
504
+ // Render input lines
505
+ let finalRow = currentRow;
506
+ let finalCol = 3;
507
+ for (let i = 0; i < visibleLines.length; i++) {
508
+ const rowNum = currentRow + i;
509
+ this.write(ESC.TO(rowNum, 1));
510
+ const line = visibleLines[i] ?? '';
511
+ const isFirstLine = (startLine + i) === 0;
512
+ const isCursorLine = i === adjustedCursorLine;
513
+ this.write(ESC.BG_DARK);
514
+ this.write(ESC.DIM);
515
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
516
+ this.write(ESC.RESET);
517
+ this.write(ESC.BG_DARK);
518
+ if (isCursorLine) {
519
+ const col = Math.min(cursorCol, line.length);
520
+ const before = line.slice(0, col);
521
+ const at = col < line.length ? line[col] : ' ';
522
+ const after = col < line.length ? line.slice(col + 1) : '';
523
+ this.write(before);
524
+ this.write(ESC.REVERSE + ESC.BOLD);
525
+ this.write(at);
526
+ this.write(ESC.RESET + ESC.BG_DARK);
527
+ this.write(after);
528
+ finalRow = rowNum;
529
+ finalCol = this.config.promptChar.length + col + 1;
530
+ }
531
+ else {
532
+ this.write(line);
533
+ }
534
+ // Pad to edge
535
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
536
+ const padding = Math.max(0, cols - lineLen - 1);
537
+ if (padding > 0)
538
+ this.write(' '.repeat(padding));
539
+ this.write(ESC.RESET);
540
+ }
541
+ // Mode controls line
542
+ const controlRow = currentRow + visibleLines.length;
543
+ this.write(ESC.TO(controlRow, 1));
544
+ this.write(this.buildModeControls(cols));
545
+ // Position cursor in input box
546
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
547
+ this.write(ESC.SHOW);
548
+ // Update state
549
+ this.lastRenderContent = this.buffer;
550
+ this.lastRenderCursor = this.cursor;
551
+ this.lastStreamingRender = streamingActive ? Date.now() : 0;
552
+ if (this.streamingRenderTimer) {
553
+ clearTimeout(this.streamingRenderTimer);
554
+ this.streamingRenderTimer = null;
555
+ }
677
556
  }
678
557
  finally {
679
558
  writeLock.unlock();
@@ -681,99 +560,217 @@ export class TerminalInput extends EventEmitter {
681
560
  }
682
561
  }
683
562
  /**
684
- * Build status bar showing streaming/ready status and key info.
685
- * This is the TOP line above the input area - minimal Claude Code style.
563
+ * Build one or more compact meta lines above the divider (thinking, status, usage).
564
+ * During streaming, shows model line pinned above streaming info.
686
565
  */
687
- buildStatusBar(cols) {
688
- const maxWidth = cols - 2;
689
- const parts = [];
690
- // Streaming status with elapsed time (left side)
691
- if (this.mode === 'streaming') {
692
- let statusText = '● Streaming';
693
- if (this.streamingStartTime) {
694
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
695
- const mins = Math.floor(elapsed / 60);
696
- const secs = elapsed % 60;
697
- statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
566
+ buildMetaLines(width) {
567
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
568
+ const lines = [];
569
+ // Model line should ALWAYS be shown (pinned above streaming content)
570
+ if (this.modelLabel) {
571
+ const modelText = this.providerLabel
572
+ ? `model ${this.modelLabel} @ ${this.providerLabel}`
573
+ : `model ${this.modelLabel}`;
574
+ lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
575
+ }
576
+ // During streaming, add a compact status line with essential info
577
+ if (streamingActive) {
578
+ const parts = [];
579
+ // Essential streaming info
580
+ if (this.metaThinkingMs !== null) {
581
+ parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
582
+ }
583
+ if (this.metaElapsedSeconds !== null) {
584
+ parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
698
585
  }
699
- parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
586
+ parts.push({ text: 'esc to stop', tone: 'warn' });
587
+ if (parts.length) {
588
+ lines.push(renderStatusLine(parts, width));
589
+ }
590
+ return lines;
700
591
  }
701
- // Queue indicator during streaming
702
- if (this.mode === 'streaming' && this.queue.length > 0) {
703
- parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
592
+ // Non-streaming: show full status info (model line already added above)
593
+ if (this.metaThinkingMs !== null) {
594
+ const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
595
+ lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
704
596
  }
705
- // Paste indicator
706
- if (this.pastePlaceholders.length > 0) {
707
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
708
- parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
597
+ const statusParts = [];
598
+ const statusLabel = this.statusMessage ?? this.streamingLabel;
599
+ if (statusLabel) {
600
+ statusParts.push({ text: statusLabel, tone: 'info' });
709
601
  }
710
- // Override/warning status
711
- if (this.overrideStatusMessage) {
712
- parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
602
+ if (this.metaElapsedSeconds !== null) {
603
+ statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
713
604
  }
714
- // If idle with empty buffer, show quick shortcuts
715
- if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
716
- return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
605
+ const tokensRemaining = this.computeTokensRemaining();
606
+ if (tokensRemaining !== null) {
607
+ statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
717
608
  }
718
- // Multi-line indicator
719
- if (this.buffer.includes('\n')) {
720
- parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
609
+ if (statusParts.length) {
610
+ lines.push(renderStatusLine(statusParts, width));
611
+ }
612
+ const usageParts = [];
613
+ if (this.metaTokensUsed !== null) {
614
+ const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
615
+ const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
616
+ usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
617
+ }
618
+ if (this.contextUsage !== null) {
619
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
620
+ const left = Math.max(0, 100 - this.contextUsage);
621
+ usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
622
+ }
623
+ if (this.queue.length > 0) {
624
+ usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
721
625
  }
722
- if (parts.length === 0) {
723
- return ''; // Empty status bar when idle
626
+ if (usageParts.length) {
627
+ lines.push(renderStatusLine(usageParts, width));
724
628
  }
725
- const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
726
- return joined.slice(0, maxWidth);
629
+ return lines;
727
630
  }
728
631
  /**
729
- * Build mode controls line showing toggles and context info.
730
- * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
731
- *
732
- * Layout: [toggles on left] ... [context info on right]
632
+ * Build Claude Code style mode controls line.
633
+ * Combines streaming label + override status + main status for simultaneous display.
733
634
  */
734
635
  buildModeControls(cols) {
735
- const maxWidth = cols - 2;
736
- // Use schema-defined colors for consistency
737
- const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
738
- // Mode toggles with colors (following ModeControlsSchema)
739
- const toggles = [];
740
- // Edit mode (green=auto, yellow=ask) - per schema.editMode
741
- if (this.editMode === 'display-edits') {
742
- toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
636
+ const width = Math.max(8, cols - 2);
637
+ const leftParts = [];
638
+ const rightParts = [];
639
+ if (this.streamingLabel) {
640
+ leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
743
641
  }
744
- else {
745
- toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
746
- }
747
- // Thinking mode (cyan when on) - per schema.thinkingMode
748
- toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
749
- // Verification (green when on) - per schema.verificationMode
750
- toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
751
- // Auto-continue (magenta when on) - per schema.autoContinueMode
752
- toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
753
- const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
754
- // Context usage with color - per schema.contextUsage thresholds
755
- let rightPart = '';
756
- if (this.contextUsage !== null) {
757
- const rem = Math.max(0, 100 - this.contextUsage);
758
- // Thresholds: critical < 10%, warning < 25%
759
- if (rem < 10)
760
- rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
761
- else if (rem < 25)
762
- rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
763
- else
764
- rightPart = `${DIM}ctx: ${rem}%${R}`;
765
- }
766
- // Calculate visible lengths (strip ANSI)
767
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
768
- const leftLen = strip(leftPart).length;
769
- const rightLen = strip(rightPart).length;
770
- if (leftLen + rightLen < maxWidth - 4) {
771
- return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
772
- }
773
- if (rightLen > 0 && leftLen + 8 < maxWidth) {
774
- return `${leftPart} ${rightPart}`;
775
- }
776
- return leftPart;
642
+ if (this.overrideStatusMessage) {
643
+ leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
644
+ }
645
+ if (this.statusMessage) {
646
+ leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
647
+ }
648
+ const editHotkey = this.formatHotkey('shift+tab');
649
+ const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
650
+ const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
651
+ leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
652
+ const verifyHotkey = this.formatHotkey(this.verificationHotkey);
653
+ const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
654
+ leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
655
+ const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
656
+ const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
657
+ leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
658
+ if (this.queue.length > 0 && this.mode !== 'streaming') {
659
+ leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
660
+ }
661
+ if (this.buffer.includes('\n')) {
662
+ const lineCount = this.buffer.split('\n').length;
663
+ leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
664
+ }
665
+ if (this.pastePlaceholders.length > 0) {
666
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
667
+ leftParts.push({
668
+ text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
669
+ tone: 'info',
670
+ });
671
+ }
672
+ const contextRemaining = this.computeContextRemaining();
673
+ if (this.thinkingModeLabel) {
674
+ const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
675
+ rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
676
+ }
677
+ // Show model in controls only when NOT streaming (during streaming it's in meta lines)
678
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
679
+ if (this.modelLabel && !streamingActive) {
680
+ const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
681
+ rightParts.push({ text: modelText, tone: 'muted' });
682
+ }
683
+ if (contextRemaining !== null) {
684
+ const tone = contextRemaining <= 10 ? 'warn' : 'muted';
685
+ const label = contextRemaining === 0 && this.contextUsage !== null
686
+ ? 'Context auto-compact imminent'
687
+ : `Context left until auto-compact: ${contextRemaining}%`;
688
+ rightParts.push({ text: label, tone });
689
+ }
690
+ if (!rightParts.length || width < 60) {
691
+ const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
692
+ return renderStatusLine(merged, width);
693
+ }
694
+ const leftWidth = Math.max(12, Math.floor(width * 0.6));
695
+ const rightWidth = Math.max(14, width - leftWidth - 1);
696
+ const leftText = renderStatusLine(leftParts, leftWidth);
697
+ const rightText = renderStatusLine(rightParts, rightWidth);
698
+ const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
699
+ return `${leftText}${' '.repeat(spacing)}${rightText}`;
700
+ }
701
+ formatHotkey(hotkey) {
702
+ const normalized = hotkey.trim().toLowerCase();
703
+ if (!normalized)
704
+ return hotkey;
705
+ const parts = normalized.split('+').filter(Boolean);
706
+ const map = {
707
+ shift: '⇧',
708
+ sh: '⇧',
709
+ alt: '⌥',
710
+ option: '⌥',
711
+ opt: '⌥',
712
+ ctrl: '⌃',
713
+ control: '⌃',
714
+ cmd: '⌘',
715
+ meta: '⌘',
716
+ };
717
+ const formatted = parts
718
+ .map((part) => {
719
+ const symbol = map[part];
720
+ if (symbol)
721
+ return symbol;
722
+ return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
723
+ })
724
+ .join('');
725
+ return formatted || hotkey;
726
+ }
727
+ computeContextRemaining() {
728
+ if (this.contextUsage === null) {
729
+ return null;
730
+ }
731
+ return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
732
+ }
733
+ computeTokensRemaining() {
734
+ if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
735
+ return null;
736
+ }
737
+ const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
738
+ return this.formatTokenCount(remaining);
739
+ }
740
+ formatElapsedLabel(seconds) {
741
+ if (seconds < 60) {
742
+ return `${seconds}s`;
743
+ }
744
+ const mins = Math.floor(seconds / 60);
745
+ const secs = seconds % 60;
746
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
747
+ }
748
+ formatTokenCount(value) {
749
+ if (!Number.isFinite(value)) {
750
+ return `${value}`;
751
+ }
752
+ if (value >= 1_000_000) {
753
+ return `${(value / 1_000_000).toFixed(1)}M`;
754
+ }
755
+ if (value >= 1_000) {
756
+ return `${(value / 1_000).toFixed(1)}k`;
757
+ }
758
+ return `${Math.round(value)}`;
759
+ }
760
+ visibleLength(value) {
761
+ const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
762
+ return value.replace(ansiPattern, '').length;
763
+ }
764
+ /**
765
+ * Debug-only snapshot used by tests to assert rendered strings without
766
+ * needing a TTY. Not used by production code.
767
+ */
768
+ getDebugUiSnapshot(width) {
769
+ const cols = Math.max(8, width ?? this.getSize().cols);
770
+ return {
771
+ meta: this.buildMetaLines(cols - 2),
772
+ controls: this.buildModeControls(cols),
773
+ };
777
774
  }
778
775
  /**
779
776
  * Force a re-render
@@ -796,32 +793,123 @@ export class TerminalInput extends EventEmitter {
796
793
  handleResize() {
797
794
  this.lastRenderContent = '';
798
795
  this.lastRenderCursor = -1;
796
+ this.resetStreamingRenderThrottle();
799
797
  this.scheduleRender();
800
798
  }
801
799
  /**
802
- * Register with display's output interceptor.
803
- * Before content write: clear chat box and position cursor at content area.
804
- * After content write: re-render chat box below content.
800
+ * Stream content with floating chat box.
801
+ *
802
+ * Clean approach - no scroll regions, just cursor positioning:
803
+ * 1. Save cursor state
804
+ * 2. Clear chat box area (it will be re-rendered)
805
+ * 3. Position at contentRow
806
+ * 4. Write content
807
+ * 5. Advance contentRow
808
+ * 6. Re-render chat box
805
809
  */
806
- registerOutputInterceptor(display) {
807
- if (this.outputInterceptorCleanup) {
808
- this.outputInterceptorCleanup();
809
- }
810
- this.outputInterceptorCleanup = display.registerOutputInterceptor({
811
- beforeWrite: () => {
812
- // Clear chat box before content write to prevent overlap
813
- this.clearInputArea();
814
- // Position cursor at content area (after last content, before where chat box was)
815
- if (this.contentEndRow > 0) {
816
- this.write(ESC.TO(this.contentEndRow, 1));
817
- }
818
- },
819
- afterWrite: () => {
820
- // Re-render chat box after content writes
821
- this.renderDirty = true;
822
- this.scheduleRender();
823
- },
824
- });
810
+ streamContent(content) {
811
+ if (!content)
812
+ return;
813
+ writeLock.lock('streamContent');
814
+ try {
815
+ // Save cursor and hide it
816
+ this.write(ESC.SAVE);
817
+ this.write(ESC.HIDE);
818
+ // Clear the chat box area first (it will be re-rendered after)
819
+ const { rows, cols } = this.getSize();
820
+ const chatBoxHeight = 6; // Approximate
821
+ const chatBoxStart = this.contentRow + 1;
822
+ for (let i = 0; i < chatBoxHeight && chatBoxStart + i <= rows; i++) {
823
+ this.write(ESC.TO(chatBoxStart + i, 1));
824
+ this.write(ESC.CLEAR_LINE);
825
+ }
826
+ // Position at contentRow and write content
827
+ this.write(ESC.TO(this.contentRow, 1));
828
+ this.write(content);
829
+ // Count newlines and advance contentRow
830
+ const newlines = (content.match(/\n/g) || []).length;
831
+ this.contentRow += newlines;
832
+ // Cap contentRow to leave room for chat box
833
+ const maxContentRow = Math.max(1, rows - chatBoxHeight);
834
+ if (this.contentRow > maxContentRow) {
835
+ this.contentRow = maxContentRow;
836
+ }
837
+ // Restore cursor
838
+ this.write(ESC.RESTORE);
839
+ this.write(ESC.SHOW);
840
+ }
841
+ finally {
842
+ writeLock.unlock();
843
+ }
844
+ // Re-render chat box at new position
845
+ this.forceRender();
846
+ }
847
+ /**
848
+ * Enable scroll region (no-op in floating mode).
849
+ */
850
+ enableScrollRegion() {
851
+ // No-op: using pure floating approach
852
+ }
853
+ /**
854
+ * Disable scroll region (no-op in floating mode).
855
+ */
856
+ disableScrollRegion() {
857
+ // No-op: using pure floating approach
858
+ }
859
+ /**
860
+ * Calculate chat box height.
861
+ */
862
+ getChatBoxHeight() {
863
+ return 6; // Fixed: meta + divider + input + controls + buffer
864
+ }
865
+ /**
866
+ * @deprecated Use streamContent() instead
867
+ * Register with display's output interceptor - kept for backwards compatibility
868
+ */
869
+ registerOutputInterceptor(_display) {
870
+ // No-op: Use streamContent() for cleaner floating chat box behavior
871
+ }
872
+ /**
873
+ * @deprecated Use streamContent() instead
874
+ * Write content above the floating chat box.
875
+ */
876
+ writeToScrollRegion(content) {
877
+ this.streamContent(content);
878
+ }
879
+ /**
880
+ * Clear the entire terminal screen and reset content position.
881
+ * This removes all content including the launching command.
882
+ */
883
+ clearScreen() {
884
+ writeLock.lock('clearScreen');
885
+ try {
886
+ this.write(ESC.HOME);
887
+ this.write(ESC.CLEAR_SCREEN);
888
+ this.contentRow = 1;
889
+ }
890
+ finally {
891
+ writeLock.unlock();
892
+ }
893
+ }
894
+ /**
895
+ * Reset content position to row 1.
896
+ * Does NOT clear the terminal - content starts from current position.
897
+ */
898
+ resetContentPosition() {
899
+ this.contentRow = 1;
900
+ }
901
+ /**
902
+ * Set the content row explicitly (used after banner is written).
903
+ * This tells the input where content should start flowing from.
904
+ */
905
+ setContentRow(row) {
906
+ this.contentRow = Math.max(1, row);
907
+ }
908
+ /**
909
+ * Get the current content row position.
910
+ */
911
+ getContentRow() {
912
+ return this.contentRow;
825
913
  }
826
914
  /**
827
915
  * Dispose and clean up
@@ -829,18 +917,10 @@ export class TerminalInput extends EventEmitter {
829
917
  dispose() {
830
918
  if (this.disposed)
831
919
  return;
832
- // Clean up streaming render timer
833
- if (this.streamingRenderTimer) {
834
- clearInterval(this.streamingRenderTimer);
835
- this.streamingRenderTimer = null;
836
- }
837
- // Clean up output interceptor
838
- if (this.outputInterceptorCleanup) {
839
- this.outputInterceptorCleanup();
840
- this.outputInterceptorCleanup = undefined;
841
- }
842
920
  this.disposed = true;
843
921
  this.enabled = false;
922
+ this.disableScrollRegion();
923
+ this.resetStreamingRenderThrottle();
844
924
  this.disableBracketedPaste();
845
925
  this.buffer = '';
846
926
  this.queue = [];
@@ -945,22 +1025,7 @@ export class TerminalInput extends EventEmitter {
945
1025
  this.toggleEditMode();
946
1026
  return true;
947
1027
  }
948
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
949
- if (this.findPlaceholderAt(this.cursor)) {
950
- this.togglePasteExpansion();
951
- }
952
- else {
953
- this.toggleThinking();
954
- }
955
- return true;
956
- case 'escape':
957
- // Esc: interrupt if streaming, otherwise clear buffer
958
- if (this.mode === 'streaming') {
959
- this.emit('interrupt');
960
- }
961
- else if (this.buffer.length > 0) {
962
- this.clear();
963
- }
1028
+ this.insertText(' ');
964
1029
  return true;
965
1030
  }
966
1031
  return false;
@@ -978,7 +1043,6 @@ export class TerminalInput extends EventEmitter {
978
1043
  this.insertPlainText(chunk, insertPos);
979
1044
  this.cursor = insertPos + chunk.length;
980
1045
  this.emit('change', this.buffer);
981
- this.updateSuggestions();
982
1046
  this.scheduleRender();
983
1047
  }
984
1048
  insertNewline() {
@@ -1003,7 +1067,6 @@ export class TerminalInput extends EventEmitter {
1003
1067
  this.cursor = Math.max(0, this.cursor - 1);
1004
1068
  }
1005
1069
  this.emit('change', this.buffer);
1006
- this.updateSuggestions();
1007
1070
  this.scheduleRender();
1008
1071
  }
1009
1072
  deleteForward() {
@@ -1231,13 +1294,12 @@ export class TerminalInput extends EventEmitter {
1231
1294
  timestamp: Date.now(),
1232
1295
  });
1233
1296
  this.emit('queue', text);
1234
- this.clear(); // Clear immediately for queued input, re-render to update queue display
1297
+ this.clear(); // Clear immediately for queued input
1235
1298
  }
1236
1299
  else {
1237
- // In idle mode, clear the input WITHOUT rendering.
1238
- // The caller will display the user message and start streaming.
1239
- // We'll render the input area again after streaming ends.
1240
- this.clear(true); // Skip render - streaming will handle display
1300
+ // In idle mode, clear the input first, then emit submit.
1301
+ // The prompt will be logged as a visible message by the caller.
1302
+ this.clear();
1241
1303
  this.emit('submit', text);
1242
1304
  }
1243
1305
  }
@@ -1254,7 +1316,9 @@ export class TerminalInput extends EventEmitter {
1254
1316
  if (available <= 0)
1255
1317
  return;
1256
1318
  const chunk = clean.slice(0, available);
1257
- if (isMultilinePaste(chunk)) {
1319
+ const isMultiline = isMultilinePaste(chunk);
1320
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1321
+ if (isMultiline && !isShortMultiline) {
1258
1322
  this.insertPastePlaceholder(chunk);
1259
1323
  }
1260
1324
  else {
@@ -1390,17 +1454,19 @@ export class TerminalInput extends EventEmitter {
1390
1454
  this.shiftPlaceholders(position, text.length);
1391
1455
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1392
1456
  }
1457
+ shouldInlineMultiline(content) {
1458
+ const lines = content.split('\n').length;
1459
+ const maxInlineLines = 4;
1460
+ const maxInlineChars = 240;
1461
+ return lines <= maxInlineLines && content.length <= maxInlineChars;
1462
+ }
1393
1463
  findPlaceholderAt(position) {
1394
1464
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1395
1465
  }
1396
- buildPlaceholder(summary) {
1466
+ buildPlaceholder(lineCount) {
1397
1467
  const id = ++this.pasteCounter;
1398
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1399
- // Show first line preview (truncated)
1400
- const preview = summary.preview.length > 30
1401
- ? `${summary.preview.slice(0, 30)}...`
1402
- : summary.preview;
1403
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1468
+ const plural = lineCount === 1 ? '' : 's';
1469
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1404
1470
  return { id, placeholder };
1405
1471
  }
1406
1472
  insertPastePlaceholder(content) {
@@ -1408,67 +1474,21 @@ export class TerminalInput extends EventEmitter {
1408
1474
  if (available <= 0)
1409
1475
  return;
1410
1476
  const cleanContent = content.slice(0, available);
1411
- const summary = generatePasteSummary(cleanContent);
1412
- // For short pastes (< 5 lines), show full content instead of placeholder
1413
- if (summary.lineCount < 5) {
1414
- const placeholder = this.findPlaceholderAt(this.cursor);
1415
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1416
- this.insertPlainText(cleanContent, insertPos);
1417
- this.cursor = insertPos + cleanContent.length;
1418
- return;
1419
- }
1420
- const { id, placeholder } = this.buildPlaceholder(summary);
1477
+ const lineCount = cleanContent.split('\n').length;
1478
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1421
1479
  const insertPos = this.cursor;
1422
1480
  this.shiftPlaceholders(insertPos, placeholder.length);
1423
1481
  this.pastePlaceholders.push({
1424
1482
  id,
1425
1483
  content: cleanContent,
1426
- lineCount: summary.lineCount,
1484
+ lineCount,
1427
1485
  placeholder,
1428
1486
  start: insertPos,
1429
1487
  end: insertPos + placeholder.length,
1430
- summary,
1431
- expanded: false,
1432
1488
  });
1433
1489
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1434
1490
  this.cursor = insertPos + placeholder.length;
1435
1491
  }
1436
- /**
1437
- * Toggle expansion of a paste placeholder at the current cursor position.
1438
- * When expanded, shows first 3 and last 2 lines of the content.
1439
- */
1440
- togglePasteExpansion() {
1441
- const placeholder = this.findPlaceholderAt(this.cursor);
1442
- if (!placeholder)
1443
- return false;
1444
- placeholder.expanded = !placeholder.expanded;
1445
- // Update the placeholder text in buffer
1446
- const newPlaceholder = placeholder.expanded
1447
- ? this.buildExpandedPlaceholder(placeholder)
1448
- : this.buildPlaceholder(placeholder.summary).placeholder;
1449
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1450
- // Update buffer
1451
- this.buffer =
1452
- this.buffer.slice(0, placeholder.start) +
1453
- newPlaceholder +
1454
- this.buffer.slice(placeholder.end);
1455
- // Update placeholder tracking
1456
- placeholder.placeholder = newPlaceholder;
1457
- placeholder.end = placeholder.start + newPlaceholder.length;
1458
- // Shift other placeholders
1459
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1460
- this.scheduleRender();
1461
- return true;
1462
- }
1463
- buildExpandedPlaceholder(ph) {
1464
- const lines = ph.content.split('\n');
1465
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1466
- const lastLines = lines.length > 5
1467
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1468
- : '';
1469
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1470
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1471
- }
1472
1492
  deletePlaceholder(placeholder) {
1473
1493
  const length = placeholder.end - placeholder.start;
1474
1494
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1476,7 +1496,11 @@ export class TerminalInput extends EventEmitter {
1476
1496
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1477
1497
  this.cursor = placeholder.start;
1478
1498
  }
1479
- updateContextUsage(value) {
1499
+ updateContextUsage(value, autoCompactThreshold) {
1500
+ if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1501
+ const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1502
+ this.contextAutoCompactThreshold = boundedThreshold;
1503
+ }
1480
1504
  if (value === null || !Number.isFinite(value)) {
1481
1505
  this.contextUsage = null;
1482
1506
  }
@@ -1503,6 +1527,22 @@ export class TerminalInput extends EventEmitter {
1503
1527
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1504
1528
  this.setEditMode(next);
1505
1529
  }
1530
+ scheduleStreamingRender(delayMs) {
1531
+ if (this.streamingRenderTimer)
1532
+ return;
1533
+ const wait = Math.max(16, delayMs);
1534
+ this.streamingRenderTimer = setTimeout(() => {
1535
+ this.streamingRenderTimer = null;
1536
+ this.render();
1537
+ }, wait);
1538
+ }
1539
+ resetStreamingRenderThrottle() {
1540
+ if (this.streamingRenderTimer) {
1541
+ clearTimeout(this.streamingRenderTimer);
1542
+ this.streamingRenderTimer = null;
1543
+ }
1544
+ this.lastStreamingRender = 0;
1545
+ }
1506
1546
  scheduleRender() {
1507
1547
  if (!this.canRender())
1508
1548
  return;