erosolar-cli 1.7.310 → 1.7.312

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 +115 -126
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +526 -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,314 @@ 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 - Claude Code style floating input.
261
+ * Clean, minimal design with dividers and mode controls.
262
+ */
263
+ renderFloatingInputArea() {
264
+ const { rows, cols } = this.getSize();
265
+ const divider = '─'.repeat(cols);
266
+ const { dim: DIM, reset: R } = UI_COLORS;
267
+ // Calculate lines needed: status (optional) + divider + input + divider + controls
268
+ const hasStatus = this.mode === 'streaming' || this.statusMessage;
269
+ const linesNeeded = (hasStatus ? 1 : 0) + 4; // status + divider + input + divider + controls
270
+ // FIRST: Clear any previously rendered chat box
271
+ this.clearInputArea();
272
+ // Hide cursor during render
273
+ this.write(ESC.HIDE);
274
+ // Calculate where to render - ALWAYS float right below content
275
+ let startRow;
276
+ if (this.contentEndRow > 0) {
277
+ startRow = this.contentEndRow + 1;
278
+ }
279
+ else {
280
+ startRow = 1;
281
+ }
282
+ // Clamp to terminal bounds
283
+ const maxStartRow = rows - linesNeeded + 1;
284
+ startRow = Math.min(startRow, maxStartRow);
285
+ startRow = Math.max(1, startRow);
286
+ // Track this position
287
+ this.inputAreaStartRow = startRow;
288
+ let currentRow = startRow;
289
+ // Streaming status line (above input area)
290
+ if (this.mode === 'streaming') {
291
+ this.write(ESC.TO(currentRow, 1));
292
+ let statusText = '✻ Streaming';
293
+ if (this.streamingStartTime) {
294
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
295
+ const mins = Math.floor(elapsed / 60);
296
+ const secs = elapsed % 60;
297
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
298
+ }
299
+ statusText += '… (esc to interrupt)';
300
+ this.write(`${UI_COLORS.cyan}${statusText}${R}`);
301
+ currentRow++;
302
+ }
303
+ else if (this.statusMessage) {
304
+ this.write(ESC.TO(currentRow, 1));
305
+ this.write(`${DIM}${this.statusMessage}${R}`);
306
+ currentRow++;
307
+ }
308
+ // Top divider
309
+ this.write(ESC.TO(currentRow, 1));
310
+ this.write(`${DIM}${divider}${R}`);
311
+ currentRow++;
312
+ // Input line with > prompt
313
+ const { lines, cursorCol } = this.wrapBuffer(cols - 3);
314
+ const displayLine = lines[0] ?? '';
315
+ const inputRow = currentRow;
316
+ this.write(ESC.TO(currentRow, 1));
317
+ this.write(`${DIM}>${R} ${displayLine}`);
318
+ currentRow++;
319
+ // Bottom divider
320
+ this.write(ESC.TO(currentRow, 1));
321
+ this.write(`${DIM}${divider}${R}`);
322
+ currentRow++;
323
+ // Mode controls line - Claude Code style
324
+ this.write(ESC.TO(currentRow, 1));
325
+ this.write(this.buildClaudeStyleControls(cols));
326
+ // Track lines rendered
327
+ this.flowModeRenderedLines = currentRow - startRow + 1;
328
+ // Position cursor in input line
329
+ this.write(ESC.TO(inputRow, 3 + cursorCol)); // "> " = 2 chars + 1 for position
330
+ // Show cursor
331
+ this.write(ESC.SHOW);
332
+ // Update tracking
333
+ this.lastRenderContent = this.buffer;
334
+ this.lastRenderCursor = this.cursor;
335
+ }
336
+ /**
337
+ * Build Claude Code style controls line.
338
+ * Shows: edit mode indicator (shift+tab to cycle)
339
+ */
340
+ buildClaudeStyleControls(cols) {
341
+ const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
342
+ // Edit mode indicator
343
+ let editModeText;
344
+ if (this.editMode === 'display-edits') {
345
+ editModeText = `${GREEN}⏵⏵${R} accept edits on`;
346
+ }
347
+ else {
348
+ editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
349
+ }
350
+ // Build controls line
351
+ const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
352
+ // Add thinking mode if enabled
353
+ if (this.thinkingEnabled) {
354
+ parts.push(`${CYAN}💭${R}`);
355
+ }
356
+ // Add context usage if available
357
+ if (this.contextUsage !== null) {
358
+ const rem = Math.max(0, 100 - this.contextUsage);
359
+ if (rem < 10) {
360
+ parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
361
+ }
362
+ else if (rem < 25) {
363
+ parts.push(`${YELLOW}ctx ${rem}%${R}`);
364
+ }
365
+ }
366
+ return parts.join(` ${DIM}·${R} `);
367
+ }
197
368
  /**
198
369
  * Set the input mode
199
370
  *
200
- * Content flows naturally - no scroll region pinning.
371
+ * UNIFIED FLOATING: Chat box always floats right below content.
372
+ * No scroll regions - pure floating with clear and re-render.
201
373
  */
