erosolar-cli 1.7.301 → 1.7.302

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 +111 -108
  74. package/dist/shell/terminalInput.d.ts.map +1 -1
  75. package/dist/shell/terminalInput.js +501 -490
  76. package/dist/shell/terminalInput.js.map +1 -1
  77. package/dist/shell/terminalInputAdapter.d.ts +47 -20
  78. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  79. package/dist/shell/terminalInputAdapter.js +53 -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,18 @@
3
3
  *
4
4
  * Design principles:
5
5
  * - Single source of truth for input state
6
+ * - Content flows naturally from top to bottom (no scroll region pinning)
6
7
  * - Native bracketed paste support (no heuristics)
7
8
  * - Clean cursor model with render-time wrapping
8
9
  * - State machine for different input modes
9
10
  * - No readline dependency for display
10
11
  */
11
12
  import { EventEmitter } from 'node:events';
12
- import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
13
+ import { isMultilinePaste } from '../core/multilinePasteHandler.js';
13
14
  import { writeLock } from '../ui/writeLock.js';
14
- import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
15
+ import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
16
+ import { isStreamingMode } from '../ui/globalWriteLock.js';
17
+ import { formatThinking } from '../ui/toolDisplay.js';
15
18
  // ANSI escape codes
16
19
  const ESC = {
17
20
  // Cursor control
@@ -21,12 +24,12 @@ const ESC = {
21
24
  SHOW: '\x1b[?25h',
22
25
  TO: (row, col) => `\x1b[${row};${col}H`,
23
26
  TO_COL: (col) => `\x1b[${col}G`,
24
- // Screen control
25
- CLEAR_SCREEN: '\x1b[2J',
26
- HOME: '\x1b[H',
27
27
  // Line control
28
28
  CLEAR_LINE: '\x1b[2K',
29
29
  CLEAR_TO_END: '\x1b[0J',
30
+ // Scroll region
31
+ SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
32
+ RESET_SCROLL: '\x1b[r',
30
33
  // Style
31
34
  RESET: '\x1b[0m',
32
35
  DIM: '\x1b[2m',
@@ -66,47 +69,44 @@ export class TerminalInput extends EventEmitter {
66
69
  statusMessage = null;
67
70
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
68
71
  streamingLabel = null; // Streaming progress indicator
72
+ metaElapsedSeconds = null; // Optional elapsed time for header line
73
+ metaTokensUsed = null; // Optional token usage
74
+ metaTokenLimit = null; // Optional token window
75
+ metaThinkingMs = null; // Optional thinking duration
76
+ metaThinkingHasContent = false; // Whether collapsed thinking content exists
69
77
  lastRenderContent = '';
70
78
  lastRenderCursor = -1;
71
79
  renderDirty = false;
72
80
  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 (for idle mode positioning)
76
- // Command suggestions (Claude Code style auto-complete)
77
- commandSuggestions = [];
78
- filteredSuggestions = [];
79
- selectedSuggestionIndex = 0;
80
- showSuggestions = false;
81
81
  // Lifecycle
82
82
  disposed = false;
83
83
  enabled = true;
84
84
  contextUsage = null;
85
+ contextAutoCompactThreshold = 90;
86
+ // Track current content row in scroll region (starts at top, moves down)
87
+ contentRow = 1;
88
+ thinkingModeLabel = null;
85
89
  editMode = 'display-edits';
86
90
  verificationEnabled = true;
87
91
  autoContinueEnabled = false;
88
92
  verificationHotkey = 'alt+v';
89
93
  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)
94
+ thinkingHotkey = '/thinking';
95
+ modelLabel = null;
96
+ providerLabel = null;
97
+ // Streaming render throttle
98
+ lastStreamingRender = 0;
99
+ streamingRenderInterval = 250; // ms between renders during streaming
97
100
  streamingRenderTimer = null;
98
- // Unified UI initialization flag
99
- unifiedUIInitialized = false;
100
101
  constructor(writeStream = process.stdout, config = {}) {
101
102
  super();
102
103
  this.out = writeStream;
103
- // Use schema defaults for configuration consistency
104
104
  this.config = {
105
- maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
106
- maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
105
+ maxLines: config.maxLines ?? 1000,
106
+ maxLength: config.maxLength ?? 10000,
107
107
  maxQueueSize: config.maxQueueSize ?? 100,
108
- promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
109
- continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
108
+ promptChar: config.promptChar ?? '> ',
109
+ continuationChar: config.continuationChar ?? '│ ',
110
110
  };
111
111
  }
112
112
  // ===========================================================================
@@ -185,288 +185,36 @@ export class TerminalInput extends EventEmitter {
185
185
  if (handled)
186
186
  return;
187
187
  }
188
- // Handle '?' for help hint (if buffer is empty)
189
- if (str === '?' && this.buffer.length === 0) {
190
- this.emit('showHelp');
191
- return;
192
- }
193
188
  // Insert printable characters
194
189
  if (str && !key?.ctrl && !key?.meta) {
195
190
  this.insertText(str);
196
191
  }
197
192
  }
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 floating input area below current content.
261
- * This is "bottom floating" - follows content, not pinned to terminal bottom.
262
- * Uses absolute positioning to prevent duplicates.
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 input area
269
- const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
270
- // FIRST: Clear any previously rendered input area
271
- this.clearInputArea();
272
- // Hide cursor during render
273
- this.write(ESC.HIDE);
274
- // Calculate where to render: after current content
275
- // Use contentEndRow if set, otherwise estimate from terminal bottom
276
- let startRow;
277
- if (this.contentEndRow > 0) {
278
- startRow = this.contentEndRow + 1;
279
- }
280
- else {
281
- // Render near bottom, leaving space for input area
282
- startRow = Math.max(1, rows - linesNeeded);
283
- }
284
- // Ensure we don't go past terminal bounds
285
- startRow = Math.min(startRow, rows - linesNeeded + 1);
286
- startRow = Math.max(1, startRow);
287
- // Track this position
288
- this.inputAreaStartRow = startRow;
289
- let currentRow = startRow;
290
- // Status bar
291
- this.write(ESC.TO(currentRow, 1));
292
- this.write(this.buildStatusBar(cols));
293
- currentRow++;
294
- // Model info line (if set)
295
- if (this.modelInfo) {
296
- this.write(ESC.TO(currentRow, 1));
297
- let modelLine = `${DIM}${this.modelInfo}${R}`;
298
- if (this.contextUsage !== null) {
299
- const rem = Math.max(0, 100 - this.contextUsage);
300
- if (rem < 10)
301
- modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
302
- else if (rem < 25)
303
- modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
304
- else
305
- modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
306
- }
307
- this.write(modelLine);
308
- currentRow++;
309
- }
310
- // Top divider
311
- this.write(ESC.TO(currentRow, 1));
312
- this.write(divider);
313
- currentRow++;
314
- // Input line with prompt and buffer content
315
- const { lines, cursorCol } = this.wrapBuffer(cols - 4);
316
- const displayLine = lines[0] ?? '';
317
- const inputRow = currentRow;
318
- this.write(ESC.TO(currentRow, 1));
319
- this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
320
- this.write(ESC.BG_DARK + displayLine);
321
- const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
322
- if (padding > 0)
323
- this.write(' '.repeat(padding));
324
- this.write(ESC.RESET);
325
- currentRow++;
326
- // Bottom divider
327
- this.write(ESC.TO(currentRow, 1));
328
- this.write(divider);
329
- currentRow++;
330
- // Mode controls
331
- this.write(ESC.TO(currentRow, 1));
332
- this.write(this.buildModeControls(cols));
333
- // Track lines rendered
334
- this.flowModeRenderedLines = currentRow - startRow + 1;
335
- // Position cursor in input line for typing
336
- this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
337
- // Show cursor
338
- this.write(ESC.SHOW);
339
- // Update tracking
340
- this.lastRenderContent = this.buffer;
341
- this.lastRenderCursor = this.cursor;
342
- }
343
193
  /**
344
194
  * Set the input mode
345
195
  *
346
- * Unified floating input - input area always floats below content.
196
+ * Content flows naturally - no scroll region pinning.
347
197
  */
