erosolar-cli 1.7.339 → 1.7.341

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 +21 -5
  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 +166 -235
  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 +119 -149
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +532 -639
  256. package/dist/shell/terminalInput.js.map +1 -1
  257. package/dist/shell/terminalInputAdapter.d.ts +21 -79
  258. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  259. package/dist/shell/terminalInputAdapter.js +30 -99
  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 +179 -25
  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,18 +21,14 @@ 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',
27
+ ALT_SCREEN_ENTER: '\x1b[?1049h', // Enter alternate screen buffer
28
+ ALT_SCREEN_EXIT: '\x1b[?1049l', // Exit alternate screen buffer
29
29
  // Line control
30
30
  CLEAR_LINE: '\x1b[2K',
31
31
  CLEAR_TO_END: '\x1b[0J',
32
- // Screen control
33
- HOME: '\x1b[H',
34
- CLEAR_SCREEN: '\x1b[2J',
35
- // Alternate screen buffer (like vim/tmux)
36
- ENTER_ALT_SCREEN: '\x1b[?1049h',
37
- EXIT_ALT_SCREEN: '\x1b[?1049l',
38
- // Scroll region
39
- SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
40
- RESET_SCROLL: '\x1b[r',
41
32
  // Style
42
33
  RESET: '\x1b[0m',
43
34
  DIM: '\x1b[2m',
@@ -77,46 +68,49 @@ export class TerminalInput extends EventEmitter {
77
68
  statusMessage = null;
78
69
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
79
70
  streamingLabel = null; // Streaming progress indicator
80
- metaElapsedSeconds = null; // Optional elapsed time for header line
81
- metaTokensUsed = null; // Optional token usage
82
- metaTokenLimit = null; // Optional token window
83
- metaThinkingMs = null; // Optional thinking duration
84
- metaThinkingHasContent = false; // Whether collapsed thinking content exists
85
71
  lastRenderContent = '';
86
72
  lastRenderCursor = -1;
87
73
  renderDirty = false;
88
74
  isRendering = false;
75
+ flowModeRenderedLines = 0; // Track lines rendered for clearing
76
+ inputAreaStartRow = 0; // Track absolute row position of input area
77
+ contentEndRow = 0; // Row where content ends (chat box renders below this)
78
+ // Command suggestions (Claude Code style auto-complete)
79
+ commandSuggestions = [];
80
+ filteredSuggestions = [];
81
+ selectedSuggestionIndex = 0;
82
+ showSuggestions = false;
89
83
  // Lifecycle
90
84
  disposed = false;
91
85
  enabled = true;
92
86
  contextUsage = null;
93
- contextAutoCompactThreshold = 90;
94
- // Track current content row (starts at top, moves down)
95
- contentRow = 1;
96
- // Track if scroll region is currently active
97
- scrollRegionActive = false;
98
- thinkingModeLabel = null;
99
87
  editMode = 'display-edits';
100
88
  verificationEnabled = true;
101
89
  autoContinueEnabled = false;
102
90
  verificationHotkey = 'alt+v';
103
91
  autoContinueHotkey = 'alt+c';
104
- thinkingHotkey = '/thinking';
105
- modelLabel = null;
106
- providerLabel = null;
107
- // Streaming render throttle
108
- lastStreamingRender = 0;
109
- streamingRenderInterval = 250; // ms between renders during streaming
92
+ // Output interceptor cleanup
93
+ outputInterceptorCleanup;
94
+ // Metrics tracking for status bar
95
+ streamingStartTime = null;
96
+ thinkingEnabled = true;
97
+ modelInfo = null; // Provider · Model info
98
+ // Streaming input area render timer (updates elapsed time display)
110
99
  streamingRenderTimer = null;
100
+ // Reference to display module for getting line counts during streaming
101
+ displayRef = null;
102
+ // Unified UI initialization flag
103
+ unifiedUIInitialized = false;
111
104
  constructor(writeStream = process.stdout, config = {}) {
112
105
  super();
113
106
  this.out = writeStream;
107
+ // Use schema defaults for configuration consistency
114
108
  this.config = {
115
- maxLines: config.maxLines ?? 1000,
116
- maxLength: config.maxLength ?? 10000,
109
+ maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
110
+ maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
117
111
  maxQueueSize: config.maxQueueSize ?? 100,
118
- promptChar: config.promptChar ?? '> ',
119
- continuationChar: config.continuationChar ?? '│ ',
112
+ promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
113
+ continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
120
114
  };
121
115
  }
122
116
  // ===========================================================================
@@ -195,36 +189,306 @@ export class TerminalInput extends EventEmitter {
195
189
  if (handled)
196
190
  return;
197
191
  }
192
+ // Handle '?' for help hint (if buffer is empty)
193
+ if (str === '?' && this.buffer.length === 0) {
194
+ this.emit('showHelp');
195
+ return;
196
+ }
198
197
  // Insert printable characters
199
198
  if (str && !key?.ctrl && !key?.meta) {
200
199
  this.insertText(str);
201
200
  }
202
201
  }
