erosolar-cli 1.7.315 → 1.7.316

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 +164 -229
  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 +116 -131
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +513 -571
  256. package/dist/shell/terminalInput.js.map +1 -1
  257. package/dist/shell/terminalInputAdapter.d.ts +20 -61
  258. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  259. package/dist/shell/terminalInputAdapter.js +30 -73
  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,15 +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
- // Screen control
33
- HOME: '\x1b[H',
34
- CLEAR_SCREEN: '\x1b[2J',
35
- // Scroll region
36
- SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
37
- RESET_SCROLL: '\x1b[r',
38
30
  // Style
39
31
  RESET: '\x1b[0m',
40
32
  DIM: '\x1b[2m',
@@ -74,46 +66,47 @@ export class TerminalInput extends EventEmitter {
74
66
  statusMessage = null;
75
67
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
76
68
  streamingLabel = null; // Streaming progress indicator
77
- metaElapsedSeconds = null; // Optional elapsed time for header line
78
- metaTokensUsed = null; // Optional token usage
79
- metaTokenLimit = null; // Optional token window
80
- metaThinkingMs = null; // Optional thinking duration
81
- metaThinkingHasContent = false; // Whether collapsed thinking content exists
82
69
  lastRenderContent = '';
83
70
  lastRenderCursor = -1;
84
71
  renderDirty = false;
85
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;
86
81
  // Lifecycle
87
82
  disposed = false;
88
83
  enabled = true;
89
84
  contextUsage = null;
90
- contextAutoCompactThreshold = 90;
91
- // Track current content row (starts at top, moves down)
92
- contentRow = 1;
93
- // Track if scroll region is currently active
94
- scrollRegionActive = false;
95
- thinkingModeLabel = null;
96
85
  editMode = 'display-edits';
97
86
  verificationEnabled = true;
98
87
  autoContinueEnabled = false;
99
88
  verificationHotkey = 'alt+v';
100
89
  autoContinueHotkey = 'alt+c';
101
- thinkingHotkey = '/thinking';
102
- modelLabel = null;
103
- providerLabel = null;
104
- // Streaming render throttle
105
- lastStreamingRender = 0;
106
- 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)
107
97
  streamingRenderTimer = null;
98
+ // Unified UI initialization flag
99
+ unifiedUIInitialized = false;
108
100
  constructor(writeStream = process.stdout, config = {}) {
109
101
  super();
110
102
  this.out = writeStream;
103
+ // Use schema defaults for configuration consistency
111
104
  this.config = {
112
- maxLines: config.maxLines ?? 1000,
113
- maxLength: config.maxLength ?? 10000,
105
+ maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
106
+ maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
114
107
  maxQueueSize: config.maxQueueSize ?? 100,
115
- promptChar: config.promptChar ?? '> ',
116
- continuationChar: config.continuationChar ?? '│ ',
108
+ promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
109
+ continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
117
110
  };
118
111
  }
119
112
  // ===========================================================================
@@ -192,36 +185,296 @@ export class TerminalInput extends EventEmitter {
192
185
  if (handled)
193
186
  return;
194
187
  }
188
+ // Handle '?' for help hint (if buffer is empty)
189
+ if (str === '?' && this.buffer.length === 0) {
190
+ this.emit('showHelp');
191
+ return;
192
+ }
195
193
  // Insert printable characters
196
194
  if (str && !key?.ctrl && !key?.meta) {
197
195
  this.insertText(str);
198
196
  }
199
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
+ // Lines needed: divider + input + divider + controls = 4 lines minimum
268
+ const linesNeeded = 4;
269
+ // FIRST: Clear any previously rendered chat box
270
+ this.clearInputArea();
271
+ // Hide cursor during render
272
+ this.write(ESC.HIDE);
273
+ // Calculate where to render - ALWAYS float right below content
274
+ let startRow;
275
+ if (this.contentEndRow > 0) {
276
+ startRow = this.contentEndRow + 1;
277
+ }
278
+ else {
279
+ startRow = 1;
280
+ }
281
+ // Clamp to terminal bounds
282
+ const maxStartRow = rows - linesNeeded + 1;
283
+ startRow = Math.min(startRow, maxStartRow);
284
+ startRow = Math.max(1, startRow);
285
+ // Track this position
286
+ this.inputAreaStartRow = startRow;
287
+ let currentRow = startRow;
288
+ // Helper to write a line (clears then writes)
289
+ const writeLine = (content) => {
290
+ this.write(ESC.TO(currentRow, 1));
291
+ this.write(ESC.CLEAR_LINE);
292
+ this.write(content);
293
+ currentRow++;
294
+ };
295
+ // Top divider
296
+ writeLine(`${DIM}${divider}${R}`);
297
+ // Input line with > prompt
298
+ const { lines, cursorCol } = this.wrapBuffer(cols - 3);
299
+ const displayLine = lines[0] ?? '';
300
+ const inputRow = currentRow;
301
+ writeLine(`${DIM}>${R} ${displayLine}`);
302
+ // Bottom divider
303
+ writeLine(`${DIM}${divider}${R}`);
304
+ // Mode controls line - Claude Code style
305
+ this.write(ESC.TO(currentRow, 1));
306
+ this.write(ESC.CLEAR_LINE);
307
+ this.write(this.buildClaudeStyleControls(cols));
308
+ // Track lines rendered
309
+ this.flowModeRenderedLines = currentRow - startRow + 1;
310
+ // Position cursor in input line
311
+ this.write(ESC.TO(inputRow, 3 + cursorCol)); // "> " = 2 chars + 1 for position
312
+ // Show cursor
313
+ this.write(ESC.SHOW);
314
+ // Update tracking
315
+ this.lastRenderContent = this.buffer;
316
+ this.lastRenderCursor = this.cursor;
317
+ }
318
+ /**
319
+ * Build Claude Code style controls line.
320
+ * Shows: edit mode indicator (shift+tab to cycle)
321
+ */
322
+ buildClaudeStyleControls(cols) {
323
+ const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
324
+ // Edit mode indicator
325
+ let editModeText;
326
+ if (this.editMode === 'display-edits') {
327
+ editModeText = `${GREEN}⏵⏵${R} accept edits on`;
328
+ }
329
+ else {
330
+ editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
331
+ }
332
+ // Build controls line
333
+ const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
334
+ // Add thinking mode if enabled
335
+ if (this.thinkingEnabled) {
336
+ parts.push(`${CYAN}💭${R}`);
337
+ }
338
+ // Add context usage if available
339
+ if (this.contextUsage !== null) {
340
+ const rem = Math.max(0, 100 - this.contextUsage);
341
+ if (rem < 10) {
342
+ parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
343
+ }
344
+ else if (rem < 25) {
345
+ parts.push(`${YELLOW}ctx ${rem}%${R}`);
346
+ }
347
+ }
348
+ return parts.join(` ${DIM}·${R} `);
349
+ }
200
350
  /**
201
351
  * Set the input mode
202
352
  *
203
- * Content flows naturally - no scroll region pinning.
353
+ * UNIFIED FLOATING: Chat box always floats right below content.
354
+ * No scroll regions - pure floating with clear and re-render.
204
355
  */
