erosolar-cli 1.7.286 → 1.7.289

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 +0 -1
  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 -22
  235. package/dist/shell/interactiveShell.d.ts.map +1 -1
  236. package/dist/shell/interactiveShell.js +159 -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 +137 -88
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +550 -557
  256. package/dist/shell/terminalInput.js.map +1 -1
  257. package/dist/shell/terminalInputAdapter.d.ts +35 -28
  258. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  259. package/dist/shell/terminalInputAdapter.js +50 -26
  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 +1 -1
  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,18 +3,15 @@
3
3
  *
4
4
  * Design principles:
5
5
  * - Single source of truth for input state
6
- * - One bottom-pinned chat box for the entire session (no inline anchors)
7
6
  * - Native bracketed paste support (no heuristics)
8
7
  * - Clean cursor model with render-time wrapping
9
8
  * - State machine for different input modes
10
9
  * - No readline dependency for display
11
10
  */
12
11
  import { EventEmitter } from 'node:events';
13
- import { isMultilinePaste } from '../core/multilinePasteHandler.js';
12
+ import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
14
13
  import { writeLock } from '../ui/writeLock.js';
15
- import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
16
- import { isStreamingMode } from '../ui/globalWriteLock.js';
17
- import { formatThinking } from '../ui/toolDisplay.js';
14
+ import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
18
15
  // ANSI escape codes