202
374
  setMode(mode) {
203
375
  const prevMode = this.mode;
204
376
  this.mode = mode;
205
377
  if (mode === 'streaming' && prevMode !== 'streaming') {
206
- this.resetStreamingRenderThrottle();
378
+ // Track streaming start time for elapsed display
379
+ this.streamingStartTime = Date.now();
380
+ // Ensure unified UI is initialized
381
+ if (!this.unifiedUIInitialized) {
382
+ this.initializeUnifiedUI();
383
+ }
207
384
  this.renderDirty = true;
208
- this.render();
385
+ this.scheduleRender();
209
386
  }
210
387
  else if (mode !== 'streaming' && prevMode === 'streaming') {
211
- // Streaming ended - render the input area
212
- this.resetStreamingRenderThrottle();
213
- this.forceRender();
388
+ // Stop streaming render timer (if any)
389
+ if (this.streamingRenderTimer) {
390
+ clearInterval(this.streamingRenderTimer);
391
+ this.streamingRenderTimer = null;
392
+ }
393
+ // Reset streaming time
394
+ this.streamingStartTime = null;
395
+ // Re-render floating input area below content
396
+ this.renderDirty = true;
397
+ this.scheduleRender();
214
398
  }
215
399
  }
216
400
  /**
217
- * Legacy method - no longer used (content flows naturally).
218
- * @deprecated Use setContentRow instead
401
+ * Set the row where content ends (for idle mode positioning).
402
+ * Input area will render starting from this row + 1.
219
403
  */
220
- setPinnedHeaderLines(_count) {
221
- // No-op: scroll region pinning removed
404
+ setContentEndRow(row) {
405
+ this.contentEndRow = Math.max(0, row);
406
+ this.renderDirty = true;
407
+ this.scheduleRender();
408
+ }
409
+ /**
410
+ * Set available slash commands for auto-complete suggestions.
411
+ */
412
+ setCommands(commands) {
413
+ this.commandSuggestions = commands;
414
+ this.updateSuggestions();
415
+ }
416
+ /**
417
+ * Update filtered suggestions based on current input.
418
+ */
419
+ updateSuggestions() {
420
+ const input = this.buffer.trim();
421
+ // Only show suggestions when input starts with "/"
422
+ if (!input.startsWith('/')) {
423
+ this.showSuggestions = false;
424
+ this.filteredSuggestions = [];
425
+ this.selectedSuggestionIndex = 0;
426
+ return;
427
+ }
428
+ const query = input.toLowerCase();
429
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
430
+ cmd.command.toLowerCase().includes(query.slice(1)));
431
+ // Show suggestions if we have matches
432
+ this.showSuggestions = this.filteredSuggestions.length > 0;
433
+ // Keep selection in bounds
434
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
435
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
436
+ }
437
+ }
438
+ /**
439
+ * Select next suggestion (arrow down / tab).
440
+ */
441
+ selectNextSuggestion() {
442
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
443
+ return;
444
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
445
+ this.renderDirty = true;
446
+ this.scheduleRender();
447
+ }
448
+ /**
449
+ * Select previous suggestion (arrow up / shift+tab).
450
+ */
451
+ selectPrevSuggestion() {
452
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
453
+ return;
454
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
455
+ ? this.filteredSuggestions.length - 1
456
+ : this.selectedSuggestionIndex - 1;
457
+ this.renderDirty = true;
458
+ this.scheduleRender();
459
+ }
460
+ /**
461
+ * Accept current suggestion and insert into buffer.
462
+ */
463
+ acceptSuggestion() {
464
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
465
+ return false;
466
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
467
+ if (!selected)
468
+ return false;
469
+ // Replace buffer with selected command
470
+ this.buffer = selected.command + ' ';
471
+ this.cursor = this.buffer.length;
472
+ this.showSuggestions = false;
473
+ this.renderDirty = true;
474
+ this.scheduleRender();
475
+ return true;
476
+ }
477
+ /**
478
+ * Check if suggestions are visible.
479
+ */
480
+ areSuggestionsVisible() {
481
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
482
+ }
483
+ /**
484
+ * Toggle thinking/reasoning mode
485
+ */
486
+ toggleThinking() {
487
+ this.thinkingEnabled = !this.thinkingEnabled;
488
+ this.emit('thinkingToggle', this.thinkingEnabled);
489
+ this.scheduleRender();
490
+ }
491
+ /**
492
+ * Get thinking enabled state
493
+ */
494
+ isThinkingEnabled() {
495
+ return this.thinkingEnabled;
222
496
  }