202
+ // Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
203
+ bannerContent = null;
204
+ /**
205
+ * Set banner content to be written when unified UI initializes.
206
+ */
207
+ setBannerContent(content) {
208
+ this.bannerContent = content;
209
+ }
210
+ /**
211
+ * Initialize the unified UI system with BOTTOM PINNED chat box.
212
+ *
213
+ * Layout:
214
+ * 1. Clear screen
215
+ * 2. Write banner at top
216
+ * 3. Set content cursor row after banner
217
+ * 4. Render chat box at bottom (sets up scroll region)
218
+ */
219
+ initializeUnifiedUI() {
220
+ if (this.unifiedUIInitialized) {
221
+ return;
222
+ }
223
+ // Enter alternate screen buffer for complete terminal control
224
+ this.write(ESC.ALT_SCREEN_ENTER);
225
+ // Hide cursor during setup
226
+ this.write(ESC.HIDE);
227
+ // Clear screen and go home (in alternate buffer)
228
+ this.write(ESC.HOME);
229
+ this.write(ESC.CLEAR_SCREEN);
230
+ // Write banner at top
231
+ let bannerLines = 0;
232
+ if (this.bannerContent) {
233
+ const lines = this.bannerContent.split('\n');
234
+ bannerLines = lines.length + 2; // +2 for trailing \n\n
235
+ process.stdout.write(this.bannerContent + '\n\n');
236
+ }
237
+ // Set content cursor row after banner
238
+ this.contentCursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
239
+ // Content ends at same row initially (no content yet)
240
+ this.contentEndRow = this.contentCursorRow - 1;
241
+ // Mark initialized
242
+ this.unifiedUIInitialized = true;
243
+ // Render floating chat box below content
244
+ this.renderFloatingInputArea();
245
+ }
246
+ /**
247
+ * Clear the input area at its tracked position.
248
+ * Returns true if something was cleared.
249
+ */
250
+ clearInputArea() {
251
+ if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
252
+ for (let i = 0; i < this.flowModeRenderedLines; i++) {
253
+ this.write(ESC.TO(this.inputAreaStartRow + i, 1));
254
+ this.write(ESC.CLEAR_LINE);
255
+ }
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ /**
261
+ * Reset input area tracking state.
262
+ */
263
+ resetInputAreaTracking() {
264
+ this.inputAreaStartRow = 0;
265
+ this.flowModeRenderedLines = 0;
266
+ }
267
+ /**
268
+ * Render chat box - FLOATING below content (no scroll regions).
269
+ * Chat box appears right after content and moves down as content grows.
270
+ * Content and banner scroll naturally off the top of the screen.
271
+ */
272
+ renderFloatingInputArea() {
273
+ const { cols } = this.getSize();
274
+ const divider = '─'.repeat(cols);
275
+ const { dim: DIM, reset: R } = UI_COLORS;
276
+ // Chat box is 4 lines: divider + input + divider + controls
277
+ const chatBoxHeight = 4;
278
+ // Chat box starts right after content
279
+ const chatBoxStartRow = this.contentEndRow + 1;
280
+ // Save cursor position before rendering
281
+ this.write('\x1b7');
282
+ // Hide cursor during render
283
+ this.write(ESC.HIDE);
284
+ // Track position
285
+ this.inputAreaStartRow = chatBoxStartRow;
286
+ let currentRow = chatBoxStartRow;
287
+ // Helper to write a line at absolute position (clears then writes)
288
+ const writeLine = (content) => {
289
+ this.write(ESC.TO(currentRow, 1));
290
+ this.write(ESC.CLEAR_LINE);
291
+ this.write(content);
292
+ currentRow++;
293
+ };
294
+ // Top divider
295
+ writeLine(`${DIM}${divider}${R}`);
296
+ // Input line with > prompt
297
+ const { lines, cursorCol } = this.wrapBuffer(cols - 3);
298
+ const displayLine = lines[0] ?? '';
299
+ const inputRow = currentRow;
300
+ writeLine(`${DIM}>${R} ${displayLine}`);
301
+ // Bottom divider
302
+ writeLine(`${DIM}${divider}${R}`);
303
+ // Mode controls line - Claude Code style
304
+ this.write(ESC.TO(currentRow, 1));
305
+ this.write(ESC.CLEAR_LINE);
306
+ this.write(this.buildClaudeStyleControls(cols));
307
+ // Track lines rendered
308
+ this.flowModeRenderedLines = chatBoxHeight;
309
+ // Restore cursor position (back to where content was being written)
310
+ // During streaming, this keeps cursor in content area
311
+ // During idle, cursor was in input area anyway
312
+ this.write('\x1b8');
313
+ // Show cursor
314
+ this.write(ESC.SHOW);
315
+ // Update tracking
316
+ this.lastRenderContent = this.buffer;
317
+ this.lastRenderCursor = this.cursor;
318
+ }
319
+ /**
320
+ * Build Claude Code style controls line.
321
+ * Shows: edit mode indicator (shift+tab to cycle)
322
+ */
323
+ buildClaudeStyleControls(cols) {
324
+ const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
325
+ // Edit mode indicator
326
+ let editModeText;
327
+ if (this.editMode === 'display-edits') {
328
+ editModeText = `${GREEN}⏵⏵${R} accept edits on`;
329
+ }
330
+ else {
331
+ editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
332
+ }
333
+ // Build controls line
334
+ const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
335
+ // Add thinking mode if enabled
336
+ if (this.thinkingEnabled) {
337
+ parts.push(`${CYAN}💭${R}`);
338
+ }
339
+ // Add context usage if available
340
+ if (this.contextUsage !== null) {
341
+ const rem = Math.max(0, 100 - this.contextUsage);
342
+ if (rem < 10) {
343
+ parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
344
+ }
345
+ else if (rem < 25) {
346
+ parts.push(`${YELLOW}ctx ${rem}%${R}`);
347
+ }
348
+ }
349
+ return parts.join(` ${DIM}·${R} `);
350
+ }
203
351
  /**
204
352
  * Set the input mode
205
353
  *
206
- * Content flows naturally - no scroll region pinning.
354
+ * BOTTOM PINNED with SSE: Chat box stays at terminal bottom.
355
+ * Scroll region protects chat box, content scrolls above it.
207
356
  */
208
357
  setMode(mode) {
209
358
  const prevMode = this.mode;
210
359
  this.mode = mode;
211
360
  if (mode === 'streaming' && prevMode !== 'streaming') {
212
- this.resetStreamingRenderThrottle();
361
+ // Track streaming start time for elapsed display
362
+ this.streamingStartTime = Date.now();
363
+ // Ensure unified UI is initialized
364
+ if (!this.unifiedUIInitialized) {
365
+ this.initializeUnifiedUI();
366
+ }
367
+ // Start periodic render timer to keep chat box updated during streaming
368
+ // This updates contentEndRow from display and re-renders chat box
369
+ if (!this.streamingRenderTimer) {
370
+ this.streamingRenderTimer = setInterval(() => {
371
+ // Update contentEndRow from display's line count
372
+ if (this.displayRef?.getTotalWrittenLines) {
373
+ this.contentEndRow = this.displayRef.getTotalWrittenLines();
374
+ }
375
+ // Re-render chat box at updated position
376
+ this.renderFloatingInputArea();
377
+ }, 100); // Update every 100ms
378
+ }
379
+ // Initial render
213
380
  this.renderDirty = true;
214
- this.render();
381
+ this.scheduleRender();
215
382
  }
216
383
  else if (mode !== 'streaming' && prevMode === 'streaming') {
217
- // Streaming ended - render the input area
218
- this.resetStreamingRenderThrottle();
219
- this.forceRender();
384
+ // Stop streaming render timer
385
+ if (this.streamingRenderTimer) {
386
+ clearInterval(this.streamingRenderTimer);
387
+ this.streamingRenderTimer = null;
388
+ }
389
+ // Reset streaming time
390
+ this.streamingStartTime = null;
391
+ // Final render with accurate position
392
+ this.renderDirty = true;
393
+ this.scheduleRender();
220
394
  }
221
395
  }
222
396
  /**
223
- * Legacy method - no longer used (content flows naturally).
224
- * @deprecated Use setContentRow instead
397
+ * Set the row where content ends (for idle mode positioning).
398
+ * Input area will render starting from this row + 1.
399
+ */
400
+ setContentEndRow(row) {
401
+ this.contentEndRow = Math.max(0, row);
402
+ this.renderDirty = true;
403
+ this.scheduleRender();
404
+ }
405
+ /**
406
+ * Set available slash commands for auto-complete suggestions.
225
407
  */
226
- setPinnedHeaderLines(_count) {
227
- // No-op: scroll region pinning removed
408
+ setCommands(commands) {
409
+ this.commandSuggestions = commands;
410
+ this.updateSuggestions();
411
+ }
412
+ /**
413
+ * Update filtered suggestions based on current input.
414
+ */
415
+ updateSuggestions() {
416
+ const input = this.buffer.trim();
417
+ // Only show suggestions when input starts with "/"
418
+ if (!input.startsWith('/')) {
419
+ this.showSuggestions = false;
420
+ this.filteredSuggestions = [];
421
+ this.selectedSuggestionIndex = 0;
422
+ return;
423
+ }
424
+ const query = input.toLowerCase();
425
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
426
+ cmd.command.toLowerCase().includes(query.slice(1)));
427
+ // Show suggestions if we have matches
428
+ this.showSuggestions = this.filteredSuggestions.length > 0;
429
+ // Keep selection in bounds
430
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
431
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
432
+ }
433
+ }
434
+ /**
435
+ * Select next suggestion (arrow down / tab).
436
+ */
437
+ selectNextSuggestion() {
438
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
439
+ return;
440
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
441
+ this.renderDirty = true;
442
+ this.scheduleRender();
443
+ }
444
+ /**
445
+ * Select previous suggestion (arrow up / shift+tab).
446
+ */
447
+ selectPrevSuggestion() {
448
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
449
+ return;
450
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
451
+ ? this.filteredSuggestions.length - 1
452
+ : this.selectedSuggestionIndex - 1;
453
+ this.renderDirty = true;
454
+ this.scheduleRender();
455
+ }
456
+ /**
457
+ * Accept current suggestion and insert into buffer.
458
+ */
459
+ acceptSuggestion() {
460
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
461
+ return false;
462
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
463
+ if (!selected)
464
+ return false;
465
+ // Replace buffer with selected command
466
+ this.buffer = selected.command + ' ';
467
+ this.cursor = this.buffer.length;
468
+ this.showSuggestions = false;
469
+ this.renderDirty = true;
470
+ this.scheduleRender();
471
+ return true;
472
+ }
473
+ /**
474
+ * Check if suggestions are visible.
475
+ */
476
+ areSuggestionsVisible() {
477
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
478
+ }
479
+ /**
480
+ * Toggle thinking/reasoning mode
481
+ */
482
+ toggleThinking() {
483
+ this.thinkingEnabled = !this.thinkingEnabled;
484
+ this.emit('thinkingToggle', this.thinkingEnabled);
485
+ this.scheduleRender();
486
+ }
487
+ /**
488
+ * Get thinking enabled state
489
+ */
490
+ isThinkingEnabled() {
491
+ return this.thinkingEnabled;
228
492
  }
