erosolar-cli 1.7.300 → 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 -107
  74. package/dist/shell/terminalInput.d.ts.map +1 -1
  75. package/dist/shell/terminalInput.js +501 -484
  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,282 +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. Render floating input area below banner
213
- */
214
- initializeUnifiedUI() {
215
- if (this.unifiedUIInitialized) {
216
- return;
217
- }
218
- // Hide cursor during setup
219
- this.write(ESC.HIDE);
220
- // Clear screen and go home
221
- this.write(ESC.HOME);
222
- this.write(ESC.CLEAR_SCREEN);
223
- // Write banner at top
224
- if (this.bannerContent) {
225
- process.stdout.write(this.bannerContent + '\n\n');
226
- }
227
- // Mark initialized
228
- this.unifiedUIInitialized = true;
229
- // Render floating input area
230
- this.renderFloatingInputArea();
231
- }
232
- /**
233
- * Clear the input area at its tracked position.
234
- * Returns true if something was cleared.
235
- */
236
- clearInputArea() {
237
- if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
238
- for (let i = 0; i < this.flowModeRenderedLines; i++) {
239
- this.write(ESC.TO(this.inputAreaStartRow + i, 1));
240
- this.write(ESC.CLEAR_LINE);
241
- }
242
- return true;
243
- }
244
- return false;
245
- }
246
- /**
247
- * Reset input area tracking state.
248
- */
249
- resetInputAreaTracking() {
250
- this.inputAreaStartRow = 0;
251
- this.flowModeRenderedLines = 0;
252
- }
253
- /**
254
- * Render floating input area below current content.
255
- * This is "bottom floating" - follows content, not pinned to terminal bottom.
256
- * Uses absolute positioning to prevent duplicates.
257
- */
258
- renderFloatingInputArea() {
259
- const { rows, cols } = this.getSize();
260
- const divider = '─'.repeat(cols - 1);
261
- const { dim: DIM, reset: R } = UI_COLORS;
262
- // Calculate lines needed for input area
263
- const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
264
- // FIRST: Clear any previously rendered input area
265
- this.clearInputArea();
266
- // Hide cursor during render
267
- this.write(ESC.HIDE);
268
- // Calculate where to render: after current content
269
- // Use contentEndRow if set, otherwise estimate from terminal bottom
270
- let startRow;
271
- if (this.contentEndRow > 0) {
272
- startRow = this.contentEndRow + 1;
273
- }
274
- else {
275
- // Render near bottom, leaving space for input area
276
- startRow = Math.max(1, rows - linesNeeded);
277
- }
278
- // Ensure we don't go past terminal bounds
279
- startRow = Math.min(startRow, rows - linesNeeded + 1);
280
- startRow = Math.max(1, startRow);
281
- // Track this position
282
- this.inputAreaStartRow = startRow;
283
- let currentRow = startRow;
284
- // Status bar
285
- this.write(ESC.TO(currentRow, 1));
286
- this.write(this.buildStatusBar(cols));
287
- currentRow++;
288
- // Model info line (if set)
289
- if (this.modelInfo) {
290
- this.write(ESC.TO(currentRow, 1));
291
- let modelLine = `${DIM}${this.modelInfo}${R}`;
292
- if (this.contextUsage !== null) {
293
- const rem = Math.max(0, 100 - this.contextUsage);
294
- if (rem < 10)
295
- modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
296
- else if (rem < 25)
297
- modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
298
- else
299
- modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
300
- }
301
- this.write(modelLine);
302
- currentRow++;
303
- }
304
- // Top divider
305
- this.write(ESC.TO(currentRow, 1));
306
- this.write(divider);
307
- currentRow++;
308
- // Input line with prompt and buffer content
309
- const { lines, cursorCol } = this.wrapBuffer(cols - 4);
310
- const displayLine = lines[0] ?? '';
311
- const inputRow = currentRow;
312
- this.write(ESC.TO(currentRow, 1));
313
- this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
314
- this.write(ESC.BG_DARK + displayLine);
315
- const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
316
- if (padding > 0)
317
- this.write(' '.repeat(padding));
318
- this.write(ESC.RESET);
319
- currentRow++;
320
- // Bottom divider
321
- this.write(ESC.TO(currentRow, 1));
322
- this.write(divider);
323
- currentRow++;
324
- // Mode controls
325
- this.write(ESC.TO(currentRow, 1));
326
- this.write(this.buildModeControls(cols));
327
- // Track lines rendered
328
- this.flowModeRenderedLines = currentRow - startRow + 1;
329
- // Position cursor in input line for typing
330
- this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
331
- // Show cursor
332
- this.write(ESC.SHOW);
333
- // Update tracking
334
- this.lastRenderContent = this.buffer;
335
- this.lastRenderCursor = this.cursor;
336
- }
337
193
  /**
338
194
  * Set the input mode
339
195
  *
340
- * Unified floating input - input area always floats below content.
196
+ * Content flows naturally - no scroll region pinning.
341
197
  */