223
497
  /**
224
498
  * Get current mode
@@ -251,14 +525,17 @@ export class TerminalInput extends EventEmitter {
251
525
  }
252
526
  /**
253
527
  * Clear the buffer
528
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
254
529
  */
255
- clear() {
530
+ clear(skipRender = false) {
256
531
  this.buffer = '';
257
532
  this.cursor = 0;
258
533
  this.historyIndex = -1;
259
534
  this.tempInput = '';
260
535
  this.pastePlaceholders = [];
261
- this.scheduleRender();
536
+ if (!skipRender) {
537
+ this.scheduleRender();
538
+ }
262
539
  }
263
540
  /**
264
541
  * Get queued inputs
@@ -329,37 +606,6 @@ export class TerminalInput extends EventEmitter {
329
606
  this.streamingLabel = next;
330
607
  this.scheduleRender();
331
608
  }
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
609
  /**
364
610
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
365
611
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -369,22 +615,26 @@ export class TerminalInput extends EventEmitter {
369
615
  const nextAutoContinue = !!options.autoContinueEnabled;
370
616
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
371
617
  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
618
  if (this.verificationEnabled === nextVerification &&
375
619
  this.autoContinueEnabled === nextAutoContinue &&
376
620
  this.verificationHotkey === nextVerifyHotkey &&
377
- this.autoContinueHotkey === nextAutoHotkey &&
378
- this.thinkingHotkey === nextThinkingHotkey &&
379
- this.thinkingModeLabel === nextThinkingLabel) {
621
+ this.autoContinueHotkey === nextAutoHotkey) {
380
622
  return;
381
623
  }
382
624
  this.verificationEnabled = nextVerification;
383
625
  this.autoContinueEnabled = nextAutoContinue;
384
626
  this.verificationHotkey = nextVerifyHotkey;
385
627
  this.autoContinueHotkey = nextAutoHotkey;
386
- this.thinkingHotkey = nextThinkingHotkey;
387
- this.thinkingModeLabel = nextThinkingLabel;
628
+ this.scheduleRender();
629
+ }
630
+ /**
631
+ * Set the model info string (e.g., "OpenAI · gpt-4")
632
+ * This is displayed persistently above the input area.
633
+ */
634
+ setModelInfo(info) {
635
+ if (this.modelInfo === info)
636
+ return;
637
+ this.modelInfo = info;
388
638
  this.scheduleRender();
389
639
  }
390
640
  /**
@@ -397,159 +647,33 @@ export class TerminalInput extends EventEmitter {
397
647
  this.scheduleRender();
398
648
  }
399
649
  /**
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.
650
+ * Render the input area.
651
+ * During streaming: renders at terminal bottom (with scroll region)
652
+ * After streaming: renders floating below content
418
653
  */
419
654
  render() {
420
655
  if (!this.canRender())
421
656
  return;
422
657
  if (this.isRendering)
423
658
  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
659
  const shouldSkip = !this.renderDirty &&
436
660
  this.buffer === this.lastRenderContent &&
437
661
  this.cursor === this.lastRenderCursor;
438
662
  this.renderDirty = false;
663
+ // Skip if nothing changed (unless explicitly forced)
439
664
  if (shouldSkip) {
440
665
  return;
441
666
  }
667
+ // If write lock is held, defer render
442
668
  if (writeLock.isLocked()) {
443
669
  writeLock.safeWrite(() => this.render());
444
670
  return;
445
671
  }
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
672
  this.isRendering = true;
673
+ writeLock.lock('terminalInput.render');
478
674
  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
- }
675
+ // Always render floating right after content (no wasted space)
676
+ this.renderFloatingInputArea();
553
677
  }