19
16
  const ESC = {
20
17
  // Cursor control
@@ -24,6 +21,9 @@ const ESC = {
24
21
  SHOW: '\x1b[?25h',
25
22
  TO: (row, col) => `\x1b[${row};${col}H`,
26
23
  TO_COL: (col) => `\x1b[${col}G`,
24
+ // Screen control
25
+ CLEAR_SCREEN: '\x1b[2J',
26
+ HOME: '\x1b[H',
27
27
  // Line control
28
28
  CLEAR_LINE: '\x1b[2K',
29
29
  CLEAR_TO_END: '\x1b[0J',
@@ -69,49 +69,55 @@ export class TerminalInput extends EventEmitter {
69
69
  statusMessage = null;
70
70
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
71
71
  streamingLabel = null; // Streaming progress indicator
72
- metaElapsedSeconds = null; // Optional elapsed time for header line
73
- metaTokensUsed = null; // Optional token usage
74
- metaTokenLimit = null; // Optional token window
75
- metaThinkingMs = null; // Optional thinking duration
76
- metaThinkingHasContent = false; // Whether collapsed thinking content exists
77
72
  reservedLines = 2;
78
- scrollRegionActive = false;
79
73
  lastRenderContent = '';
80
74
  lastRenderCursor = -1;
81
75
  renderDirty = false;
82
76
  isRendering = false;
83
77
  pinnedTopRows = 0;
78
+ inlineAnchorRow = null;
79
+ inlineLayout = false;
80
+ anchorProvider = null;
81
+ // Flow mode: when true, renders inline after content (no absolute positioning)
82
+ flowMode = true;
83
+ flowModeRenderedLines = 0; // Track lines rendered for clearing
84
+ contentEndRow = 0; // Row where content ends (for idle mode positioning)
85
+ // Command suggestions (Claude Code style auto-complete)
86
+ commandSuggestions = [];
87
+ filteredSuggestions = [];
88
+ selectedSuggestionIndex = 0;
89
+ showSuggestions = false;
90
+ maxVisibleSuggestions = 10;
84
91
  // Lifecycle
85
92
  disposed = false;
86
93
  enabled = true;
87
94
  contextUsage = null;
88
- contextAutoCompactThreshold = 90;
89
- // Track current content row in scroll region (starts at top, moves down)
90
- contentRow = 1;
91
- thinkingModeLabel = null;
92
95
  editMode = 'display-edits';
93
96
  verificationEnabled = true;
94
97
  autoContinueEnabled = false;
95
98
  verificationHotkey = 'alt+v';
96
99
  autoContinueHotkey = 'alt+c';
97
- thinkingHotkey = '/thinking';
98
- modelLabel = null;
99
- providerLabel = null;
100
100
  // Output interceptor cleanup
101
101
  outputInterceptorCleanup;
102
- // Streaming render throttle
103
- lastStreamingRender = 0;
104
- streamingRenderInterval = 250; // ms between renders during streaming
102
+ // Metrics tracking for status bar
103
+ streamingStartTime = null;
104
+ tokensUsed = 0;
105
+ thinkingEnabled = true;
106
+ modelInfo = null; // Provider · Model info
107
+ // Streaming input area render timer (updates elapsed time display)
105
108
  streamingRenderTimer = null;
109
+ // Unified UI initialization flag
110
+ unifiedUIInitialized = false;
106
111
  constructor(writeStream = process.stdout, config = {}) {
107
112
  super();
108
113
  this.out = writeStream;
114
+ // Use schema defaults for configuration consistency
109
115
  this.config = {
110
- maxLines: config.maxLines ?? 1000,
111
- maxLength: config.maxLength ?? 10000,
116
+ maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
117
+ maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
112
118
  maxQueueSize: config.maxQueueSize ?? 100,
113
- promptChar: config.promptChar ?? '> ',
114
- continuationChar: config.continuationChar ?? '│ ',
119
+ promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
120
+ continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
115
121
  };
116
122
  }
117
123
  // ===========================================================================
@@ -190,45 +196,345 @@ export class TerminalInput extends EventEmitter {
190
196
  if (handled)
191
197
  return;
192
198
  }
199
+ // Handle '?' for help hint (if buffer is empty)
200
+ if (str === '?' && this.buffer.length === 0) {
201
+ this.emit('showHelp');
202
+ return;
203
+ }
193
204
  // Insert printable characters
194
205
  if (str && !key?.ctrl && !key?.meta) {
195
206
  this.insertText(str);
196
207
  }
197
208
  }
209
+ // Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
210
+ bannerContent = null;
211
+ /**
212
+ * Set banner content to be written when unified UI initializes.
213
+ */
214
+ setBannerContent(content) {
215
+ this.bannerContent = content;
216
+ }
217
+ /**
218
+ * Initialize the unified UI system immediately.
219
+ * Clears screen, writes banner, renders input area immediately below.
220
+ * This creates a compact layout on launch (no empty space).
221
+ * Scroll region is set up later when streaming starts.
222
+ */
223
+ initializeUnifiedUI() {
224
+ if (this.unifiedUIInitialized) {
225
+ return;
226
+ }
227
+ // Reserve lines for input area (used later when scroll region is set up)
228
+ this.pinnedTopRows = 0;
229
+ this.reservedLines = 6; // status + model + divider + input + divider + controls
230
+ // Hide cursor during setup
231
+ this.write(ESC.HIDE);
232
+ // Clear screen
233
+ this.write(ESC.HOME);
234
+ this.write(ESC.CLEAR_SCREEN);
235
+ // Position cursor at row 1
236
+ this.write(ESC.TO(1, 1));
237
+ // Write banner as first content
238
+ let currentRow = 1;
239
+ if (this.bannerContent) {
240
+ process.stdout.write(this.bannerContent + '\n');
241
+ // Count banner lines
242
+ currentRow += this.bannerContent.split('\n').length;
243
+ }
244
+ // Mark unified UI as initialized
245
+ this.unifiedUIInitialized = true;
246
+ // Render input area immediately after banner (not at bottom)
247
+ this.renderInlineInputArea(currentRow);
248
+ // Show cursor
249
+ this.write(ESC.SHOW);
250
+ }
251
+ /**
252
+ * Render input area at a specific row (inline, not pinned to bottom).
253
+ * Used on launch for compact layout.
254
+ */
255
+ renderInlineInputArea(startRow) {
256
+ const { cols } = this.getSize();
257
+ const divider = '─'.repeat(cols - 1);
258
+ // Move to start row
259
+ this.write(ESC.TO(startRow, 1));
260
+ // Status bar
261
+ process.stdout.write(this.buildStatusBar(cols) + '\n');
262
+ // Model info line (if set)
263
+ if (this.modelInfo) {
264
+ const { dim: DIM, reset: R } = UI_COLORS;
265
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
266
+ if (this.contextUsage !== null) {
267
+ const rem = Math.max(0, 100 - this.contextUsage);
268
+ if (rem < 10)
269
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
270
+ else if (rem < 25)
271
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
272
+ else
273
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
274
+ }
275
+ process.stdout.write(modelLine + '\n');
276
+ }
277
+ // Top divider
278
+ process.stdout.write(divider + '\n');
279
+ // Input line with prompt
280
+ process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
281
+ process.stdout.write(ESC.BG_DARK + ' '.repeat(Math.max(0, cols - this.config.promptChar.length - 1)) + ESC.RESET + '\n');
282
+ // Bottom divider
283
+ process.stdout.write(divider + '\n');
284
+ // Mode controls
285
+ process.stdout.write(this.buildModeControls(cols) + '\n');
286
+ // Position cursor in input area
287
+ this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
288
+ }
289
+ /**
290
+ * Render input area at current cursor position (inline, not pinned).
291
+ * Used after streaming ends - renders input below the streamed content.
292
+ */
293
+ renderInlineInputAreaAtCursor() {
294
+ const { cols } = this.getSize();
295
+ const divider = '─'.repeat(cols - 1);
296
+ const { dim: DIM, reset: R } = UI_COLORS;
297
+ // Status bar - shows "Type a message" hint
298
+ process.stdout.write(this.buildStatusBar(cols) + '\n');
299
+ // Model info line (if set)
300
+ if (this.modelInfo) {
301
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
302
+ if (this.contextUsage !== null) {
303
+ const rem = Math.max(0, 100 - this.contextUsage);
304
+ if (rem < 10)
305
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
306
+ else if (rem < 25)
307
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
308
+ else
309
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
310
+ }
311
+ process.stdout.write(modelLine + '\n');
312
+ }
313
+ // Top divider
314
+ process.stdout.write(divider + '\n');
315
+ // Input line with prompt and any buffer content
316
+ const { lines, cursorCol } = this.wrapBuffer(cols - 4);
317
+ const displayLine = lines[0] ?? '';
318
+ process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
319
+ process.stdout.write(ESC.BG_DARK + displayLine);
320
+ const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
321
+ if (padding > 0)
322
+ process.stdout.write(' '.repeat(padding));
323
+ process.stdout.write(ESC.RESET + '\n');
324
+ // Bottom divider
325
+ process.stdout.write(divider + '\n');
326
+ // Mode controls
327
+ process.stdout.write(this.buildModeControls(cols) + '\n');
328
+ // Show cursor
329
+ this.write(ESC.SHOW);
330
+ // Update tracking
331
+ this.lastRenderContent = this.buffer;
332
+ this.lastRenderCursor = this.cursor;
333
+ }
198
334
  /**
199
335
  * Set the input mode
200
336
  *
201
- * Streaming keeps the scroll region active so the prompt/status stay pinned
202
- * below the streaming output. When streaming ends, we refresh the input area.
337
+ * Streaming mode: NO scroll region, NO bottom-pinned input.
338
+ * Content flows naturally after the initial launch layout.
339
+ * After streaming ends, input area renders inline at current position.
203
340
  */
204
341
  setMode(mode) {
205
342
  const prevMode = this.mode;
206
343
  this.mode = mode;
207
344
  if (mode === 'streaming' && prevMode !== 'streaming') {
208
- // Keep scroll region active so status/prompt stay pinned while streaming
209
- this.resetStreamingRenderThrottle();
210
- this.enableScrollRegion();
345
+ // Track streaming start time for elapsed display
346
+ this.streamingStartTime = Date.now();
347
+ // Ensure unified UI is initialized (if not already done on launch)
348
+ if (!this.unifiedUIInitialized) {
349
+ this.initializeUnifiedUI();
350
+ }
351
+ // NO scroll region - let content flow naturally
352
+ // NO bottom-pinned input area
353
+ // Don't clear anything - content flows from current cursor position
354
+ // The cursor should already be positioned after the user's prompt
211
355
  this.renderDirty = true;
212
- this.render();
213
356
  }
214
357
  else if (mode !== 'streaming' && prevMode === 'streaming') {
215
- // Streaming ended - render the input area
216
- this.resetStreamingRenderThrottle();
217
- this.enableScrollRegion();
218
- this.forceRender();
358
+ // Stop streaming render timer (if any)
359
+ if (this.streamingRenderTimer) {
360
+ clearInterval(this.streamingRenderTimer);
361
+ this.streamingRenderTimer = null;
362
+ }
363
+ // Reset streaming time
364
+ this.streamingStartTime = null;
365
+ // Reset flow mode tracking
366
+ this.flowModeRenderedLines = 0;
367
+ // Add spacing after streamed content
368
+ this.write('\n\n');
369
+ // Render input area inline at current position (below streamed content)
370
+ writeLock.withLock(() => {
371
+ this.renderInlineInputAreaAtCursor();
372
+ }, 'terminalInput.streamingEnd');
219
373
  }
220
374
  }
221
375
  /**
222
- * Keep the top N rows pinned outside the scroll region (used for the launch banner).
376
+ * Enable or disable flow mode.
377
+ * In flow mode, the input renders immediately after content (wherever cursor is).
378
+ * When disabled, input renders at the absolute bottom of terminal.
379
+ */
380
+ setFlowMode(enabled) {
381
+ if (this.flowMode === enabled)
382
+ return;
383
+ this.flowMode = enabled;
384
+ this.renderDirty = true;
385
+ this.scheduleRender();
386
+ }
387
+ /**
388
+ * Check if flow mode is enabled.
389
+ */
390
+ isFlowMode() {
391
+ return this.flowMode;
392
+ }
393
+ /**
394
+ * Set the row where content ends (for idle mode positioning).
395
+ * Input area will render starting from this row + 1.
396
+ */
397
+ setContentEndRow(row) {
398
+ this.contentEndRow = Math.max(0, row);
399
+ this.renderDirty = true;
400
+ this.scheduleRender();
401
+ }
402
+ /**
403
+ * Set available slash commands for auto-complete suggestions.
404
+ */
405
+ setCommands(commands) {
406
+ this.commandSuggestions = commands;
407
+ this.updateSuggestions();
408
+ }
409
+ /**
410
+ * Update filtered suggestions based on current input.
411
+ */
412
+ updateSuggestions() {
413
+ const input = this.buffer.trim();
414
+ // Only show suggestions when input starts with "/"
415
+ if (!input.startsWith('/')) {
416
+ this.showSuggestions = false;
417
+ this.filteredSuggestions = [];
418
+ this.selectedSuggestionIndex = 0;
419
+ return;
420
+ }
421
+ const query = input.toLowerCase();
422
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
423
+ cmd.command.toLowerCase().includes(query.slice(1)));
424
+ // Show suggestions if we have matches
425
+ this.showSuggestions = this.filteredSuggestions.length > 0;
426
+ // Keep selection in bounds
427
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
428
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
429
+ }
430
+ }
431
+ /**
432
+ * Select next suggestion (arrow down / tab).
433
+ */
434
+ selectNextSuggestion() {
435
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
436
+ return;
437
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
438
+ this.renderDirty = true;
439
+ this.scheduleRender();
440
+ }
441
+ /**
442
+ * Select previous suggestion (arrow up / shift+tab).
443
+ */
444
+ selectPrevSuggestion() {
445
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
446
+ return;
447
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
448
+ ? this.filteredSuggestions.length - 1
449
+ : this.selectedSuggestionIndex - 1;
450
+ this.renderDirty = true;
451
+ this.scheduleRender();
452
+ }
453
+ /**
454
+ * Accept current suggestion and insert into buffer.
455
+ */
456
+ acceptSuggestion() {
457
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
458
+ return false;
459
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
460
+ if (!selected)
461
+ return false;
462
+ // Replace buffer with selected command
463
+ this.buffer = selected.command + ' ';
464
+ this.cursor = this.buffer.length;
465
+ this.showSuggestions = false;
466
+ this.renderDirty = true;
467
+ this.scheduleRender();
468
+ return true;
469
+ }
470
+ /**
471
+ * Check if suggestions are visible.
472
+ */
473
+ areSuggestionsVisible() {
474
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
475
+ }
476
+ /**
477
+ * Update token count for metrics display
478
+ */
479
+ setTokensUsed(tokens) {
480
+ this.tokensUsed = tokens;
481
+ }
482
+ /**
483
+ * Toggle thinking/reasoning mode
484
+ */
485
+ toggleThinking() {
486
+ this.thinkingEnabled = !this.thinkingEnabled;
487
+ this.emit('thinkingToggle', this.thinkingEnabled);
488
+ this.scheduleRender();
489
+ }
490
+ /**
491
+ * Get thinking enabled state
492
+ */
493
+ isThinkingEnabled() {
494
+ return this.thinkingEnabled;
495
+ }
496
+ /**
497
+ * Keep the top N rows pinned (used for the launch banner tracking).
498
+ * Note: No longer uses scroll regions - inline rendering only.
223
499
  */