229
493
  /**
230
494
  * Get current mode
@@ -257,14 +521,17 @@ export class TerminalInput extends EventEmitter {
257
521
  }
258
522
  /**
259
523
  * Clear the buffer
524
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
260
525
  */
261
- clear() {
526
+ clear(skipRender = false) {
262
527
  this.buffer = '';
263
528
  this.cursor = 0;
264
529
  this.historyIndex = -1;
265
530
  this.tempInput = '';
266
531
  this.pastePlaceholders = [];
267
- this.scheduleRender();
532
+ if (!skipRender) {
533
+ this.scheduleRender();
534
+ }
268
535
  }
269
536
  /**
270
537
  * Get queued inputs
@@ -335,37 +602,6 @@ export class TerminalInput extends EventEmitter {
335
602
  this.streamingLabel = next;
336
603
  this.scheduleRender();
337
604
  }
338
- /**
339
- * Surface meta status just above the divider (e.g., elapsed time or token usage).
340
- */
341
- setMetaStatus(meta) {
342
- const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
343
- ? Math.floor(meta.elapsedSeconds)
344
- : null;
345
- const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
346
- ? Math.floor(meta.tokensUsed)
347
- : null;
348
- const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
349
- ? Math.floor(meta.tokenLimit)
350
- : null;
351
- const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
352
- ? Math.floor(meta.thinkingMs)
353
- : null;
354
- const nextThinkingHasContent = !!meta.thinkingHasContent;
355
- if (this.metaElapsedSeconds === nextElapsed &&
356
- this.metaTokensUsed === nextTokens &&
357
- this.metaTokenLimit === nextLimit &&
358
- this.metaThinkingMs === nextThinking &&
359
- this.metaThinkingHasContent === nextThinkingHasContent) {
360
- return;
361
- }
362
- this.metaElapsedSeconds = nextElapsed;
363
- this.metaTokensUsed = nextTokens;
364
- this.metaTokenLimit = nextLimit;
365
- this.metaThinkingMs = nextThinking;
366
- this.metaThinkingHasContent = nextThinkingHasContent;
367
- this.scheduleRender();
368
- }
369
605
  /**
370
606
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
371
607
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -375,22 +611,26 @@ export class TerminalInput extends EventEmitter {
375
611
  const nextAutoContinue = !!options.autoContinueEnabled;
376
612
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
377
613
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
378
- const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
379
- const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
380
614
  if (this.verificationEnabled === nextVerification &&
381
615
  this.autoContinueEnabled === nextAutoContinue &&
382
616
  this.verificationHotkey === nextVerifyHotkey &&
383
- this.autoContinueHotkey === nextAutoHotkey &&
384
- this.thinkingHotkey === nextThinkingHotkey &&
385
- this.thinkingModeLabel === nextThinkingLabel) {
617
+ this.autoContinueHotkey === nextAutoHotkey) {
386
618
  return;
387
619
  }
388
620
  this.verificationEnabled = nextVerification;
389
621
  this.autoContinueEnabled = nextAutoContinue;
390
622
  this.verificationHotkey = nextVerifyHotkey;
391
623
  this.autoContinueHotkey = nextAutoHotkey;
392
- this.thinkingHotkey = nextThinkingHotkey;
393
- this.thinkingModeLabel = nextThinkingLabel;
624
+ this.scheduleRender();
625
+ }
626
+ /**
627
+ * Set the model info string (e.g., "OpenAI · gpt-4")
628
+ * This is displayed persistently above the input area.
629
+ */
630
+ setModelInfo(info) {
631
+ if (this.modelInfo === info)
632
+ return;
633
+ this.modelInfo = info;
394
634
  this.scheduleRender();
395
635
  }
396
636
  /**
@@ -403,171 +643,33 @@ export class TerminalInput extends EventEmitter {
403
643
  this.scheduleRender();
404
644
  }
405
645
  /**
406
- * Surface model/provider context in the controls bar.
407
- */
408
- setModelContext(options) {
409
- const nextModel = options.model?.trim() || null;
410
- const nextProvider = options.provider?.trim() || null;
411
- if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
412
- return;
413
- }
414
- this.modelLabel = nextModel;
415
- this.providerLabel = nextProvider;
416
- this.scheduleRender();
417
- }
418
- /**
419
- * Render the floating input area at contentRow.
420
- *
421
- * The chat box "floats" - it renders right below the last streamed content.
422
- * As content is added, contentRow advances, and the chat box moves down.
423
- * No scroll regions - pure floating behavior.
646
+ * Render the input area.
647
+ * During streaming: renders at terminal bottom (with scroll region)
648
+ * After streaming: renders floating below content
424
649
  */
