erosolar-cli 1.7.274 → 1.7.275

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 -10
  235. package/dist/shell/interactiveShell.d.ts.map +1 -1
  236. package/dist/shell/interactiveShell.js +160 -198
  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 +1 -36
  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 +176 -74
  254. package/dist/shell/terminalInput.d.ts.map +1 -1
  255. package/dist/shell/terminalInput.js +879 -490
  256. package/dist/shell/terminalInput.js.map +1 -1
  257. package/dist/shell/terminalInputAdapter.d.ts +33 -28
  258. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  259. package/dist/shell/terminalInputAdapter.js +46 -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 +8 -23
  273. package/dist/ui/display.d.ts.map +1 -1
  274. package/dist/ui/display.js +55 -141
  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,16 @@
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 { renderDivider } from '../ui/unified/layout.js';
15
+ import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
18
16
  // ANSI escape codes
19
17
  const ESC = {
20
18
  // Cursor control
@@ -24,6 +22,9 @@ const ESC = {
24
22
  SHOW: '\x1b[?25h',
25
23
  TO: (row, col) => `\x1b[${row};${col}H`,
26
24
  TO_COL: (col) => `\x1b[${col}G`,
25
+ // Screen control
26
+ CLEAR_SCREEN: '\x1b[2J',
27
+ HOME: '\x1b[H',
27
28
  // Line control
28
29
  CLEAR_LINE: '\x1b[2K',
29
30
  CLEAR_TO_END: '\x1b[0J',
@@ -69,11 +70,6 @@ export class TerminalInput extends EventEmitter {
69
70
  statusMessage = null;
70
71
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
71
72
  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
73
  reservedLines = 2;
78
74
  scrollRegionActive = false;
79
75
  lastRenderContent = '';
@@ -81,35 +77,51 @@ export class TerminalInput extends EventEmitter {
81
77
  renderDirty = false;
82
78
  isRendering = false;
83
79
  pinnedTopRows = 0;
80
+ inlineAnchorRow = null;
81
+ inlineLayout = false;
82
+ anchorProvider = null;
83
+ // Flow mode: when true, renders inline after content (no absolute positioning)
84
+ flowMode = true;
85
+ flowModeRenderedLines = 0; // Track lines rendered for clearing
86
+ contentEndRow = 0; // Row where content ends (for idle mode positioning)
87
+ // Command suggestions (Claude Code style auto-complete)
88
+ commandSuggestions = [];
89
+ filteredSuggestions = [];
90
+ selectedSuggestionIndex = 0;
91
+ showSuggestions = false;
92
+ maxVisibleSuggestions = 10;
84
93
  // Lifecycle
85
94
  disposed = false;
86
95
  enabled = true;
87
96
  contextUsage = null;
88
- contextAutoCompactThreshold = 90;
89
- thinkingModeLabel = null;
90
97
  editMode = 'display-edits';
91
98
  verificationEnabled = true;
92
99
  autoContinueEnabled = false;
93
100
  verificationHotkey = 'alt+v';
94
101
  autoContinueHotkey = 'alt+c';
95
- thinkingHotkey = '/thinking';
96
- modelLabel = null;
97
- providerLabel = null;
98
102
  // Output interceptor cleanup
99
103
  outputInterceptorCleanup;
100
- // Streaming render throttle
101
- lastStreamingRender = 0;
102
- streamingRenderInterval = 250; // ms between renders during streaming
104
+ // Metrics tracking for status bar
105
+ streamingStartTime = null;
106
+ tokensUsed = 0;
107
+ thinkingEnabled = true;
108
+ modelInfo = null; // Provider · Model info
109
+ // Streaming input area render timer (updates elapsed time display)
103
110
  streamingRenderTimer = null;
111
+ // Banner renderer callback - called when entering streaming mode to re-render banner
112
+ // inside the scroll region (unified UI system)
113
+ bannerRenderer = null;
114
+ unifiedUIInitialized = false;
104
115
  constructor(writeStream = process.stdout, config = {}) {
105
116
  super();
106
117
  this.out = writeStream;
118
+ // Use schema defaults for configuration consistency
107
119
  this.config = {
108
- maxLines: config.maxLines ?? 1000,
109
- maxLength: config.maxLength ?? 10000,
120
+ maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
121
+ maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
110
122
  maxQueueSize: config.maxQueueSize ?? 100,
111
- promptChar: config.promptChar ?? '> ',
112
- continuationChar: config.continuationChar ?? '│ ',
123
+ promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
124
+ continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
113
125
  };
114
126
  }
115
127
  // ===========================================================================
@@ -188,6 +200,11 @@ export class TerminalInput extends EventEmitter {
188
200
  if (handled)
189
201
  return;
190
202
  }
203
+ // Handle '?' for help hint (if buffer is empty)
204
+ if (str === '?' && this.buffer.length === 0) {
205
+ this.emit('showHelp');
206
+ return;
207
+ }
191
208
  // Insert printable characters
192
209
  if (str && !key?.ctrl && !key?.meta) {
193
210
  this.insertText(str);
@@ -196,38 +213,525 @@ export class TerminalInput extends EventEmitter {
196
213
  /**
197
214
  * Set the input mode
198
215
  *
199
- * Streaming keeps the scroll region active so the prompt/status stay pinned
200
- * below the streaming output. When streaming ends, we refresh the input area.
216
+ * Streaming mode disables scroll region and lets content flow naturally.
217
+ * The input area will be re-rendered after streaming ends at wherever
218
+ * the cursor is (below the streamed content).
201
219
  */
202
220
  setMode(mode) {
203
221
  const prevMode = this.mode;
204
222
  this.mode = mode;
205
223
  if (mode === 'streaming' && prevMode !== 'streaming') {
206
- // Keep scroll region active so status/prompt stay pinned while streaming
207
- this.resetStreamingRenderThrottle();
208
- this.enableScrollRegion();
224
+ // Track streaming start time for elapsed display
225
+ this.streamingStartTime = Date.now();
226
+ const { rows } = this.getSize();
227
+ // Set up scroll region to reserve bottom for persistent input area
228
+ this.pinnedTopRows = 0;
229
+ this.reservedLines = 6; // status + model + divider + input + divider + controls
230
+ // UNIFIED UI INITIALIZATION: On first streaming transition, clear screen
231
+ // and re-render banner inside the scroll region for a consistent layout
232
+ if (!this.unifiedUIInitialized && this.bannerRenderer) {
233
+ // Hide cursor during screen redraw
234
+ this.write(ESC.HIDE);
235
+ // Clear screen and move to home position
236
+ this.write(ESC.HOME);
237
+ this.write(ESC.CLEAR_SCREEN);
238
+ // Set up scroll region FIRST (reserve bottom for input area)
239
+ this.enableScrollRegion();
240
+ // Move to top of content area (row 1 inside scroll region)
241
+ this.write(ESC.TO(1, 1));
242
+ // Re-render banner inside the scroll region
243
+ const bannerLines = this.bannerRenderer();
244
+ // Position cursor just after banner for content flow
245
+ const contentStartRow = Math.max(1, bannerLines + 1);
246
+ this.write(ESC.TO(contentStartRow, 1));
247
+ // Mark unified UI as initialized
248
+ this.unifiedUIInitialized = true;
249
+ // Initial render of bottom input area
250
+ this.renderBottomInputArea();
251
+ // Show cursor
252
+ this.write(ESC.SHOW);
253
+ }
254
+ else {
255
+ // Normal streaming transition (not first time)
256
+ // CRITICAL: Position cursor in content area BEFORE enabling scroll region
257
+ // Content area is rows 1 to (rows - reservedLines)
258
+ // Move cursor to just after the banner (where content should appear)
259
+ const contentBottomRow = Math.max(1, rows - this.reservedLines);
260
+ this.write(ESC.TO(contentBottomRow, 1));
261
+ // Enable scroll region: content scrolls above, bottom is reserved
262
+ this.enableScrollRegion();
263
+ // Initial render of bottom input area (will save cursor at content area position)
264
+ this.renderBottomInputArea();
265
+ }
266
+ // Start timer to update bottom input area (updates elapsed time)
267
+ this.streamingRenderTimer = setInterval(() => {
268
+ if (this.mode === 'streaming') {
269
+ this.updateStreamingStatus();
270
+ this.renderBottomInputArea();
271
+ }
272
+ }, 1000);
209
273
  this.renderDirty = true;
210
- this.render();
211
274
  }
212
275
  else if (mode !== 'streaming' && prevMode === 'streaming') {
213
- // Streaming ended - render the input area
214
- this.resetStreamingRenderThrottle();
276
+ // Stop streaming render timer
277
+ if (this.streamingRenderTimer) {
278
+ clearInterval(this.streamingRenderTimer);
279
+ this.streamingRenderTimer = null;
280
+ }
281
+ // Reset streaming time
282
+ this.streamingStartTime = null;
283
+ // Keep scroll region active for consistent bottom-pinned UI
284
+ // (scroll region reserves bottom for input area in all modes)
285
+ // Reset flow mode tracking
286
+ this.flowModeRenderedLines = 0;
287
+ // Render using unified bottom input area (same layout as streaming)
288
+ writeLock.withLock(() => {
289
+ this.renderBottomInputArea();
290
+ }, 'terminalInput.streamingEnd');
291
+ }
292
+ }
293
+ /**
294
+ * Update streaming status label (called by timer)
295
+ */
296
+ updateStreamingStatus() {
297
+ if (this.mode !== 'streaming' || !this.streamingStartTime)
298
+ return;
299
+ // Calculate elapsed time
300
+ const elapsed = Date.now() - this.streamingStartTime;
301
+ const seconds = Math.floor(elapsed / 1000);
302
+ const minutes = Math.floor(seconds / 60);
303
+ const secs = seconds % 60;
304
+ // Format elapsed time
305
+ let elapsedStr;
306
+ if (minutes > 0) {
307
+ elapsedStr = `${minutes}m ${secs}s`;
308
+ }
309
+ else {
310
+ elapsedStr = `${secs}s`;
311
+ }
312
+ // Update streaming label
313
+ this.streamingLabel = `Streaming ${elapsedStr}`;
314
+ }
315
+ /**
316
+ * Render input area - unified for streaming and normal modes.
317
+ *
318
+ * In streaming mode: renders at absolute bottom, uses cursor save/restore
319
+ * In normal mode: renders right after the banner (pinnedTopRows + 1)
320
+ */
321
+ renderPinnedInputArea() {
322
+ const { rows, cols } = this.getSize();
323
+ const maxWidth = Math.max(8, cols - 4);
324
+ const divider = renderDivider(cols - 2);
325
+ const isStreaming = this.mode === 'streaming';
326
+ // Wrap buffer into display lines (multi-line support)
327
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
328
+ const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
329
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
330
+ const displayLines = Math.min(lines.length, maxVisible);
331
+ // Calculate display window (keep cursor visible)
332
+ let startLine = 0;
333
+ if (lines.length > displayLines) {
334
+ startLine = Math.max(0, cursorLine - displayLines + 1);
335
+ startLine = Math.min(startLine, lines.length - displayLines);
336
+ }
337
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
338
+ const adjustedCursorLine = cursorLine - startLine;
339
+ // Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
340
+ const hasModelInfo = !!this.modelInfo;
341
+ const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
342
+ // Save cursor position during streaming (so content flow resumes correctly)
343
+ if (isStreaming) {
344
+ this.write(ESC.SAVE);
345
+ }
346
+ this.write(ESC.HIDE);
347
+ this.write(ESC.RESET);
348
+ // Calculate start row based on mode:
349
+ // - Streaming: absolute bottom (rows - totalHeight + 1)
350
+ // - Normal: right after content (contentEndRow + 1)
351
+ let currentRow;
352
+ if (isStreaming) {
353
+ currentRow = Math.max(1, rows - totalHeight + 1);
354
+ }
355
+ else {
356
+ // In normal mode, render right after content
357
+ // Use contentEndRow if set, otherwise use pinnedTopRows
358
+ const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
359
+ currentRow = Math.max(1, contentRow + 1);
360
+ }
361
+ let finalRow = currentRow;
362
+ let finalCol = 3;
363
+ // Clear from current position to end of screen to remove any "ghost" content
364
+ this.write(ESC.TO(currentRow, 1));
365
+ this.write(ESC.CLEAR_TO_END);
366
+ // Status bar
367
+ this.write(ESC.TO(currentRow, 1));
368
+ this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
369
+ currentRow++;
370
+ // Model info line (if set) - displayed below status, above input
371
+ if (hasModelInfo) {
372
+ const { dim: DIM, reset: R } = UI_COLORS;
373
+ this.write(ESC.TO(currentRow, 1));
374
+ // Build model info with context usage
375
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
376
+ if (this.contextUsage !== null) {
377
+ const rem = Math.max(0, 100 - this.contextUsage);
378
+ if (rem < 10)
379
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
380
+ else if (rem < 25)
381
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
382
+ else
383
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
384
+ }
385
+ this.write(modelLine);
386
+ currentRow++;
387
+ }
388
+ // Top divider
389
+ this.write(ESC.TO(currentRow, 1));
390
+ this.write(divider);
391
+ currentRow++;
392
+ // Input lines with background styling
393
+ for (let i = 0; i < visibleLines.length; i++) {
394
+ this.write(ESC.TO(currentRow, 1));
395
+ const line = visibleLines[i] ?? '';
396
+ const absoluteLineIdx = startLine + i;
397
+ const isFirstLine = absoluteLineIdx === 0;
398
+ const isCursorLine = i === adjustedCursorLine;
399
+ // Background
400
+ this.write(ESC.BG_DARK);
401
+ // Prompt prefix
402
+ this.write(ESC.DIM);
403
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
404
+ this.write(ESC.RESET);
405
+ this.write(ESC.BG_DARK);
406
+ if (isCursorLine) {
407
+ const col = Math.min(cursorCol, line.length);
408
+ const before = line.slice(0, col);
409
+ const at = col < line.length ? line[col] : ' ';
410
+ const after = col < line.length ? line.slice(col + 1) : '';
411
+ this.write(before);
412
+ this.write(ESC.REVERSE + ESC.BOLD);
413
+ this.write(at);
414
+ this.write(ESC.RESET + ESC.BG_DARK);
415
+ this.write(after);
416
+ finalRow = currentRow;
417
+ finalCol = this.config.promptChar.length + col + 1;
418
+ }
419
+ else {
420
+ this.write(line);
421
+ }
422
+ // Pad to edge
423
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
424
+ const padding = Math.max(0, cols - lineLen - 1);
425
+ if (padding > 0)
426
+ this.write(' '.repeat(padding));
427
+ this.write(ESC.RESET);
428
+ currentRow++;
429
+ }
430
+ // Bottom divider
431
+ this.write(ESC.TO(currentRow, 1));
432
+ this.write(divider);
433
+ currentRow++;
434
+ // Mode controls line
435
+ this.write(ESC.TO(currentRow, 1));
436
+ this.write(this.buildModeControls(cols));
437
+ // Restore cursor position during streaming, or show cursor in normal mode
438
+ if (isStreaming) {
439
+ this.write(ESC.RESTORE);
440
+ }
441
+ else {
442
+ // Position cursor in input area
443
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
444
+ this.write(ESC.SHOW);
445
+ }
446
+ // Update reserved lines for scroll region calculations
447
+ this.updateReservedLines(totalHeight);
448
+ }
449
+ /**
450
+ * Render input area during streaming (alias for unified method)
451
+ */
452
+ renderStreamingInputArea() {
453
+ this.renderPinnedInputArea();
454
+ }
455
+ /**
456
+ * Render bottom input area - UNIFIED for all modes.
457
+ * Uses cursor save/restore to update bottom without affecting content flow.
458
+ *
459
+ * Layout (same for idle/streaming/ready):
460
+ * - Status bar (streaming timer or "Type a message")
461
+ * - Model info line (provider · model · ctx)
462
+ * - Divider
463
+ * - Input area
464
+ * - Divider
465
+ * - Mode controls
466
+ */
467
+ renderBottomInputArea() {
468
+ const { rows, cols } = this.getSize();
469
+ const maxWidth = Math.max(8, cols - 4);
470
+ const divider = renderDivider(cols - 2);
471
+ const { dim: DIM, reset: R } = UI_COLORS;
472
+ const isStreaming = this.mode === 'streaming';
473
+ // Wrap buffer into display lines
474
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
475
+ // Allow multi-line in non-streaming, single line during streaming
476
+ const maxDisplayLines = isStreaming ? 1 : 3;
477
+ const displayLines = Math.min(lines.length, maxDisplayLines);
478
+ const visibleLines = lines.slice(0, displayLines);
479
+ // Calculate total height for bottom area
480
+ const hasModelInfo = !!this.modelInfo;
481
+ const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
482
+ // Ensure scroll region is always enabled (unified behavior)
483
+ if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
484
+ this.reservedLines = totalHeight;
215
485
  this.enableScrollRegion();
216
- this.forceRender();
217
486
  }
487
+ const startRow = Math.max(1, rows - totalHeight + 1);
488
+ // Save cursor, hide it
489
+ this.write(ESC.SAVE);
490
+ this.write(ESC.HIDE);
491
+ let currentRow = startRow;
492
+ // Clear the bottom reserved area
493
+ for (let r = startRow; r <= rows; r++) {
494
+ this.write(ESC.TO(r, 1));
495
+ this.write(ESC.CLEAR_LINE);
496
+ }
497
+ // Status bar - UNIFIED: same format for all modes
498
+ this.write(ESC.TO(currentRow, 1));
499
+ this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
500
+ currentRow++;
501
+ // Model info line (if set)
502
+ if (hasModelInfo) {
503
+ this.write(ESC.TO(currentRow, 1));
504
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
505
+ if (this.contextUsage !== null) {
506
+ const rem = Math.max(0, 100 - this.contextUsage);
507
+ if (rem < 10)
508
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
509
+ else if (rem < 25)
510
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
511
+ else
512
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
513
+ }
514
+ this.write(modelLine);
515
+ currentRow++;
516
+ }
517
+ // Top divider
518
+ this.write(ESC.TO(currentRow, 1));
519
+ this.write(divider);
520
+ currentRow++;
521
+ // Input lines with background styling
522
+ for (let i = 0; i < visibleLines.length; i++) {
523
+ this.write(ESC.TO(currentRow, 1));
524
+ const line = visibleLines[i] ?? '';
525
+ const isFirstLine = i === 0;
526
+ this.write(ESC.BG_DARK);
527
+ this.write(ESC.DIM);
528
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
529
+ this.write(ESC.RESET);
530
+ this.write(ESC.BG_DARK);
531
+ this.write(line);
532
+ // Pad to edge
533
+ const lineLen = this.config.promptChar.length + line.length;
534
+ const padding = Math.max(0, cols - lineLen - 1);
535
+ if (padding > 0)
536
+ this.write(' '.repeat(padding));
537
+ this.write(ESC.RESET);
538
+ currentRow++;
539
+ }
540
+ // Bottom divider
541
+ this.write(ESC.TO(currentRow, 1));
542
+ this.write(divider);
543
+ currentRow++;
544
+ // Mode controls
545
+ this.write(ESC.TO(currentRow, 1));
546
+ this.write(this.buildModeControls(cols));
547
+ // Cursor positioning depends on mode:
548
+ // - Streaming: restore to content area (where streaming output continues)
549
+ // - Normal: position in input area for typing
550
+ if (isStreaming) {
551
+ this.write(ESC.RESTORE);
552
+ }
553
+ else {
554
+ // Position cursor in input area
555
+ // Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
556
+ const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
557
+ const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
558
+ const targetCol = this.config.promptChar.length + cursorCol + 1;
559
+ this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
560
+ }
561
+ this.write(ESC.SHOW);
562
+ // Track last render state
563
+ this.lastRenderContent = this.buffer;
564
+ this.lastRenderCursor = this.cursor;
565
+ }
566
+ /**
567
+ * Enable or disable flow mode.
568
+ * In flow mode, the input renders immediately after content (wherever cursor is).
569
+ * When disabled, input renders at the absolute bottom of terminal.
570
+ */
571
+ setFlowMode(enabled) {
572
+ if (this.flowMode === enabled)
573
+ return;
574
+ this.flowMode = enabled;
575
+ this.renderDirty = true;
576
+ this.scheduleRender();
577
+ }
578
+ /**
579
+ * Check if flow mode is enabled.
580
+ */
581
+ isFlowMode() {
582
+ return this.flowMode;
583
+ }
584
+ /**
585
+ * Set the row where content ends (for idle mode positioning).
586
+ * Input area will render starting from this row + 1.
587
+ */
588
+ setContentEndRow(row) {
589
+ this.contentEndRow = Math.max(0, row);
590
+ this.renderDirty = true;
591
+ this.scheduleRender();
592
+ }
593
+ /**
594
+ * Set available slash commands for auto-complete suggestions.
595
+ */
596
+ setCommands(commands) {
597
+ this.commandSuggestions = commands;
598
+ this.updateSuggestions();
599
+ }
600
+ /**
601
+ * Update filtered suggestions based on current input.
602
+ */
603
+ updateSuggestions() {
604
+ const input = this.buffer.trim();
605
+ // Only show suggestions when input starts with "/"
606
+ if (!input.startsWith('/')) {
607
+ this.showSuggestions = false;
608
+ this.filteredSuggestions = [];
609
+ this.selectedSuggestionIndex = 0;
610
+ return;
611
+ }
612
+ const query = input.toLowerCase();
613
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
614
+ cmd.command.toLowerCase().includes(query.slice(1)));
615
+ // Show suggestions if we have matches
616
+ this.showSuggestions = this.filteredSuggestions.length > 0;
617
+ // Keep selection in bounds
618
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
619
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
620
+ }
621
+ }
622
+ /**
623
+ * Select next suggestion (arrow down / tab).
624
+ */
625
+ selectNextSuggestion() {
626
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
627
+ return;
628
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
629
+ this.renderDirty = true;
630
+ this.scheduleRender();
631
+ }
632
+ /**
633
+ * Select previous suggestion (arrow up / shift+tab).
634
+ */
635
+ selectPrevSuggestion() {
636
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
637
+ return;
638
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
639
+ ? this.filteredSuggestions.length - 1
640
+ : this.selectedSuggestionIndex - 1;
641
+ this.renderDirty = true;
642
+ this.scheduleRender();
643
+ }
644
+ /**
645
+ * Accept current suggestion and insert into buffer.
646
+ */
647
+ acceptSuggestion() {
648
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
649
+ return false;
650
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
651
+ if (!selected)
652
+ return false;
653
+ // Replace buffer with selected command
654
+ this.buffer = selected.command + ' ';
655
+ this.cursor = this.buffer.length;
656
+ this.showSuggestions = false;
657
+ this.renderDirty = true;
658
+ this.scheduleRender();
659
+ return true;
660
+ }
661
+ /**
662
+ * Check if suggestions are visible.
663
+ */
664
+ areSuggestionsVisible() {
665
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
666
+ }
667
+ /**
668
+ * Update token count for metrics display
669
+ */
670
+ setTokensUsed(tokens) {
671
+ this.tokensUsed = tokens;
672
+ }
673
+ /**
674
+ * Toggle thinking/reasoning mode
675
+ */
676
+ toggleThinking() {
677
+ this.thinkingEnabled = !this.thinkingEnabled;
678
+ this.emit('thinkingToggle', this.thinkingEnabled);
679
+ this.scheduleRender();
680
+ }
681
+ /**
682
+ * Get thinking enabled state
683
+ */
684
+ isThinkingEnabled() {
685
+ return this.thinkingEnabled;
218
686
  }
219
687
  /**
220
688
  * Keep the top N rows pinned outside the scroll region (used for the launch banner).
221
689
  */
222
690
  setPinnedHeaderLines(count) {
223
- // No pinned header rows anymore; keep everything in the scroll region.
224
- if (this.pinnedTopRows !== 0) {
225
- this.pinnedTopRows = 0;
691
+ // Set pinned header rows (banner area that scroll region excludes)
692
+ if (this.pinnedTopRows !== count) {
693
+ this.pinnedTopRows = count;
226
694
  if (this.scrollRegionActive) {
227
695
  this.applyScrollRegion();
228
696
  }
229
697
  }
230
698
  }
699
+ /**
700
+ * Anchor prompt rendering near a specific row (inline layout). Pass null to
701
+ * restore the default bottom-aligned layout.
702
+ */
703
+ setInlineAnchor(row) {
704
+ if (row === null || row === undefined) {
705
+ this.inlineAnchorRow = null;
706
+ this.inlineLayout = false;
707
+ this.renderDirty = true;
708
+ this.render();
709
+ return;
710
+ }
711
+ const { rows } = this.getSize();
712
+ const clamped = Math.max(1, Math.min(Math.floor(row), rows));
713
+ this.inlineAnchorRow = clamped;
714
+ this.inlineLayout = true;
715
+ this.renderDirty = true;
716
+ this.render();
717
+ }
718
+ /**
719
+ * Provide a dynamic anchor callback. When set, the prompt will follow the
720
+ * output by re-evaluating the anchor before each render.
721
+ */
722
+ setInlineAnchorProvider(provider) {
723
+ this.anchorProvider = provider;
724
+ if (!provider) {
725
+ this.inlineLayout = false;
726
+ this.inlineAnchorRow = null;
727
+ this.renderDirty = true;
728
+ this.render();
729
+ return;
730
+ }
731
+ this.inlineLayout = true;
732
+ this.renderDirty = true;
733
+ this.render();
734
+ }
231
735
  /**
232
736
  * Get current mode
233
737
  */
@@ -337,37 +841,6 @@ export class TerminalInput extends EventEmitter {
337
841
  this.streamingLabel = next;
338
842
  this.scheduleRender();
339
843
  }
340
- /**
341
- * Surface meta status just above the divider (e.g., elapsed time or token usage).
342
- */
343
- setMetaStatus(meta) {
344
- const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
345
- ? Math.floor(meta.elapsedSeconds)
346
- : null;
347
- const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
348
- ? Math.floor(meta.tokensUsed)
349
- : null;
350
- const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
351
- ? Math.floor(meta.tokenLimit)
352
- : null;
353
- const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
354
- ? Math.floor(meta.thinkingMs)
355
- : null;
356
- const nextThinkingHasContent = !!meta.thinkingHasContent;
357
- if (this.metaElapsedSeconds === nextElapsed &&
358
- this.metaTokensUsed === nextTokens &&
359
- this.metaTokenLimit === nextLimit &&
360
- this.metaThinkingMs === nextThinking &&
361
- this.metaThinkingHasContent === nextThinkingHasContent) {
362
- return;
363
- }
364
- this.metaElapsedSeconds = nextElapsed;
365
- this.metaTokensUsed = nextTokens;
366
- this.metaTokenLimit = nextLimit;
367
- this.metaThinkingMs = nextThinking;
368
- this.metaThinkingHasContent = nextThinkingHasContent;
369
- this.scheduleRender();
370
- }
371
844
  /**
372
845
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
373
846
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -377,22 +850,26 @@ export class TerminalInput extends EventEmitter {
377
850
  const nextAutoContinue = !!options.autoContinueEnabled;
378
851
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
379
852
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
380
- const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
381
- const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
382
853
  if (this.verificationEnabled === nextVerification &&
383
854
  this.autoContinueEnabled === nextAutoContinue &&
384
855
  this.verificationHotkey === nextVerifyHotkey &&
385
- this.autoContinueHotkey === nextAutoHotkey &&
386
- this.thinkingHotkey === nextThinkingHotkey &&
387
- this.thinkingModeLabel === nextThinkingLabel) {
856
+ this.autoContinueHotkey === nextAutoHotkey) {
388
857
  return;
389
858
  }
390
859
  this.verificationEnabled = nextVerification;
391
860
  this.autoContinueEnabled = nextAutoContinue;
392
861
  this.verificationHotkey = nextVerifyHotkey;
393
862
  this.autoContinueHotkey = nextAutoHotkey;
394
- this.thinkingHotkey = nextThinkingHotkey;
395
- this.thinkingModeLabel = nextThinkingLabel;
863
+ this.scheduleRender();
864
+ }
865
+ /**
866
+ * Set the model info string (e.g., "OpenAI · gpt-4")
867
+ * This is displayed persistently above the input area.
868
+ */
869
+ setModelInfo(info) {
870
+ if (this.modelInfo === info)
871
+ return;
872
+ this.modelInfo = info;
396
873
  this.scheduleRender();
397
874
  }
398
875
  /**
@@ -405,390 +882,298 @@ export class TerminalInput extends EventEmitter {
405
882
  this.scheduleRender();
406
883
  }
407
884
  /**
408
- * Surface model/provider context in the controls bar.
409
- */
410
- setModelContext(options) {
411
- const nextModel = options.model?.trim() || null;
412
- const nextProvider = options.provider?.trim() || null;
413
- if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
414
- return;
415
- }
416
- this.modelLabel = nextModel;
417
- this.providerLabel = nextProvider;
418
- this.scheduleRender();
419
- }
420
- /**
421
- * Render the input area - Claude Code style with mode controls
885
+ * Render the input area - UNIFIED for all modes
422
886
  *
423
- * During streaming we keep the scroll region active and repaint only the
424
- * pinned status/input block (throttled) so streamed content can scroll
425
- * naturally above while elapsed time and status stay fresh.
887
+ * Uses the same bottom-pinned layout with scroll regions for:
888
+ * - Idle mode: Shows "Type a message" hint
889
+ * - Streaming mode: Shows "● Streaming Xs" timer
890
+ * - Ready mode: Shows status info
426
891
  */
427
892
  render() {
428
893
  if (!this.canRender())
429
894
  return;
430
895
  if (this.isRendering)
431
896
  return;
432
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
433
- // During streaming we still render the pinned input/status region, but throttle
434
- // to avoid fighting with the streamed content flow.
435
- if (streamingActive && this.lastStreamingRender > 0) {
436
- const elapsed = Date.now() - this.lastStreamingRender;
437
- const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
438
- if (waitMs > 0) {
439
- this.renderDirty = true;
440
- this.scheduleStreamingRender(waitMs);
441
- return;
442
- }
443
- }
444
897
  const shouldSkip = !this.renderDirty &&
445
898
  this.buffer === this.lastRenderContent &&
446
899
  this.cursor === this.lastRenderCursor;
447
900
  this.renderDirty = false;
448
- // Skip if nothing changed and no explicit refresh requested
901
+ // Skip if nothing changed (unless explicitly forced)
449
902
  if (shouldSkip) {
450
903
  return;
451
904
  }
452
- // If write lock is held, defer render to avoid race conditions
905
+ // If write lock is held, defer render
453
906
  if (writeLock.isLocked()) {
454
907
  writeLock.safeWrite(() => this.render());
455
908
  return;
456
909
  }
457
- const performRender = () => {
458
- if (!this.scrollRegionActive) {
459
- this.enableScrollRegion();
460
- }
461
- const { rows, cols } = this.getSize();
462
- const maxWidth = Math.max(8, cols - 4);
463
- // Wrap buffer into display lines
464
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
465
- const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
466
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
467
- const displayLines = Math.min(lines.length, maxVisible);
468
- const metaLines = this.buildMetaLines(cols - 2);
469
- // Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
470
- this.updateReservedLines(displayLines + 2 + metaLines.length);
471
- // Calculate display window (keep cursor visible)
472
- let startLine = 0;
473
- if (lines.length > displayLines) {
474
- startLine = Math.max(0, cursorLine - displayLines + 1);
475
- startLine = Math.min(startLine, lines.length - displayLines);
476
- }
477
- const visibleLines = lines.slice(startLine, startLine + displayLines);
478
- const adjustedCursorLine = cursorLine - startLine;
479
- // Hide cursor during render to prevent flicker
480
- this.write(ESC.HIDE);
481
- this.write(ESC.RESET);
482
- const startRow = Math.max(1, rows - this.reservedLines + 1);
483
- let currentRow = startRow;
484
- // Clear the reserved block to avoid stale meta/status lines
485
- this.clearReservedArea(startRow, this.reservedLines, cols);
486
- // Meta/status header (elapsed, tokens/context)
487
- for (const metaLine of metaLines) {
488
- this.write(ESC.TO(currentRow, 1));
489
- this.write(ESC.CLEAR_LINE);
490
- this.write(metaLine);
491
- currentRow += 1;
492
- }
493
- // Separator line
910
+ this.isRendering = true;
911
+ writeLock.lock('terminalInput.render');
912
+ try {
913
+ // UNIFIED: Use the same bottom input area for all modes
914
+ this.renderBottomInputArea();
915
+ }
916
+ finally {
917
+ writeLock.unlock();
918
+ this.isRendering = false;
919
+ }
920
+ }
921
+ /**
922
+ * Render in flow mode - delegates to bottom-pinned for stability.
923
+ *
924
+ * Flow mode attempted inline rendering but caused duplicate renders
925
+ * due to unreliable cursor position tracking. Bottom-pinned is reliable.
926
+ */
927
+ renderFlowMode() {
928
+ // Use stable bottom-pinned approach
929
+ this.renderBottomPinned();
930
+ }
931
+ /**
932
+ * Render in bottom-pinned mode - Claude Code style with suggestions
933
+ *
934
+ * Works for both normal and streaming modes:
935
+ * - During streaming: saves/restores cursor position
936
+ * - Status bar shows streaming info or "Type a message"
937
+ *
938
+ * Layout when suggestions visible:
939
+ * - Top divider
940
+ * - Input line(s)
941
+ * - Bottom divider
942
+ * - Suggestions (command list)
943
+ *
944
+ * Layout when suggestions hidden:
945
+ * - Status bar (Ready/Streaming)
946
+ * - Top divider
947
+ * - Input line(s)
948
+ * - Bottom divider
949
+ * - Mode controls
950
+ */
951
+ renderBottomPinned() {
952
+ const { rows, cols } = this.getSize();
953
+ const maxWidth = Math.max(8, cols - 4);
954
+ const isStreaming = this.mode === 'streaming';
955
+ // Use unified pinned input area (works for both streaming and normal)
956
+ // Only use complex rendering when suggestions are visible
957
+ const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
958
+ if (!hasSuggestions) {
959
+ this.renderPinnedInputArea();
960
+ return;
961
+ }
962
+ // Wrap buffer into display lines
963
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
964
+ const availableForContent = Math.max(1, rows - 3);
965
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
966
+ const displayLines = Math.min(lines.length, maxVisible);
967
+ // Calculate display window (keep cursor visible)
968
+ let startLine = 0;
969
+ if (lines.length > displayLines) {
970
+ startLine = Math.max(0, cursorLine - displayLines + 1);
971
+ startLine = Math.min(startLine, lines.length - displayLines);
972
+ }
973
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
974
+ const adjustedCursorLine = cursorLine - startLine;
975
+ // Calculate suggestion display (not during streaming)
976
+ const suggestionsToShow = (!isStreaming && this.showSuggestions)
977
+ ? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
978
+ : [];
979
+ const suggestionLines = suggestionsToShow.length;
980
+ this.write(ESC.HIDE);
981
+ this.write(ESC.RESET);
982
+ const divider = renderDivider(cols - 2);
983
+ // Calculate positions from absolute bottom
984
+ let currentRow;
985
+ if (suggestionLines > 0) {
986
+ // With suggestions: input area + dividers + suggestions
987
+ // Layout: [topDiv] [input] [bottomDiv] [suggestions...]
988
+ const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
989
+ currentRow = Math.max(1, rows - totalHeight + 1);
990
+ this.updateReservedLines(totalHeight);
991
+ // Clear from current position to end of screen to remove any "ghost" content
992
+ this.write(ESC.TO(currentRow, 1));
993
+ this.write(ESC.CLEAR_TO_END);
994
+ // Top divider
494
995
  this.write(ESC.TO(currentRow, 1));
495
- this.write(ESC.CLEAR_LINE);
496
- const divider = renderDivider(cols - 2);
497
996
  this.write(divider);
498
- currentRow += 1;
499
- // Render input lines
997
+ currentRow++;
998
+ // Input lines
500
999
  let finalRow = currentRow;
501
1000
  let finalCol = 3;
502
1001
  for (let i = 0; i < visibleLines.length; i++) {
503
- const rowNum = currentRow + i;
504
- this.write(ESC.TO(rowNum, 1));
505
- this.write(ESC.CLEAR_LINE);
1002
+ this.write(ESC.TO(currentRow, 1));
506
1003
  const line = visibleLines[i] ?? '';
507
1004
  const absoluteLineIdx = startLine + i;
508
1005
  const isFirstLine = absoluteLineIdx === 0;
509
1006
  const isCursorLine = i === adjustedCursorLine;
510
- // Background
511
- this.write(ESC.BG_DARK);
512
- // Prompt prefix
513
- this.write(ESC.DIM);
514
1007
  this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
515
- this.write(ESC.RESET);
516
- this.write(ESC.BG_DARK);
517
1008
  if (isCursorLine) {
518
- // Render with block cursor
519
1009
  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;
1010
+ this.write(line.slice(0, col));
1011
+ this.write(ESC.REVERSE);
1012
+ this.write(col < line.length ? line[col] : ' ');
1013
+ this.write(ESC.RESET);
1014
+ this.write(line.slice(col + 1));
1015
+ finalRow = currentRow;
529
1016
  finalCol = this.config.promptChar.length + col + 1;
530
1017
  }
531
1018
  else {
532
1019
  this.write(line);
533
1020
  }
534
- // Pad to edge for clean look
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);
1021
+ currentRow++;
540
1022
  }
541
- // Mode controls line (Claude Code style)
542
- const controlRow = currentRow + visibleLines.length;
543
- this.write(ESC.TO(controlRow, 1));
544
- this.write(ESC.CLEAR_LINE);
545
- this.write(this.buildModeControls(cols));
546
- // Position cursor in the input box for user editing
547
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
548
- this.write(ESC.SHOW);
549
- // Update state
550
- this.lastRenderContent = this.buffer;
551
- this.lastRenderCursor = this.cursor;
552
- this.lastStreamingRender = streamingActive ? Date.now() : 0;
553
- if (this.streamingRenderTimer) {
554
- clearTimeout(this.streamingRenderTimer);
555
- this.streamingRenderTimer = null;
1023
+ // Bottom divider
1024
+ this.write(ESC.TO(currentRow, 1));
1025
+ this.write(divider);
1026
+ currentRow++;
1027
+ // Suggestions (Claude Code style)
1028
+ for (let i = 0; i < suggestionsToShow.length; i++) {
1029
+ this.write(ESC.TO(currentRow, 1));
1030
+ const suggestion = suggestionsToShow[i];
1031
+ const isSelected = i === this.selectedSuggestionIndex;
1032
+ // Indent and highlight selected
1033
+ this.write(' ');
1034
+ if (isSelected) {
1035
+ this.write(ESC.REVERSE);
1036
+ this.write(ESC.BOLD);
1037
+ }
1038
+ this.write(suggestion.command);
1039
+ if (isSelected) {
1040
+ this.write(ESC.RESET);
1041
+ }
1042
+ // Description (dimmed)
1043
+ const descSpace = cols - suggestion.command.length - 8;
1044
+ if (descSpace > 10 && suggestion.description) {
1045
+ const desc = suggestion.description.slice(0, descSpace);
1046
+ this.write(ESC.RESET);
1047
+ this.write(ESC.DIM);
1048
+ this.write(' ');
1049
+ this.write(desc);
1050
+ this.write(ESC.RESET);
1051
+ }
1052
+ currentRow++;
556
1053
  }
557
- };
558
- // Use write lock during render to prevent interleaved output
559
- writeLock.lock('terminalInput.render');
560
- this.isRendering = true;
561
- try {
562
- performRender();
563
- }
564
- finally {
565
- writeLock.unlock();
566
- this.isRendering = false;
1054
+ // Position cursor in input area
1055
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
567
1056
  }
1057
+ this.write(ESC.SHOW);
1058
+ // Update state
1059
+ this.lastRenderContent = this.buffer;
1060
+ this.lastRenderCursor = this.cursor;
568
1061
  }
569
1062
  /**
570
- * Build one or more compact meta lines above the divider (thinking, status, usage).
571
- * During streaming, shows model line pinned above streaming info.
1063
+ * Build status bar for streaming mode (shows elapsed time, queue count).
572
1064
  */
573
- buildMetaLines(width) {
574
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
575
- const lines = [];
576
- // Model line should ALWAYS be shown (pinned above streaming content)
577
- if (this.modelLabel) {
578
- const modelText = this.providerLabel
579
- ? `model ${this.modelLabel} @ ${this.providerLabel}`
580
- : `model ${this.modelLabel}`;
581
- lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
582
- }
583
- // During streaming, add a compact status line with essential info
584
- if (streamingActive) {
585
- const parts = [];
586
- // Essential streaming info
587
- if (this.metaThinkingMs !== null) {
588
- parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
589
- }
590
- if (this.metaElapsedSeconds !== null) {
591
- parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
592
- }
593
- parts.push({ text: 'esc to stop', tone: 'warn' });
594
- if (parts.length) {
595
- lines.push(renderStatusLine(parts, width));
596
- }
597
- return lines;
598
- }
599
- // Non-streaming: show full status info (model line already added above)
600
- if (this.metaThinkingMs !== null) {
601
- const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
602
- lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
603
- }
604
- const statusParts = [];
605
- const statusLabel = this.statusMessage ?? this.streamingLabel;
606
- if (statusLabel) {
607
- statusParts.push({ text: statusLabel, tone: 'info' });
608
- }
609
- if (this.metaElapsedSeconds !== null) {
610
- statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
611
- }
612
- const tokensRemaining = this.computeTokensRemaining();
613
- if (tokensRemaining !== null) {
614
- statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
615
- }
616
- if (statusParts.length) {
617
- lines.push(renderStatusLine(statusParts, width));
618
- }
619
- const usageParts = [];
620
- if (this.metaTokensUsed !== null) {
621
- const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
622
- const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
623
- usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
624
- }
625
- if (this.contextUsage !== null) {
626
- const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
627
- const left = Math.max(0, 100 - this.contextUsage);
628
- usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
629
- }
1065
+ buildStreamingStatusBar(cols) {
1066
+ const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
1067
+ // Streaming status with elapsed time
1068
+ let elapsed = '0s';
1069
+ if (this.streamingStartTime) {
1070
+ const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
1071
+ const mins = Math.floor(secs / 60);
1072
+ elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
1073
+ }
1074
+ let status = `${GREEN}● Streaming${R} ${elapsed}`;
1075
+ // Queue indicator
630
1076
  if (this.queue.length > 0) {
631
- usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
632
- }
633
- if (usageParts.length) {
634
- lines.push(renderStatusLine(usageParts, width));
635
- }
636
- return lines;
637
- }
638
- /**
639
- * Clear the reserved bottom block (meta + divider + input + controls) before repainting.
640
- */
641
- clearReservedArea(startRow, reservedLines, cols) {
642
- const width = Math.max(1, cols);
643
- for (let i = 0; i < reservedLines; i++) {
644
- const row = startRow + i;
645
- this.write(ESC.TO(row, 1));
646
- this.write(' '.repeat(width));
1077
+ status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
647
1078
  }
1079
+ // Hint for typing
1080
+ status += ` ${DIM}· type to queue message${R}`;
1081
+ return status;
648
1082
  }
649
1083
  /**
650
- * Build Claude Code style mode controls line.
651
- * Combines streaming label + override status + main status for simultaneous display.
1084
+ * Build status bar showing streaming/ready status and key info.
1085
+ * This is the TOP line above the input area - minimal Claude Code style.
652
1086
  */
653
- buildModeControls(cols) {
654
- const width = Math.max(8, cols - 2);
655
- const leftParts = [];
656
- const rightParts = [];
657
- if (this.streamingLabel) {
658
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
659
- }
660
- if (this.overrideStatusMessage) {
661
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
662
- }
663
- if (this.statusMessage) {
664
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
665
- }
666
- const editHotkey = this.formatHotkey('shift+tab');
667
- const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
668
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
669
- leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
670
- const verifyHotkey = this.formatHotkey(this.verificationHotkey);
671
- const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
672
- leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
673
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
674
- const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
675
- leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
676
- if (this.queue.length > 0 && this.mode !== 'streaming') {
677
- leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
1087
+ buildStatusBar(cols) {
1088
+ const maxWidth = cols - 2;
1089
+ const parts = [];
1090
+ // Streaming status with elapsed time (left side)
1091
+ if (this.mode === 'streaming') {
1092
+ let statusText = '● Streaming';
1093
+ if (this.streamingStartTime) {
1094
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
1095
+ const mins = Math.floor(elapsed / 60);
1096
+ const secs = elapsed % 60;
1097
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
1098
+ }
1099
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
678
1100
  }
679
- if (this.buffer.includes('\n')) {
680
- const lineCount = this.buffer.split('\n').length;
681
- leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
1101
+ // Queue indicator during streaming
1102
+ if (this.mode === 'streaming' && this.queue.length > 0) {
1103
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
682
1104
  }
1105
+ // Paste indicator
683
1106
  if (this.pastePlaceholders.length > 0) {
684
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
685
- leftParts.push({
686
- text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
687
- tone: 'info',
688
- });
1107
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
1108
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
689
1109
  }
690
- const contextRemaining = this.computeContextRemaining();
691
- if (this.thinkingModeLabel) {
692
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
693
- rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
694
- }
695
- // Show model in controls only when NOT streaming (during streaming it's in meta lines)
696
- const streamingActive = this.mode === 'streaming' || isStreamingMode();
697
- if (this.modelLabel && !streamingActive) {
698
- const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
699
- rightParts.push({ text: modelText, tone: 'muted' });
700
- }
701
- if (contextRemaining !== null) {
702
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
703
- const label = contextRemaining === 0 && this.contextUsage !== null
704
- ? 'Context auto-compact imminent'
705
- : `Context left until auto-compact: ${contextRemaining}%`;
706
- rightParts.push({ text: label, tone });
707
- }
708
- if (!rightParts.length || width < 60) {
709
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
710
- return renderStatusLine(merged, width);
711
- }
712
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
713
- const rightWidth = Math.max(14, width - leftWidth - 1);
714
- const leftText = renderStatusLine(leftParts, leftWidth);
715
- const rightText = renderStatusLine(rightParts, rightWidth);
716
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
717
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
718
- }
719
- formatHotkey(hotkey) {
720
- const normalized = hotkey.trim().toLowerCase();
721
- if (!normalized)
722
- return hotkey;
723
- const parts = normalized.split('+').filter(Boolean);
724
- const map = {
725
- shift: '⇧',
726
- sh: '⇧',
727
- alt: '⌥',
728
- option: '⌥',
729
- opt: '⌥',
730
- ctrl: '⌃',
731
- control: '⌃',
732
- cmd: '⌘',
733
- meta: '⌘',
734
- };
735
- const formatted = parts
736
- .map((part) => {
737
- const symbol = map[part];
738
- if (symbol)
739
- return symbol;
740
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
741
- })
742
- .join('');
743
- return formatted || hotkey;
744
- }
745
- computeContextRemaining() {
746
- if (this.contextUsage === null) {
747
- return null;
748
- }
749
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
750
- }
751
- computeTokensRemaining() {
752
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
753
- return null;
754
- }
755
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
756
- return this.formatTokenCount(remaining);
757
- }
758
- formatElapsedLabel(seconds) {
759
- if (seconds < 60) {
760
- return `${seconds}s`;
761
- }
762
- const mins = Math.floor(seconds / 60);
763
- const secs = seconds % 60;
764
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
765
- }
766
- formatTokenCount(value) {
767
- if (!Number.isFinite(value)) {
768
- return `${value}`;
1110
+ // Override/warning status
1111
+ if (this.overrideStatusMessage) {
1112
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
769
1113
  }
770
- if (value >= 1_000_000) {
771
- return `${(value / 1_000_000).toFixed(1)}M`;
1114
+ // If idle with empty buffer, show quick shortcuts
1115
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
1116
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
772
1117
  }
773
- if (value >= 1_000) {
774
- return `${(value / 1_000).toFixed(1)}k`;
1118
+ // Multi-line indicator
1119
+ if (this.buffer.includes('\n')) {
1120
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
775
1121
  }
776
- return `${Math.round(value)}`;
777
- }
778
- visibleLength(value) {
779
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
780
- return value.replace(ansiPattern, '').length;
1122
+ if (parts.length === 0) {
1123
+ return ''; // Empty status bar when idle
1124
+ }
1125
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
1126
+ return joined.slice(0, maxWidth);
781
1127
  }
782
1128
  /**
783
- * Debug-only snapshot used by tests to assert rendered strings without
784
- * needing a TTY. Not used by production code.
1129
+ * Build mode controls line showing toggles and context info.
1130
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
1131
+ *
1132
+ * Layout: [toggles on left] ... [context info on right]
785
1133
  */
786
- getDebugUiSnapshot(width) {
787
- const cols = Math.max(8, width ?? this.getSize().cols);
788
- return {
789
- meta: this.buildMetaLines(cols - 2),
790
- controls: this.buildModeControls(cols),
791
- };
1134
+ buildModeControls(cols) {
1135
+ const maxWidth = cols - 2;
1136
+ // Use schema-defined colors for consistency
1137
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
1138
+ // Mode toggles with colors (following ModeControlsSchema)
1139
+ const toggles = [];
1140
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
1141
+ if (this.editMode === 'display-edits') {
1142
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
1143
+ }
1144
+ else {
1145
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
1146
+ }
1147
+ // Thinking mode (cyan when on) - per schema.thinkingMode
1148
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
1149
+ // Verification (green when on) - per schema.verificationMode
1150
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
1151
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
1152
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
1153
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
1154
+ // Context usage with color - per schema.contextUsage thresholds
1155
+ let rightPart = '';
1156
+ if (this.contextUsage !== null) {
1157
+ const rem = Math.max(0, 100 - this.contextUsage);
1158
+ // Thresholds: critical < 10%, warning < 25%
1159
+ if (rem < 10)
1160
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
1161
+ else if (rem < 25)
1162
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
1163
+ else
1164
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
1165
+ }
1166
+ // Calculate visible lengths (strip ANSI)
1167
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
1168
+ const leftLen = strip(leftPart).length;
1169
+ const rightLen = strip(rightPart).length;
1170
+ if (leftLen + rightLen < maxWidth - 4) {
1171
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
1172
+ }
1173
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
1174
+ return `${leftPart} ${rightPart}`;
1175
+ }
1176
+ return leftPart;
792
1177
  }
793
1178
  /**
794
1179
  * Force a re-render
@@ -811,19 +1196,17 @@ export class TerminalInput extends EventEmitter {
811
1196
  handleResize() {
812
1197
  this.lastRenderContent = '';
813
1198
  this.lastRenderCursor = -1;
814
- this.resetStreamingRenderThrottle();
815
1199
  // Re-clamp pinned header rows to the new terminal height
816
1200
  this.setPinnedHeaderLines(this.pinnedTopRows);
817
- if (this.scrollRegionActive) {
818
- this.disableScrollRegion();
819
- this.enableScrollRegion();
820
- }
821
1201
  this.scheduleRender();
822
1202
  }
823
1203
  /**
824
1204
  * Register with display's output interceptor to position cursor correctly.
825
1205
  * When scroll region is active, output needs to go to the scroll region,
826
1206
  * not the protected bottom area where the input is rendered.
1207
+ *
1208
+ * NOTE: With scroll region properly set, content naturally stays within
1209
+ * the region boundaries - no cursor manipulation needed per-write.
827
1210
  */
828
1211
  registerOutputInterceptor(display) {
829
1212
  if (this.outputInterceptorCleanup) {
@@ -831,59 +1214,23 @@ export class TerminalInput extends EventEmitter {
831
1214
  }
832
1215
  this.outputInterceptorCleanup = display.registerOutputInterceptor({
833
1216
  beforeWrite: () => {
834
- // Position cursor at scroll region bottom for content.
835
- // Terminal handles scrolling automatically when bottom is reached.
836
- if (this.scrollRegionActive) {
837
- const { rows } = this.getSize();
838
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
839
- this.write(ESC.SAVE);
840
- this.write(ESC.TO(scrollBottom, 1));
841
- }
1217
+ // Scroll region handles content containment automatically
1218
+ // No per-write cursor manipulation needed
842
1219
  },
843
1220
  afterWrite: () => {
844
- // Restore cursor position after content output.
845
- if (this.scrollRegionActive) {
846
- this.write(ESC.RESTORE);
847
- }
1221
+ // No cursor manipulation needed
848
1222
  },
849
1223
  });
850
1224
  }
851
1225
  /**
852
- * Write content directly into the scroll region (for banner, user prompts, etc.).
853
- * Content appears at scroll bottom and pushes previous content up.
854
- */
855
- writeToScrollRegion(content) {
856
- if (!content)
857
- return;
858
- // Ensure scroll region is active
859
- if (!this.scrollRegionActive) {
860
- this.enableScrollRegion();
861
- }
862
- const { rows } = this.getSize();
863
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
864
- // Save cursor, write at scroll bottom, restore
865
- this.write(ESC.SAVE);
866
- this.write(ESC.TO(scrollBottom, 1));
867
- this.write(content);
868
- this.write(ESC.RESTORE);
869
- }
870
- /**
871
- * Clear the scroll region and prepare for fresh content.
1226
+ * Set the banner renderer callback for unified UI initialization.
1227
+ * This callback is called when entering streaming mode for the first time,
1228
+ * to re-render the banner inside the scroll region.
1229
+ *
1230
+ * @param renderer Function that renders the banner and returns the number of lines written
872
1231
  */
873
- clearScrollRegion() {
874
- if (!this.scrollRegionActive) {
875
- this.enableScrollRegion();
876
- }
877
- const { rows, cols } = this.getSize();
878
- const scrollTop = Math.max(1, this.pinnedTopRows + 1);
879
- const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
880
- // Clear each line in the scroll region
881
- this.write(ESC.SAVE);
882
- for (let row = scrollTop; row <= scrollBottom; row++) {
883
- this.write(ESC.TO(row, 1));
884
- this.write(' '.repeat(cols));
885
- }
886
- this.write(ESC.RESTORE);
1232
+ setBannerRenderer(renderer) {
1233
+ this.bannerRenderer = renderer;
887
1234
  }
888
1235
  /**
889
1236
  * Dispose and clean up
@@ -891,6 +1238,11 @@ export class TerminalInput extends EventEmitter {
891
1238
  dispose() {
892
1239
  if (this.disposed)
893
1240
  return;
1241
+ // Clean up streaming render timer
1242
+ if (this.streamingRenderTimer) {
1243
+ clearInterval(this.streamingRenderTimer);
1244
+ this.streamingRenderTimer = null;
1245
+ }
894
1246
  // Clean up output interceptor
895
1247
  if (this.outputInterceptorCleanup) {
896
1248
  this.outputInterceptorCleanup();
@@ -898,7 +1250,6 @@ export class TerminalInput extends EventEmitter {
898
1250
  }
899
1251
  this.disposed = true;
900
1252
  this.enabled = false;
901
- this.resetStreamingRenderThrottle();
902
1253
  this.disableScrollRegion();
903
1254
  this.disableBracketedPaste();
904
1255
  this.buffer = '';
@@ -1004,7 +1355,22 @@ export class TerminalInput extends EventEmitter {
1004
1355
  this.toggleEditMode();
1005
1356
  return true;
1006
1357
  }
1007
- this.insertText(' ');
1358
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
1359
+ if (this.findPlaceholderAt(this.cursor)) {
1360
+ this.togglePasteExpansion();
1361
+ }
1362
+ else {
1363
+ this.toggleThinking();
1364
+ }
1365
+ return true;
1366
+ case 'escape':
1367
+ // Esc: interrupt if streaming, otherwise clear buffer
1368
+ if (this.mode === 'streaming') {
1369
+ this.emit('interrupt');
1370
+ }
1371
+ else if (this.buffer.length > 0) {
1372
+ this.clear();
1373
+ }
1008
1374
  return true;
1009
1375
  }
1010
1376
  return false;
@@ -1022,6 +1388,7 @@ export class TerminalInput extends EventEmitter {
1022
1388
  this.insertPlainText(chunk, insertPos);
1023
1389
  this.cursor = insertPos + chunk.length;
1024
1390
  this.emit('change', this.buffer);
1391
+ this.updateSuggestions();
1025
1392
  this.scheduleRender();
1026
1393
  }
1027
1394
  insertNewline() {
@@ -1046,6 +1413,7 @@ export class TerminalInput extends EventEmitter {
1046
1413
  this.cursor = Math.max(0, this.cursor - 1);
1047
1414
  }
1048
1415
  this.emit('change', this.buffer);
1416
+ this.updateSuggestions();
1049
1417
  this.scheduleRender();
1050
1418
  }
1051
1419
  deleteForward() {
@@ -1295,9 +1663,7 @@ export class TerminalInput extends EventEmitter {
1295
1663
  if (available <= 0)
1296
1664
  return;
1297
1665
  const chunk = clean.slice(0, available);
1298
- const isMultiline = isMultilinePaste(chunk);
1299
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1300
- if (isMultiline && !isShortMultiline) {
1666
+ if (isMultilinePaste(chunk)) {
1301
1667
  this.insertPastePlaceholder(chunk);
1302
1668
  }
1303
1669
  else {
@@ -1317,7 +1683,6 @@ export class TerminalInput extends EventEmitter {
1317
1683
  return;
1318
1684
  this.applyScrollRegion();
1319
1685
  this.scrollRegionActive = true;
1320
- this.forceRender();
1321
1686
  }
1322
1687
  disableScrollRegion() {
1323
1688
  if (!this.scrollRegionActive)
@@ -1468,19 +1833,17 @@ export class TerminalInput extends EventEmitter {
1468
1833
  this.shiftPlaceholders(position, text.length);
1469
1834
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1470
1835
  }
1471
- shouldInlineMultiline(content) {
1472
- const lines = content.split('\n').length;
1473
- const maxInlineLines = 4;
1474
- const maxInlineChars = 240;
1475
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1476
- }
1477
1836
  findPlaceholderAt(position) {
1478
1837
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1479
1838
  }
1480
- buildPlaceholder(lineCount) {
1839
+ buildPlaceholder(summary) {
1481
1840
  const id = ++this.pasteCounter;
1482
- const plural = lineCount === 1 ? '' : 's';
1483
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1841
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1842
+ // Show first line preview (truncated)
1843
+ const preview = summary.preview.length > 30
1844
+ ? `${summary.preview.slice(0, 30)}...`
1845
+ : summary.preview;
1846
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1484
1847
  return { id, placeholder };
1485
1848
  }
1486
1849
  insertPastePlaceholder(content) {
@@ -1488,21 +1851,67 @@ export class TerminalInput extends EventEmitter {
1488
1851
  if (available <= 0)
1489
1852
  return;
1490
1853
  const cleanContent = content.slice(0, available);
1491
- const lineCount = cleanContent.split('\n').length;
1492
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1854
+ const summary = generatePasteSummary(cleanContent);
1855
+ // For short pastes (< 5 lines), show full content instead of placeholder
1856
+ if (summary.lineCount < 5) {
1857
+ const placeholder = this.findPlaceholderAt(this.cursor);
1858
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1859
+ this.insertPlainText(cleanContent, insertPos);
1860
+ this.cursor = insertPos + cleanContent.length;
1861
+ return;
1862
+ }
1863
+ const { id, placeholder } = this.buildPlaceholder(summary);
1493
1864
  const insertPos = this.cursor;
1494
1865
  this.shiftPlaceholders(insertPos, placeholder.length);
1495
1866
  this.pastePlaceholders.push({
1496
1867
  id,
1497
1868
  content: cleanContent,
1498
- lineCount,
1869
+ lineCount: summary.lineCount,
1499
1870
  placeholder,
1500
1871
  start: insertPos,
1501
1872
  end: insertPos + placeholder.length,
1873
+ summary,
1874
+ expanded: false,
1502
1875
  });
1503
1876
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1504
1877
  this.cursor = insertPos + placeholder.length;
1505
1878
  }
1879
+ /**
1880
+ * Toggle expansion of a paste placeholder at the current cursor position.
1881
+ * When expanded, shows first 3 and last 2 lines of the content.
1882
+ */
1883
+ togglePasteExpansion() {
1884
+ const placeholder = this.findPlaceholderAt(this.cursor);
1885
+ if (!placeholder)
1886
+ return false;
1887
+ placeholder.expanded = !placeholder.expanded;
1888
+ // Update the placeholder text in buffer
1889
+ const newPlaceholder = placeholder.expanded
1890
+ ? this.buildExpandedPlaceholder(placeholder)
1891
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1892
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1893
+ // Update buffer
1894
+ this.buffer =
1895
+ this.buffer.slice(0, placeholder.start) +
1896
+ newPlaceholder +
1897
+ this.buffer.slice(placeholder.end);
1898
+ // Update placeholder tracking
1899
+ placeholder.placeholder = newPlaceholder;
1900
+ placeholder.end = placeholder.start + newPlaceholder.length;
1901
+ // Shift other placeholders
1902
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1903
+ this.scheduleRender();
1904
+ return true;
1905
+ }
1906
+ buildExpandedPlaceholder(ph) {
1907
+ const lines = ph.content.split('\n');
1908
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1909
+ const lastLines = lines.length > 5
1910
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1911
+ : '';
1912
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1913
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1914
+ }
1506
1915
  deletePlaceholder(placeholder) {
1507
1916
  const length = placeholder.end - placeholder.start;
1508
1917
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1510,11 +1919,7 @@ export class TerminalInput extends EventEmitter {
1510
1919
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1511
1920
  this.cursor = placeholder.start;
1512
1921
  }
1513
- updateContextUsage(value, autoCompactThreshold) {
1514
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1515
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1516
- this.contextAutoCompactThreshold = boundedThreshold;
1517
- }
1922
+ updateContextUsage(value) {
1518
1923
  if (value === null || !Number.isFinite(value)) {
1519
1924
  this.contextUsage = null;
1520
1925
  }
@@ -1541,22 +1946,6 @@ export class TerminalInput extends EventEmitter {
1541
1946
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1542
1947
  this.setEditMode(next);
1543
1948
  }
1544
- scheduleStreamingRender(delayMs) {
1545
- if (this.streamingRenderTimer)
1546
- return;
1547
- const wait = Math.max(16, delayMs);
1548
- this.streamingRenderTimer = setTimeout(() => {
1549
- this.streamingRenderTimer = null;
1550
- this.render();
1551
- }, wait);
1552
- }
1553
- resetStreamingRenderThrottle() {
1554
- if (this.streamingRenderTimer) {
1555
- clearTimeout(this.streamingRenderTimer);
1556
- this.streamingRenderTimer = null;
1557
- }
1558
- this.lastStreamingRender = 0;
1559
- }
1560
1949
  scheduleRender() {
1561
1950
  if (!this.canRender())
1562
1951
  return;