342
198
  setMode(mode) {
343
199
  const prevMode = this.mode;
344
200
  this.mode = mode;
345
201
  if (mode === 'streaming' && prevMode !== 'streaming') {
346
- // Track streaming start time for elapsed display
347
- this.streamingStartTime = Date.now();
348
- // Ensure unified UI is initialized
349
- if (!this.unifiedUIInitialized) {
350
- this.initializeUnifiedUI();
351
- }
202
+ this.resetStreamingRenderThrottle();
352
203
  this.renderDirty = true;
353
- this.scheduleRender();
204
+ this.render();
354
205
  }
355
206
  else if (mode !== 'streaming' && prevMode === 'streaming') {
356
- // Stop streaming render timer (if any)
357
- if (this.streamingRenderTimer) {
358
- clearInterval(this.streamingRenderTimer);
359
- this.streamingRenderTimer = null;
360
- }
361
- // Reset streaming time
362
- this.streamingStartTime = null;
363
- // Re-render floating input area
364
- this.renderDirty = true;
365
- this.scheduleRender();
366
- }
367
- }
368
- /**
369
- * Set the row where content ends (for idle mode positioning).
370
- * Input area will render starting from this row + 1.
371
- */
372
- setContentEndRow(row) {
373
- this.contentEndRow = Math.max(0, row);
374
- this.renderDirty = true;
375
- this.scheduleRender();
376
- }
377
- /**
378
- * Set available slash commands for auto-complete suggestions.
379
- */
380
- setCommands(commands) {
381
- this.commandSuggestions = commands;
382
- this.updateSuggestions();
383
- }
384
- /**
385
- * Update filtered suggestions based on current input.
386
- */
387
- updateSuggestions() {
388
- const input = this.buffer.trim();
389
- // Only show suggestions when input starts with "/"
390
- if (!input.startsWith('/')) {
391
- this.showSuggestions = false;
392
- this.filteredSuggestions = [];
393
- this.selectedSuggestionIndex = 0;
394
- return;
395
- }
396
- const query = input.toLowerCase();
397
- this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
398
- cmd.command.toLowerCase().includes(query.slice(1)));
399
- // Show suggestions if we have matches
400
- this.showSuggestions = this.filteredSuggestions.length > 0;
401
- // Keep selection in bounds
402
- if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
403
- this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
207
+ // Streaming ended - render the input area
208
+ this.resetStreamingRenderThrottle();
209
+ this.forceRender();
404
210
  }
405
211
  }
406
212
  /**
407
- * Select next suggestion (arrow down / tab).
213
+ * Legacy method - no longer used (content flows naturally).
214
+ * @deprecated Use setContentRow instead
408
215
  */