554
678
  finally {
555
679
  writeLock.unlock();
@@ -557,217 +681,99 @@ export class TerminalInput extends EventEmitter {
557
681
  }
558
682
  }
559
683
  /**
560
- * Build one or more compact meta lines above the divider (thinking, status, usage).
561
- * During streaming, shows model line pinned above streaming info.
684
+ * Build status bar showing streaming/ready status and key info.
685
+ * This is the TOP line above the input area - minimal Claude Code style.
562
686
  */
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));
687
+ buildStatusBar(cols) {
688
+ const maxWidth = cols - 2;
689
+ const parts = [];
690
+ // Streaming status with elapsed time (left side)
691
+ if (this.mode === 'streaming') {
692
+ let statusText = '● Streaming';
693
+ if (this.streamingStartTime) {
694
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
695
+ const mins = Math.floor(elapsed / 60);
696
+ const secs = elapsed % 60;
697
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
586
698
  }
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));
593
- }
594
- const statusParts = [];
595
- const statusLabel = this.statusMessage ?? this.streamingLabel;
596
- if (statusLabel) {
597
- statusParts.push({ text: statusLabel, tone: 'info' });
699
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
598
700
  }
599
- if (this.metaElapsedSeconds !== null) {
600
- statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
701
+ // Queue indicator during streaming
702
+ if (this.mode === 'streaming' && this.queue.length > 0) {
703
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
601
704
  }
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));
705
+ // Paste indicator
706
+ if (this.pastePlaceholders.length > 0) {
707
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
708
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
608
709
  }
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' });
710
+ // Override/warning status
711
+ if (this.overrideStatusMessage) {
712
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
614
713
  }
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 });
714
+ // If idle with empty buffer, show quick shortcuts
715
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
716
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
619
717
  }
620
- if (this.queue.length > 0) {
621
- usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
718
+ // Multi-line indicator
719
+ if (this.buffer.includes('\n')) {
720
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
622
721
  }
623
- if (usageParts.length) {
624
- lines.push(renderStatusLine(usageParts, width));
722
+ if (parts.length === 0) {
723
+ return ''; // Empty status bar when idle
625
724
  }
626
- return lines;
725
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
726
+ return joined.slice(0, maxWidth);
627
727
  }
628
728
  /**
629
- * Build Claude Code style mode controls line.
630
- * Combines streaming label + override status + main status for simultaneous display.
729
+ * Build mode controls line showing toggles and context info.
730
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
731
+ *
732
+ * Layout: [toggles on left] ... [context info on right]
631
733
  */
632
734
  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;
727
- }
728
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
729
- }
730
- computeTokensRemaining() {
731
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
732
- return null;
735
+ const maxWidth = cols - 2;
736
+ // Use schema-defined colors for consistency
737
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
738
+ // Mode toggles with colors (following ModeControlsSchema)
739
+ const toggles = [];
740
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
741
+ if (this.editMode === 'display-edits') {
742
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
733
743
  }
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
- };
744
+ else {
745
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
746
+ }
747
+ // Thinking mode (cyan when on) - per schema.thinkingMode
748
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
749
+ // Verification (green when on) - per schema.verificationMode
750
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
751
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
752
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
753
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
754
+ // Context usage with color - per schema.contextUsage thresholds
755
+ let rightPart = '';
756
+ if (this.contextUsage !== null) {
757
+ const rem = Math.max(0, 100 - this.contextUsage);
758
+ // Thresholds: critical < 10%, warning < 25%
759
+ if (rem < 10)
760
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
761
+ else if (rem < 25)
762
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
763
+ else
764
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
765
+ }
766
+ // Calculate visible lengths (strip ANSI)
767
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
768
+ const leftLen = strip(leftPart).length;
769
+ const rightLen = strip(rightPart).length;
770
+ if (leftLen + rightLen < maxWidth - 4) {
771
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
772
+ }
773
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
774
+ return `${leftPart} ${rightPart}`;
775
+ }
776
+ return leftPart;
771
777
  }