205
356
  setMode(mode) {
206
357
  const prevMode = this.mode;
207
358
  this.mode = mode;
208
359
  if (mode === 'streaming' && prevMode !== 'streaming') {
209
- this.resetStreamingRenderThrottle();
360
+ // Track streaming start time for elapsed display
361
+ this.streamingStartTime = Date.now();
362
+ // Ensure unified UI is initialized
363
+ if (!this.unifiedUIInitialized) {
364
+ this.initializeUnifiedUI();
365
+ }
210
366
  this.renderDirty = true;
211
- this.render();
367
+ this.scheduleRender();
212
368
  }
213
369
  else if (mode !== 'streaming' && prevMode === 'streaming') {
214
- // Streaming ended - render the input area
215
- this.resetStreamingRenderThrottle();
216
- this.forceRender();
370
+ // Stop streaming render timer (if any)
371
+ if (this.streamingRenderTimer) {
372
+ clearInterval(this.streamingRenderTimer);
373
+ this.streamingRenderTimer = null;
374
+ }
375
+ // Reset streaming time
376
+ this.streamingStartTime = null;
377
+ // Re-render floating input area below content
378
+ this.renderDirty = true;
379
+ this.scheduleRender();
380
+ }
381
+ }
382
+ /**
383
+ * Set the row where content ends (for idle mode positioning).
384
+ * Input area will render starting from this row + 1.
385
+ */
386
+ setContentEndRow(row) {
387
+ this.contentEndRow = Math.max(0, row);
388
+ this.renderDirty = true;
389
+ this.scheduleRender();
390
+ }
391
+ /**
392
+ * Set available slash commands for auto-complete suggestions.
393
+ */
394
+ setCommands(commands) {
395
+ this.commandSuggestions = commands;
396
+ this.updateSuggestions();
397
+ }
398
+ /**
399
+ * Update filtered suggestions based on current input.
400
+ */
401
+ updateSuggestions() {
402
+ const input = this.buffer.trim();
403
+ // Only show suggestions when input starts with "/"
404
+ if (!input.startsWith('/')) {
405
+ this.showSuggestions = false;
406
+ this.filteredSuggestions = [];
407
+ this.selectedSuggestionIndex = 0;
408
+ return;
217
409
  }
410
+ const query = input.toLowerCase();
411
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
412
+ cmd.command.toLowerCase().includes(query.slice(1)));
413
+ // Show suggestions if we have matches
414
+ this.showSuggestions = this.filteredSuggestions.length > 0;
415
+ // Keep selection in bounds
416
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
417
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
418
+ }
419
+ }
420
+ /**
421
+ * Select next suggestion (arrow down / tab).
422
+ */
423
+ selectNextSuggestion() {
424
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
425
+ return;
426
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
427
+ this.renderDirty = true;
428
+ this.scheduleRender();
429
+ }
430
+ /**
431
+ * Select previous suggestion (arrow up / shift+tab).
432
+ */
433
+ selectPrevSuggestion() {
434
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
435
+ return;
436
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
437
+ ? this.filteredSuggestions.length - 1
438
+ : this.selectedSuggestionIndex - 1;
439
+ this.renderDirty = true;
440
+ this.scheduleRender();
441
+ }
442
+ /**
443
+ * Accept current suggestion and insert into buffer.
444
+ */
445
+ acceptSuggestion() {
446
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
447
+ return false;
448
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
449
+ if (!selected)
450
+ return false;
451
+ // Replace buffer with selected command
452
+ this.buffer = selected.command + ' ';
453
+ this.cursor = this.buffer.length;
454
+ this.showSuggestions = false;
455
+ this.renderDirty = true;
456
+ this.scheduleRender();
457
+ return true;
218
458
  }
219
459
  /**
220
- * Legacy method - no longer used (content flows naturally).
221
- * @deprecated Use setContentRow instead
460
+ * Check if suggestions are visible.
222
461
  */