409
- selectNextSuggestion() {
410
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
411
- return;
412
- this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
413
- this.renderDirty = true;
414
- this.scheduleRender();
415
- }
416
- /**
417
- * Select previous suggestion (arrow up / shift+tab).
418
- */
419
- selectPrevSuggestion() {
420
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
421
- return;
422
- this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
423
- ? this.filteredSuggestions.length - 1
424
- : this.selectedSuggestionIndex - 1;
425
- this.renderDirty = true;
426
- this.scheduleRender();
427
- }
428
- /**
429
- * Accept current suggestion and insert into buffer.
430
- */
431
- acceptSuggestion() {
432
- if (!this.showSuggestions || this.filteredSuggestions.length === 0)
433
- return false;
434
- const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
435
- if (!selected)
436
- return false;
437
- // Replace buffer with selected command
438
- this.buffer = selected.command + ' ';
439
- this.cursor = this.buffer.length;
440
- this.showSuggestions = false;
441
- this.renderDirty = true;
442
- this.scheduleRender();
443
- return true;
444
- }
445
- /**
446
- * Check if suggestions are visible.
447
- */
448
- areSuggestionsVisible() {
449
- return this.showSuggestions && this.filteredSuggestions.length > 0;
450
- }
451
- /**
452
- * Toggle thinking/reasoning mode
453
- */
454
- toggleThinking() {
455
- this.thinkingEnabled = !this.thinkingEnabled;
456
- this.emit('thinkingToggle', this.thinkingEnabled);
457
- this.scheduleRender();
458
- }
459
- /**
460
- * Get thinking enabled state
461
- */
462
- isThinkingEnabled() {
463
- return this.thinkingEnabled;
216
+ setPinnedHeaderLines(_count) {
217
+ // No-op: scroll region pinning removed
464
218
  }
465
219
  /**
466
220
  * Get current mode
@@ -493,17 +247,14 @@ export class TerminalInput extends EventEmitter {
493
247
  }
494
248
  /**
495
249
  * Clear the buffer
496
- * @param skipRender - If true, don't trigger a re-render (used during submit flow)
497
250
  */
498
- clear(skipRender = false) {
251
+ clear() {
499
252
  this.buffer = '';
500
253
  this.cursor = 0;
501
254
  this.historyIndex = -1;
502
255
  this.tempInput = '';
503
256
  this.pastePlaceholders = [];
504
- if (!skipRender) {
505
- this.scheduleRender();
506
- }
257
+ this.scheduleRender();
507
258
  }
508
259
  /**
509
260
  * Get queued inputs
@@ -574,6 +325,37 @@ export class TerminalInput extends EventEmitter {
574
325
  this.streamingLabel = next;
575
326
  this.scheduleRender();
576
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
+ }
577
359
  /**
578
360
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
579
361
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -583,26 +365,22 @@ export class TerminalInput extends EventEmitter {
583
365
  const nextAutoContinue = !!options.autoContinueEnabled;
584
366
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
585
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);
586
370
  if (this.verificationEnabled === nextVerification &&
587
371
  this.autoContinueEnabled === nextAutoContinue &&
588
372
  this.verificationHotkey === nextVerifyHotkey &&
589
- this.autoContinueHotkey === nextAutoHotkey) {
373
+ this.autoContinueHotkey === nextAutoHotkey &&
374
+ this.thinkingHotkey === nextThinkingHotkey &&
375
+ this.thinkingModeLabel === nextThinkingLabel) {
590
376
  return;
591
377
  }
592
378
  this.verificationEnabled = nextVerification;
593
379
  this.autoContinueEnabled = nextAutoContinue;
594
380
  this.verificationHotkey = nextVerifyHotkey;
595
381
  this.autoContinueHotkey = nextAutoHotkey;
596
- this.scheduleRender();
597
- }
598
- /**
599
- * Set the model info string (e.g., "OpenAI · gpt-4")
600
- * This is displayed persistently above the input area.
601
- */
602
- setModelInfo(info) {
603
- if (this.modelInfo === info)
604
- return;
605
- this.modelInfo = info;
382
+ this.thinkingHotkey = nextThinkingHotkey;
383
+ this.thinkingModeLabel = nextThinkingLabel;
606
384
  this.scheduleRender();
607
385
  }
608
386
  /**
@@ -615,30 +393,155 @@ export class TerminalInput extends EventEmitter {
615
393
  this.scheduleRender();
616
394
  }
617
395
  /**
618
- * 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.
619
414
  */
620
415
  render() {
621
416
  if (!this.canRender())
622
417
  return;
623
418
  if (this.isRendering)
624
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
+ }
625
431
  const shouldSkip = !this.renderDirty &&
626
432
  this.buffer === this.lastRenderContent &&
627
433
  this.cursor === this.lastRenderCursor;
628
434
  this.renderDirty = false;
629
- // Skip if nothing changed (unless explicitly forced)
630
435
  if (shouldSkip) {
631
436
  return;
632
437
  }
633
- // If write lock is held, defer render
634
438
  if (writeLock.isLocked()) {
635
439
  writeLock.safeWrite(() => this.render());
636
440
  return;
637
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');
638
472
  this.isRendering = true;
639
- writeLock.lock('terminalInput.render');
640
473
  try {
641
- 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
+ }
642
545
  }
643
546
  finally {
644
547
  writeLock.unlock();
@@ -646,99 +549,217 @@ export class TerminalInput extends EventEmitter {
646
549
  }
647
550
  }
648
551
  /**
649
- * Build status bar showing streaming/ready status and key info.
650
- * 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.
651
554
  */
652
- buildStatusBar(cols) {
653
- const maxWidth = cols - 2;
654
- const parts = [];
655
- // Streaming status with elapsed time (left side)
656
- if (this.mode === 'streaming') {
657
- let statusText = '● Streaming';
658
- if (this.streamingStartTime) {
659
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
660
- const mins = Math.floor(elapsed / 60);
661
- const secs = elapsed % 60;
662
- 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));
663
578
  }
664
- parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
579
+ return lines;
665
580
  }
666
- // Queue indicator during streaming
667
- if (this.mode === 'streaming' && this.queue.length > 0) {
668
- 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));
669
585
  }
