erosolar-cli 1.7.310 → 1.7.311

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 +111 -126
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +503 -553
  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,291 @@ 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
+ * NO scroll regions - pure floating with clear and re-render.
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
+ // Track this position
290
+ this.inputAreaStartRow = startRow;
291
+ let currentRow = startRow;
292
+ // Status bar
293
+ this.write(ESC.TO(currentRow, 1));
294
+ this.write(this.buildStatusBar(cols));
295
+ currentRow++;
296
+ // Model info line (if set)
297
+ if (this.modelInfo) {
298
+ this.write(ESC.TO(currentRow, 1));
299
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
300
+ if (this.contextUsage !== null) {
301
+ const rem = Math.max(0, 100 - this.contextUsage);
302
+ if (rem < 10)
303
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
304
+ else if (rem < 25)
305
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
306
+ else
307
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
308
+ }
309
+ this.write(modelLine);
310
+ currentRow++;
311
+ }
312
+ // Top divider
313
+ this.write(ESC.TO(currentRow, 1));
314
+ this.write(divider);
315
+ currentRow++;
316
+ // Input line with prompt and buffer content
317
+ const { lines, cursorCol } = this.wrapBuffer(cols - 4);
318
+ const displayLine = lines[0] ?? '';
319
+ const inputRow = currentRow;
320
+ this.write(ESC.TO(currentRow, 1));
321
+ this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
322
+ this.write(ESC.BG_DARK + displayLine);
323
+ const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
324
+ if (padding > 0)
325
+ this.write(' '.repeat(padding));
326
+ this.write(ESC.RESET);
327
+ currentRow++;
328
+ // Bottom divider
329
+ this.write(ESC.TO(currentRow, 1));
330
+ this.write(divider);
331
+ currentRow++;
332
+ // Mode controls
333
+ this.write(ESC.TO(currentRow, 1));
334
+ this.write(this.buildModeControls(cols));
335
+ // Track lines rendered
336
+ this.flowModeRenderedLines = currentRow - startRow + 1;
337
+ // Position cursor in input line for typing
338
+ this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
339
+ // Show cursor
340
+ this.write(ESC.SHOW);
341
+ // Update tracking
342
+ this.lastRenderContent = this.buffer;
343
+ this.lastRenderCursor = this.cursor;
344
+ }
197
345
  /**
198
346
  * Set the input mode
199
347
  *
200
- * Content flows naturally - no scroll region pinning.
348
+ * UNIFIED FLOATING: Chat box always floats right below content.
349
+ * No scroll regions - pure floating with clear and re-render.
201
350
  */
202
351
  setMode(mode) {
203
352
  const prevMode = this.mode;
204
353
  this.mode = mode;
205
354
  if (mode === 'streaming' && prevMode !== 'streaming') {
206
- this.resetStreamingRenderThrottle();
355
+ // Track streaming start time for elapsed display
356
+ this.streamingStartTime = Date.now();
357
+ // Ensure unified UI is initialized
358
+ if (!this.unifiedUIInitialized) {
359
+ this.initializeUnifiedUI();
360
+ }
207
361
  this.renderDirty = true;
208
- this.render();
362
+ this.scheduleRender();
209
363
  }
210
364
  else if (mode !== 'streaming' && prevMode === 'streaming') {
211
- // Streaming ended - render the input area
212
- this.resetStreamingRenderThrottle();
213
- this.forceRender();
365
+ // Stop streaming render timer (if any)
366
+ if (this.streamingRenderTimer) {
367
+ clearInterval(this.streamingRenderTimer);
368
+ this.streamingRenderTimer = null;
369
+ }
370
+ // Reset streaming time
371
+ this.streamingStartTime = null;
372
+ // Re-render floating input area below content
373
+ this.renderDirty = true;
374
+ this.scheduleRender();
375
+ }
376
+ }
377
+ /**
378
+ * Set the row where content ends (for idle mode positioning).
379
+ * Input area will render starting from this row + 1.
380
+ */
381
+ setContentEndRow(row) {
382
+ this.contentEndRow = Math.max(0, row);
383
+ this.renderDirty = true;
384
+ this.scheduleRender();
385
+ }
386
+ /**
387
+ * Set available slash commands for auto-complete suggestions.
388
+ */
389
+ setCommands(commands) {
390
+ this.commandSuggestions = commands;
391
+ this.updateSuggestions();
392
+ }
393
+ /**
394
+ * Update filtered suggestions based on current input.
395
+ */
396
+ updateSuggestions() {
397
+ const input = this.buffer.trim();
398
+ // Only show suggestions when input starts with "/"
399
+ if (!input.startsWith('/')) {
400
+ this.showSuggestions = false;
401
+ this.filteredSuggestions = [];
402
+ this.selectedSuggestionIndex = 0;
403
+ return;
214
404
  }
405
+ const query = input.toLowerCase();
406
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
407
+ cmd.command.toLowerCase().includes(query.slice(1)));
408
+ // Show suggestions if we have matches
409
+ this.showSuggestions = this.filteredSuggestions.length > 0;
410
+ // Keep selection in bounds
411
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
412
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
413
+ }
414
+ }
415
+ /**
416
+ * Select next suggestion (arrow down / tab).
417
+ */
418
+ selectNextSuggestion() {
419
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
420
+ return;
421
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
422
+ this.renderDirty = true;
423
+ this.scheduleRender();
424
+ }
425
+ /**
426
+ * Select previous suggestion (arrow up / shift+tab).
427
+ */
428
+ selectPrevSuggestion() {
429
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
430
+ return;
431
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
432
+ ? this.filteredSuggestions.length - 1
433
+ : this.selectedSuggestionIndex - 1;
434
+ this.renderDirty = true;
435
+ this.scheduleRender();
436
+ }
437
+ /**
438
+ * Accept current suggestion and insert into buffer.
439
+ */
440
+ acceptSuggestion() {
441
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
442
+ return false;
443
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
444
+ if (!selected)
445
+ return false;
446
+ // Replace buffer with selected command
447
+ this.buffer = selected.command + ' ';
448
+ this.cursor = this.buffer.length;
449
+ this.showSuggestions = false;
450
+ this.renderDirty = true;
451
+ this.scheduleRender();
452
+ return true;
453
+ }
454
+ /**
455
+ * Check if suggestions are visible.
456
+ */
457
+ areSuggestionsVisible() {
458
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
215
459
  }