348
198
  setMode(mode) {
349
199
  const prevMode = this.mode;
350
200
  this.mode = mode;
351
201
  if (mode === 'streaming' && prevMode !== 'streaming') {
352
- // Track streaming start time for elapsed display
353
- this.streamingStartTime = Date.now();
354
- // Ensure unified UI is initialized
355
- if (!this.unifiedUIInitialized) {
356
- this.initializeUnifiedUI();
357
- }
202
+ this.resetStreamingRenderThrottle();
358
203
  this.renderDirty = true;
359
- this.scheduleRender();
204
+ this.render();
360
205
  }
361
206
  else if (mode !== 'streaming' && prevMode === 'streaming') {
362
- // Stop streaming render timer (if any)
363
- if (this.streamingRenderTimer) {
364
- clearInterval(this.streamingRenderTimer);
365
- this.streamingRenderTimer = null;
366
- }
367
- // Reset streaming time
368
- this.streamingStartTime = null;
369
- // Re-render floating input area
370
- this.renderDirty = true;
371
- this.scheduleRender();
372
- }
373
- }
374
- /**
375
- * Set the row where content ends (for idle mode positioning).
376
- * Input area will render starting from this row + 1.
377
- */
378
- setContentEndRow(row) {
379
- this.contentEndRow = Math.max(0, row);
380
- this.renderDirty = true;
381
- this.scheduleRender();
382
- }
383
- /**
384
- * Set available slash commands for auto-complete suggestions.
385
- */
386
- setCommands(commands) {
387
- this.commandSuggestions = commands;
388
- this.updateSuggestions();
389
- }
390
- /**
391
- * Update filtered suggestions based on current input.
392
- */
393
- updateSuggestions() {
394
- const input = this.buffer.trim();
395
- // Only show suggestions when input starts with "/"
396
- if (!input.startsWith('/')) {
397
- this.showSuggestions = false;
398
- this.filteredSuggestions = [];
399
- this.selectedSuggestionIndex = 0;
400
- return;
401
- }
402
- const query = input.toLowerCase();
403
- this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
404
- cmd.command.toLowerCase().includes(query.slice(1)));
405
- // Show suggestions if we have matches
406
- this.showSuggestions = this.filteredSuggestions.length > 0;
407
- // Keep selection in bounds
408
- if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
409
- this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
207
+ // Streaming ended - render the input area
208
+ this.resetStreamingRenderThrottle();
209
+ this.forceRender();
410
210
  }
411
211
  }
412
212
  /**
413
- * Select next suggestion (arrow down / tab).
213
+ * Legacy method - no longer used (content flows naturally).
214
+ * @deprecated Use setContentRow instead
414
215
  */