670
- // Paste indicator
671
- if (this.pastePlaceholders.length > 0) {
672
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
673
- 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' });
674
590
  }
675
- // Override/warning status
676
- if (this.overrideStatusMessage) {
677
- parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
591
+ if (this.metaElapsedSeconds !== null) {
592
+ statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
678
593
  }
679
- // If idle with empty buffer, show quick shortcuts
680
- if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
681
- 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' });
682
597
  }
683
- // Multi-line indicator
684
- if (this.buffer.includes('\n')) {
685
- parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
598
+ if (statusParts.length) {
599
+ lines.push(renderStatusLine(statusParts, width));
686
600
  }
687
- if (parts.length === 0) {
688
- 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' });
689
606
  }
690
- const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
691
- 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;
692
619
  }
693
620
  /**
694
- * Build mode controls line showing toggles and context info.
695
- * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
696
- *
697
- * 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.
698
623
  */
699
624
  buildModeControls(cols) {
700
- const maxWidth = cols - 2;
701
- // Use schema-defined colors for consistency
702
- const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
703
- // Mode toggles with colors (following ModeControlsSchema)
704
- const toggles = [];
705
- // Edit mode (green=auto, yellow=ask) - per schema.editMode
706
- if (this.editMode === 'display-edits') {
707
- 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' });
708
630
  }
709
- else {
710
- toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
711
- }
712
- // Thinking mode (cyan when on) - per schema.thinkingMode
713
- toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
714
- // Verification (green when on) - per schema.verificationMode
715
- toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
716
- // Auto-continue (magenta when on) - per schema.autoContinueMode
717
- toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
718
- const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
719
- // Context usage with color - per schema.contextUsage thresholds
720
- let rightPart = '';
721
- if (this.contextUsage !== null) {
722
- const rem = Math.max(0, 100 - this.contextUsage);
723
- // Thresholds: critical < 10%, warning < 25%
724
- if (rem < 10)
725
- rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
726
- else if (rem < 25)
727
- rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
728
- else
729
- rightPart = `${DIM}ctx: ${rem}%${R}`;
730
- }
731
- // Calculate visible lengths (strip ANSI)
732
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
733
- const leftLen = strip(leftPart).length;
734
- const rightLen = strip(rightPart).length;
735
- if (leftLen + rightLen < maxWidth - 4) {
736
- return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
737
- }
738
- if (rightLen > 0 && leftLen + 8 < maxWidth) {
739
- return `${leftPart} ${rightPart}`;
740
- }
741
- 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
+ };
742
763
  }
743
764
  /**
744
765
  * Force a re-render
@@ -761,20 +782,65 @@ export class TerminalInput extends EventEmitter {
761
782
  handleResize() {
762
783
  this.lastRenderContent = '';
763
784
  this.lastRenderCursor = -1;
785
+ this.resetStreamingRenderThrottle();
764
786
  this.scheduleRender();
765
787
  }
766
788
  /**
767
- * Register with display's output interceptor (kept for API compatibility).
768
- * 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.
769
798
  */