216
460
  /**
217
- * Legacy method - no longer used (content flows naturally).
218
- * @deprecated Use setContentRow instead
461
+ * Toggle thinking/reasoning mode
219
462
  */
220
- setPinnedHeaderLines(_count) {
221
- // No-op: scroll region pinning removed
463
+ toggleThinking() {
464
+ this.thinkingEnabled = !this.thinkingEnabled;
465
+ this.emit('thinkingToggle', this.thinkingEnabled);
466
+ this.scheduleRender();
467
+ }
468
+ /**
469
+ * Get thinking enabled state
470
+ */
471
+ isThinkingEnabled() {
472
+ return this.thinkingEnabled;
222
473
  }
223
474
  /**
224
475
  * Get current mode
@@ -251,14 +502,17 @@ export class TerminalInput extends EventEmitter {
251
502
  }
252
503
  /**
253
504
  * Clear the buffer
505
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
254
506
  */
255
- clear() {
507
+ clear(skipRender = false) {
256
508
  this.buffer = '';
257
509
  this.cursor = 0;
258
510
  this.historyIndex = -1;
259
511
  this.tempInput = '';
260
512
  this.pastePlaceholders = [];
261
- this.scheduleRender();
513
+ if (!skipRender) {
514
+ this.scheduleRender();
515
+ }
262
516
  }
263
517
  /**
264
518
  * Get queued inputs
@@ -329,37 +583,6 @@ export class TerminalInput extends EventEmitter {
329
583
  this.streamingLabel = next;
330
584
  this.scheduleRender();
331
585
  }
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
586
  /**
364
587
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
365
588
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -369,22 +592,26 @@ export class TerminalInput extends EventEmitter {
369
592
  const nextAutoContinue = !!options.autoContinueEnabled;
370
593
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
371
594
  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
595
  if (this.verificationEnabled === nextVerification &&
375
596
  this.autoContinueEnabled === nextAutoContinue &&
376
597
  this.verificationHotkey === nextVerifyHotkey &&
377
- this.autoContinueHotkey === nextAutoHotkey &&
378
- this.thinkingHotkey === nextThinkingHotkey &&
379
- this.thinkingModeLabel === nextThinkingLabel) {
598
+ this.autoContinueHotkey === nextAutoHotkey) {
380
599
  return;
381
600
  }
382
601
  this.verificationEnabled = nextVerification;
383
602
  this.autoContinueEnabled = nextAutoContinue;
384
603
  this.verificationHotkey = nextVerifyHotkey;
385
604
  this.autoContinueHotkey = nextAutoHotkey;
386
- this.thinkingHotkey = nextThinkingHotkey;
387
- this.thinkingModeLabel = nextThinkingLabel;
605
+ this.scheduleRender();
606
+ }
607
+ /**
608
+ * Set the model info string (e.g., "OpenAI · gpt-4")
609
+ * This is displayed persistently above the input area.
610
+ */
611
+ setModelInfo(info) {
612
+ if (this.modelInfo === info)
613
+ return;
614
+ this.modelInfo = info;
388
615
  this.scheduleRender();
389
616
  }
390
617
  /**
@@ -397,159 +624,33 @@ export class TerminalInput extends EventEmitter {
397
624
  this.scheduleRender();
398
625
  }
399
626
  /**
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.
627
+ * Render the input area.
628
+ * During streaming: renders at terminal bottom (with scroll region)
629
+ * After streaming: renders floating below content
418
630
  */
419
631
  render() {
420
632
  if (!this.canRender())
421
633
  return;
422
634
  if (this.isRendering)
423
635
  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
636
  const shouldSkip = !this.renderDirty &&
436
637
  this.buffer === this.lastRenderContent &&
437
638
  this.cursor === this.lastRenderCursor;
438
639
  this.renderDirty = false;
640
+ // Skip if nothing changed (unless explicitly forced)
439
641
  if (shouldSkip) {
440
642
  return;
441
643
  }
644
+ // If write lock is held, defer render
442
645
  if (writeLock.isLocked()) {
443
646
  writeLock.safeWrite(() => this.render());
444
647
  return;
445
648
  }
446
- this.renderFloatingInputArea();
447
- }
448
- /**
449
- * Core floating input area renderer.
450
- * Chat box always floats at contentRow (below streamed content).
451
- * This creates "persistent bottom floating" behavior.
452
- */
453
- renderFloatingInputArea() {
454
- const { rows, cols } = this.getSize();
455
- const maxWidth = Math.max(8, cols - 4);
456
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
457
- // Wrap buffer into display lines
458
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
459
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
460
- const displayLines = Math.min(lines.length, maxVisible);
461
- const metaLines = this.buildMetaLines(cols - 2);
462
- // Calculate display window (keep cursor visible)
463
- let startLine = 0;
464
- if (lines.length > displayLines) {
465
- startLine = Math.max(0, cursorLine - displayLines + 1);
466
- startLine = Math.min(startLine, lines.length - displayLines);
467
- }
468
- const visibleLines = lines.slice(startLine, startLine + displayLines);
469
- const adjustedCursorLine = cursorLine - startLine;
470
- // Chat box height (must match getChatBoxHeight calculation)
471
- const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
472
- // Unified floating: chat box always at contentRow + 1
473
- // When scroll region is active, contentRow is capped at maxContentRow
474
- // so chat box ends up at the bottom but still "floats" below content
475
- const chatBoxStartRow = this.contentRow + 1;
476
- writeLock.lock('terminalInput.renderFloating');
477
649
  this.isRendering = true;
650
+ writeLock.lock('terminalInput.render');
478
651
  try {
479
- // Hide cursor during render
480
- this.write(ESC.HIDE);
481
- this.write(ESC.RESET);
482
- // Clear the chat box area
483
- for (let i = 0; i < chatBoxHeight; i++) {
484
- const row = chatBoxStartRow + i;
485
- if (row <= rows) {
486
- this.write(ESC.TO(row, 1));
487
- this.write(ESC.CLEAR_LINE);
488
- }
489
- }
490
- let currentRow = chatBoxStartRow;
491
- // Meta/status header
492
- for (const metaLine of metaLines) {
493
- this.write(ESC.TO(currentRow, 1));
494
- this.write(metaLine);
495
- currentRow += 1;
496
- }
497
- // Separator line
498
- this.write(ESC.TO(currentRow, 1));
499
- this.write(renderDivider(cols - 2));
500
- currentRow += 1;
501
- // Render input lines
502
- let finalRow = currentRow;
503
- let finalCol = 3;
504
- for (let i = 0; i < visibleLines.length; i++) {
505
- const rowNum = currentRow + i;
506
- this.write(ESC.TO(rowNum, 1));
507
- const line = visibleLines[i] ?? '';
508
- const isFirstLine = (startLine + i) === 0;
509
- const isCursorLine = i === adjustedCursorLine;
510
- this.write(ESC.BG_DARK);
511
- this.write(ESC.DIM);
512
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
513
- this.write(ESC.RESET);
514
- this.write(ESC.BG_DARK);
515
- if (isCursorLine) {
516
- const col = Math.min(cursorCol, line.length);
517
- const before = line.slice(0, col);
518
- const at = col < line.length ? line[col] : ' ';
519
- const after = col < line.length ? line.slice(col + 1) : '';
520
- this.write(before);
521
- this.write(ESC.REVERSE + ESC.BOLD);
522
- this.write(at);
523
- this.write(ESC.RESET + ESC.BG_DARK);
524
- this.write(after);
525
- finalRow = rowNum;
526
- finalCol = this.config.promptChar.length + col + 1;
527
- }
528
- else {
529
- this.write(line);
530
- }
531
- // Pad to edge
532
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
533
- const padding = Math.max(0, cols - lineLen - 1);
534
- if (padding > 0)
535
- this.write(' '.repeat(padding));
536
- this.write(ESC.RESET);
537
- }
538
- // Mode controls line
539
- const controlRow = currentRow + visibleLines.length;
540
- this.write(ESC.TO(controlRow, 1));
541
- this.write(this.buildModeControls(cols));
542
- // Position cursor in input box
543
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
544
- this.write(ESC.SHOW);
545
- // Update state
546
- this.lastRenderContent = this.buffer;
547
- this.lastRenderCursor = this.cursor;
548
- this.lastStreamingRender = streamingActive ? Date.now() : 0;
549
- if (this.streamingRenderTimer) {
550
- clearTimeout(this.streamingRenderTimer);
551
- this.streamingRenderTimer = null;
552
- }
652
+ // Always render floating right after content (no wasted space)
653
+ this.renderFloatingInputArea();
553
654
  }
554
655
  finally {
555
656
  writeLock.unlock();
@@ -557,217 +658,99 @@ export class TerminalInput extends EventEmitter {
557
658
  }
558
659
  }
559
660
  /**
560
- * Build one or more compact meta lines above the divider (thinking, status, usage).
561
- * During streaming, shows model line pinned above streaming info.
661
+ * Build status bar showing streaming/ready status and key info.
662
+ * This is the TOP line above the input area - minimal Claude Code style.
562
663
  */
563
- buildMetaLines(width) {
564
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
565
- const lines = [];
566
- // Model line should ALWAYS be shown (pinned above streaming content)
567
- if (this.modelLabel) {
568
- const modelText = this.providerLabel
569
- ? `model ${this.modelLabel} @ ${this.providerLabel}`
570
- : `model ${this.modelLabel}`;
571
- lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
572
- }
573
- // During streaming, add a compact status line with essential info
574
- if (streamingActive) {
575
- const parts = [];
576
- // Essential streaming info
577
- if (this.metaThinkingMs !== null) {
578
- parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
579
- }
580
- if (this.metaElapsedSeconds !== null) {
581
- parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
582
- }
583
- parts.push({ text: 'esc to stop', tone: 'warn' });
584
- if (parts.length) {
585
- lines.push(renderStatusLine(parts, width));
664
+ buildStatusBar(cols) {
665
+ const maxWidth = cols - 2;
666
+ const parts = [];
667
+ // Streaming status with elapsed time (left side)
668
+ if (this.mode === 'streaming') {
669
+ let statusText = '● Streaming';
670
+ if (this.streamingStartTime) {
671
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
672
+ const mins = Math.floor(elapsed / 60);
673
+ const secs = elapsed % 60;
674
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
586
675
  }
587
- return lines;
588
- }
589
- // Non-streaming: show full status info (model line already added above)
590
- if (this.metaThinkingMs !== null) {
591
- const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
592
- lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
676
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
593
677
  }
594
- const statusParts = [];
595
- const statusLabel = this.statusMessage ?? this.streamingLabel;
596
- if (statusLabel) {
597
- statusParts.push({ text: statusLabel, tone: 'info' });
678
+ // Queue indicator during streaming
679
+ if (this.mode === 'streaming' && this.queue.length > 0) {
680
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
598
681
  }
599
- if (this.metaElapsedSeconds !== null) {
600
- statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
601
- }
602
- const tokensRemaining = this.computeTokensRemaining();
603
- if (tokensRemaining !== null) {
604
- statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
605
- }
606
- if (statusParts.length) {
607
- lines.push(renderStatusLine(statusParts, width));
682
+ // Paste indicator
683
+ if (this.pastePlaceholders.length > 0) {
684
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
685
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
608
686
  }
609
- const usageParts = [];
610
- if (this.metaTokensUsed !== null) {
611
- const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
612
- const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
613
- usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
687
+ // Override/warning status
688
+ if (this.overrideStatusMessage) {
689
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
614
690
  }
615
- if (this.contextUsage !== null) {
616
- const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
617
- const left = Math.max(0, 100 - this.contextUsage);
618
- usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
691
+ // If idle with empty buffer, show quick shortcuts
692
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
693
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
619
694
  }
620
- if (this.queue.length > 0) {
621
- usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
695
+ // Multi-line indicator
696
+ if (this.buffer.includes('\n')) {
697
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
622
698
  }
623
- if (usageParts.length) {
624
- lines.push(renderStatusLine(usageParts, width));
699
+ if (parts.length === 0) {
700
+ return ''; // Empty status bar when idle
625
701
  }
626
- return lines;
702
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
703
+ return joined.slice(0, maxWidth);
627
704
  }
628
705
  /**
629
- * Build Claude Code style mode controls line.
630
- * Combines streaming label + override status + main status for simultaneous display.
706
+ * Build mode controls line showing toggles and context info.
707
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
708
+ *
709
+ * Layout: [toggles on left] ... [context info on right]
631
710
  */
632
711
  buildModeControls(cols) {
633
- const width = Math.max(8, cols - 2);
634
- const leftParts = [];
635
- const rightParts = [];
636
- if (this.streamingLabel) {
637
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
638
- }
639
- if (this.overrideStatusMessage) {
640
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
641
- }
642
- if (this.statusMessage) {
643
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
644
- }
645
- const editHotkey = this.formatHotkey('shift+tab');
646
- const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
647
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
648
- leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
649
- const verifyHotkey = this.formatHotkey(this.verificationHotkey);
650
- const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
651
- leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
652
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
653
- const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
654
- leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
655
- if (this.queue.length > 0 && this.mode !== 'streaming') {
656
- leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
657
- }
658
- if (this.buffer.includes('\n')) {
659
- const lineCount = this.buffer.split('\n').length;
660
- leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
661
- }
662
- if (this.pastePlaceholders.length > 0) {
663
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
664
- leftParts.push({
665
- text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
666
- tone: 'info',
667
- });
668
- }
669
- const contextRemaining = this.computeContextRemaining();
670
- if (this.thinkingModeLabel) {
671
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
672
- rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
673
- }
674
- // Show model in controls only when NOT streaming (during streaming it's in meta lines)
675
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
676
- if (this.modelLabel && !streamingActive) {
677
- const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
678
- rightParts.push({ text: modelText, tone: 'muted' });
679
- }
680
- if (contextRemaining !== null) {
681
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
682
- const label = contextRemaining === 0 && this.contextUsage !== null
683
- ? 'Context auto-compact imminent'
684
- : `Context left until auto-compact: ${contextRemaining}%`;
685
- rightParts.push({ text: label, tone });
686
- }
687
- if (!rightParts.length || width < 60) {
688
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
689
- return renderStatusLine(merged, width);
690
- }
691
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
692
- const rightWidth = Math.max(14, width - leftWidth - 1);
693
- const leftText = renderStatusLine(leftParts, leftWidth);
694
- const rightText = renderStatusLine(rightParts, rightWidth);
695
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
696
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
697
- }
698
- formatHotkey(hotkey) {
699
- const normalized = hotkey.trim().toLowerCase();
700
- if (!normalized)
701
- return hotkey;
702
- const parts = normalized.split('+').filter(Boolean);
703
- const map = {
704
- shift: '⇧',
705
- sh: '⇧',
706
- alt: '⌥',
707
- option: '⌥',
708
- opt: '⌥',
709
- ctrl: '⌃',
710
- control: '⌃',
711
- cmd: '⌘',
712
- meta: '⌘',
713
- };
714
- const formatted = parts
715
- .map((part) => {
716
- const symbol = map[part];
717
- if (symbol)
718
- return symbol;
719
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
720
- })
721
- .join('');
722
- return formatted || hotkey;
723
- }
724
- computeContextRemaining() {
725
- if (this.contextUsage === null) {
726
- return null;
712
+ const maxWidth = cols - 2;
713
+ // Use schema-defined colors for consistency
714
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
715
+ // Mode toggles with colors (following ModeControlsSchema)
716
+ const toggles = [];
717
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
718
+ if (this.editMode === 'display-edits') {
719
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
727
720
  }
728
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
729
- }
730
- computeTokensRemaining() {
731
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
732
- return null;
733
- }
734
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
735
- return this.formatTokenCount(remaining);
736
- }
737
- formatElapsedLabel(seconds) {
738
- if (seconds < 60) {
739
- return `${seconds}s`;
740
- }
741
- const mins = Math.floor(seconds / 60);
742
- const secs = seconds % 60;
743
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
744
- }
745
- formatTokenCount(value) {
746
- if (!Number.isFinite(value)) {
747
- return `${value}`;
748
- }
749
- if (value >= 1_000_000) {
750
- return `${(value / 1_000_000).toFixed(1)}M`;
751
- }
752
- if (value >= 1_000) {
753
- return `${(value / 1_000).toFixed(1)}k`;
754
- }
755
- return `${Math.round(value)}`;
756
- }
757
- visibleLength(value) {
758
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
759
- return value.replace(ansiPattern, '').length;
760
- }
761
- /**
762
- * Debug-only snapshot used by tests to assert rendered strings without
763
- * needing a TTY. Not used by production code.
764
- */
765
- getDebugUiSnapshot(width) {
766
- const cols = Math.max(8, width ?? this.getSize().cols);
767
- return {
768
- meta: this.buildMetaLines(cols - 2),
769
- controls: this.buildModeControls(cols),
770
- };
721
+ else {
722
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
723
+ }
724
+ // Thinking mode (cyan when on) - per schema.thinkingMode
725
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
726
+ // Verification (green when on) - per schema.verificationMode
727
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
728
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
729
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
730
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
731
+ // Context usage with color - per schema.contextUsage thresholds
732
+ let rightPart = '';
733
+ if (this.contextUsage !== null) {
734
+ const rem = Math.max(0, 100 - this.contextUsage);
735
+ // Thresholds: critical < 10%, warning < 25%
736
+ if (rem < 10)
737
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
738
+ else if (rem < 25)
739
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
740
+ else
741
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
742
+ }
743
+ // Calculate visible lengths (strip ANSI)
744
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
745
+ const leftLen = strip(leftPart).length;
746
+ const rightLen = strip(rightPart).length;
747
+ if (leftLen + rightLen < maxWidth - 4) {
748
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
749
+ }
750
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
751
+ return `${leftPart} ${rightPart}`;
752
+ }
753
+ return leftPart;
771
754
  }
772
755
  /**
773
756
  * Force a re-render
@@ -790,108 +773,27 @@ export class TerminalInput extends EventEmitter {
790
773
  handleResize() {
791
774
  this.lastRenderContent = '';
792
775
  this.lastRenderCursor = -1;
793
- this.resetStreamingRenderThrottle();
794
776
  this.scheduleRender();
795
777
  }
796
778
  /**
797
- * Stream content with floating chat box.
798
- *
799
- * Clean approach - no scroll regions, just cursor positioning:
800
- * 1. Save cursor state
801
- * 2. Clear chat box area (it will be re-rendered)
802
- * 3. Position at contentRow
803
- * 4. Write content
804
- * 5. Advance contentRow
805
- * 6. Re-render chat box
806
- */
807
- streamContent(content) {
808
- if (!content)
809
- return;
810
- writeLock.lock('streamContent');
811
- try {
812
- // Save cursor and hide it
813
- this.write(ESC.SAVE);
814
- this.write(ESC.HIDE);
815
- // Clear the chat box area first (it will be re-rendered after)
816
- const { rows, cols } = this.getSize();
817
- const chatBoxHeight = 6; // Approximate
818
- const chatBoxStart = this.contentRow + 1;
819
- for (let i = 0; i < chatBoxHeight && chatBoxStart + i <= rows; i++) {
820
- this.write(ESC.TO(chatBoxStart + i, 1));
821
- this.write(ESC.CLEAR_LINE);
822
- }
823
- // Position at contentRow and write content
824
- this.write(ESC.TO(this.contentRow, 1));
825
- this.write(content);
826
- // Count newlines and advance contentRow
827
- const newlines = (content.match(/\n/g) || []).length;
828
- this.contentRow += newlines;
829
- // Cap contentRow to leave room for chat box
830
- const maxContentRow = Math.max(1, rows - chatBoxHeight);
831
- if (this.contentRow > maxContentRow) {
832
- this.contentRow = maxContentRow;
833
- }
834
- // Restore cursor
835
- this.write(ESC.RESTORE);
836
- this.write(ESC.SHOW);
837
- }
838
- finally {
839
- writeLock.unlock();
840
- }
841
- // Re-render chat box at new position
842
- this.forceRender();
843
- }
844
- /**
845
- * Enable scroll region (no-op in floating mode).
846
- */
847
- enableScrollRegion() {
848
- // No-op: using pure floating approach
849
- }
850
- /**
851
- * Disable scroll region (no-op in floating mode).
852
- */
853
- disableScrollRegion() {
854
- // No-op: using pure floating approach
855
- }
856
- /**
857
- * Calculate chat box height.
858
- */
859
- getChatBoxHeight() {
860
- return 6; // Fixed: meta + divider + input + controls + buffer
861
- }
862
- /**
863
- * @deprecated Use streamContent() instead
864
- * Register with display's output interceptor - kept for backwards compatibility
865
- */
866
- registerOutputInterceptor(_display) {
867
- // No-op: Use streamContent() for cleaner floating chat box behavior
868
- }
869
- /**
870
- * @deprecated Use streamContent() instead
871
- * Write content above the floating chat box.
779
+ * Register with display's output interceptor.
780
+ * No scroll regions - chat box is cleared and re-rendered after content writes.
872
781
  */
873
- writeToScrollRegion(content) {
874
- this.streamContent(content);
875
- }
876
- /**
877
- * Reset content position to row 1.
878
- * Does NOT clear the terminal - content starts from current position.
879
- */
880
- resetContentPosition() {
881
- this.contentRow = 1;
882
- }
883
- /**
884
- * Set the content row explicitly (used after banner is written).
885
- * This tells the input where content should start flowing from.
886
- */
887
- setContentRow(row) {
888
- this.contentRow = Math.max(1, row);
889
- }
890
- /**
891
- * Get the current content row position.
892
- */
893
- getContentRow() {
894
- return this.contentRow;
782
+ registerOutputInterceptor(display) {
783
+ if (this.outputInterceptorCleanup) {
784
+ this.outputInterceptorCleanup();
785
+ }
786
+ this.outputInterceptorCleanup = display.registerOutputInterceptor({
787
+ beforeWrite: () => {
788
+ // Clear chat box before content write to prevent overlap
789
+ this.clearInputArea();
790
+ },
791
+ afterWrite: () => {
792
+ // Re-render chat box after content writes
793
+ this.renderDirty = true;
794
+ this.scheduleRender();
795
+ },
796
+ });
895
797
  }
896
798
  /**
897
799
  * Dispose and clean up
@@ -899,10 +801,18 @@ export class TerminalInput extends EventEmitter {
899
801
  dispose() {
900
802
  if (this.disposed)
901
803
  return;
804
+ // Clean up streaming render timer
805
+ if (this.streamingRenderTimer) {
806
+ clearInterval(this.streamingRenderTimer);
807
+ this.streamingRenderTimer = null;
808
+ }
809
+ // Clean up output interceptor
810
+ if (this.outputInterceptorCleanup) {
811
+ this.outputInterceptorCleanup();
812
+ this.outputInterceptorCleanup = undefined;
813
+ }
902
814
  this.disposed = true;
903
815
  this.enabled = false;
904
- this.disableScrollRegion();
905
- this.resetStreamingRenderThrottle();
906
816
  this.disableBracketedPaste();
907
817
  this.buffer = '';
908
818
  this.queue = [];
@@ -1007,7 +917,22 @@ export class TerminalInput extends EventEmitter {
1007
917
  this.toggleEditMode();
1008
918
  return true;
1009
919
  }
1010
- this.insertText(' ');
920
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
921
+ if (this.findPlaceholderAt(this.cursor)) {
922
+ this.togglePasteExpansion();
923
+ }
924
+ else {
925
+ this.toggleThinking();
926
+ }
927
+ return true;
928
+ case 'escape':
929
+ // Esc: interrupt if streaming, otherwise clear buffer
930
+ if (this.mode === 'streaming') {
931
+ this.emit('interrupt');
932
+ }
933
+ else if (this.buffer.length > 0) {
934
+ this.clear();
935
+ }
1011
936
  return true;
1012
937
  }
1013
938
  return false;
@@ -1025,6 +950,7 @@ export class TerminalInput extends EventEmitter {
1025
950
  this.insertPlainText(chunk, insertPos);
1026
951
  this.cursor = insertPos + chunk.length;
1027
952
  this.emit('change', this.buffer);
953
+ this.updateSuggestions();
1028
954
  this.scheduleRender();
1029
955
  }
1030
956
  insertNewline() {
@@ -1049,6 +975,7 @@ export class TerminalInput extends EventEmitter {
1049
975
  this.cursor = Math.max(0, this.cursor - 1);
1050
976
  }
1051
977
  this.emit('change', this.buffer);
978
+ this.updateSuggestions();
1052
979
  this.scheduleRender();
1053
980
  }
1054
981
  deleteForward() {
@@ -1276,12 +1203,13 @@ export class TerminalInput extends EventEmitter {
1276
1203
  timestamp: Date.now(),
1277
1204
  });
1278
1205
  this.emit('queue', text);
1279
- this.clear(); // Clear immediately for queued input
1206
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1280
1207
  }
1281
1208
  else {
1282
- // In idle mode, clear the input first, then emit submit.
1283
- // The prompt will be logged as a visible message by the caller.
1284
- this.clear();
1209
+ // In idle mode, clear the input WITHOUT rendering.
1210
+ // The caller will display the user message and start streaming.
1211
+ // We'll render the input area again after streaming ends.
1212
+ this.clear(true); // Skip render - streaming will handle display
1285
1213
  this.emit('submit', text);
1286
1214
  }
1287
1215
  }
@@ -1298,9 +1226,7 @@ export class TerminalInput extends EventEmitter {
1298
1226
  if (available <= 0)
1299
1227
  return;
1300
1228
  const chunk = clean.slice(0, available);
1301
- const isMultiline = isMultilinePaste(chunk);
1302
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1303
- if (isMultiline && !isShortMultiline) {
1229
+ if (isMultilinePaste(chunk)) {
1304
1230
  this.insertPastePlaceholder(chunk);
1305
1231
  }
1306
1232
  else {
@@ -1436,19 +1362,17 @@ export class TerminalInput extends EventEmitter {
1436
1362
  this.shiftPlaceholders(position, text.length);
1437
1363
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1438
1364
  }
1439
- shouldInlineMultiline(content) {
1440
- const lines = content.split('\n').length;
1441
- const maxInlineLines = 4;
1442
- const maxInlineChars = 240;
1443
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1444
- }
1445
1365
  findPlaceholderAt(position) {
1446
1366
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1447
1367
  }
1448
- buildPlaceholder(lineCount) {
1368
+ buildPlaceholder(summary) {
1449
1369
  const id = ++this.pasteCounter;
1450
- const plural = lineCount === 1 ? '' : 's';
1451
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1370
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1371
+ // Show first line preview (truncated)
1372
+ const preview = summary.preview.length > 30
1373
+ ? `${summary.preview.slice(0, 30)}...`
1374
+ : summary.preview;
1375
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1452
1376
  return { id, placeholder };
1453
1377
  }
1454
1378
  insertPastePlaceholder(content) {
@@ -1456,21 +1380,67 @@ export class TerminalInput extends EventEmitter {
1456
1380
  if (available <= 0)
1457
1381
  return;
1458
1382
  const cleanContent = content.slice(0, available);
1459
- const lineCount = cleanContent.split('\n').length;
1460
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1383
+ const summary = generatePasteSummary(cleanContent);
1384
+ // For short pastes (< 5 lines), show full content instead of placeholder
1385
+ if (summary.lineCount < 5) {
1386
+ const placeholder = this.findPlaceholderAt(this.cursor);
1387
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1388
+ this.insertPlainText(cleanContent, insertPos);
1389
+ this.cursor = insertPos + cleanContent.length;
1390
+ return;
1391
+ }
1392
+ const { id, placeholder } = this.buildPlaceholder(summary);
1461
1393
  const insertPos = this.cursor;
1462
1394
  this.shiftPlaceholders(insertPos, placeholder.length);
1463
1395
  this.pastePlaceholders.push({
1464
1396
  id,
1465
1397
  content: cleanContent,
1466
- lineCount,
1398
+ lineCount: summary.lineCount,
1467
1399
  placeholder,
1468
1400
  start: insertPos,
1469
1401
  end: insertPos + placeholder.length,
1402
+ summary,
1403
+ expanded: false,
1470
1404
  });
1471
1405
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1472
1406
  this.cursor = insertPos + placeholder.length;
1473
1407
  }
1408
+ /**
1409
+ * Toggle expansion of a paste placeholder at the current cursor position.
1410
+ * When expanded, shows first 3 and last 2 lines of the content.
1411
+ */
1412
+ togglePasteExpansion() {
1413
+ const placeholder = this.findPlaceholderAt(this.cursor);
1414
+ if (!placeholder)
1415
+ return false;
1416
+ placeholder.expanded = !placeholder.expanded;
1417
+ // Update the placeholder text in buffer
1418
+ const newPlaceholder = placeholder.expanded
1419
+ ? this.buildExpandedPlaceholder(placeholder)
1420
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1421
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1422
+ // Update buffer
1423
+ this.buffer =
1424
+ this.buffer.slice(0, placeholder.start) +
1425
+ newPlaceholder +
1426
+ this.buffer.slice(placeholder.end);
1427
+ // Update placeholder tracking
1428
+ placeholder.placeholder = newPlaceholder;
1429
+ placeholder.end = placeholder.start + newPlaceholder.length;
1430
+ // Shift other placeholders
1431
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1432
+ this.scheduleRender();
1433
+ return true;
1434
+ }
1435
+ buildExpandedPlaceholder(ph) {
1436
+ const lines = ph.content.split('\n');
1437
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1438
+ const lastLines = lines.length > 5
1439
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1440
+ : '';
1441
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1442
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1443
+ }
1474
1444
  deletePlaceholder(placeholder) {
1475
1445
  const length = placeholder.end - placeholder.start;
1476
1446
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1478,11 +1448,7 @@ export class TerminalInput extends EventEmitter {
1478
1448
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1479
1449
  this.cursor = placeholder.start;
1480
1450
  }
1481
- updateContextUsage(value, autoCompactThreshold) {
1482
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1483
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1484
- this.contextAutoCompactThreshold = boundedThreshold;
1485
- }
1451
+ updateContextUsage(value) {
1486
1452
  if (value === null || !Number.isFinite(value)) {
1487
1453
  this.contextUsage = null;
1488
1454
  }
@@ -1509,22 +1475,6 @@ export class TerminalInput extends EventEmitter {
1509
1475
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1510
1476
  this.setEditMode(next);
1511
1477
  }
1512
- scheduleStreamingRender(delayMs) {
1513
- if (this.streamingRenderTimer)
1514
- return;
1515
- const wait = Math.max(16, delayMs);
1516
- this.streamingRenderTimer = setTimeout(() => {
1517
- this.streamingRenderTimer = null;
1518
- this.render();
1519
- }, wait);
1520
- }
1521
- resetStreamingRenderThrottle() {
1522
- if (this.streamingRenderTimer) {
1523
- clearTimeout(this.streamingRenderTimer);
1524
- this.streamingRenderTimer = null;
1525
- }
1526
- this.lastStreamingRender = 0;
1527
- }
1528
1478
  scheduleRender() {
1529
1479
  if (!this.canRender())
1530
1480
  return;