erosolar-cli 1.7.307 → 1.7.309

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