425
650
  render() {
426
651
  if (!this.canRender())
427
652
  return;
428
653
  if (this.isRendering)
429
654
  return;
430
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
431
- // During streaming, throttle re-renders
432
- if (streamingActive && this.lastStreamingRender > 0) {
433
- const elapsed = Date.now() - this.lastStreamingRender;
434
- const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
435
- if (waitMs > 0) {
436
- this.renderDirty = true;
437
- this.scheduleStreamingRender(waitMs);
438
- return;
439
- }
440
- }
441
655
  const shouldSkip = !this.renderDirty &&
442
656
  this.buffer === this.lastRenderContent &&
443
657
  this.cursor === this.lastRenderCursor;
444
658
  this.renderDirty = false;
659
+ // Skip if nothing changed (unless explicitly forced)
445
660
  if (shouldSkip) {
446
661
  return;
447
662
  }
663
+ // If write lock is held, defer render
448
664
  if (writeLock.isLocked()) {
449
665
  writeLock.safeWrite(() => this.render());
450
666
  return;
451
667
  }
452
- this.renderPinnedChatBox();
453
- }
454
- /**
455
- * Unified scroll region renderer.
456
- * Chat box is ALWAYS pinned at the bottom of the terminal.
457
- * Content scrolls in the region above the chat box.
458
- */
459
- renderPinnedChatBox() {
460
- const { rows, cols } = this.getSize();
461
- const maxWidth = Math.max(8, cols - 4);
462
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
463
- // Wrap buffer into display lines
464
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
465
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
466
- const displayLines = Math.min(lines.length, maxVisible);
467
- const metaLines = this.buildMetaLines(cols - 2);
468
- // Calculate display window (keep cursor visible)
469
- let startLine = 0;
470
- if (lines.length > displayLines) {
471
- startLine = Math.max(0, cursorLine - displayLines + 1);
472
- startLine = Math.min(startLine, lines.length - displayLines);
473
- }
474
- const visibleLines = lines.slice(startLine, startLine + displayLines);
475
- const adjustedCursorLine = cursorLine - startLine;
476
- // Chat box height
477
- const chatBoxHeight = this.getChatBoxHeight();
478
- // ALWAYS pin chat box at absolute bottom
479
- const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
480
- const scrollEnd = chatBoxStartRow - 1;
481
- writeLock.lock('terminalInput.renderPinned');
482
668
  this.isRendering = true;
669
+ writeLock.lock('terminalInput.render');
483
670
  try {
484
- this.write(ESC.SAVE);
485
- this.write(ESC.HIDE);
486
- // Temporarily reset scroll region to write chat box cleanly
487
- if (this.scrollRegionActive) {
488
- this.write(ESC.RESET_SCROLL);
489
- }
490
- // Clear the chat box area
491
- for (let i = 0; i < chatBoxHeight; i++) {
492
- const row = chatBoxStartRow + i;
493
- if (row <= rows) {
494
- this.write(ESC.TO(row, 1));
495
- this.write(ESC.CLEAR_LINE);
496
- }
497
- }
498
- let currentRow = chatBoxStartRow;
499
- // Meta/status header
500
- for (const metaLine of metaLines) {
501
- this.write(ESC.TO(currentRow, 1));
502
- this.write(metaLine);
503
- currentRow += 1;
504
- }
505
- // Separator line
506
- this.write(ESC.TO(currentRow, 1));
507
- this.write(renderDivider(cols - 2));
508
- currentRow += 1;
509
- // Render input lines
510
- let finalRow = currentRow;
511
- let finalCol = 3;
512
- for (let i = 0; i < visibleLines.length; i++) {
513
- const rowNum = currentRow + i;
514
- this.write(ESC.TO(rowNum, 1));
515
- const line = visibleLines[i] ?? '';
516
- const isFirstLine = (startLine + i) === 0;
517
- const isCursorLine = i === adjustedCursorLine;
518
- this.write(ESC.BG_DARK);
519
- this.write(ESC.DIM);
520
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
521
- this.write(ESC.RESET);
522
- this.write(ESC.BG_DARK);
523
- if (isCursorLine) {
524
- const col = Math.min(cursorCol, line.length);
525
- const before = line.slice(0, col);
526
- const at = col < line.length ? line[col] : ' ';
527
- const after = col < line.length ? line.slice(col + 1) : '';
528
- this.write(before);
529
- this.write(ESC.REVERSE + ESC.BOLD);
530
- this.write(at);
531
- this.write(ESC.RESET + ESC.BG_DARK);
532
- this.write(after);
533
- finalRow = rowNum;
534
- finalCol = this.config.promptChar.length + col + 1;
535
- }
536
- else {
537
- this.write(line);
538
- }
539
- // Pad to edge
540
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
541
- const padding = Math.max(0, cols - lineLen - 1);
542
- if (padding > 0)
543
- this.write(' '.repeat(padding));
544
- this.write(ESC.RESET);
545
- }
546
- // Mode controls line with all keyboard shortcuts
547
- const controlRow = currentRow + visibleLines.length;
548
- this.write(ESC.TO(controlRow, 1));
549
- this.write(this.buildModeControls(cols));
550
- // Restore scroll region and cursor
551
- if (this.scrollRegionActive) {
552
- // Restore scroll region
553
- this.write(ESC.SET_SCROLL(1, scrollEnd));
554
- // Restore cursor to where it was before rendering (preserves column position)
555
- this.write(ESC.RESTORE);
556
- }
557
- else {
558
- // Not streaming - position cursor in input box
559
- this.write(ESC.RESTORE);
560
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
561
- }
562
- this.write(ESC.SHOW);
563
- // Update state
564
- this.lastRenderContent = this.buffer;
565
- this.lastRenderCursor = this.cursor;
566
- this.lastStreamingRender = streamingActive ? Date.now() : 0;
567
- if (this.streamingRenderTimer) {
568
- clearTimeout(this.streamingRenderTimer);
569
- this.streamingRenderTimer = null;
570
- }
671
+ // Always render floating right after content (no wasted space)
672
+ this.renderFloatingInputArea();
571
673
  }
572
674
  finally {
573
675
  writeLock.unlock();
@@ -575,198 +677,99 @@ export class TerminalInput extends EventEmitter {
575
677
  }
576
678
  }
577
679
  /**
578
- * Build compact meta line above the divider.
579
- * Shows model/provider and key metrics in a single line.
580
- * Status message is shown in mode controls to avoid duplication.
680
+ * Build status bar showing streaming/ready status and key info.
681
+ * This is the TOP line above the input area - minimal Claude Code style.
581
682
  */
582
- buildMetaLines(width) {
683
+ buildStatusBar(cols) {
684
+ const maxWidth = cols - 2;
583
685
  const parts = [];
584
- // Model/provider info
585
- if (this.modelLabel) {
586
- const modelText = this.providerLabel
587
- ? `${this.modelLabel} @ ${this.providerLabel}`
588
- : this.modelLabel;
589
- parts.push({ text: modelText, tone: 'info' });
590
- }
591
- // Elapsed time
592
- if (this.metaElapsedSeconds !== null) {
593
- parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
594
- }
595
- // Token usage (compact)
596
- if (this.metaTokensUsed !== null) {
597
- const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
598
- const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
599
- parts.push({ text: `${formattedUsed}${formattedLimit}`, tone: 'muted' });
600
- }
601
- // Context remaining (only show if concerning)
602
- const tokensRemaining = this.computeTokensRemaining();
603
- if (tokensRemaining !== null) {
604
- parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
605
- }
606
- // Thinking indicator
607
- if (this.metaThinkingMs !== null) {
608
- parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
609
- }
610
- if (!parts.length) {
611
- return [];
612
- }
613
- return [renderStatusLine(parts, width)];
614
- }
615
- /**
616
- * Build mode controls line with all keyboard shortcuts.
617
- * Shows status, all toggles, and contextual information.
618
- */
619
- buildModeControls(cols) {
620
- const width = Math.max(8, cols - 2);
621
- const leftParts = [];
622
- const rightParts = [];
623
- // Streaming indicator
624
- if (this.streamingLabel) {
625
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
626
- }
627
- // Override status (warnings, errors)
628
- if (this.overrideStatusMessage) {
629
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
630
- }
631
- // Main status message
632
- if (this.statusMessage) {
633
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
634
- }
635
- // === KEYBOARD SHORTCUTS ===
636
- // Interrupt shortcut (during streaming)
637
- if (this.mode === 'streaming' || this.scrollRegionActive) {
638
- leftParts.push({ text: `${this.formatHotkey('esc')} stop`, tone: 'warn' });
639
- }
640
- // Edit mode toggle (Shift+Tab)
641
- const editHotkey = this.formatHotkey('shift+tab');
642
- const editLabel = this.editMode === 'display-edits' ? 'edits:accept' : 'edits:ask';
643
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
644
- leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
645
- // Verification toggle (Alt+V)
646
- const verifyHotkey = this.formatHotkey(this.verificationHotkey || 'alt+v');
647
- const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
648
- leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
649
- // Auto-continue toggle (Alt+C)
650
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey || 'alt+c');
651
- const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
652
- leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
653
- // Thinking mode toggle (if available)
654
- if (this.thinkingModeLabel) {
655
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey || 'alt+t');
656
- rightParts.push({ text: `${thinkingHotkey} think:${this.thinkingModeLabel}`, tone: 'info' });
657
- }
658
- // === CONTEXTUAL INFO ===
659
- // Queued commands
660
- if (this.queue.length > 0 && this.mode !== 'streaming') {
661
- leftParts.push({ text: `queued:${this.queue.length}`, tone: 'info' });
686
+ // Streaming status with elapsed time (left side)
687
+ if (this.mode === 'streaming') {
688
+ let statusText = '● Streaming';
689
+ if (this.streamingStartTime) {
690
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
691
+ const mins = Math.floor(elapsed / 60);
692
+ const secs = elapsed % 60;
693
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
694
+ }
695
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
662
696
  }
663
- // Multi-line indicator
664
- if (this.buffer.includes('\n')) {
665
- const lineCount = this.buffer.split('\n').length;
666
- rightParts.push({ text: `${lineCount}L`, tone: 'muted' });
697
+ // Queue indicator during streaming
698
+ if (this.mode === 'streaming' && this.queue.length > 0) {
699
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
667
700
  }
668
701
  // Paste indicator
669
702
  if (this.pastePlaceholders.length > 0) {
670
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
671
- rightParts.push({
672
- text: `paste#${latest.id} +${latest.lineCount}L ${this.formatHotkey('backspace')}drop`,
673
- tone: 'info',
674
- });
675
- }
676
- // Context remaining warning
677
- const contextRemaining = this.computeContextRemaining();
678
- if (contextRemaining !== null) {
679
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
680
- const label = contextRemaining === 0 && this.contextUsage !== null
681
- ? 'compact imminent'
682
- : `ctx:${contextRemaining}%`;
683
- rightParts.push({ text: label, tone });
684
- }
685
- // Render: left-aligned shortcuts, right-aligned context info
686
- if (!rightParts.length || width < 60) {
687
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
688
- return renderStatusLine(merged, width);
689
- }
690
- const leftWidth = Math.max(12, Math.floor(width * 0.65));
691
- const rightWidth = Math.max(14, width - leftWidth - 1);
692
- const leftText = renderStatusLine(leftParts, leftWidth);
693
- const rightText = renderStatusLine(rightParts, rightWidth);
694
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
695
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
696
- }
697
- formatHotkey(hotkey) {
698
- const normalized = hotkey.trim().toLowerCase();
699
- if (!normalized)
700
- return hotkey;
701
- const parts = normalized.split('+').filter(Boolean);
702
- const map = {
703
- shift: '⇧',
704
- sh: '⇧',
705
- alt: '⌥',
706
- option: '⌥',
707
- opt: '⌥',
708
- ctrl: '⌃',
709
- control: '⌃',
710
- cmd: '⌘',
711
- meta: '⌘',
712
- };
713
- const formatted = parts
714
- .map((part) => {
715
- const symbol = map[part];
716
- if (symbol)
717
- return symbol;
718
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
719
- })
720
- .join('');
721
- return formatted || hotkey;
722
- }
723
- computeContextRemaining() {
724
- if (this.contextUsage === null) {
725
- return null;
726
- }
727
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
728
- }
729
- computeTokensRemaining() {
730
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
731
- return null;
703
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
704
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
732
705
  }
733
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
734
- return this.formatTokenCount(remaining);
735
- }
736
- formatElapsedLabel(seconds) {
737
- if (seconds < 60) {
738
- return `${seconds}s`;
706
+ // Override/warning status
707
+ if (this.overrideStatusMessage) {
708
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
739
709
  }
740
- const mins = Math.floor(seconds / 60);
741
- const secs = seconds % 60;
742
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
743
- }
744
- formatTokenCount(value) {
745
- if (!Number.isFinite(value)) {
746
- return `${value}`;
710
+ // If idle with empty buffer, show quick shortcuts
711
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
712
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
747
713
  }
748
- if (value >= 1_000_000) {
749
- return `${(value / 1_000_000).toFixed(1)}M`;
714
+ // Multi-line indicator
715
+ if (this.buffer.includes('\n')) {
716
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
750
717
  }
751
- if (value >= 1_000) {
752
- return `${(value / 1_000).toFixed(1)}k`;
718
+ if (parts.length === 0) {
719
+ return ''; // Empty status bar when idle
753
720
  }
754
- return `${Math.round(value)}`;
755
- }
756
- visibleLength(value) {
757
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
758
- return value.replace(ansiPattern, '').length;
721
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
722
+ return joined.slice(0, maxWidth);
759
723
  }
760
724
  /**
761
- * Debug-only snapshot used by tests to assert rendered strings without
762
- * needing a TTY. Not used by production code.
725
+ * Build mode controls line showing toggles and context info.
726
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
727
+ *
728
+ * Layout: [toggles on left] ... [context info on right]
763
729
  */
764
- getDebugUiSnapshot(width) {
765
- const cols = Math.max(8, width ?? this.getSize().cols);
766
- return {
767
- meta: this.buildMetaLines(cols - 2),
768
- controls: this.buildModeControls(cols),
769
- };
730
+ buildModeControls(cols) {
731
+ const maxWidth = cols - 2;
732
+ // Use schema-defined colors for consistency
733
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
734
+ // Mode toggles with colors (following ModeControlsSchema)
735
+ const toggles = [];
736
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
737
+ if (this.editMode === 'display-edits') {
738
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
739
+ }
740
+ else {
741
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
742
+ }
743
+ // Thinking mode (cyan when on) - per schema.thinkingMode
744
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
745
+ // Verification (green when on) - per schema.verificationMode
746
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
747
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
748
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
749
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
750
+ // Context usage with color - per schema.contextUsage thresholds
751
+ let rightPart = '';
752
+ if (this.contextUsage !== null) {
753
+ const rem = Math.max(0, 100 - this.contextUsage);
754
+ // Thresholds: critical < 10%, warning < 25%
755
+ if (rem < 10)
756
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
757
+ else if (rem < 25)
758
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
759
+ else
760
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
761
+ }
762
+ // Calculate visible lengths (strip ANSI)
763
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
764
+ const leftLen = strip(leftPart).length;
765
+ const rightLen = strip(rightPart).length;
766
+ if (leftLen + rightLen < maxWidth - 4) {
767
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
768
+ }
769
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
770
+ return `${leftPart} ${rightPart}`;
771
+ }
772
+ return leftPart;
770
773
  }
771
774
  /**
772
775
  * Force a re-render
@@ -789,189 +792,31 @@ export class TerminalInput extends EventEmitter {
789
792
  handleResize() {
790
793
  this.lastRenderContent = '';
791
794
  this.lastRenderCursor = -1;
792
- this.resetStreamingRenderThrottle();
793
795
  this.scheduleRender();
794
796
  }
797
+ // Track current content row for writing
798
+ contentCursorRow = 1;
795
799
  /**
796
- * Enter streaming mode with scroll region.
797
- * Sets up terminal scroll region to exclude chat box.
798
- */
799
- enterStreamingScrollRegion() {
800
- const { rows } = this.getSize();
801
- const chatBoxHeight = this.getChatBoxHeight();
802
- const scrollEnd = Math.max(1, rows - chatBoxHeight);
803
- writeLock.lock('enterStreamingScrollRegion');
804
- try {
805
- // Set scroll region for content area (above chat box)
806
- this.write(ESC.SET_SCROLL(1, scrollEnd));
807
- // Position cursor at current content row
808
- this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
809
- this.scrollRegionActive = true;
810
- this.setStatusMessage('esc to interrupt');
811
- }
812
- finally {
813
- writeLock.unlock();
814
- }
815
- // Render pinned chat box at bottom
816
- this.forceRender();
817
- }
818
- /**
819
- * Exit streaming mode and restore normal operation.
800
+ * Register with display's output interceptor.
801
+ * Clears chat box before writes, re-renders after with updated position.
820
802
  */
821
- exitStreamingScrollRegion() {
822
- writeLock.lock('exitStreamingScrollRegion');
823
- try {
824
- // Reset scroll region to full terminal
825
- this.write(ESC.RESET_SCROLL);
826
- this.scrollRegionActive = false;
827
- this.setStatusMessage('Ready for prompts');
828
- }
829
- finally {
830
- writeLock.unlock();
831
- }
832
- // Final render
833
- this.forceRender();
834
- }
835
- /**
836
- * Render chat box at bottom - now uses unified renderer.
837
- * @deprecated Use renderPinnedChatBox() directly via render()/forceRender()
838
- */
839
- renderChatBoxAtBottom() {
840
- this.renderPinnedChatBox();
841
- }
842
- /**
843
- * Stream content within the scroll region.
844
- * Content is written directly and scrolls naturally.
845
- */
846
- streamContent(content) {
847
- if (!content)
848
- return;
849
- writeLock.lock('streamContent');
850
- try {
851
- // Write content - scroll region handles scrolling
852
- this.write(content);
853
- // Track newlines
854
- const newlines = (content.match(/\n/g) || []).length;
855
- this.contentRow += newlines;
856
- }
857
- finally {
858
- writeLock.unlock();
859
- }
860
- // Throttle chat box updates during streaming
861
- this.scheduleStreamingRender(200);
862
- }
863
- /**
864
- * Enable scroll region (no-op in floating mode).
865
- */
866
- enableScrollRegion() {
867
- // No-op: using pure floating approach
868
- }
869
- /**
870
- * Disable scroll region (no-op in floating mode).
871
- */
872
- disableScrollRegion() {
873
- // No-op: using pure floating approach
874
- }
875
- /**
876
- * Calculate chat box height.
877
- */
878
- getChatBoxHeight() {
879
- return 6; // Fixed: meta + divider + input + controls + buffer
880
- }
881
- /**
882
- * @deprecated Use streamContent() instead
883
- * Register with display's output interceptor - kept for backwards compatibility
884
- */
885
- registerOutputInterceptor(_display) {
886
- // No-op: Use streamContent() for cleaner floating chat box behavior
887
- }
888
- /**
889
- * Write content above the floating chat box.
890
- * Works both during streaming and when idle.
891
- */
892
- writeToScrollRegion(content) {
893
- if (!content)
894
- return;
895
- writeLock.lock('writeToScrollRegion');
896
- try {
897
- // Position cursor at content row and write
898
- this.write(ESC.TO(this.contentRow, 1));
899
- this.write(content);
900
- // Track newlines
901
- const newlines = (content.match(/\n/g) || []).length;
902
- this.contentRow += newlines;
903
- }
904
- finally {
905
- writeLock.unlock();
906
- }
907
- // Re-render chat box below new content (only when not streaming)
908
- if (!this.scrollRegionActive) {
909
- this.forceRender();
910
- }
911
- }
912
- /**
913
- * Enter alternate screen buffer and clear it.
914
- * This gives us full control over the terminal without affecting user's history.
915
- */
916
- enterAlternateScreen() {
917
- writeLock.lock('enterAltScreen');
918
- try {
919
- this.write(ESC.ENTER_ALT_SCREEN);
920
- this.write(ESC.HOME);
921
- this.write(ESC.CLEAR_SCREEN);
922
- this.contentRow = 1;
923
- }
924
- finally {
925
- writeLock.unlock();
926
- }
927
- }
928
- /**
929
- * Exit alternate screen buffer.
930
- * Restores the user's previous terminal content.
931
- */
932
- exitAlternateScreen() {
933
- writeLock.lock('exitAltScreen');
934
- try {
935
- this.write(ESC.EXIT_ALT_SCREEN);
936
- }
937
- finally {
938
- writeLock.unlock();
939
- }
940
- }
941
- /**
942
- * Clear the entire terminal screen and reset content position.
943
- * This removes all content including the launching command.
944
- */
945
- clearScreen() {
946
- writeLock.lock('clearScreen');
947
- try {
948
- this.write(ESC.HOME);
949
- this.write(ESC.CLEAR_SCREEN);
950
- this.contentRow = 1;
951
- }
952
- finally {
953
- writeLock.unlock();
954
- }
955
- }
956
- /**
957
- * Reset content position to row 1.
958
- * Does NOT clear the terminal - content starts from current position.
959
- */
960
- resetContentPosition() {
961
- this.contentRow = 1;
962
- }
963
- /**
964
- * Set the content row explicitly (used after banner is written).
965
- * This tells the input where content should start flowing from.
966
- */
967
- setContentRow(row) {
968
- this.contentRow = Math.max(1, row);
969
- }
970
- /**
971
- * Get the current content row position.
972
- */
973
- getContentRow() {
974
- return this.contentRow;
803
+ registerOutputInterceptor(display) {
804
+ if (this.outputInterceptorCleanup) {
805
+ this.outputInterceptorCleanup();
806
+ }
807
+ // Store display reference for streaming timer to use
808
+ this.displayRef = display;
809
+ // Clear chat box before writes to make room for content
810
+ // Re-render is done via periodic timer during streaming, or setContentEndRow after
811
+ this.outputInterceptorCleanup = display.registerOutputInterceptor({
812
+ beforeWrite: () => {
813
+ // Clear chat box to make room for content
814
+ this.clearInputArea();
815
+ },
816
+ afterWrite: () => {
817
+ // Render is handled by streaming timer or setContentEndRow
818
+ },
819
+ });
975
820
  }
976
821
  /**
977
822
  * Dispose and clean up
@@ -979,10 +824,24 @@ export class TerminalInput extends EventEmitter {
979
824
  dispose() {
980
825
  if (this.disposed)
981
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
+ }
837
+ // Reset scroll region
838
+ this.write('\x1b[r');
839
+ // Exit alternate screen buffer (restores main terminal)
840
+ if (this.unifiedUIInitialized) {
841
+ this.write(ESC.ALT_SCREEN_EXIT);
842
+ }
982
843
  this.disposed = true;
983
844
  this.enabled = false;
984
- this.disableScrollRegion();
985
- this.resetStreamingRenderThrottle();
986
845
  this.disableBracketedPaste();
987
846
  this.buffer = '';
988
847
  this.queue = [];
@@ -1087,7 +946,22 @@ export class TerminalInput extends EventEmitter {
1087
946
  this.toggleEditMode();
1088
947
  return true;
1089
948
  }
1090
- this.insertText(' ');
949
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
950
+ if (this.findPlaceholderAt(this.cursor)) {
951
+ this.togglePasteExpansion();
952
+ }
953
+ else {
954
+ this.toggleThinking();
955
+ }
956
+ return true;
957
+ case 'escape':
958
+ // Esc: interrupt if streaming, otherwise clear buffer
959
+ if (this.mode === 'streaming') {
960
+ this.emit('interrupt');
961
+ }
962
+ else if (this.buffer.length > 0) {
963
+ this.clear();
964
+ }
1091
965
  return true;
1092
966
  }
1093
967
  return false;
@@ -1105,6 +979,7 @@ export class TerminalInput extends EventEmitter {
1105
979
  this.insertPlainText(chunk, insertPos);
1106
980
  this.cursor = insertPos + chunk.length;
1107
981
  this.emit('change', this.buffer);
982
+ this.updateSuggestions();
1108
983
  this.scheduleRender();
1109
984
  }
1110
985
  insertNewline() {
@@ -1129,6 +1004,7 @@ export class TerminalInput extends EventEmitter {
1129
1004
  this.cursor = Math.max(0, this.cursor - 1);
1130
1005
  }
1131
1006
  this.emit('change', this.buffer);
1007
+ this.updateSuggestions();
1132
1008
  this.scheduleRender();
1133
1009
  }
1134
1010
  deleteForward() {
@@ -1356,12 +1232,13 @@ export class TerminalInput extends EventEmitter {
1356
1232
  timestamp: Date.now(),
1357
1233
  });
1358
1234
  this.emit('queue', text);
1359
- this.clear(); // Clear immediately for queued input
1235
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1360
1236
  }
1361
1237
  else {
1362
- // In idle mode, clear the input first, then emit submit.
1363
- // The prompt will be logged as a visible message by the caller.
1364
- this.clear();
1238
+ // In idle mode, clear the input WITHOUT rendering.
1239
+ // The caller will display the user message and start streaming.
1240
+ // We'll render the input area again after streaming ends.
1241
+ this.clear(true); // Skip render - streaming will handle display
1365
1242
  this.emit('submit', text);
1366
1243
  }
1367
1244
  }
@@ -1378,9 +1255,7 @@ export class TerminalInput extends EventEmitter {
1378
1255
  if (available <= 0)
1379
1256
  return;
1380
1257
  const chunk = clean.slice(0, available);
1381
- const isMultiline = isMultilinePaste(chunk);
1382
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1383
- if (isMultiline && !isShortMultiline) {
1258
+ if (isMultilinePaste(chunk)) {
1384
1259
  this.insertPastePlaceholder(chunk);
1385
1260
  }
1386
1261
  else {
@@ -1516,19 +1391,17 @@ export class TerminalInput extends EventEmitter {
1516
1391
  this.shiftPlaceholders(position, text.length);
1517
1392
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1518
1393
  }
1519
- shouldInlineMultiline(content) {
1520
- const lines = content.split('\n').length;
1521
- const maxInlineLines = 4;
1522
- const maxInlineChars = 240;
1523
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1524
- }
1525
1394
  findPlaceholderAt(position) {
1526
1395
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1527
1396
  }
1528
- buildPlaceholder(lineCount) {
1397
+ buildPlaceholder(summary) {
1529
1398
  const id = ++this.pasteCounter;
1530
- const plural = lineCount === 1 ? '' : 's';
1531
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1399
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1400
+ // Show first line preview (truncated)
1401
+ const preview = summary.preview.length > 30
1402
+ ? `${summary.preview.slice(0, 30)}...`
1403
+ : summary.preview;
1404
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1532
1405
  return { id, placeholder };
1533
1406
  }
1534
1407
  insertPastePlaceholder(content) {
@@ -1536,21 +1409,67 @@ export class TerminalInput extends EventEmitter {
1536
1409
  if (available <= 0)
1537
1410
  return;
1538
1411
  const cleanContent = content.slice(0, available);
1539
- const lineCount = cleanContent.split('\n').length;
1540
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1412
+ const summary = generatePasteSummary(cleanContent);
1413
+ // For short pastes (< 5 lines), show full content instead of placeholder
1414
+ if (summary.lineCount < 5) {
1415
+ const placeholder = this.findPlaceholderAt(this.cursor);
1416
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1417
+ this.insertPlainText(cleanContent, insertPos);
1418
+ this.cursor = insertPos + cleanContent.length;
1419
+ return;
1420
+ }
1421
+ const { id, placeholder } = this.buildPlaceholder(summary);
1541
1422
  const insertPos = this.cursor;
1542
1423
  this.shiftPlaceholders(insertPos, placeholder.length);
1543
1424
  this.pastePlaceholders.push({
1544
1425
  id,
1545
1426
  content: cleanContent,
1546
- lineCount,
1427
+ lineCount: summary.lineCount,
1547
1428
  placeholder,
1548
1429
  start: insertPos,
1549
1430
  end: insertPos + placeholder.length,
1431
+ summary,
1432
+ expanded: false,
1550
1433
  });
1551
1434
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1552
1435
  this.cursor = insertPos + placeholder.length;
1553
1436
  }
1437
+ /**
1438
+ * Toggle expansion of a paste placeholder at the current cursor position.
1439
+ * When expanded, shows first 3 and last 2 lines of the content.
1440
+ */
1441
+ togglePasteExpansion() {
1442
+ const placeholder = this.findPlaceholderAt(this.cursor);
1443
+ if (!placeholder)
1444
+ return false;
1445
+ placeholder.expanded = !placeholder.expanded;
1446
+ // Update the placeholder text in buffer
1447
+ const newPlaceholder = placeholder.expanded
1448
+ ? this.buildExpandedPlaceholder(placeholder)
1449
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1450
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1451
+ // Update buffer
1452
+ this.buffer =
1453
+ this.buffer.slice(0, placeholder.start) +
1454
+ newPlaceholder +
1455
+ this.buffer.slice(placeholder.end);
1456
+ // Update placeholder tracking
1457
+ placeholder.placeholder = newPlaceholder;
1458
+ placeholder.end = placeholder.start + newPlaceholder.length;
1459
+ // Shift other placeholders
1460
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1461
+ this.scheduleRender();
1462
+ return true;
1463
+ }
1464
+ buildExpandedPlaceholder(ph) {
1465
+ const lines = ph.content.split('\n');
1466
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1467
+ const lastLines = lines.length > 5
1468
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1469
+ : '';
1470
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1471
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1472
+ }
1554
1473
  deletePlaceholder(placeholder) {
1555
1474
  const length = placeholder.end - placeholder.start;
1556
1475
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1558,11 +1477,7 @@ export class TerminalInput extends EventEmitter {
1558
1477
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1559
1478
  this.cursor = placeholder.start;
1560
1479
  }
1561
- updateContextUsage(value, autoCompactThreshold) {
1562
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1563
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1564
- this.contextAutoCompactThreshold = boundedThreshold;
1565
- }
1480
+ updateContextUsage(value) {
1566
1481
  if (value === null || !Number.isFinite(value)) {
1567
1482
  this.contextUsage = null;
1568
1483
  }
@@ -1589,28 +1504,6 @@ export class TerminalInput extends EventEmitter {
1589
1504
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1590
1505
  this.setEditMode(next);
1591
1506
  }
1592
- scheduleStreamingRender(delayMs) {
1593
- if (this.streamingRenderTimer)
1594
- return;
1595
- const wait = Math.max(16, delayMs);
1596
- this.streamingRenderTimer = setTimeout(() => {
1597
- this.streamingRenderTimer = null;
1598
- // During streaming, only update chat box (not full render)
1599
- if (this.scrollRegionActive) {
1600
- this.renderChatBoxAtBottom();
1601
- }
1602
- else {
1603
- this.render();
1604
- }
1605
- }, wait);
1606
- }
1607
- resetStreamingRenderThrottle() {
1608
- if (this.streamingRenderTimer) {
1609
- clearTimeout(this.streamingRenderTimer);
1610
- this.streamingRenderTimer = null;
1611
- }
1612
- this.lastStreamingRender = 0;
1613
- }
1614
1507
  scheduleRender() {
1615
1508
  if (!this.canRender())
1616
1509
  return;