770
- registerOutputInterceptor(display) {
771
- if (this.outputInterceptorCleanup) {
772
- this.outputInterceptorCleanup();
773
- }
774
- this.outputInterceptorCleanup = display.registerOutputInterceptor({
775
- beforeWrite: () => { },
776
- afterWrite: () => { },
777
- });
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;
778
844
  }
779
845
  /**
780
846
  * Dispose and clean up
@@ -782,18 +848,9 @@ export class TerminalInput extends EventEmitter {
782
848
  dispose() {
783
849
  if (this.disposed)
784
850
  return;
785
- // Clean up streaming render timer
786
- if (this.streamingRenderTimer) {
787
- clearInterval(this.streamingRenderTimer);
788
- this.streamingRenderTimer = null;
789
- }
790
- // Clean up output interceptor
791
- if (this.outputInterceptorCleanup) {
792
- this.outputInterceptorCleanup();
793
- this.outputInterceptorCleanup = undefined;
794
- }
795
851
  this.disposed = true;
796
852
  this.enabled = false;
853
+ this.resetStreamingRenderThrottle();
797
854
  this.disableBracketedPaste();
798
855
  this.buffer = '';
799
856
  this.queue = [];
@@ -898,22 +955,7 @@ export class TerminalInput extends EventEmitter {
898
955
  this.toggleEditMode();
899
956
  return true;
900
957
  }
901
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
902
- if (this.findPlaceholderAt(this.cursor)) {
903
- this.togglePasteExpansion();
904
- }
905
- else {
906
- this.toggleThinking();
907
- }
908
- return true;
909
- case 'escape':
910
- // Esc: interrupt if streaming, otherwise clear buffer
911
- if (this.mode === 'streaming') {
912
- this.emit('interrupt');
913
- }
914
- else if (this.buffer.length > 0) {
915
- this.clear();
916
- }
958
+ this.insertText(' ');
917
959
  return true;
918
960
  }
919
961
  return false;
@@ -931,7 +973,6 @@ export class TerminalInput extends EventEmitter {
931
973
  this.insertPlainText(chunk, insertPos);
932
974
  this.cursor = insertPos + chunk.length;
933
975
  this.emit('change', this.buffer);
934
- this.updateSuggestions();
935
976
  this.scheduleRender();
936
977
  }
937
978
  insertNewline() {
@@ -956,7 +997,6 @@ export class TerminalInput extends EventEmitter {
956
997
  this.cursor = Math.max(0, this.cursor - 1);
957
998
  }
958
999
  this.emit('change', this.buffer);
959
- this.updateSuggestions();
960
1000
  this.scheduleRender();
961
1001
  }
962
1002
  deleteForward() {
@@ -1184,13 +1224,12 @@ export class TerminalInput extends EventEmitter {
1184
1224
  timestamp: Date.now(),
1185
1225
  });
1186
1226
  this.emit('queue', text);
1187
- this.clear(); // Clear immediately for queued input, re-render to update queue display
1227
+ this.clear(); // Clear immediately for queued input
1188
1228
  }
1189
1229
  else {
1190
- // In idle mode, clear the input WITHOUT rendering.
1191
- // The caller will display the user message and start streaming.
1192
- // We'll render the input area again after streaming ends.
1193
- 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();
1194
1233
  this.emit('submit', text);
1195
1234
  }
1196
1235
  }
@@ -1207,7 +1246,9 @@ export class TerminalInput extends EventEmitter {
1207
1246
  if (available <= 0)
1208
1247
  return;
1209
1248
  const chunk = clean.slice(0, available);
1210
- if (isMultilinePaste(chunk)) {
1249
+ const isMultiline = isMultilinePaste(chunk);
1250
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1251
+ if (isMultiline && !isShortMultiline) {
1211
1252
  this.insertPastePlaceholder(chunk);
1212
1253
  }
1213
1254
  else {
@@ -1343,17 +1384,19 @@ export class TerminalInput extends EventEmitter {
1343
1384
  this.shiftPlaceholders(position, text.length);
1344
1385
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1345
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
+ }
1346
1393
  findPlaceholderAt(position) {
1347
1394
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1348
1395
  }
1349
- buildPlaceholder(summary) {
1396
+ buildPlaceholder(lineCount) {
1350
1397
  const id = ++this.pasteCounter;
1351
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1352
- // Show first line preview (truncated)
1353
- const preview = summary.preview.length > 30
1354
- ? `${summary.preview.slice(0, 30)}...`
1355
- : summary.preview;
1356
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1398
+ const plural = lineCount === 1 ? '' : 's';
1399
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1357
1400
  return { id, placeholder };
1358
1401
  }
1359
1402
  insertPastePlaceholder(content) {
@@ -1361,67 +1404,21 @@ export class TerminalInput extends EventEmitter {
1361
1404
  if (available <= 0)
1362
1405
  return;
1363
1406
  const cleanContent = content.slice(0, available);
1364
- const summary = generatePasteSummary(cleanContent);
1365
- // For short pastes (< 5 lines), show full content instead of placeholder
1366
- if (summary.lineCount < 5) {
1367
- const placeholder = this.findPlaceholderAt(this.cursor);
1368
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1369
- this.insertPlainText(cleanContent, insertPos);
1370
- this.cursor = insertPos + cleanContent.length;
1371
- return;
1372
- }
1373
- const { id, placeholder } = this.buildPlaceholder(summary);
1407
+ const lineCount = cleanContent.split('\n').length;
1408
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1374
1409
  const insertPos = this.cursor;
1375
1410
  this.shiftPlaceholders(insertPos, placeholder.length);
1376
1411
  this.pastePlaceholders.push({
1377
1412
  id,
1378
1413
  content: cleanContent,
1379
- lineCount: summary.lineCount,
1414
+ lineCount,
1380
1415
  placeholder,
1381
1416
  start: insertPos,
1382
1417
  end: insertPos + placeholder.length,
1383
- summary,
1384
- expanded: false,
1385
1418
  });
1386
1419
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1387
1420
  this.cursor = insertPos + placeholder.length;
1388
1421
  }
1389
- /**
1390
- * Toggle expansion of a paste placeholder at the current cursor position.
1391
- * When expanded, shows first 3 and last 2 lines of the content.
1392
- */
1393
- togglePasteExpansion() {
1394
- const placeholder = this.findPlaceholderAt(this.cursor);
1395
- if (!placeholder)
1396
- return false;
1397
- placeholder.expanded = !placeholder.expanded;
1398
- // Update the placeholder text in buffer
1399
- const newPlaceholder = placeholder.expanded
1400
- ? this.buildExpandedPlaceholder(placeholder)
1401
- : this.buildPlaceholder(placeholder.summary).placeholder;
1402
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1403
- // Update buffer
1404
- this.buffer =
1405
- this.buffer.slice(0, placeholder.start) +
1406
- newPlaceholder +
1407
- this.buffer.slice(placeholder.end);
1408
- // Update placeholder tracking
1409
- placeholder.placeholder = newPlaceholder;
1410
- placeholder.end = placeholder.start + newPlaceholder.length;
1411
- // Shift other placeholders
1412
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1413
- this.scheduleRender();
1414
- return true;
1415
- }
1416
- buildExpandedPlaceholder(ph) {
1417
- const lines = ph.content.split('\n');
1418
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1419
- const lastLines = lines.length > 5
1420
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1421
- : '';
1422
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1423
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1424
- }
1425
1422
  deletePlaceholder(placeholder) {
1426
1423
  const length = placeholder.end - placeholder.start;
1427
1424
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1429,7 +1426,11 @@ export class TerminalInput extends EventEmitter {
1429
1426
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1430
1427
  this.cursor = placeholder.start;
1431
1428
  }
1432
- 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
+ }
1433
1434
  if (value === null || !Number.isFinite(value)) {
1434
1435
  this.contextUsage = null;
1435
1436
  }
@@ -1456,6 +1457,22 @@ export class TerminalInput extends EventEmitter {
1456
1457
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1457
1458
  this.setEditMode(next);
1458
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
+ }
1459
1476
  scheduleRender() {
1460
1477
  if (!this.canRender())
1461
1478
  return;