415
- selectNextSuggestion() {
416
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
417
- return;
418
- this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
419
- this.renderDirty = true;
420
- this.scheduleRender();
421
- }
422
- /**
423
- * Select previous suggestion (arrow up / shift+tab).
424
- */
425
- selectPrevSuggestion() {
426
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
427
- return;
428
- this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
429
- ? this.filteredSuggestions.length - 1
430
- : this.selectedSuggestionIndex - 1;
431
- this.renderDirty = true;
432
- this.scheduleRender();
433
- }
434
- /**
435
- * Accept current suggestion and insert into buffer.
436
- */
437
- acceptSuggestion() {
438
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
439
- return false;
440
- const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
441
- if (!selected)
442
- return false;
443
- // Replace buffer with selected command
444
- this.buffer = selected.command + ' ';
445
- this.cursor = this.buffer.length;
446
- this.showSuggestions = false;
447
- this.renderDirty = true;
448
- this.scheduleRender();
449
- return true;
450
- }
451
- /**
452
- * Check if suggestions are visible.
453
- */
454
- areSuggestionsVisible() {
455
- return this.showSuggestions && this.filteredSuggestions.length > 0;
456
- }
457
- /**
458
- * Toggle thinking/reasoning mode
459
- */
460
- toggleThinking() {
461
- this.thinkingEnabled = !this.thinkingEnabled;
462
- this.emit('thinkingToggle', this.thinkingEnabled);
463
- this.scheduleRender();
464
- }
465
- /**
466
- * Get thinking enabled state
467
- */
468
- isThinkingEnabled() {
469
- return this.thinkingEnabled;
216
+ setPinnedHeaderLines(_count) {
217
+ // No-op: scroll region pinning removed
470
218
  }
471
219
  /**
472
220
  * Get current mode
@@ -499,17 +247,14 @@ export class TerminalInput extends EventEmitter {
499
247
  }
500
248
  /**
501
249
  * Clear the buffer
502
- * @param skipRender - If true, don't trigger a re-render (used during submit flow)
503
250
  */
504
- clear(skipRender = false) {
251
+ clear() {
505
252
  this.buffer = '';
506
253
  this.cursor = 0;
507
254
  this.historyIndex = -1;
508
255
  this.tempInput = '';
509
256
  this.pastePlaceholders = [];
510
- if (!skipRender) {
511
- this.scheduleRender();
512
- }
257
+ this.scheduleRender();
513
258
  }
514
259
  /**
515
260
  * Get queued inputs
@@ -580,6 +325,37 @@ export class TerminalInput extends EventEmitter {
580
325
  this.streamingLabel = next;
581
326
  this.scheduleRender();
582
327
  }
328
+ /**
329
+ * Surface meta status just above the divider (e.g., elapsed time or token usage).
330
+ */
331
+ setMetaStatus(meta) {
332
+ const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
333
+ ? Math.floor(meta.elapsedSeconds)
334
+ : null;
335
+ const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
336
+ ? Math.floor(meta.tokensUsed)
337
+ : null;
338
+ const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
339
+ ? Math.floor(meta.tokenLimit)
340
+ : null;
341
+ const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
342
+ ? Math.floor(meta.thinkingMs)
343
+ : null;
344
+ const nextThinkingHasContent = !!meta.thinkingHasContent;
345
+ if (this.metaElapsedSeconds === nextElapsed &&
346
+ this.metaTokensUsed === nextTokens &&
347
+ this.metaTokenLimit === nextLimit &&
348
+ this.metaThinkingMs === nextThinking &&
349
+ this.metaThinkingHasContent === nextThinkingHasContent) {
350
+ return;
351
+ }
352
+ this.metaElapsedSeconds = nextElapsed;
353
+ this.metaTokensUsed = nextTokens;
354
+ this.metaTokenLimit = nextLimit;
355
+ this.metaThinkingMs = nextThinking;
356
+ this.metaThinkingHasContent = nextThinkingHasContent;
357
+ this.scheduleRender();
358
+ }
583
359
  /**
584
360
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
585
361
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -589,26 +365,22 @@ export class TerminalInput extends EventEmitter {
589
365
  const nextAutoContinue = !!options.autoContinueEnabled;
590
366
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
591
367
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
368
+ const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
369
+ const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
592
370
  if (this.verificationEnabled === nextVerification &&
593
371
  this.autoContinueEnabled === nextAutoContinue &&
594
372
  this.verificationHotkey === nextVerifyHotkey &&
595
- this.autoContinueHotkey === nextAutoHotkey) {
373
+ this.autoContinueHotkey === nextAutoHotkey &&
374
+ this.thinkingHotkey === nextThinkingHotkey &&
375
+ this.thinkingModeLabel === nextThinkingLabel) {
596
376
  return;
597
377
  }
598
378
  this.verificationEnabled = nextVerification;
599
379
  this.autoContinueEnabled = nextAutoContinue;
600
380
  this.verificationHotkey = nextVerifyHotkey;
601
381
  this.autoContinueHotkey = nextAutoHotkey;
602
- this.scheduleRender();
603
- }
604
- /**
605
- * Set the model info string (e.g., "OpenAI · gpt-4")
606
- * This is displayed persistently above the input area.
607
- */
608
- setModelInfo(info) {
609
- if (this.modelInfo === info)
610
- return;
611
- this.modelInfo = info;
382
+ this.thinkingHotkey = nextThinkingHotkey;
383
+ this.thinkingModeLabel = nextThinkingLabel;
612
384
  this.scheduleRender();
613
385
  }
614
386
  /**
@@ -621,30 +393,155 @@ export class TerminalInput extends EventEmitter {
621
393
  this.scheduleRender();
622
394
  }
623
395
  /**
624
- * Render the input area - always floating below content.
396
+ * Surface model/provider context in the controls bar.
397
+ */
398
+ setModelContext(options) {
399
+ const nextModel = options.model?.trim() || null;
400
+ const nextProvider = options.provider?.trim() || null;
401
+ if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
402
+ return;
403
+ }
404
+ this.modelLabel = nextModel;
405
+ this.providerLabel = nextProvider;
406
+ this.scheduleRender();
407
+ }
408
+ /**
409
+ * Render the floating input area at contentRow.
410
+ *
411
+ * The chat box "floats" - it renders right below the last streamed content.
412
+ * As content is added, contentRow advances, and the chat box moves down.
413
+ * No scroll regions - pure floating behavior.
625
414
  */
626
415
  render() {
627
416
  if (!this.canRender())
628
417
  return;
629
418
  if (this.isRendering)
630
419
  return;
420
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
421
+ // During streaming, throttle re-renders
422
+ if (streamingActive && this.lastStreamingRender > 0) {
423
+ const elapsed = Date.now() - this.lastStreamingRender;
424
+ const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
425
+ if (waitMs > 0) {
426
+ this.renderDirty = true;
427
+ this.scheduleStreamingRender(waitMs);
428
+ return;
429
+ }
430
+ }
631
431
  const shouldSkip = !this.renderDirty &&
632
432
  this.buffer === this.lastRenderContent &&
633
433
  this.cursor === this.lastRenderCursor;
634
434
  this.renderDirty = false;
635
- // Skip if nothing changed (unless explicitly forced)
636
435
  if (shouldSkip) {
637
436
  return;
638
437
  }
639
- // If write lock is held, defer render
640
438
  if (writeLock.isLocked()) {
641
439
  writeLock.safeWrite(() => this.render());
642
440
  return;
643
441
  }
442
+ this.renderFloatingInputArea();
443
+ }
444
+ /**
445
+ * Core floating input area renderer.
446
+ * Chat box always floats at contentRow (below streamed content).
447
+ * This creates "persistent bottom floating" behavior.
448
+ */
449
+ renderFloatingInputArea() {
450
+ const { rows, cols } = this.getSize();
451
+ const maxWidth = Math.max(8, cols - 4);
452
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
453
+ // Wrap buffer into display lines
454
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
455
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
456
+ const displayLines = Math.min(lines.length, maxVisible);
457
+ const metaLines = this.buildMetaLines(cols - 2);
458
+ // Calculate display window (keep cursor visible)
459
+ let startLine = 0;
460
+ if (lines.length > displayLines) {
461
+ startLine = Math.max(0, cursorLine - displayLines + 1);
462
+ startLine = Math.min(startLine, lines.length - displayLines);
463
+ }
464
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
465
+ const adjustedCursorLine = cursorLine - startLine;
466
+ // Chat box height
467
+ const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
468
+ // Chat box floats at contentRow - right below the last content
469
+ // This is "persistent bottom floating" - always at bottom of content
470
+ const chatBoxStartRow = this.contentRow;
471
+ writeLock.lock('terminalInput.renderFloating');
644
472
  this.isRendering = true;
645
- writeLock.lock('terminalInput.render');
646
473
  try {
647
- this.renderFloatingInputArea();
474
+ // Hide cursor during render
475
+ this.write(ESC.HIDE);
476
+ this.write(ESC.RESET);
477
+ // Clear the chat box area
478
+ for (let i = 0; i < chatBoxHeight; i++) {
479
+ this.write(ESC.TO(chatBoxStartRow + i, 1));
480
+ this.write(ESC.CLEAR_LINE);
481
+ }
482
+ let currentRow = chatBoxStartRow;
483
+ // Meta/status header
484
+ for (const metaLine of metaLines) {
485
+ this.write(ESC.TO(currentRow, 1));
486
+ this.write(metaLine);
487
+ currentRow += 1;
488
+ }
489
+ // Separator line
490
+ this.write(ESC.TO(currentRow, 1));
491
+ this.write(renderDivider(cols - 2));
492
+ currentRow += 1;
493
+ // Render input lines
494
+ let finalRow = currentRow;
495
+ let finalCol = 3;
496
+ for (let i = 0; i < visibleLines.length; i++) {
497
+ const rowNum = currentRow + i;
498
+ this.write(ESC.TO(rowNum, 1));
499
+ const line = visibleLines[i] ?? '';
500
+ const isFirstLine = (startLine + i) === 0;
501
+ const isCursorLine = i === adjustedCursorLine;
502
+ this.write(ESC.BG_DARK);
503
+ this.write(ESC.DIM);
504
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
505
+ this.write(ESC.RESET);
506
+ this.write(ESC.BG_DARK);
507
+ if (isCursorLine) {
508
+ const col = Math.min(cursorCol, line.length);
509
+ const before = line.slice(0, col);
510
+ const at = col < line.length ? line[col] : ' ';
511
+ const after = col < line.length ? line.slice(col + 1) : '';
512
+ this.write(before);
513
+ this.write(ESC.REVERSE + ESC.BOLD);
514
+ this.write(at);
515
+ this.write(ESC.RESET + ESC.BG_DARK);
516
+ this.write(after);
517
+ finalRow = rowNum;
518
+ finalCol = this.config.promptChar.length + col + 1;
519
+ }
520
+ else {
521
+ this.write(line);
522
+ }
523
+ // Pad to edge
524
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
525
+ const padding = Math.max(0, cols - lineLen - 1);
526
+ if (padding > 0)
527
+ this.write(' '.repeat(padding));
528
+ this.write(ESC.RESET);
529
+ }
530
+ // Mode controls line
531
+ const controlRow = currentRow + visibleLines.length;
532
+ this.write(ESC.TO(controlRow, 1));
533
+ this.write(this.buildModeControls(cols));
534
+ // Position cursor in input box
535
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
536
+ this.write(ESC.SHOW);
537
+ // Update state
538
+ this.lastRenderContent = this.buffer;
539
+ this.lastRenderCursor = this.cursor;
540
+ this.lastStreamingRender = streamingActive ? Date.now() : 0;
541
+ if (this.streamingRenderTimer) {
542
+ clearTimeout(this.streamingRenderTimer);
543
+ this.streamingRenderTimer = null;
544
+ }
648
545
  }
649
546
  finally {
650
547
  writeLock.unlock();
@@ -652,99 +549,217 @@ export class TerminalInput extends EventEmitter {
652
549
  }
653
550
  }
654
551
  /**
655
- * Build status bar showing streaming/ready status and key info.
656
- * This is the TOP line above the input area - minimal Claude Code style.
552
+ * Build one or more compact meta lines above the divider (thinking, status, usage).
553
+ * During streaming, shows model line pinned above streaming info.
657
554
  */
658
- buildStatusBar(cols) {
659
- const maxWidth = cols - 2;
660
- const parts = [];
661
- // Streaming status with elapsed time (left side)
662
- if (this.mode === 'streaming') {
663
- let statusText = '● Streaming';
664
- if (this.streamingStartTime) {
665
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
666
- const mins = Math.floor(elapsed / 60);
667
- const secs = elapsed % 60;
668
- statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
555
+ buildMetaLines(width) {
556
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
557
+ const lines = [];
558
+ // Model line should ALWAYS be shown (pinned above streaming content)
559
+ if (this.modelLabel) {
560
+ const modelText = this.providerLabel
561
+ ? `model ${this.modelLabel} @ ${this.providerLabel}`
562
+ : `model ${this.modelLabel}`;
563
+ lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
564
+ }
565
+ // During streaming, add a compact status line with essential info
566
+ if (streamingActive) {
567
+ const parts = [];
568
+ // Essential streaming info
569
+ if (this.metaThinkingMs !== null) {
570
+ parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
571
+ }
572
+ if (this.metaElapsedSeconds !== null) {
573
+ parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
574
+ }
575
+ parts.push({ text: 'esc to stop', tone: 'warn' });
576
+ if (parts.length) {
577
+ lines.push(renderStatusLine(parts, width));
669
578
  }
670
- parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
579
+ return lines;
671
580
  }
672
- // Queue indicator during streaming
673
- if (this.mode === 'streaming' && this.queue.length > 0) {
674
- parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
581
+ // Non-streaming: show full status info (model line already added above)
582
+ if (this.metaThinkingMs !== null) {
583
+ const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
584
+ lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
675
585
  }
676
- // Paste indicator
677
- if (this.pastePlaceholders.length > 0) {
678
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
679
- parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
586
+ const statusParts = [];
587
+ const statusLabel = this.statusMessage ?? this.streamingLabel;
588
+ if (statusLabel) {
589
+ statusParts.push({ text: statusLabel, tone: 'info' });
680
590
  }
681
- // Override/warning status
682
- if (this.overrideStatusMessage) {
683
- parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
591
+ if (this.metaElapsedSeconds !== null) {
592
+ statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
684
593
  }
685
- // If idle with empty buffer, show quick shortcuts
686
- if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
687
- return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
594
+ const tokensRemaining = this.computeTokensRemaining();
595
+ if (tokensRemaining !== null) {
596
+ statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
688
597
  }
689
- // Multi-line indicator
690
- if (this.buffer.includes('\n')) {
691
- parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
598
+ if (statusParts.length) {
599
+ lines.push(renderStatusLine(statusParts, width));
692
600
  }
693
- if (parts.length === 0) {
694
- return ''; // Empty status bar when idle
601
+ const usageParts = [];
602
+ if (this.metaTokensUsed !== null) {
603
+ const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
604
+ const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
605
+ usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
695
606
  }
696
- const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
697
- return joined.slice(0, maxWidth);
607
+ if (this.contextUsage !== null) {
608
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
609
+ const left = Math.max(0, 100 - this.contextUsage);
610
+ usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
611
+ }
612
+ if (this.queue.length > 0) {
613
+ usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
614
+ }
615
+ if (usageParts.length) {
616
+ lines.push(renderStatusLine(usageParts, width));
617
+ }
618
+ return lines;
698
619
  }
699
620
  /**
700
- * Build mode controls line showing toggles and context info.
701
- * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
702
- *
703
- * Layout: [toggles on left] ... [context info on right]
621
+ * Build Claude Code style mode controls line.
622
+ * Combines streaming label + override status + main status for simultaneous display.
704
623
  */
705
624
  buildModeControls(cols) {
706
- const maxWidth = cols - 2;
707
- // Use schema-defined colors for consistency
708
- const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
709
- // Mode toggles with colors (following ModeControlsSchema)
710
- const toggles = [];
711
- // Edit mode (green=auto, yellow=ask) - per schema.editMode
712
- if (this.editMode === 'display-edits') {
713
- toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
625
+ const width = Math.max(8, cols - 2);
626
+ const leftParts = [];
627
+ const rightParts = [];
628
+ if (this.streamingLabel) {
629
+ leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
714
630
  }
715
- else {
716
- toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
717
- }
718
- // Thinking mode (cyan when on) - per schema.thinkingMode
719
- toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
720
- // Verification (green when on) - per schema.verificationMode
721
- toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
722
- // Auto-continue (magenta when on) - per schema.autoContinueMode
723
- toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
724
- const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
725
- // Context usage with color - per schema.contextUsage thresholds
726
- let rightPart = '';
727
- if (this.contextUsage !== null) {
728
- const rem = Math.max(0, 100 - this.contextUsage);
729
- // Thresholds: critical < 10%, warning < 25%
730
- if (rem < 10)
731
- rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
732
- else if (rem < 25)
733
- rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
734
- else
735
- rightPart = `${DIM}ctx: ${rem}%${R}`;
736
- }
737
- // Calculate visible lengths (strip ANSI)
738
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
739
- const leftLen = strip(leftPart).length;
740
- const rightLen = strip(rightPart).length;
741
- if (leftLen + rightLen < maxWidth - 4) {
742
- return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
743
- }
744
- if (rightLen > 0 && leftLen + 8 < maxWidth) {
745
- return `${leftPart} ${rightPart}`;
746
- }
747
- return leftPart;
631
+ if (this.overrideStatusMessage) {
632
+ leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
633
+ }
634
+ if (this.statusMessage) {
635
+ leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
636
+ }
637
+ const editHotkey = this.formatHotkey('shift+tab');
638
+ const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
639
+ const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
640
+ leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
641
+ const verifyHotkey = this.formatHotkey(this.verificationHotkey);
642
+ const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
643
+ leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
644
+ const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
645
+ const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
646
+ leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
647
+ if (this.queue.length > 0 && this.mode !== 'streaming') {
648
+ leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
649
+ }
650
+ if (this.buffer.includes('\n')) {
651
+ const lineCount = this.buffer.split('\n').length;
652
+ leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
653
+ }
654
+ if (this.pastePlaceholders.length > 0) {
655
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
656
+ leftParts.push({
657
+ text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
658
+ tone: 'info',
659
+ });
660
+ }
661
+ const contextRemaining = this.computeContextRemaining();
662
+ if (this.thinkingModeLabel) {
663
+ const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
664
+ rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
665
+ }
666
+ // Show model in controls only when NOT streaming (during streaming it's in meta lines)
667
+ const streamingActive = this.mode === 'streaming' || isStreamingMode();
668
+ if (this.modelLabel && !streamingActive) {
669
+ const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
670
+ rightParts.push({ text: modelText, tone: 'muted' });
671
+ }
672
+ if (contextRemaining !== null) {
673
+ const tone = contextRemaining <= 10 ? 'warn' : 'muted';
674
+ const label = contextRemaining === 0 && this.contextUsage !== null
675
+ ? 'Context auto-compact imminent'
676
+ : `Context left until auto-compact: ${contextRemaining}%`;
677
+ rightParts.push({ text: label, tone });
678
+ }
679
+ if (!rightParts.length || width < 60) {
680
+ const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
681
+ return renderStatusLine(merged, width);
682
+ }
683
+ const leftWidth = Math.max(12, Math.floor(width * 0.6));
684
+ const rightWidth = Math.max(14, width - leftWidth - 1);
685
+ const leftText = renderStatusLine(leftParts, leftWidth);
686
+ const rightText = renderStatusLine(rightParts, rightWidth);
687
+ const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
688
+ return `${leftText}${' '.repeat(spacing)}${rightText}`;
689
+ }
690
+ formatHotkey(hotkey) {
691
+ const normalized = hotkey.trim().toLowerCase();
692
+ if (!normalized)
693
+ return hotkey;
694
+ const parts = normalized.split('+').filter(Boolean);
695
+ const map = {
696
+ shift: '⇧',
697
+ sh: '⇧',
698
+ alt: '⌥',
699
+ option: '⌥',
700
+ opt: '⌥',
701
+ ctrl: '⌃',
702
+ control: '⌃',
703
+ cmd: '⌘',
704
+ meta: '⌘',
705
+ };
706
+ const formatted = parts
707
+ .map((part) => {
708
+ const symbol = map[part];
709
+ if (symbol)
710
+ return symbol;
711
+ return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
712
+ })
713
+ .join('');
714
+ return formatted || hotkey;
715
+ }
716
+ computeContextRemaining() {
717
+ if (this.contextUsage === null) {
718
+ return null;
719
+ }
720
+ return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
721
+ }
722
+ computeTokensRemaining() {
723
+ if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
724
+ return null;
725
+ }
726
+ const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
727
+ return this.formatTokenCount(remaining);
728
+ }
729
+ formatElapsedLabel(seconds) {
730
+ if (seconds < 60) {
731
+ return `${seconds}s`;
732
+ }
733
+ const mins = Math.floor(seconds / 60);
734
+ const secs = seconds % 60;
735
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
736
+ }
737
+ formatTokenCount(value) {
738
+ if (!Number.isFinite(value)) {
739
+ return `${value}`;
740
+ }
741
+ if (value >= 1_000_000) {
742
+ return `${(value / 1_000_000).toFixed(1)}M`;
743
+ }
744
+ if (value >= 1_000) {
745
+ return `${(value / 1_000).toFixed(1)}k`;
746
+ }
747
+ return `${Math.round(value)}`;
748
+ }
749
+ visibleLength(value) {
750
+ const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
751
+ return value.replace(ansiPattern, '').length;
752
+ }
753
+ /**
754
+ * Debug-only snapshot used by tests to assert rendered strings without
755
+ * needing a TTY. Not used by production code.
756
+ */
757
+ getDebugUiSnapshot(width) {
758
+ const cols = Math.max(8, width ?? this.getSize().cols);
759
+ return {
760
+ meta: this.buildMetaLines(cols - 2),
761
+ controls: this.buildModeControls(cols),
762
+ };
748
763
  }
749
764
  /**
750
765
  * Force a re-render
@@ -767,20 +782,65 @@ export class TerminalInput extends EventEmitter {
767
782
  handleResize() {
768
783
  this.lastRenderContent = '';
769
784
  this.lastRenderCursor = -1;
785
+ this.resetStreamingRenderThrottle();
770
786
  this.scheduleRender();
771
787
  }
772
788
  /**
773
- * Register with display's output interceptor (kept for API compatibility).
774
- * With unified floating input, no special cursor manipulation is needed.
789
+ * Stream content above the floating chat box.
790
+ *
791
+ * This is the CLEAN method for streaming - no output interceptor needed.
792
+ * 1. Position cursor at contentRow
793
+ * 2. Write content directly (overwrites any chat box visually)
794
+ * 3. Advance contentRow by newlines
795
+ * 4. Re-render chat box at new position
796
+ *
797
+ * The chat box always re-renders after content, so it appears to "float" below.
775
798
  */
776
- registerOutputInterceptor(display) {
777
- if (this.outputInterceptorCleanup) {
778
- this.outputInterceptorCleanup();
779
- }
780
- this.outputInterceptorCleanup = display.registerOutputInterceptor({
781
- beforeWrite: () => { },
782
- afterWrite: () => { },
783
- });
799
+ streamContent(content) {
800
+ if (!content)
801
+ return;
802
+ // Position cursor at contentRow and write content
803
+ this.write(ESC.TO(this.contentRow, 1));
804
+ this.write(content);
805
+ // Advance contentRow by number of newlines
806
+ const newlines = (content.match(/\n/g) || []).length;
807
+ this.contentRow += newlines;
808
+ // Re-render chat box at new position (below the content just written)
809
+ this.forceRender();
810
+ }
811
+ /**
812
+ * @deprecated Use streamContent() instead
813
+ * Register with display's output interceptor - kept for backwards compatibility
814
+ */
815
+ registerOutputInterceptor(_display) {
816
+ // No-op: Use streamContent() for cleaner floating chat box behavior
817
+ }
818
+ /**
819
+ * @deprecated Use streamContent() instead
820
+ * Write content above the floating chat box.
821
+ */
822
+ writeToScrollRegion(content) {
823
+ this.streamContent(content);
824
+ }
825
+ /**
826
+ * Reset content position to row 1.
827
+ * Does NOT clear the terminal - content starts from current position.
828
+ */
829
+ resetContentPosition() {
830
+ this.contentRow = 1;
831
+ }
832
+ /**
833
+ * Set the content row explicitly (used after banner is written).
834
+ * This tells the input where content should start flowing from.
835
+ */
836
+ setContentRow(row) {
837
+ this.contentRow = Math.max(1, row);
838
+ }
839
+ /**
840
+ * Get the current content row position.
841
+ */
842
+ getContentRow() {
843
+ return this.contentRow;
784
844
  }
785
845
  /**
786
846
  * Dispose and clean up
@@ -788,18 +848,9 @@ export class TerminalInput extends EventEmitter {
788
848
  dispose() {
789
849
  if (this.disposed)
790
850
  return;
791
- // Clean up streaming render timer
792
- if (this.streamingRenderTimer) {
793
- clearInterval(this.streamingRenderTimer);
794
- this.streamingRenderTimer = null;
795
- }
796
- // Clean up output interceptor
797
- if (this.outputInterceptorCleanup) {
798
- this.outputInterceptorCleanup();
799
- this.outputInterceptorCleanup = undefined;
800
- }
801
851
  this.disposed = true;
802
852
  this.enabled = false;
853
+ this.resetStreamingRenderThrottle();
803
854
  this.disableBracketedPaste();
804
855
  this.buffer = '';
805
856
  this.queue = [];
@@ -904,22 +955,7 @@ export class TerminalInput extends EventEmitter {
904
955
  this.toggleEditMode();
905
956
  return true;
906
957
  }
907
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
908
- if (this.findPlaceholderAt(this.cursor)) {
909
- this.togglePasteExpansion();
910
- }
911
- else {
912
- this.toggleThinking();
913
- }
914
- return true;
915
- case 'escape':
916
- // Esc: interrupt if streaming, otherwise clear buffer
917
- if (this.mode === 'streaming') {
918
- this.emit('interrupt');
919
- }
920
- else if (this.buffer.length > 0) {
921
- this.clear();
922
- }
958
+ this.insertText(' ');
923
959
  return true;
924
960
  }
925
961
  return false;
@@ -937,7 +973,6 @@ export class TerminalInput extends EventEmitter {
937
973
  this.insertPlainText(chunk, insertPos);
938
974
  this.cursor = insertPos + chunk.length;
939
975
  this.emit('change', this.buffer);
940
- this.updateSuggestions();
941
976
  this.scheduleRender();
942
977
  }
943
978
  insertNewline() {
@@ -962,7 +997,6 @@ export class TerminalInput extends EventEmitter {
962
997
  this.cursor = Math.max(0, this.cursor - 1);
963
998
  }
964
999
  this.emit('change', this.buffer);
965
- this.updateSuggestions();
966
1000
  this.scheduleRender();
967
1001
  }
968
1002
  deleteForward() {
@@ -1190,13 +1224,12 @@ export class TerminalInput extends EventEmitter {
1190
1224
  timestamp: Date.now(),
1191
1225
  });
1192
1226
  this.emit('queue', text);
1193
- this.clear(); // Clear immediately for queued input, re-render to update queue display
1227
+ this.clear(); // Clear immediately for queued input
1194
1228
  }
1195
1229
  else {
1196
- // In idle mode, clear the input WITHOUT rendering.
1197
- // The caller will display the user message and start streaming.
1198
- // We'll render the input area again after streaming ends.
1199
- this.clear(true); // Skip render - streaming will handle display
1230
+ // In idle mode, clear the input first, then emit submit.
1231
+ // The prompt will be logged as a visible message by the caller.
1232
+ this.clear();
1200
1233
  this.emit('submit', text);
1201
1234
  }
1202
1235
  }
@@ -1213,7 +1246,9 @@ export class TerminalInput extends EventEmitter {
1213
1246
  if (available <= 0)
1214
1247
  return;
1215
1248
  const chunk = clean.slice(0, available);
1216
- if (isMultilinePaste(chunk)) {
1249
+ const isMultiline = isMultilinePaste(chunk);
1250
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1251
+ if (isMultiline && !isShortMultiline) {
1217
1252
  this.insertPastePlaceholder(chunk);
1218
1253
  }
1219
1254
  else {
@@ -1349,17 +1384,19 @@ export class TerminalInput extends EventEmitter {
1349
1384
  this.shiftPlaceholders(position, text.length);
1350
1385
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1351
1386
  }
1387
+ shouldInlineMultiline(content) {
1388
+ const lines = content.split('\n').length;
1389
+ const maxInlineLines = 4;
1390
+ const maxInlineChars = 240;
1391
+ return lines <= maxInlineLines && content.length <= maxInlineChars;
1392
+ }
1352
1393
  findPlaceholderAt(position) {
1353
1394
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1354
1395
  }
1355
- buildPlaceholder(summary) {
1396
+ buildPlaceholder(lineCount) {
1356
1397
  const id = ++this.pasteCounter;
1357
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1358
- // Show first line preview (truncated)
1359
- const preview = summary.preview.length > 30
1360
- ? `${summary.preview.slice(0, 30)}...`
1361
- : summary.preview;
1362
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1398
+ const plural = lineCount === 1 ? '' : 's';
1399
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1363
1400
  return { id, placeholder };
1364
1401
  }
1365
1402
  insertPastePlaceholder(content) {
@@ -1367,67 +1404,21 @@ export class TerminalInput extends EventEmitter {
1367
1404
  if (available <= 0)
1368
1405
  return;
1369
1406
  const cleanContent = content.slice(0, available);
1370
- const summary = generatePasteSummary(cleanContent);
1371
- // For short pastes (< 5 lines), show full content instead of placeholder
1372
- if (summary.lineCount < 5) {
1373
- const placeholder = this.findPlaceholderAt(this.cursor);
1374
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1375
- this.insertPlainText(cleanContent, insertPos);
1376
- this.cursor = insertPos + cleanContent.length;
1377
- return;
1378
- }
1379
- const { id, placeholder } = this.buildPlaceholder(summary);
1407
+ const lineCount = cleanContent.split('\n').length;
1408
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1380
1409
  const insertPos = this.cursor;
1381
1410
  this.shiftPlaceholders(insertPos, placeholder.length);
1382
1411
  this.pastePlaceholders.push({
1383
1412
  id,
1384
1413
  content: cleanContent,
1385
- lineCount: summary.lineCount,
1414
+ lineCount,
1386
1415
  placeholder,
1387
1416
  start: insertPos,
1388
1417
  end: insertPos + placeholder.length,
1389
- summary,
1390
- expanded: false,
1391
1418
  });
1392
1419
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1393
1420
  this.cursor = insertPos + placeholder.length;
1394
1421
  }
1395
- /**
1396
- * Toggle expansion of a paste placeholder at the current cursor position.
1397
- * When expanded, shows first 3 and last 2 lines of the content.
1398
- */
1399
- togglePasteExpansion() {
1400
- const placeholder = this.findPlaceholderAt(this.cursor);
1401
- if (!placeholder)
1402
- return false;
1403
- placeholder.expanded = !placeholder.expanded;
1404
- // Update the placeholder text in buffer
1405
- const newPlaceholder = placeholder.expanded
1406
- ? this.buildExpandedPlaceholder(placeholder)
1407
- : this.buildPlaceholder(placeholder.summary).placeholder;
1408
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1409
- // Update buffer
1410
- this.buffer =
1411
- this.buffer.slice(0, placeholder.start) +
1412
- newPlaceholder +
1413
- this.buffer.slice(placeholder.end);
1414
- // Update placeholder tracking
1415
- placeholder.placeholder = newPlaceholder;
1416
- placeholder.end = placeholder.start + newPlaceholder.length;
1417
- // Shift other placeholders
1418
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1419
- this.scheduleRender();
1420
- return true;
1421
- }
1422
- buildExpandedPlaceholder(ph) {
1423
- const lines = ph.content.split('\n');
1424
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1425
- const lastLines = lines.length > 5
1426
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1427
- : '';
1428
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1429
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1430
- }
1431
1422
  deletePlaceholder(placeholder) {
1432
1423
  const length = placeholder.end - placeholder.start;
1433
1424
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1435,7 +1426,11 @@ export class TerminalInput extends EventEmitter {
1435
1426
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1436
1427
  this.cursor = placeholder.start;
1437
1428
  }
1438
- updateContextUsage(value) {
1429
+ updateContextUsage(value, autoCompactThreshold) {
1430
+ if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1431
+ const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1432
+ this.contextAutoCompactThreshold = boundedThreshold;
1433
+ }
1439
1434
  if (value === null || !Number.isFinite(value)) {
1440
1435
  this.contextUsage = null;
1441
1436
  }
@@ -1462,6 +1457,22 @@ export class TerminalInput extends EventEmitter {
1462
1457
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1463
1458
  this.setEditMode(next);
1464
1459
  }
1460
+ scheduleStreamingRender(delayMs) {
1461
+ if (this.streamingRenderTimer)
1462
+ return;
1463
+ const wait = Math.max(16, delayMs);
1464
+ this.streamingRenderTimer = setTimeout(() => {
1465
+ this.streamingRenderTimer = null;
1466
+ this.render();
1467
+ }, wait);
1468
+ }
1469
+ resetStreamingRenderThrottle() {
1470
+ if (this.streamingRenderTimer) {
1471
+ clearTimeout(this.streamingRenderTimer);
1472
+ this.streamingRenderTimer = null;
1473
+ }
1474
+ this.lastStreamingRender = 0;
1475
+ }
1465
1476
  scheduleRender() {
1466
1477
  if (!this.canRender())
1467
1478
  return;