223
- setPinnedHeaderLines(_count) {
224
- // No-op: scroll region pinning removed
462
+ areSuggestionsVisible() {
463
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
464
+ }
465
+ /**
466
+ * Toggle thinking/reasoning mode
467
+ */
468
+ toggleThinking() {
469
+ this.thinkingEnabled = !this.thinkingEnabled;
470
+ this.emit('thinkingToggle', this.thinkingEnabled);
471
+ this.scheduleRender();
472
+ }
473
+ /**
474
+ * Get thinking enabled state
475
+ */
476
+ isThinkingEnabled() {
477
+ return this.thinkingEnabled;
225
478
  }
226
479
  /**
227
480
  * Get current mode
@@ -254,14 +507,17 @@ export class TerminalInput extends EventEmitter {
254
507
  }
255
508
  /**
256
509
  * Clear the buffer
510
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
257
511
  */
258
- clear() {
512
+ clear(skipRender = false) {
259
513
  this.buffer = '';
260
514
  this.cursor = 0;
261
515
  this.historyIndex = -1;
262
516
  this.tempInput = '';
263
517
  this.pastePlaceholders = [];
264
- this.scheduleRender();
518
+ if (!skipRender) {
519
+ this.scheduleRender();
520
+ }
265
521
  }
266
522
  /**
267
523
  * Get queued inputs
@@ -332,37 +588,6 @@ export class TerminalInput extends EventEmitter {
332
588
  this.streamingLabel = next;
333
589
  this.scheduleRender();
334
590
  }
335
- /**
336
- * Surface meta status just above the divider (e.g., elapsed time or token usage).
337
- */
338
- setMetaStatus(meta) {
339
- const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
340
- ? Math.floor(meta.elapsedSeconds)
341
- : null;
342
- const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
343
- ? Math.floor(meta.tokensUsed)
344
- : null;
345
- const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
346
- ? Math.floor(meta.tokenLimit)
347
- : null;
348
- const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
349
- ? Math.floor(meta.thinkingMs)
350
- : null;
351
- const nextThinkingHasContent = !!meta.thinkingHasContent;
352
- if (this.metaElapsedSeconds === nextElapsed &&
353
- this.metaTokensUsed === nextTokens &&
354
- this.metaTokenLimit === nextLimit &&
355
- this.metaThinkingMs === nextThinking &&
356
- this.metaThinkingHasContent === nextThinkingHasContent) {
357
- return;
358
- }
359
- this.metaElapsedSeconds = nextElapsed;
360
- this.metaTokensUsed = nextTokens;
361
- this.metaTokenLimit = nextLimit;
362
- this.metaThinkingMs = nextThinking;
363
- this.metaThinkingHasContent = nextThinkingHasContent;
364
- this.scheduleRender();
365
- }
366
591
  /**
367
592
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
368
593
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -372,22 +597,26 @@ export class TerminalInput extends EventEmitter {
372
597
  const nextAutoContinue = !!options.autoContinueEnabled;
373
598
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
374
599
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
375
- const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
376
- const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
377
600
  if (this.verificationEnabled === nextVerification &&
378
601
  this.autoContinueEnabled === nextAutoContinue &&
379
602
  this.verificationHotkey === nextVerifyHotkey &&
380
- this.autoContinueHotkey === nextAutoHotkey &&
381
- this.thinkingHotkey === nextThinkingHotkey &&
382
- this.thinkingModeLabel === nextThinkingLabel) {
603
+ this.autoContinueHotkey === nextAutoHotkey) {
383
604
  return;
384
605
  }
385
606
  this.verificationEnabled = nextVerification;
386
607
  this.autoContinueEnabled = nextAutoContinue;
387
608
  this.verificationHotkey = nextVerifyHotkey;
388
609
  this.autoContinueHotkey = nextAutoHotkey;
389
- this.thinkingHotkey = nextThinkingHotkey;
390
- this.thinkingModeLabel = nextThinkingLabel;
610
+ this.scheduleRender();
611
+ }
612
+ /**
613
+ * Set the model info string (e.g., "OpenAI · gpt-4")
614
+ * This is displayed persistently above the input area.
615
+ */
616
+ setModelInfo(info) {
617
+ if (this.modelInfo === info)
618
+ return;
619
+ this.modelInfo = info;
391
620
  this.scheduleRender();
392
621
  }
393
622
  /**
@@ -400,159 +629,33 @@ export class TerminalInput extends EventEmitter {
400
629
  this.scheduleRender();
401
630
  }
402
631
  /**
403
- * Surface model/provider context in the controls bar.
404
- */
405
- setModelContext(options) {
406
- const nextModel = options.model?.trim() || null;
407
- const nextProvider = options.provider?.trim() || null;
408
- if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
409
- return;
410
- }
411
- this.modelLabel = nextModel;
412
- this.providerLabel = nextProvider;
413
- this.scheduleRender();
414
- }
415
- /**
416
- * Render the floating input area at contentRow.
417
- *
418
- * The chat box "floats" - it renders right below the last streamed content.
419
- * As content is added, contentRow advances, and the chat box moves down.
420
- * No scroll regions - pure floating behavior.
632
+ * Render the input area.
633
+ * During streaming: renders at terminal bottom (with scroll region)
634
+ * After streaming: renders floating below content
421
635
  */
422
636
  render() {
423
637
  if (!this.canRender())
424
638
  return;
425
639
  if (this.isRendering)
426
640
  return;
427
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
428
- // During streaming, throttle re-renders
429
- if (streamingActive && this.lastStreamingRender > 0) {
430
- const elapsed = Date.now() - this.lastStreamingRender;
431
- const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
432
- if (waitMs > 0) {
433
- this.renderDirty = true;
434
- this.scheduleStreamingRender(waitMs);
435
- return;
436
- }
437
- }
438
641
  const shouldSkip = !this.renderDirty &&
439
642
  this.buffer === this.lastRenderContent &&
440
643
  this.cursor === this.lastRenderCursor;
441
644
  this.renderDirty = false;
645
+ // Skip if nothing changed (unless explicitly forced)
442
646
  if (shouldSkip) {
443
647
  return;
444
648
  }
649
+ // If write lock is held, defer render
445
650
  if (writeLock.isLocked()) {
446
651
  writeLock.safeWrite(() => this.render());
447
652
  return;
448
653
  }
449
- this.renderFloatingInputArea();
450
- }
451
- /**
452
- * Core floating input area renderer.
453
- * Chat box always floats at contentRow (below streamed content).
454
- * This creates "persistent bottom floating" behavior.
455
- */
456
- renderFloatingInputArea() {
457
- const { rows, cols } = this.getSize();
458
- const maxWidth = Math.max(8, cols - 4);
459
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
460
- // Wrap buffer into display lines
461
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
462
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
463
- const displayLines = Math.min(lines.length, maxVisible);
464
- const metaLines = this.buildMetaLines(cols - 2);
465
- // Calculate display window (keep cursor visible)
466
- let startLine = 0;
467
- if (lines.length > displayLines) {
468
- startLine = Math.max(0, cursorLine - displayLines + 1);
469
- startLine = Math.min(startLine, lines.length - displayLines);
470
- }
471
- const visibleLines = lines.slice(startLine, startLine + displayLines);
472
- const adjustedCursorLine = cursorLine - startLine;
473
- // Chat box height (must match getChatBoxHeight calculation)
474
- const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
475
- // Unified floating: chat box always at contentRow + 1
476
- // When scroll region is active, contentRow is capped at maxContentRow
477
- // so chat box ends up at the bottom but still "floats" below content
478
- const chatBoxStartRow = this.contentRow + 1;
479
- writeLock.lock('terminalInput.renderFloating');
480
654
  this.isRendering = true;
655
+ writeLock.lock('terminalInput.render');
481
656
  try {
482
- // Hide cursor during render
483
- this.write(ESC.HIDE);
484
- this.write(ESC.RESET);
485
- // Clear the chat box area
486
- for (let i = 0; i < chatBoxHeight; i++) {
487
- const row = chatBoxStartRow + i;
488
- if (row <= rows) {
489
- this.write(ESC.TO(row, 1));
490
- this.write(ESC.CLEAR_LINE);
491
- }
492
- }
493
- let currentRow = chatBoxStartRow;
494
- // Meta/status header
495
- for (const metaLine of metaLines) {
496
- this.write(ESC.TO(currentRow, 1));
497
- this.write(metaLine);
498
- currentRow += 1;
499
- }
500
- // Separator line
501
- this.write(ESC.TO(currentRow, 1));
502
- this.write(renderDivider(cols - 2));
503
- currentRow += 1;
504
- // Render input lines
505
- let finalRow = currentRow;
506
- let finalCol = 3;
507
- for (let i = 0; i < visibleLines.length; i++) {
508
- const rowNum = currentRow + i;
509
- this.write(ESC.TO(rowNum, 1));
510
- const line = visibleLines[i] ?? '';
511
- const isFirstLine = (startLine + i) === 0;
512
- const isCursorLine = i === adjustedCursorLine;
513
- this.write(ESC.BG_DARK);
514
- this.write(ESC.DIM);
515
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
516
- this.write(ESC.RESET);
517
- this.write(ESC.BG_DARK);
518
- if (isCursorLine) {
519
- const col = Math.min(cursorCol, line.length);
520
- const before = line.slice(0, col);
521
- const at = col < line.length ? line[col] : ' ';
522
- const after = col < line.length ? line.slice(col + 1) : '';
523
- this.write(before);
524
- this.write(ESC.REVERSE + ESC.BOLD);
525
- this.write(at);
526
- this.write(ESC.RESET + ESC.BG_DARK);
527
- this.write(after);
528
- finalRow = rowNum;
529
- finalCol = this.config.promptChar.length + col + 1;
530
- }
531
- else {
532
- this.write(line);
533
- }
534
- // Pad to edge
535
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
536
- const padding = Math.max(0, cols - lineLen - 1);
537
- if (padding > 0)
538
- this.write(' '.repeat(padding));
539
- this.write(ESC.RESET);
540
- }
541
- // Mode controls line
542
- const controlRow = currentRow + visibleLines.length;
543
- this.write(ESC.TO(controlRow, 1));
544
- this.write(this.buildModeControls(cols));
545
- // Position cursor in input box
546
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
547
- this.write(ESC.SHOW);
548
- // Update state
549
- this.lastRenderContent = this.buffer;
550
- this.lastRenderCursor = this.cursor;
551
- this.lastStreamingRender = streamingActive ? Date.now() : 0;
552
- if (this.streamingRenderTimer) {
553
- clearTimeout(this.streamingRenderTimer);
554
- this.streamingRenderTimer = null;
555
- }
657
+ // Always render floating right after content (no wasted space)
658
+ this.renderFloatingInputArea();
556
659
  }
557
660
  finally {
558
661
  writeLock.unlock();
@@ -560,217 +663,99 @@ export class TerminalInput extends EventEmitter {
560
663
  }
561
664
  }
562
665
  /**
563
- * Build one or more compact meta lines above the divider (thinking, status, usage).
564
- * During streaming, shows model line pinned above streaming info.
666
+ * Build status bar showing streaming/ready status and key info.
667
+ * This is the TOP line above the input area - minimal Claude Code style.
565
668
  */
566
- buildMetaLines(width) {
567
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
568
- const lines = [];
569
- // Model line should ALWAYS be shown (pinned above streaming content)
570
- if (this.modelLabel) {
571
- const modelText = this.providerLabel
572
- ? `model ${this.modelLabel} @ ${this.providerLabel}`
573
- : `model ${this.modelLabel}`;
574
- lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
575
- }
576
- // During streaming, add a compact status line with essential info
577
- if (streamingActive) {
578
- const parts = [];
579
- // Essential streaming info
580
- if (this.metaThinkingMs !== null) {
581
- parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
582
- }
583
- if (this.metaElapsedSeconds !== null) {
584
- parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
585
- }
586
- parts.push({ text: 'esc to stop', tone: 'warn' });
587
- if (parts.length) {
588
- lines.push(renderStatusLine(parts, width));
669
+ buildStatusBar(cols) {
670
+ const maxWidth = cols - 2;
671
+ const parts = [];
672
+ // Streaming status with elapsed time (left side)
673
+ if (this.mode === 'streaming') {
674
+ let statusText = '● Streaming';
675
+ if (this.streamingStartTime) {
676
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
677
+ const mins = Math.floor(elapsed / 60);
678
+ const secs = elapsed % 60;
679
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
589
680
  }
590
- return lines;
591
- }
592
- // Non-streaming: show full status info (model line already added above)
593
- if (this.metaThinkingMs !== null) {
594
- const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
595
- lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
596
- }
597
- const statusParts = [];
598
- const statusLabel = this.statusMessage ?? this.streamingLabel;
599
- if (statusLabel) {
600
- statusParts.push({ text: statusLabel, tone: 'info' });
681
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
601
682
  }
602
- if (this.metaElapsedSeconds !== null) {
603
- statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
683
+ // Queue indicator during streaming
684
+ if (this.mode === 'streaming' && this.queue.length > 0) {
685
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
604
686
  }
605
- const tokensRemaining = this.computeTokensRemaining();
606
- if (tokensRemaining !== null) {
607
- statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
608
- }
609
- if (statusParts.length) {
610
- lines.push(renderStatusLine(statusParts, width));
687
+ // Paste indicator
688
+ if (this.pastePlaceholders.length > 0) {
689
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
690
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
611
691
  }
612
- const usageParts = [];
613
- if (this.metaTokensUsed !== null) {
614
- const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
615
- const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
616
- usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
692
+ // Override/warning status
693
+ if (this.overrideStatusMessage) {
694
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
617
695
  }
618
- if (this.contextUsage !== null) {
619
- const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
620
- const left = Math.max(0, 100 - this.contextUsage);
621
- usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
696
+ // If idle with empty buffer, show quick shortcuts
697
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
698
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
622
699
  }
623
- if (this.queue.length > 0) {
624
- usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
700
+ // Multi-line indicator
701
+ if (this.buffer.includes('\n')) {
702
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
625
703
  }
626
- if (usageParts.length) {
627
- lines.push(renderStatusLine(usageParts, width));
704
+ if (parts.length === 0) {
705
+ return ''; // Empty status bar when idle
628
706
  }
629
- return lines;
707
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
708
+ return joined.slice(0, maxWidth);
630
709
  }
631
710
  /**
632
- * Build Claude Code style mode controls line.
633
- * Combines streaming label + override status + main status for simultaneous display.
711
+ * Build mode controls line showing toggles and context info.
712
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
713
+ *
714
+ * Layout: [toggles on left] ... [context info on right]
634
715
  */
635
716
  buildModeControls(cols) {
636
- const width = Math.max(8, cols - 2);
637
- const leftParts = [];
638
- const rightParts = [];
639
- if (this.streamingLabel) {
640
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
641
- }
642
- if (this.overrideStatusMessage) {
643
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
644
- }
645
- if (this.statusMessage) {
646
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
647
- }
648
- const editHotkey = this.formatHotkey('shift+tab');
649
- const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
650
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
651
- leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
652
- const verifyHotkey = this.formatHotkey(this.verificationHotkey);
653
- const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
654
- leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
655
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
656
- const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
657
- leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
658
- if (this.queue.length > 0 && this.mode !== 'streaming') {
659
- leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
660
- }
661
- if (this.buffer.includes('\n')) {
662
- const lineCount = this.buffer.split('\n').length;
663
- leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
664
- }
665
- if (this.pastePlaceholders.length > 0) {
666
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
667
- leftParts.push({
668
- text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
669
- tone: 'info',
670
- });
671
- }
672
- const contextRemaining = this.computeContextRemaining();
673
- if (this.thinkingModeLabel) {
674
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
675
- rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
676
- }
677
- // Show model in controls only when NOT streaming (during streaming it's in meta lines)
678
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
679
- if (this.modelLabel && !streamingActive) {
680
- const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
681
- rightParts.push({ text: modelText, tone: 'muted' });
682
- }
683
- if (contextRemaining !== null) {
684
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
685
- const label = contextRemaining === 0 && this.contextUsage !== null
686
- ? 'Context auto-compact imminent'
687
- : `Context left until auto-compact: ${contextRemaining}%`;
688
- rightParts.push({ text: label, tone });
689
- }
690
- if (!rightParts.length || width < 60) {
691
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
692
- return renderStatusLine(merged, width);
693
- }
694
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
695
- const rightWidth = Math.max(14, width - leftWidth - 1);
696
- const leftText = renderStatusLine(leftParts, leftWidth);
697
- const rightText = renderStatusLine(rightParts, rightWidth);
698
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
699
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
700
- }
701
- formatHotkey(hotkey) {
702
- const normalized = hotkey.trim().toLowerCase();
703
- if (!normalized)
704
- return hotkey;
705
- const parts = normalized.split('+').filter(Boolean);
706
- const map = {
707
- shift: '⇧',
708
- sh: '⇧',
709
- alt: '⌥',
710
- option: '⌥',
711
- opt: '⌥',
712
- ctrl: '⌃',
713
- control: '⌃',
714
- cmd: '⌘',
715
- meta: '⌘',
716
- };
717
- const formatted = parts
718
- .map((part) => {
719
- const symbol = map[part];
720
- if (symbol)
721
- return symbol;
722
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
723
- })
724
- .join('');
725
- return formatted || hotkey;
726
- }
727
- computeContextRemaining() {
728
- if (this.contextUsage === null) {
729
- return null;
717
+ const maxWidth = cols - 2;
718
+ // Use schema-defined colors for consistency
719
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
720
+ // Mode toggles with colors (following ModeControlsSchema)
721
+ const toggles = [];
722
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
723
+ if (this.editMode === 'display-edits') {
724
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
730
725
  }
731
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
732
- }
733
- computeTokensRemaining() {
734
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
735
- return null;
736
- }
737
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
738
- return this.formatTokenCount(remaining);
739
- }
740
- formatElapsedLabel(seconds) {
741
- if (seconds < 60) {
742
- return `${seconds}s`;
743
- }
744
- const mins = Math.floor(seconds / 60);
745
- const secs = seconds % 60;
746
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
747
- }
748
- formatTokenCount(value) {
749
- if (!Number.isFinite(value)) {
750
- return `${value}`;
751
- }
752
- if (value >= 1_000_000) {
753
- return `${(value / 1_000_000).toFixed(1)}M`;
754
- }
755
- if (value >= 1_000) {
756
- return `${(value / 1_000).toFixed(1)}k`;
757
- }
758
- return `${Math.round(value)}`;
759
- }
760
- visibleLength(value) {
761
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
762
- return value.replace(ansiPattern, '').length;
763
- }
764
- /**
765
- * Debug-only snapshot used by tests to assert rendered strings without
766
- * needing a TTY. Not used by production code.
767
- */
768
- getDebugUiSnapshot(width) {
769
- const cols = Math.max(8, width ?? this.getSize().cols);
770
- return {
771
- meta: this.buildMetaLines(cols - 2),
772
- controls: this.buildModeControls(cols),
773
- };
726
+ else {
727
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
728
+ }
729
+ // Thinking mode (cyan when on) - per schema.thinkingMode
730
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
731
+ // Verification (green when on) - per schema.verificationMode
732
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
733
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
734
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
735
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
736
+ // Context usage with color - per schema.contextUsage thresholds
737
+ let rightPart = '';
738
+ if (this.contextUsage !== null) {
739
+ const rem = Math.max(0, 100 - this.contextUsage);
740
+ // Thresholds: critical < 10%, warning < 25%
741
+ if (rem < 10)
742
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
743
+ else if (rem < 25)
744
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
745
+ else
746
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
747
+ }
748
+ // Calculate visible lengths (strip ANSI)
749
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
750
+ const leftLen = strip(leftPart).length;
751
+ const rightLen = strip(rightPart).length;
752
+ if (leftLen + rightLen < maxWidth - 4) {
753
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
754
+ }
755
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
756
+ return `${leftPart} ${rightPart}`;
757
+ }
758
+ return leftPart;
774
759
  }
775
760
  /**
776
761
  * Force a re-render
@@ -793,123 +778,32 @@ export class TerminalInput extends EventEmitter {
793
778
  handleResize() {
794
779
  this.lastRenderContent = '';
795
780
  this.lastRenderCursor = -1;
796
- this.resetStreamingRenderThrottle();
797
781
  this.scheduleRender();
798
782
  }
799
783
  /**
800
- * Stream content with floating chat box.
801
- *
802
- * Clean approach - no scroll regions, just cursor positioning:
803
- * 1. Save cursor state
804
- * 2. Clear chat box area (it will be re-rendered)
805
- * 3. Position at contentRow
806
- * 4. Write content
807
- * 5. Advance contentRow
808
- * 6. Re-render chat box
809
- */
810
- streamContent(content) {
811
- if (!content)
812
- return;
813
- writeLock.lock('streamContent');
814
- try {
815
- // Save cursor and hide it
816
- this.write(ESC.SAVE);
817
- this.write(ESC.HIDE);
818
- // Clear the chat box area first (it will be re-rendered after)
819
- const { rows, cols } = this.getSize();
820
- const chatBoxHeight = 6; // Approximate
821
- const chatBoxStart = this.contentRow + 1;
822
- for (let i = 0; i < chatBoxHeight && chatBoxStart + i <= rows; i++) {
823
- this.write(ESC.TO(chatBoxStart + i, 1));
824
- this.write(ESC.CLEAR_LINE);
825
- }
826
- // Position at contentRow and write content
827
- this.write(ESC.TO(this.contentRow, 1));
828
- this.write(content);
829
- // Count newlines and advance contentRow
830
- const newlines = (content.match(/\n/g) || []).length;
831
- this.contentRow += newlines;
832
- // Cap contentRow to leave room for chat box
833
- const maxContentRow = Math.max(1, rows - chatBoxHeight);
834
- if (this.contentRow > maxContentRow) {
835
- this.contentRow = maxContentRow;
836
- }
837
- // Restore cursor
838
- this.write(ESC.RESTORE);
839
- this.write(ESC.SHOW);
840
- }
841
- finally {
842
- writeLock.unlock();
843
- }
844
- // Re-render chat box at new position
845
- this.forceRender();
846
- }
847
- /**
848
- * Enable scroll region (no-op in floating mode).
784
+ * Register with display's output interceptor.
785
+ * Before content write: clear chat box and position cursor at content area.
786
+ * After content write: re-render chat box below content.
849
787
  */
850
- enableScrollRegion() {
851
- // No-op: using pure floating approach
852
- }
853
- /**
854
- * Disable scroll region (no-op in floating mode).
855
- */
856
- disableScrollRegion() {
857
- // No-op: using pure floating approach
858
- }
859
- /**
860
- * Calculate chat box height.
861
- */
862
- getChatBoxHeight() {
863
- return 6; // Fixed: meta + divider + input + controls + buffer
864
- }
865
- /**
866
- * @deprecated Use streamContent() instead
867
- * Register with display's output interceptor - kept for backwards compatibility
868
- */
869
- registerOutputInterceptor(_display) {
870
- // No-op: Use streamContent() for cleaner floating chat box behavior
871
- }
872
- /**
873
- * @deprecated Use streamContent() instead
874
- * Write content above the floating chat box.
875
- */
876
- writeToScrollRegion(content) {
877
- this.streamContent(content);
878
- }
879
- /**
880
- * Clear the entire terminal screen and reset content position.
881
- * This removes all content including the launching command.
882
- */
883
- clearScreen() {
884
- writeLock.lock('clearScreen');
885
- try {
886
- this.write(ESC.HOME);
887
- this.write(ESC.CLEAR_SCREEN);
888
- this.contentRow = 1;
889
- }
890
- finally {
891
- writeLock.unlock();
892
- }
893
- }
894
- /**
895
- * Reset content position to row 1.
896
- * Does NOT clear the terminal - content starts from current position.
897
- */
898
- resetContentPosition() {
899
- this.contentRow = 1;
900
- }
901
- /**
902
- * Set the content row explicitly (used after banner is written).
903
- * This tells the input where content should start flowing from.
904
- */
905
- setContentRow(row) {
906
- this.contentRow = Math.max(1, row);
907
- }
908
- /**
909
- * Get the current content row position.
910
- */
911
- getContentRow() {
912
- return this.contentRow;
788
+ registerOutputInterceptor(display) {
789
+ if (this.outputInterceptorCleanup) {
790
+ this.outputInterceptorCleanup();
791
+ }
792
+ this.outputInterceptorCleanup = display.registerOutputInterceptor({
793
+ beforeWrite: () => {
794
+ // Clear chat box before content write to prevent overlap
795
+ this.clearInputArea();
796
+ // Position cursor at content area (after last content, before where chat box was)
797
+ if (this.contentEndRow > 0) {
798
+ this.write(ESC.TO(this.contentEndRow, 1));
799
+ }
800
+ },
801
+ afterWrite: () => {
802
+ // Re-render chat box after content writes
803
+ this.renderDirty = true;
804
+ this.scheduleRender();
805
+ },
806
+ });
913
807
  }
914
808
  /**
915
809
  * Dispose and clean up
@@ -917,10 +811,18 @@ export class TerminalInput extends EventEmitter {
917
811
  dispose() {
918
812
  if (this.disposed)
919
813
  return;
814
+ // Clean up streaming render timer
815
+ if (this.streamingRenderTimer) {
816
+ clearInterval(this.streamingRenderTimer);
817
+ this.streamingRenderTimer = null;
818
+ }
819
+ // Clean up output interceptor
820
+ if (this.outputInterceptorCleanup) {
821
+ this.outputInterceptorCleanup();
822
+ this.outputInterceptorCleanup = undefined;
823
+ }
920
824
  this.disposed = true;
921
825
  this.enabled = false;
922
- this.disableScrollRegion();
923
- this.resetStreamingRenderThrottle();
924
826
  this.disableBracketedPaste();
925
827
  this.buffer = '';
926
828
  this.queue = [];
@@ -1025,7 +927,22 @@ export class TerminalInput extends EventEmitter {
1025
927
  this.toggleEditMode();
1026
928
  return true;
1027
929
  }
1028
- this.insertText(' ');
930
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
931
+ if (this.findPlaceholderAt(this.cursor)) {
932
+ this.togglePasteExpansion();
933
+ }
934
+ else {
935
+ this.toggleThinking();
936
+ }
937
+ return true;
938
+ case 'escape':
939
+ // Esc: interrupt if streaming, otherwise clear buffer
940
+ if (this.mode === 'streaming') {
941
+ this.emit('interrupt');
942
+ }
943
+ else if (this.buffer.length > 0) {
944
+ this.clear();
945
+ }
1029
946
  return true;
1030
947
  }
1031
948
  return false;
@@ -1043,6 +960,7 @@ export class TerminalInput extends EventEmitter {
1043
960
  this.insertPlainText(chunk, insertPos);
1044
961
  this.cursor = insertPos + chunk.length;
1045
962
  this.emit('change', this.buffer);
963
+ this.updateSuggestions();
1046
964
  this.scheduleRender();
1047
965
  }
1048
966
  insertNewline() {
@@ -1067,6 +985,7 @@ export class TerminalInput extends EventEmitter {
1067
985
  this.cursor = Math.max(0, this.cursor - 1);
1068
986
  }
1069
987
  this.emit('change', this.buffer);
988
+ this.updateSuggestions();
1070
989
  this.scheduleRender();
1071
990
  }
1072
991
  deleteForward() {
@@ -1294,12 +1213,13 @@ export class TerminalInput extends EventEmitter {
1294
1213
  timestamp: Date.now(),
1295
1214
  });
1296
1215
  this.emit('queue', text);
1297
- this.clear(); // Clear immediately for queued input
1216
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1298
1217
  }
1299
1218
  else {
1300
- // In idle mode, clear the input first, then emit submit.
1301
- // The prompt will be logged as a visible message by the caller.
1302
- this.clear();
1219
+ // In idle mode, clear the input WITHOUT rendering.
1220
+ // The caller will display the user message and start streaming.
1221
+ // We'll render the input area again after streaming ends.
1222
+ this.clear(true); // Skip render - streaming will handle display
1303
1223
  this.emit('submit', text);
1304
1224
  }
1305
1225
  }
@@ -1316,9 +1236,7 @@ export class TerminalInput extends EventEmitter {
1316
1236
  if (available <= 0)
1317
1237
  return;
1318
1238
  const chunk = clean.slice(0, available);
1319
- const isMultiline = isMultilinePaste(chunk);
1320
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1321
- if (isMultiline && !isShortMultiline) {
1239
+ if (isMultilinePaste(chunk)) {
1322
1240
  this.insertPastePlaceholder(chunk);
1323
1241
  }
1324
1242
  else {
@@ -1454,19 +1372,17 @@ export class TerminalInput extends EventEmitter {
1454
1372
  this.shiftPlaceholders(position, text.length);
1455
1373
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1456
1374
  }
1457
- shouldInlineMultiline(content) {
1458
- const lines = content.split('\n').length;
1459
- const maxInlineLines = 4;
1460
- const maxInlineChars = 240;
1461
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1462
- }
1463
1375
  findPlaceholderAt(position) {
1464
1376
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1465
1377
  }
1466
- buildPlaceholder(lineCount) {
1378
+ buildPlaceholder(summary) {
1467
1379
  const id = ++this.pasteCounter;
1468
- const plural = lineCount === 1 ? '' : 's';
1469
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1380
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1381
+ // Show first line preview (truncated)
1382
+ const preview = summary.preview.length > 30
1383
+ ? `${summary.preview.slice(0, 30)}...`
1384
+ : summary.preview;
1385
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1470
1386
  return { id, placeholder };
1471
1387
  }
1472
1388
  insertPastePlaceholder(content) {
@@ -1474,21 +1390,67 @@ export class TerminalInput extends EventEmitter {
1474
1390
  if (available <= 0)
1475
1391
  return;
1476
1392
  const cleanContent = content.slice(0, available);
1477
- const lineCount = cleanContent.split('\n').length;
1478
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1393
+ const summary = generatePasteSummary(cleanContent);
1394
+ // For short pastes (< 5 lines), show full content instead of placeholder
1395
+ if (summary.lineCount < 5) {
1396
+ const placeholder = this.findPlaceholderAt(this.cursor);
1397
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1398
+ this.insertPlainText(cleanContent, insertPos);
1399
+ this.cursor = insertPos + cleanContent.length;
1400
+ return;
1401
+ }
1402
+ const { id, placeholder } = this.buildPlaceholder(summary);
1479
1403
  const insertPos = this.cursor;
1480
1404
  this.shiftPlaceholders(insertPos, placeholder.length);
1481
1405
  this.pastePlaceholders.push({
1482
1406
  id,
1483
1407
  content: cleanContent,
1484
- lineCount,
1408
+ lineCount: summary.lineCount,
1485
1409
  placeholder,
1486
1410
  start: insertPos,
1487
1411
  end: insertPos + placeholder.length,
1412
+ summary,
1413
+ expanded: false,
1488
1414
  });
1489
1415
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1490
1416
  this.cursor = insertPos + placeholder.length;
1491
1417
  }
1418
+ /**
1419
+ * Toggle expansion of a paste placeholder at the current cursor position.
1420
+ * When expanded, shows first 3 and last 2 lines of the content.
1421
+ */
1422
+ togglePasteExpansion() {
1423
+ const placeholder = this.findPlaceholderAt(this.cursor);
1424
+ if (!placeholder)
1425
+ return false;
1426
+ placeholder.expanded = !placeholder.expanded;
1427
+ // Update the placeholder text in buffer
1428
+ const newPlaceholder = placeholder.expanded
1429
+ ? this.buildExpandedPlaceholder(placeholder)
1430
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1431
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1432
+ // Update buffer
1433
+ this.buffer =
1434
+ this.buffer.slice(0, placeholder.start) +
1435
+ newPlaceholder +
1436
+ this.buffer.slice(placeholder.end);
1437
+ // Update placeholder tracking
1438
+ placeholder.placeholder = newPlaceholder;
1439
+ placeholder.end = placeholder.start + newPlaceholder.length;
1440
+ // Shift other placeholders
1441
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1442
+ this.scheduleRender();
1443
+ return true;
1444
+ }
1445
+ buildExpandedPlaceholder(ph) {
1446
+ const lines = ph.content.split('\n');
1447
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1448
+ const lastLines = lines.length > 5
1449
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1450
+ : '';
1451
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1452
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1453
+ }
1492
1454
  deletePlaceholder(placeholder) {
1493
1455
  const length = placeholder.end - placeholder.start;
1494
1456
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1496,11 +1458,7 @@ export class TerminalInput extends EventEmitter {
1496
1458
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1497
1459
  this.cursor = placeholder.start;
1498
1460
  }
1499
- updateContextUsage(value, autoCompactThreshold) {
1500
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1501
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1502
- this.contextAutoCompactThreshold = boundedThreshold;
1503
- }
1461
+ updateContextUsage(value) {
1504
1462
  if (value === null || !Number.isFinite(value)) {
1505
1463
  this.contextUsage = null;
1506
1464
  }
@@ -1527,22 +1485,6 @@ export class TerminalInput extends EventEmitter {
1527
1485
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1528
1486
  this.setEditMode(next);
1529
1487
  }
1530
- scheduleStreamingRender(delayMs) {
1531
- if (this.streamingRenderTimer)
1532
- return;
1533
- const wait = Math.max(16, delayMs);
1534
- this.streamingRenderTimer = setTimeout(() => {
1535
- this.streamingRenderTimer = null;
1536
- this.render();
1537
- }, wait);
1538
- }
1539
- resetStreamingRenderThrottle() {
1540
- if (this.streamingRenderTimer) {
1541
- clearTimeout(this.streamingRenderTimer);
1542
- this.streamingRenderTimer = null;
1543
- }
1544
- this.lastStreamingRender = 0;
1545
- }
1546
1488
  scheduleRender() {
1547
1489
  if (!this.canRender())
1548
1490
  return;