224
500
  setPinnedHeaderLines(count) {
225
- // No pinned header rows anymore; keep everything in the scroll region.
226
- if (this.pinnedTopRows !== 0) {
227
- this.pinnedTopRows = 0;
228
- if (this.scrollRegionActive) {
229
- this.applyScrollRegion();
230
- }
501
+ this.pinnedTopRows = count;
502
+ }
503
+ /**
504
+ * Anchor prompt rendering near a specific row (inline layout). Pass null to
505
+ * restore the default bottom-aligned layout.
506
+ */
507
+ setInlineAnchor(row) {
508
+ if (row === null || row === undefined) {
509
+ this.inlineAnchorRow = null;
510
+ this.inlineLayout = false;
511
+ this.renderDirty = true;
512
+ this.render();
513
+ return;
514
+ }
515
+ const { rows } = this.getSize();
516
+ const clamped = Math.max(1, Math.min(Math.floor(row), rows));
517
+ this.inlineAnchorRow = clamped;
518
+ this.inlineLayout = true;
519
+ this.renderDirty = true;
520
+ this.render();
521
+ }
522
+ /**
523
+ * Provide a dynamic anchor callback. When set, the prompt will follow the
524
+ * output by re-evaluating the anchor before each render.
525
+ */
526
+ setInlineAnchorProvider(provider) {
527
+ this.anchorProvider = provider;
528
+ if (!provider) {
529
+ this.inlineLayout = false;
530
+ this.inlineAnchorRow = null;
531
+ this.renderDirty = true;
532
+ this.render();
533
+ return;
231
534
  }
535
+ this.inlineLayout = true;
536
+ this.renderDirty = true;
537
+ this.render();
232
538
  }
233
539
  /**
234
540
  * Get current mode
@@ -261,14 +567,17 @@ export class TerminalInput extends EventEmitter {
261
567
  }
262
568
  /**
263
569
  * Clear the buffer
570
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
264
571
  */
265
- clear() {
572
+ clear(skipRender = false) {
266
573
  this.buffer = '';
267
574
  this.cursor = 0;
268
575
  this.historyIndex = -1;
269
576
  this.tempInput = '';
270
577
  this.pastePlaceholders = [];
271
- this.scheduleRender();
578
+ if (!skipRender) {
579
+ this.scheduleRender();
580
+ }
272
581
  }
273
582
  /**
274
583
  * Get queued inputs
@@ -339,37 +648,6 @@ export class TerminalInput extends EventEmitter {
339
648
  this.streamingLabel = next;
340
649
  this.scheduleRender();
341
650
  }
342
- /**
343
- * Surface meta status just above the divider (e.g., elapsed time or token usage).
344
- */
345
- setMetaStatus(meta) {
346
- const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
347
- ? Math.floor(meta.elapsedSeconds)
348
- : null;
349
- const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
350
- ? Math.floor(meta.tokensUsed)
351
- : null;
352
- const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
353
- ? Math.floor(meta.tokenLimit)
354
- : null;
355
- const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
356
- ? Math.floor(meta.thinkingMs)
357
- : null;
358
- const nextThinkingHasContent = !!meta.thinkingHasContent;
359
- if (this.metaElapsedSeconds === nextElapsed &&
360
- this.metaTokensUsed === nextTokens &&
361
- this.metaTokenLimit === nextLimit &&
362
- this.metaThinkingMs === nextThinking &&
363
- this.metaThinkingHasContent === nextThinkingHasContent) {
364
- return;
365
- }
366
- this.metaElapsedSeconds = nextElapsed;
367
- this.metaTokensUsed = nextTokens;
368
- this.metaTokenLimit = nextLimit;
369
- this.metaThinkingMs = nextThinking;
370
- this.metaThinkingHasContent = nextThinkingHasContent;
371
- this.scheduleRender();
372
- }
373
651
  /**
374
652
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
375
653
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -379,22 +657,26 @@ export class TerminalInput extends EventEmitter {
379
657
  const nextAutoContinue = !!options.autoContinueEnabled;
380
658
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
381
659
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
382
- const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
383
- const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
384
660
  if (this.verificationEnabled === nextVerification &&
385
661
  this.autoContinueEnabled === nextAutoContinue &&
386
662
  this.verificationHotkey === nextVerifyHotkey &&
387
- this.autoContinueHotkey === nextAutoHotkey &&
388
- this.thinkingHotkey === nextThinkingHotkey &&
389
- this.thinkingModeLabel === nextThinkingLabel) {
663
+ this.autoContinueHotkey === nextAutoHotkey) {
390
664
  return;
391
665
  }
392
666
  this.verificationEnabled = nextVerification;
393
667
  this.autoContinueEnabled = nextAutoContinue;
394
668
  this.verificationHotkey = nextVerifyHotkey;
395
669
  this.autoContinueHotkey = nextAutoHotkey;
396
- this.thinkingHotkey = nextThinkingHotkey;
397
- this.thinkingModeLabel = nextThinkingLabel;
670
+ this.scheduleRender();
671
+ }
672
+ /**
673
+ * Set the model info string (e.g., "OpenAI · gpt-4")
674
+ * This is displayed persistently above the input area.
675
+ */
676
+ setModelInfo(info) {
677
+ if (this.modelInfo === info)
678
+ return;
679
+ this.modelInfo = info;
398
680
  this.scheduleRender();
399
681
  }
400
682
  /**
@@ -407,161 +689,39 @@ export class TerminalInput extends EventEmitter {
407
689
  this.scheduleRender();
408
690
  }
409
691
  /**
410
- * Surface model/provider context in the controls bar.
411
- */
412
- setModelContext(options) {
413
- const nextModel = options.model?.trim() || null;
414
- const nextProvider = options.provider?.trim() || null;
415
- if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
416
- return;
417
- }
418
- this.modelLabel = nextModel;
419
- this.providerLabel = nextProvider;
420
- this.scheduleRender();
421
- }
422
- /**
423
- * Render the input area - Claude Code style with mode controls
692
+ * Render the input area
424
693
  *
425
- * During streaming we keep the scroll region active and repaint only the
426
- * pinned status/input block (throttled) so streamed content can scroll
427
- * naturally above while elapsed time and status stay fresh.
694
+ * - Idle mode: Renders inline input area
695
+ * - Streaming mode: NO render (content flows naturally, no UI updates)
696
+ * - Ready mode: Renders inline input area
428
697
  */
429
698
  render() {
430
699
  if (!this.canRender())
431
700
  return;
432
701
  if (this.isRendering)
433
702
  return;
434
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
435
- // During streaming we still render the pinned input/status region, but throttle
436
- // to avoid fighting with the streamed content flow.
437
- if (streamingActive && this.lastStreamingRender > 0) {
438
- const elapsed = Date.now() - this.lastStreamingRender;
439
- const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
440
- if (waitMs > 0) {
441
- this.renderDirty = true;
442
- this.scheduleStreamingRender(waitMs);
443
- return;
444
- }
703
+ // During streaming, do NOT render - let content flow naturally
704
+ if (this.mode === 'streaming') {
705
+ return;
445
706
  }
446
707
  const shouldSkip = !this.renderDirty &&
447
708
  this.buffer === this.lastRenderContent &&
448
709
  this.cursor === this.lastRenderCursor;
449
710
  this.renderDirty = false;
450
- // Skip if nothing changed and no explicit refresh requested
711
+ // Skip if nothing changed (unless explicitly forced)
451
712
  if (shouldSkip) {
452
713
  return;
453
714
  }
454
- // If write lock is held, defer render to avoid race conditions
715
+ // If write lock is held, defer render
455
716
  if (writeLock.isLocked()) {
456
717
  writeLock.safeWrite(() => this.render());
457
718
  return;
458
719
  }
459
- const performRender = () => {
460
- if (!this.scrollRegionActive) {
461
- this.enableScrollRegion();
462
- }
463
- const { rows, cols } = this.getSize();
464
- const maxWidth = Math.max(8, cols - 4);
465
- // Wrap buffer into display lines
466
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
467
- const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
468
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
469
- const displayLines = Math.min(lines.length, maxVisible);
470
- const metaLines = this.buildMetaLines(cols - 2);
471
- // Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
472
- this.updateReservedLines(displayLines + 2 + metaLines.length);
473
- // Calculate display window (keep cursor visible)
474
- let startLine = 0;
475
- if (lines.length > displayLines) {
476
- startLine = Math.max(0, cursorLine - displayLines + 1);
477
- startLine = Math.min(startLine, lines.length - displayLines);
478
- }
479
- const visibleLines = lines.slice(startLine, startLine + displayLines);
480
- const adjustedCursorLine = cursorLine - startLine;
481
- // Hide cursor during render to prevent flicker
482
- this.write(ESC.HIDE);
483
- this.write(ESC.RESET);
484
- const startRow = Math.max(1, rows - this.reservedLines + 1);
485
- let currentRow = startRow;
486
- // Clear the reserved block to avoid stale meta/status lines
487
- this.clearReservedArea(startRow, this.reservedLines, cols);
488
- // Meta/status header (elapsed, tokens/context)
489
- for (const metaLine of metaLines) {
490
- this.write(ESC.TO(currentRow, 1));
491
- this.write(ESC.CLEAR_LINE);
492
- this.write(metaLine);
493
- currentRow += 1;
494
- }
495
- // Separator line
496
- this.write(ESC.TO(currentRow, 1));
497
- this.write(ESC.CLEAR_LINE);
498
- const divider = renderDivider(cols - 2);
499
- this.write(divider);
500
- currentRow += 1;
501
- // Render input lines
502
- let finalRow = currentRow;
503
- let finalCol = 3;
504
- for (let i = 0; i < visibleLines.length; i++) {
505
- const rowNum = currentRow + i;
506
- this.write(ESC.TO(rowNum, 1));
507
- this.write(ESC.CLEAR_LINE);
508
- const line = visibleLines[i] ?? '';
509
- const absoluteLineIdx = startLine + i;
510
- const isFirstLine = absoluteLineIdx === 0;
511
- const isCursorLine = i === adjustedCursorLine;
512
- // Background
513
- this.write(ESC.BG_DARK);
514
- // Prompt prefix
515
- this.write(ESC.DIM);
516
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
517
- this.write(ESC.RESET);
518
- this.write(ESC.BG_DARK);
519
- if (isCursorLine) {
520
- // Render with block cursor
521
- const col = Math.min(cursorCol, line.length);
522
- const before = line.slice(0, col);
523
- const at = col < line.length ? line[col] : ' ';
524
- const after = col < line.length ? line.slice(col + 1) : '';
525
- this.write(before);
526
- this.write(ESC.REVERSE + ESC.BOLD);
527
- this.write(at);
528
- this.write(ESC.RESET + ESC.BG_DARK);
529
- this.write(after);
530
- finalRow = rowNum;
531
- finalCol = this.config.promptChar.length + col + 1;
532
- }
533
- else {
534
- this.write(line);
535
- }
536
- // Pad to edge for clean look
537
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
538
- const padding = Math.max(0, cols - lineLen - 1);
539
- if (padding > 0)
540
- this.write(' '.repeat(padding));
541
- this.write(ESC.RESET);
542
- }
543
- // Mode controls line (Claude Code style)
544
- const controlRow = currentRow + visibleLines.length;
545
- this.write(ESC.TO(controlRow, 1));
546
- this.write(ESC.CLEAR_LINE);
547
- this.write(this.buildModeControls(cols));
548
- // Position cursor in the input box for user editing
549
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
550
- this.write(ESC.SHOW);
551
- // Update state
552
- this.lastRenderContent = this.buffer;
553
- this.lastRenderCursor = this.cursor;
554
- this.lastStreamingRender = streamingActive ? Date.now() : 0;
555
- if (this.streamingRenderTimer) {
556
- clearTimeout(this.streamingRenderTimer);
557
- this.streamingRenderTimer = null;
558
- }
559
- };
560
- // Use write lock during render to prevent interleaved output
561
- writeLock.lock('terminalInput.render');
562
720
  this.isRendering = true;
721
+ writeLock.lock('terminalInput.render');
563
722
  try {
564
- performRender();
723
+ // Render inline input area at current position
724
+ this.renderInlineInputAreaAtCursor();
565
725
  }
566
726
  finally {
567
727
  writeLock.unlock();
@@ -569,228 +729,99 @@ export class TerminalInput extends EventEmitter {
569
729
  }
570
730
  }
571
731
  /**
572
- * Build one or more compact meta lines above the divider (thinking, status, usage).
573
- * During streaming, shows model line pinned above streaming info.
732
+ * Build status bar showing streaming/ready status and key info.
733
+ * This is the TOP line above the input area - minimal Claude Code style.
574
734
  */
575
- buildMetaLines(width) {
576
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
577
- const lines = [];
578
- // Model line should ALWAYS be shown (pinned above streaming content)
579
- if (this.modelLabel) {
580
- const modelText = this.providerLabel
581
- ? `model ${this.modelLabel} @ ${this.providerLabel}`
582
- : `model ${this.modelLabel}`;
583
- lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
584
- }
585
- // During streaming, add a compact status line with essential info
586
- if (streamingActive) {
587
- const parts = [];
588
- // Essential streaming info
589
- if (this.metaThinkingMs !== null) {
590
- parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
591
- }
592
- if (this.metaElapsedSeconds !== null) {
593
- parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
594
- }
595
- parts.push({ text: 'esc to stop', tone: 'warn' });
596
- if (parts.length) {
597
- lines.push(renderStatusLine(parts, width));
735
+ buildStatusBar(cols) {
736
+ const maxWidth = cols - 2;
737
+ const parts = [];
738
+ // Streaming status with elapsed time (left side)
739
+ if (this.mode === 'streaming') {
740
+ let statusText = '● Streaming';
741
+ if (this.streamingStartTime) {
742
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
743
+ const mins = Math.floor(elapsed / 60);
744
+ const secs = elapsed % 60;
745
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
598
746
  }
599
- return lines;
600
- }
601
- // Non-streaming: show full status info (model line already added above)
602
- if (this.metaThinkingMs !== null) {
603
- const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
604
- lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
605
- }
606
- const statusParts = [];
607
- const statusLabel = this.statusMessage ?? this.streamingLabel;
608
- if (statusLabel) {
609
- statusParts.push({ text: statusLabel, tone: 'info' });
610
- }
611
- if (this.metaElapsedSeconds !== null) {
612
- statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
747
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
613
748
  }
614
- const tokensRemaining = this.computeTokensRemaining();
615
- if (tokensRemaining !== null) {
616
- statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
749
+ // Queue indicator during streaming
750
+ if (this.mode === 'streaming' && this.queue.length > 0) {
751
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
617
752
  }
618
- if (statusParts.length) {
619
- lines.push(renderStatusLine(statusParts, width));
620
- }
621
- const usageParts = [];
622
- if (this.metaTokensUsed !== null) {
623
- const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
624
- const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
625
- usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
753
+ // Paste indicator
754
+ if (this.pastePlaceholders.length > 0) {
755
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
756
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
626
757
  }
627
- if (this.contextUsage !== null) {
628
- const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
629
- const left = Math.max(0, 100 - this.contextUsage);
630
- usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
758
+ // Override/warning status
759
+ if (this.overrideStatusMessage) {
760
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
631
761
  }
632
- if (this.queue.length > 0) {
633
- usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
762
+ // If idle with empty buffer, show quick shortcuts
763
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
764
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
634
765
  }
635
- if (usageParts.length) {
636
- lines.push(renderStatusLine(usageParts, width));
766
+ // Multi-line indicator
767
+ if (this.buffer.includes('\n')) {
768
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
637
769
  }
638
- return lines;
639
- }
640
- /**
641
- * Clear the reserved bottom block (meta + divider + input + controls) before repainting.
642
- */
643
- clearReservedArea(startRow, reservedLines, cols) {
644
- const width = Math.max(1, cols);
645
- for (let i = 0; i < reservedLines; i++) {
646
- const row = startRow + i;
647
- this.write(ESC.TO(row, 1));
648
- this.write(' '.repeat(width));
770
+ if (parts.length === 0) {
771
+ return ''; // Empty status bar when idle
649
772
  }
773
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
774
+ return joined.slice(0, maxWidth);
650
775
  }
651
776
  /**
652
- * Build Claude Code style mode controls line.
653
- * Combines streaming label + override status + main status for simultaneous display.
777
+ * Build mode controls line showing toggles and context info.
778
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
779
+ *
780
+ * Layout: [toggles on left] ... [context info on right]
654
781
  */
655
782
  buildModeControls(cols) {
656
- const width = Math.max(8, cols - 2);
657
- const leftParts = [];
658
- const rightParts = [];
659
- if (this.streamingLabel) {
660
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
661
- }
662
- if (this.overrideStatusMessage) {
663
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
664
- }
665
- if (this.statusMessage) {
666
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
667
- }
668
- const editHotkey = this.formatHotkey('shift+tab');
669
- const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
670
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
671
- leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
672
- const verifyHotkey = this.formatHotkey(this.verificationHotkey);
673
- const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
674
- leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
675
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
676
- const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
677
- leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
678
- if (this.queue.length > 0 && this.mode !== 'streaming') {
679
- leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
783
+ const maxWidth = cols - 2;
784
+ // Use schema-defined colors for consistency
785
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
786
+ // Mode toggles with colors (following ModeControlsSchema)
787
+ const toggles = [];
788
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
789
+ if (this.editMode === 'display-edits') {
790
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
680
791
  }
681
- if (this.buffer.includes('\n')) {
682
- const lineCount = this.buffer.split('\n').length;
683
- leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
684
- }
685
- if (this.pastePlaceholders.length > 0) {
686
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
687
- leftParts.push({
688
- text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
689
- tone: 'info',
690
- });
691
- }
692
- const contextRemaining = this.computeContextRemaining();
693
- if (this.thinkingModeLabel) {
694
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
695
- rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
696
- }
697
- // Show model in controls only when NOT streaming (during streaming it's in meta lines)
698
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
699
- if (this.modelLabel && !streamingActive) {
700
- const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
701
- rightParts.push({ text: modelText, tone: 'muted' });
702
- }
703
- if (contextRemaining !== null) {
704
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
705
- const label = contextRemaining === 0 && this.contextUsage !== null
706
- ? 'Context auto-compact imminent'
707
- : `Context left until auto-compact: ${contextRemaining}%`;
708
- rightParts.push({ text: label, tone });
709
- }
710
- if (!rightParts.length || width < 60) {
711
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
712
- return renderStatusLine(merged, width);
713
- }
714
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
715
- const rightWidth = Math.max(14, width - leftWidth - 1);
716
- const leftText = renderStatusLine(leftParts, leftWidth);
717
- const rightText = renderStatusLine(rightParts, rightWidth);
718
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
719
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
720
- }
721
- formatHotkey(hotkey) {
722
- const normalized = hotkey.trim().toLowerCase();
723
- if (!normalized)
724
- return hotkey;
725
- const parts = normalized.split('+').filter(Boolean);
726
- const map = {
727
- shift: '⇧',
728
- sh: '⇧',
729
- alt: '⌥',
730
- option: '⌥',
731
- opt: '⌥',
732
- ctrl: '⌃',
733
- control: '⌃',
734
- cmd: '⌘',
735
- meta: '⌘',
736
- };
737
- const formatted = parts
738
- .map((part) => {
739
- const symbol = map[part];
740
- if (symbol)
741
- return symbol;
742
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
743
- })
744
- .join('');
745
- return formatted || hotkey;
746
- }
747
- computeContextRemaining() {
748
- if (this.contextUsage === null) {
749
- return null;
750
- }
751
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
752
- }
753
- computeTokensRemaining() {
754
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
755
- return null;
756
- }
757
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
758
- return this.formatTokenCount(remaining);
759
- }
760
- formatElapsedLabel(seconds) {
761
- if (seconds < 60) {
762
- return `${seconds}s`;
763
- }
764
- const mins = Math.floor(seconds / 60);
765
- const secs = seconds % 60;
766
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
767
- }
768
- formatTokenCount(value) {
769
- if (!Number.isFinite(value)) {
770
- return `${value}`;
771
- }
772
- if (value >= 1_000_000) {
773
- return `${(value / 1_000_000).toFixed(1)}M`;
774
- }
775
- if (value >= 1_000) {
776
- return `${(value / 1_000).toFixed(1)}k`;
777
- }
778
- return `${Math.round(value)}`;
779
- }
780
- visibleLength(value) {
781
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
782
- return value.replace(ansiPattern, '').length;
783
- }
784
- /**
785
- * Debug-only snapshot used by tests to assert rendered strings without
786
- * needing a TTY. Not used by production code.
787
- */
788
- getDebugUiSnapshot(width) {
789
- const cols = Math.max(8, width ?? this.getSize().cols);
790
- return {
791
- meta: this.buildMetaLines(cols - 2),
792
- controls: this.buildModeControls(cols),
793
- };
792
+ else {
793
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
794
+ }
795
+ // Thinking mode (cyan when on) - per schema.thinkingMode
796
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
797
+ // Verification (green when on) - per schema.verificationMode
798
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
799
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
800
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
801
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
802
+ // Context usage with color - per schema.contextUsage thresholds
803
+ let rightPart = '';
804
+ if (this.contextUsage !== null) {
805
+ const rem = Math.max(0, 100 - this.contextUsage);
806
+ // Thresholds: critical < 10%, warning < 25%
807
+ if (rem < 10)
808
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
809
+ else if (rem < 25)
810
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
811
+ else
812
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
813
+ }
814
+ // Calculate visible lengths (strip ANSI)
815
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
816
+ const leftLen = strip(leftPart).length;
817
+ const rightLen = strip(rightPart).length;
818
+ if (leftLen + rightLen < maxWidth - 4) {
819
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
820
+ }
821
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
822
+ return `${leftPart} ${rightPart}`;
823
+ }
824
+ return leftPart;
794
825
  }
795
826
  /**
796
827
  * Force a re-render
@@ -813,19 +844,17 @@ export class TerminalInput extends EventEmitter {
813
844
  handleResize() {
814
845
  this.lastRenderContent = '';
815
846
  this.lastRenderCursor = -1;
816
- this.resetStreamingRenderThrottle();
817
847
  // Re-clamp pinned header rows to the new terminal height
818
848
  this.setPinnedHeaderLines(this.pinnedTopRows);
819
- if (this.scrollRegionActive) {
820
- this.disableScrollRegion();
821
- this.enableScrollRegion();
822
- }
823
849
  this.scheduleRender();
824
850
  }
825
851
  /**
826
852
  * Register with display's output interceptor to position cursor correctly.
827
853
  * When scroll region is active, output needs to go to the scroll region,
828
854
  * not the protected bottom area where the input is rendered.
855
+ *
856
+ * NOTE: With scroll region properly set, content naturally stays within
857
+ * the region boundaries - no cursor manipulation needed per-write.
829
858
  */
830
859
  registerOutputInterceptor(display) {
831
860
  if (this.outputInterceptorCleanup) {
@@ -833,66 +862,25 @@ export class TerminalInput extends EventEmitter {
833
862
  }
834
863
  this.outputInterceptorCleanup = display.registerOutputInterceptor({
835
864
  beforeWrite: () => {
836
- // Position cursor at current content row (starts at top, moves down).
837
- // When contentRow reaches scrollBottom, terminal handles scrolling.
838
- if (this.scrollRegionActive) {
839
- const { rows } = this.getSize();
840
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
841
- const targetRow = Math.min(this.contentRow, scrollBottom);
842
- this.write(ESC.SAVE);
843
- this.write(ESC.TO(targetRow, 1));
844
- }
865
+ // Scroll region handles content containment automatically
866
+ // No per-write cursor manipulation needed
845
867
  },
846
- afterWrite: (content) => {
847
- // Advance content row by number of lines written and restore cursor.
848
- if (this.scrollRegionActive) {
849
- const { rows } = this.getSize();
850
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
851
- // Count newlines in content to advance by correct amount
852
- const lineCount = content ? (content.match(/\n/g) || []).length + 1 : 1;
853
- this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
854
- this.write(ESC.RESTORE);
855
- }
868
+ afterWrite: () => {
869
+ // No cursor manipulation needed
856
870
  },
857
871
  });
858
872
  }
859
- /**
860
- * Write content directly into the scroll region (for banner, user prompts, etc.).
861
- * Content starts at top and flows down, then scrolls when bottom is reached.
862
- */
863
- writeToScrollRegion(content) {
864
- if (!content)
865
- return;
866
- // Ensure scroll region is active
867
- if (!this.scrollRegionActive) {
868
- this.enableScrollRegion();
869
- }
870
- const { rows } = this.getSize();
871
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
872
- const targetRow = Math.min(this.contentRow, scrollBottom);
873
- // Write at current content position
874
- this.write(ESC.SAVE);
875
- this.write(ESC.TO(targetRow, 1));
876
- this.write(content);
877
- this.write(ESC.RESTORE);
878
- // Advance contentRow by number of lines written
879
- const lineCount = (content.match(/\n/g) || []).length + 1;
880
- this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
881
- }
882
- /**
883
- * Reset content position to start of scroll region.
884
- * Does NOT clear the terminal - content starts from current position.
885
- */
886
- resetContentPosition() {
887
- const scrollTop = Math.max(1, this.pinnedTopRows + 1);
888
- this.contentRow = scrollTop;
889
- }
890
873
  /**
891
874
  * Dispose and clean up
892
875
  */
893
876
  dispose() {
894
877
  if (this.disposed)
895
878
  return;
879
+ // Clean up streaming render timer
880
+ if (this.streamingRenderTimer) {
881
+ clearInterval(this.streamingRenderTimer);
882
+ this.streamingRenderTimer = null;
883
+ }
896
884
  // Clean up output interceptor
897
885
  if (this.outputInterceptorCleanup) {
898
886
  this.outputInterceptorCleanup();
@@ -900,8 +888,8 @@ export class TerminalInput extends EventEmitter {
900
888
  }
901
889
  this.disposed = true;
902
890
  this.enabled = false;
903
- this.resetStreamingRenderThrottle();
904
- this.disableScrollRegion();
891
+ // Reset scroll region if it was set
892
+ this.write(ESC.RESET_SCROLL);
905
893
  this.disableBracketedPaste();
906
894
  this.buffer = '';
907
895
  this.queue = [];
@@ -1006,7 +994,22 @@ export class TerminalInput extends EventEmitter {
1006
994
  this.toggleEditMode();
1007
995
  return true;
1008
996
  }
1009
- this.insertText(' ');
997
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
998
+ if (this.findPlaceholderAt(this.cursor)) {
999
+ this.togglePasteExpansion();
1000
+ }
1001
+ else {
1002
+ this.toggleThinking();
1003
+ }
1004
+ return true;
1005
+ case 'escape':
1006
+ // Esc: interrupt if streaming, otherwise clear buffer
1007
+ if (this.mode === 'streaming') {
1008
+ this.emit('interrupt');
1009
+ }
1010
+ else if (this.buffer.length > 0) {
1011
+ this.clear();
1012
+ }
1010
1013
  return true;
1011
1014
  }
1012
1015
  return false;
@@ -1024,6 +1027,7 @@ export class TerminalInput extends EventEmitter {
1024
1027
  this.insertPlainText(chunk, insertPos);
1025
1028
  this.cursor = insertPos + chunk.length;
1026
1029
  this.emit('change', this.buffer);
1030
+ this.updateSuggestions();
1027
1031
  this.scheduleRender();
1028
1032
  }
1029
1033
  insertNewline() {
@@ -1048,6 +1052,7 @@ export class TerminalInput extends EventEmitter {
1048
1052
  this.cursor = Math.max(0, this.cursor - 1);
1049
1053
  }
1050
1054
  this.emit('change', this.buffer);
1055
+ this.updateSuggestions();
1051
1056
  this.scheduleRender();
1052
1057
  }
1053
1058
  deleteForward() {
@@ -1275,12 +1280,13 @@ export class TerminalInput extends EventEmitter {
1275
1280
  timestamp: Date.now(),
1276
1281
  });
1277
1282
  this.emit('queue', text);
1278
- this.clear(); // Clear immediately for queued input
1283
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1279
1284
  }
1280
1285
  else {
1281
- // In idle mode, clear the input first, then emit submit.
1282
- // The prompt will be logged as a visible message by the caller.
1283
- this.clear();
1286
+ // In idle mode, clear the input WITHOUT rendering.
1287
+ // The caller will display the user message and start streaming.
1288
+ // We'll render the input area again after streaming ends.
1289
+ this.clear(true); // Skip render - streaming will handle display
1284
1290
  this.emit('submit', text);
1285
1291
  }
1286
1292
  }
@@ -1297,9 +1303,7 @@ export class TerminalInput extends EventEmitter {
1297
1303
  if (available <= 0)
1298
1304
  return;
1299
1305
  const chunk = clean.slice(0, available);
1300
- const isMultiline = isMultilinePaste(chunk);
1301
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1302
- if (isMultiline && !isShortMultiline) {
1306
+ if (isMultilinePaste(chunk)) {
1303
1307
  this.insertPastePlaceholder(chunk);
1304
1308
  }
1305
1309
  else {
@@ -1312,41 +1316,6 @@ export class TerminalInput extends EventEmitter {
1312
1316
  this.scheduleRender();
1313
1317
  }
1314
1318
  // ===========================================================================
1315
- // SCROLL REGION
1316
- // ===========================================================================
1317
- enableScrollRegion() {
1318
- if (this.scrollRegionActive || !this.isTTY())
1319
- return;
1320
- this.applyScrollRegion();
1321
- this.scrollRegionActive = true;
1322
- this.forceRender();
1323
- }
1324
- disableScrollRegion() {
1325
- if (!this.scrollRegionActive)
1326
- return;
1327
- this.write(ESC.RESET_SCROLL);
1328
- this.scrollRegionActive = false;
1329
- }
1330
- applyScrollRegion() {
1331
- const { rows } = this.getSize();
1332
- const scrollTop = Math.max(1, this.pinnedTopRows + 1);
1333
- const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
1334
- this.write(ESC.SET_SCROLL(scrollTop, scrollBottom));
1335
- }
1336
- updateReservedLines(contentLines) {
1337
- const { rows } = this.getSize();
1338
- const baseLines = 2; // separator + control bar
1339
- const needed = baseLines + contentLines;
1340
- const maxAllowed = Math.max(baseLines, rows - 1 - this.pinnedTopRows);
1341
- const newReserved = Math.min(Math.max(baseLines, needed), maxAllowed);
1342
- if (newReserved !== this.reservedLines) {
1343
- this.reservedLines = newReserved;
1344
- if (this.scrollRegionActive) {
1345
- this.applyScrollRegion();
1346
- }
1347
- }
1348
- }
1349
- // ===========================================================================
1350
1319
  // BUFFER WRAPPING
1351
1320
  // ===========================================================================
1352
1321
  wrapBuffer(maxWidth) {
@@ -1470,19 +1439,17 @@ export class TerminalInput extends EventEmitter {
1470
1439
  this.shiftPlaceholders(position, text.length);
1471
1440
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1472
1441
  }
1473
- shouldInlineMultiline(content) {
1474
- const lines = content.split('\n').length;
1475
- const maxInlineLines = 4;
1476
- const maxInlineChars = 240;
1477
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1478
- }
1479
1442
  findPlaceholderAt(position) {
1480
1443
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1481
1444
  }
1482
- buildPlaceholder(lineCount) {
1445
+ buildPlaceholder(summary) {
1483
1446
  const id = ++this.pasteCounter;
1484
- const plural = lineCount === 1 ? '' : 's';
1485
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1447
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1448
+ // Show first line preview (truncated)
1449
+ const preview = summary.preview.length > 30
1450
+ ? `${summary.preview.slice(0, 30)}...`
1451
+ : summary.preview;
1452
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1486
1453
  return { id, placeholder };
1487
1454
  }
1488
1455
  insertPastePlaceholder(content) {
@@ -1490,21 +1457,67 @@ export class TerminalInput extends EventEmitter {
1490
1457
  if (available <= 0)
1491
1458
  return;
1492
1459
  const cleanContent = content.slice(0, available);
1493
- const lineCount = cleanContent.split('\n').length;
1494
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1460
+ const summary = generatePasteSummary(cleanContent);
1461
+ // For short pastes (< 5 lines), show full content instead of placeholder
1462
+ if (summary.lineCount < 5) {
1463
+ const placeholder = this.findPlaceholderAt(this.cursor);
1464
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1465
+ this.insertPlainText(cleanContent, insertPos);
1466
+ this.cursor = insertPos + cleanContent.length;
1467
+ return;
1468
+ }
1469
+ const { id, placeholder } = this.buildPlaceholder(summary);
1495
1470
  const insertPos = this.cursor;
1496
1471
  this.shiftPlaceholders(insertPos, placeholder.length);
1497
1472
  this.pastePlaceholders.push({
1498
1473
  id,
1499
1474
  content: cleanContent,
1500
- lineCount,
1475
+ lineCount: summary.lineCount,
1501
1476
  placeholder,
1502
1477
  start: insertPos,
1503
1478
  end: insertPos + placeholder.length,
1479
+ summary,
1480
+ expanded: false,
1504
1481
  });
1505
1482
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1506
1483
  this.cursor = insertPos + placeholder.length;
1507
1484
  }
1485
+ /**
1486
+ * Toggle expansion of a paste placeholder at the current cursor position.
1487
+ * When expanded, shows first 3 and last 2 lines of the content.
1488
+ */
1489
+ togglePasteExpansion() {
1490
+ const placeholder = this.findPlaceholderAt(this.cursor);
1491
+ if (!placeholder)
1492
+ return false;
1493
+ placeholder.expanded = !placeholder.expanded;
1494
+ // Update the placeholder text in buffer
1495
+ const newPlaceholder = placeholder.expanded
1496
+ ? this.buildExpandedPlaceholder(placeholder)
1497
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1498
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1499
+ // Update buffer
1500
+ this.buffer =
1501
+ this.buffer.slice(0, placeholder.start) +
1502
+ newPlaceholder +
1503
+ this.buffer.slice(placeholder.end);
1504
+ // Update placeholder tracking
1505
+ placeholder.placeholder = newPlaceholder;
1506
+ placeholder.end = placeholder.start + newPlaceholder.length;
1507
+ // Shift other placeholders
1508
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1509
+ this.scheduleRender();
1510
+ return true;
1511
+ }
1512
+ buildExpandedPlaceholder(ph) {
1513
+ const lines = ph.content.split('\n');
1514
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1515
+ const lastLines = lines.length > 5
1516
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1517
+ : '';
1518
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1519
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1520
+ }
1508
1521
  deletePlaceholder(placeholder) {
1509
1522
  const length = placeholder.end - placeholder.start;
1510
1523
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1512,11 +1525,7 @@ export class TerminalInput extends EventEmitter {
1512
1525
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1513
1526
  this.cursor = placeholder.start;
1514
1527
  }
1515
- updateContextUsage(value, autoCompactThreshold) {
1516
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1517
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1518
- this.contextAutoCompactThreshold = boundedThreshold;
1519
- }
1528
+ updateContextUsage(value) {
1520
1529
  if (value === null || !Number.isFinite(value)) {
1521
1530
  this.contextUsage = null;
1522
1531
  }
@@ -1543,22 +1552,6 @@ export class TerminalInput extends EventEmitter {
1543
1552
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1544
1553
  this.setEditMode(next);
1545
1554
  }
1546
- scheduleStreamingRender(delayMs) {
1547
- if (this.streamingRenderTimer)
1548
- return;
1549
- const wait = Math.max(16, delayMs);
1550
- this.streamingRenderTimer = setTimeout(() => {
1551
- this.streamingRenderTimer = null;
1552
- this.render();
1553
- }, wait);
1554
- }
1555
- resetStreamingRenderThrottle() {
1556
- if (this.streamingRenderTimer) {
1557
- clearTimeout(this.streamingRenderTimer);
1558
- this.streamingRenderTimer = null;
1559
- }
1560
- this.lastStreamingRender = 0;
1561
- }
1562
1555
  scheduleRender() {
1563
1556
  if (!this.canRender())
1564
1557
  return;