772
778
  /**
773
779
  * Force a re-render
@@ -790,108 +796,27 @@ export class TerminalInput extends EventEmitter {
790
796
  handleResize() {
791
797
  this.lastRenderContent = '';
792
798
  this.lastRenderCursor = -1;
793
- this.resetStreamingRenderThrottle();
794
799
  this.scheduleRender();
795
800
  }
796
801
  /**
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
802
+ * Register with display's output interceptor.
803
+ * No scroll regions - chat box is cleared and re-rendered after content writes.
806
804
  */
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.
872
- */
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;
805
+ registerOutputInterceptor(display) {
806
+ if (this.outputInterceptorCleanup) {
807
+ this.outputInterceptorCleanup();
808
+ }
809
+ this.outputInterceptorCleanup = display.registerOutputInterceptor({
810
+ beforeWrite: () => {
811
+ // Clear chat box before content write to prevent overlap
812
+ this.clearInputArea();
813
+ },
814
+ afterWrite: () => {
815
+ // Re-render chat box after content writes
816
+ this.renderDirty = true;
817
+ this.scheduleRender();
818
+ },
819
+ });
895
820
  }
896
821
  /**
897
822
  * Dispose and clean up
@@ -899,10 +824,18 @@ export class TerminalInput extends EventEmitter {
899
824
  dispose() {
900
825
  if (this.disposed)
901
826
  return;
827
+ // Clean up streaming render timer
828
+ if (this.streamingRenderTimer) {
829
+ clearInterval(this.streamingRenderTimer);
830
+ this.streamingRenderTimer = null;
831
+ }
832
+ // Clean up output interceptor
833
+ if (this.outputInterceptorCleanup) {
834
+ this.outputInterceptorCleanup();
835
+ this.outputInterceptorCleanup = undefined;
836
+ }
902
837
  this.disposed = true;
903
838
  this.enabled = false;
904
- this.disableScrollRegion();
905
- this.resetStreamingRenderThrottle();
906
839
  this.disableBracketedPaste();
907
840
  this.buffer = '';
908
841
  this.queue = [];
@@ -1007,7 +940,22 @@ export class TerminalInput extends EventEmitter {
1007
940
  this.toggleEditMode();
1008
941
  return true;
1009
942
  }
1010
- this.insertText(' ');
943
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
944
+ if (this.findPlaceholderAt(this.cursor)) {
945
+ this.togglePasteExpansion();
946
+ }
947
+ else {
948
+ this.toggleThinking();
949
+ }
950
+ return true;
951
+ case 'escape':
952
+ // Esc: interrupt if streaming, otherwise clear buffer
953
+ if (this.mode === 'streaming') {
954
+ this.emit('interrupt');
955
+ }
956
+ else if (this.buffer.length > 0) {
957
+ this.clear();
958
+ }
1011
959
  return true;
1012
960
  }
1013
961
  return false;
@@ -1025,6 +973,7 @@ export class TerminalInput extends EventEmitter {
1025
973
  this.insertPlainText(chunk, insertPos);
1026
974
  this.cursor = insertPos + chunk.length;
1027
975
  this.emit('change', this.buffer);
976
+ this.updateSuggestions();
1028
977
  this.scheduleRender();
1029
978
  }
1030
979
  insertNewline() {
@@ -1049,6 +998,7 @@ export class TerminalInput extends EventEmitter {
1049
998
  this.cursor = Math.max(0, this.cursor - 1);
1050
999
  }
1051
1000
  this.emit('change', this.buffer);
1001
+ this.updateSuggestions();
1052
1002
  this.scheduleRender();
1053
1003
  }
1054
1004
  deleteForward() {
@@ -1276,12 +1226,13 @@ export class TerminalInput extends EventEmitter {
1276
1226
  timestamp: Date.now(),
1277
1227
  });
1278
1228
  this.emit('queue', text);
1279
- this.clear(); // Clear immediately for queued input
1229
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1280
1230
  }
1281
1231
  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();
1232
+ // In idle mode, clear the input WITHOUT rendering.
1233
+ // The caller will display the user message and start streaming.
1234
+ // We'll render the input area again after streaming ends.
1235
+ this.clear(true); // Skip render - streaming will handle display
1285
1236
  this.emit('submit', text);
1286
1237
  }
1287
1238
  }
@@ -1298,9 +1249,7 @@ export class TerminalInput extends EventEmitter {
1298
1249
  if (available <= 0)
1299
1250
  return;
1300
1251
  const chunk = clean.slice(0, available);
1301
- const isMultiline = isMultilinePaste(chunk);
1302
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1303
- if (isMultiline && !isShortMultiline) {
1252
+ if (isMultilinePaste(chunk)) {
1304
1253
  this.insertPastePlaceholder(chunk);
1305
1254
  }
1306
1255
  else {
@@ -1436,19 +1385,17 @@ export class TerminalInput extends EventEmitter {
1436
1385
  this.shiftPlaceholders(position, text.length);
1437
1386
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1438
1387
  }
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
1388
  findPlaceholderAt(position) {
1446
1389
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1447
1390
  }
1448
- buildPlaceholder(lineCount) {
1391
+ buildPlaceholder(summary) {
1449
1392
  const id = ++this.pasteCounter;
1450
- const plural = lineCount === 1 ? '' : 's';
1451
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1393
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1394
+ // Show first line preview (truncated)
1395
+ const preview = summary.preview.length > 30
1396
+ ? `${summary.preview.slice(0, 30)}...`
1397
+ : summary.preview;
1398
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1452
1399
  return { id, placeholder };
1453
1400
  }
1454
1401
  insertPastePlaceholder(content) {
@@ -1456,21 +1403,67 @@ export class TerminalInput extends EventEmitter {
1456
1403
  if (available <= 0)
1457
1404
  return;
1458
1405
  const cleanContent = content.slice(0, available);
1459
- const lineCount = cleanContent.split('\n').length;
1460
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1406
+ const summary = generatePasteSummary(cleanContent);
1407
+ // For short pastes (< 5 lines), show full content instead of placeholder
1408
+ if (summary.lineCount < 5) {
1409
+ const placeholder = this.findPlaceholderAt(this.cursor);
1410
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1411
+ this.insertPlainText(cleanContent, insertPos);
1412
+ this.cursor = insertPos + cleanContent.length;
1413
+ return;
1414
+ }
1415
+ const { id, placeholder } = this.buildPlaceholder(summary);
1461
1416
  const insertPos = this.cursor;
1462
1417
  this.shiftPlaceholders(insertPos, placeholder.length);
1463
1418
  this.pastePlaceholders.push({
1464
1419
  id,
1465
1420
  content: cleanContent,
1466
- lineCount,
1421
+ lineCount: summary.lineCount,
1467
1422
  placeholder,
1468
1423
  start: insertPos,
1469
1424
  end: insertPos + placeholder.length,
1425
+ summary,
1426
+ expanded: false,
1470
1427
  });
1471
1428
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1472
1429
  this.cursor = insertPos + placeholder.length;
1473
1430
  }
1431
+ /**
1432
+ * Toggle expansion of a paste placeholder at the current cursor position.
1433
+ * When expanded, shows first 3 and last 2 lines of the content.
1434
+ */
1435
+ togglePasteExpansion() {
1436
+ const placeholder = this.findPlaceholderAt(this.cursor);
1437
+ if (!placeholder)
1438
+ return false;
1439
+ placeholder.expanded = !placeholder.expanded;
1440
+ // Update the placeholder text in buffer
1441
+ const newPlaceholder = placeholder.expanded
1442
+ ? this.buildExpandedPlaceholder(placeholder)
1443
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1444
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1445
+ // Update buffer
1446
+ this.buffer =
1447
+ this.buffer.slice(0, placeholder.start) +
1448
+ newPlaceholder +
1449
+ this.buffer.slice(placeholder.end);
1450
+ // Update placeholder tracking
1451
+ placeholder.placeholder = newPlaceholder;
1452
+ placeholder.end = placeholder.start + newPlaceholder.length;
1453
+ // Shift other placeholders
1454
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1455
+ this.scheduleRender();
1456
+ return true;
1457
+ }
1458
+ buildExpandedPlaceholder(ph) {
1459
+ const lines = ph.content.split('\n');
1460
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1461
+ const lastLines = lines.length > 5
1462
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1463
+ : '';
1464
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1465
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1466
+ }
1474
1467
  deletePlaceholder(placeholder) {
1475
1468
  const length = placeholder.end - placeholder.start;
1476
1469
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1478,11 +1471,7 @@ export class TerminalInput extends EventEmitter {
1478
1471
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1479
1472
  this.cursor = placeholder.start;
1480
1473
  }
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
- }
1474
+ updateContextUsage(value) {
1486
1475
  if (value === null || !Number.isFinite(value)) {
1487
1476
  this.contextUsage = null;
1488
1477
  }
@@ -1509,22 +1498,6 @@ export class TerminalInput extends EventEmitter {
1509
1498
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1510
1499
  this.setEditMode(next);
1511
1500
  }
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
1501
  scheduleRender() {
1529
1502
  if (!this.canRender())
1530
1503
  return;