erosolar-cli 1.7.264 → 1.7.266

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 (321) 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/capabilities/agentSpawningCapability.d.ts.map +1 -1
  55. package/dist/capabilities/agentSpawningCapability.js +31 -56
  56. package/dist/capabilities/agentSpawningCapability.js.map +1 -1
  57. package/dist/capabilities/securityTestingCapability.d.ts +13 -0
  58. package/dist/capabilities/securityTestingCapability.d.ts.map +1 -0
  59. package/dist/capabilities/securityTestingCapability.js +25 -0
  60. package/dist/capabilities/securityTestingCapability.js.map +1 -0
  61. package/dist/contracts/agent-schemas.json +15 -0
  62. package/dist/contracts/tools.schema.json +9 -0
  63. package/dist/core/agent.d.ts +2 -2
  64. package/dist/core/agent.d.ts.map +1 -1
  65. package/dist/core/agent.js.map +1 -1
  66. package/dist/core/aiFlowOptimizer.d.ts +26 -0
  67. package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
  68. package/dist/core/aiFlowOptimizer.js +31 -0
  69. package/dist/core/aiFlowOptimizer.js.map +1 -0
  70. package/dist/core/aiOptimizationEngine.d.ts +158 -0
  71. package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
  72. package/dist/core/aiOptimizationEngine.js +428 -0
  73. package/dist/core/aiOptimizationEngine.js.map +1 -0
  74. package/dist/core/aiOptimizationIntegration.d.ts +93 -0
  75. package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
  76. package/dist/core/aiOptimizationIntegration.js +250 -0
  77. package/dist/core/aiOptimizationIntegration.js.map +1 -0
  78. package/dist/core/customCommands.d.ts +0 -1
  79. package/dist/core/customCommands.d.ts.map +1 -1
  80. package/dist/core/customCommands.js +0 -3
  81. package/dist/core/customCommands.js.map +1 -1
  82. package/dist/core/enhancedErrorRecovery.d.ts +100 -0
  83. package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
  84. package/dist/core/enhancedErrorRecovery.js +345 -0
  85. package/dist/core/enhancedErrorRecovery.js.map +1 -0
  86. package/dist/core/hooksSystem.d.ts +65 -0
  87. package/dist/core/hooksSystem.d.ts.map +1 -0
  88. package/dist/core/hooksSystem.js +273 -0
  89. package/dist/core/hooksSystem.js.map +1 -0
  90. package/dist/core/memorySystem.d.ts +48 -0
  91. package/dist/core/memorySystem.d.ts.map +1 -0
  92. package/dist/core/memorySystem.js +271 -0
  93. package/dist/core/memorySystem.js.map +1 -0
  94. package/dist/core/toolPreconditions.d.ts.map +1 -1
  95. package/dist/core/toolPreconditions.js +14 -0
  96. package/dist/core/toolPreconditions.js.map +1 -1
  97. package/dist/core/toolRuntime.d.ts +1 -22
  98. package/dist/core/toolRuntime.d.ts.map +1 -1
  99. package/dist/core/toolRuntime.js +5 -0
  100. package/dist/core/toolRuntime.js.map +1 -1
  101. package/dist/core/toolValidation.d.ts.map +1 -1
  102. package/dist/core/toolValidation.js +3 -14
  103. package/dist/core/toolValidation.js.map +1 -1
  104. package/dist/core/unified/errors.d.ts +189 -0
  105. package/dist/core/unified/errors.d.ts.map +1 -0
  106. package/dist/core/unified/errors.js +497 -0
  107. package/dist/core/unified/errors.js.map +1 -0
  108. package/dist/core/unified/index.d.ts +19 -0
  109. package/dist/core/unified/index.d.ts.map +1 -0
  110. package/dist/core/unified/index.js +68 -0
  111. package/dist/core/unified/index.js.map +1 -0
  112. package/dist/core/unified/schema.d.ts +101 -0
  113. package/dist/core/unified/schema.d.ts.map +1 -0
  114. package/dist/core/unified/schema.js +350 -0
  115. package/dist/core/unified/schema.js.map +1 -0
  116. package/dist/core/unified/toolRuntime.d.ts +179 -0
  117. package/dist/core/unified/toolRuntime.d.ts.map +1 -0
  118. package/dist/core/unified/toolRuntime.js +517 -0
  119. package/dist/core/unified/toolRuntime.js.map +1 -0
  120. package/dist/core/unified/tools.d.ts +127 -0
  121. package/dist/core/unified/tools.d.ts.map +1 -0
  122. package/dist/core/unified/tools.js +1333 -0
  123. package/dist/core/unified/tools.js.map +1 -0
  124. package/dist/core/unified/types.d.ts +352 -0
  125. package/dist/core/unified/types.d.ts.map +1 -0
  126. package/dist/core/unified/types.js +12 -0
  127. package/dist/core/unified/types.js.map +1 -0
  128. package/dist/core/unified/version.d.ts +209 -0
  129. package/dist/core/unified/version.d.ts.map +1 -0
  130. package/dist/core/unified/version.js +454 -0
  131. package/dist/core/unified/version.js.map +1 -0
  132. package/dist/core/validationRunner.d.ts +3 -1
  133. package/dist/core/validationRunner.d.ts.map +1 -1
  134. package/dist/core/validationRunner.js.map +1 -1
  135. package/dist/headless/headlessApp.d.ts.map +1 -1
  136. package/dist/headless/headlessApp.js +0 -21
  137. package/dist/headless/headlessApp.js.map +1 -1
  138. package/dist/mcp/sseClient.d.ts.map +1 -1
  139. package/dist/mcp/sseClient.js +18 -9
  140. package/dist/mcp/sseClient.js.map +1 -1
  141. package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
  142. package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
  143. package/dist/plugins/tools/build/buildPlugin.js +10 -4
  144. package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
  145. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  146. package/dist/plugins/tools/nodeDefaults.js +2 -0
  147. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  148. package/dist/plugins/tools/security/securityPlugin.d.ts +3 -0
  149. package/dist/plugins/tools/security/securityPlugin.d.ts.map +1 -0
  150. package/dist/plugins/tools/security/securityPlugin.js +12 -0
  151. package/dist/plugins/tools/security/securityPlugin.js.map +1 -0
  152. package/dist/runtime/agentSession.d.ts +2 -2
  153. package/dist/runtime/agentSession.d.ts.map +1 -1
  154. package/dist/runtime/agentSession.js +2 -2
  155. package/dist/runtime/agentSession.js.map +1 -1
  156. package/dist/security/active-stack-security.d.ts +112 -0
  157. package/dist/security/active-stack-security.d.ts.map +1 -0
  158. package/dist/security/active-stack-security.js +296 -0
  159. package/dist/security/active-stack-security.js.map +1 -0
  160. package/dist/security/advanced-persistence-research.d.ts +92 -0
  161. package/dist/security/advanced-persistence-research.d.ts.map +1 -0
  162. package/dist/security/advanced-persistence-research.js +195 -0
  163. package/dist/security/advanced-persistence-research.js.map +1 -0
  164. package/dist/security/advanced-targeting.d.ts +119 -0
  165. package/dist/security/advanced-targeting.d.ts.map +1 -0
  166. package/dist/security/advanced-targeting.js +233 -0
  167. package/dist/security/advanced-targeting.js.map +1 -0
  168. package/dist/security/assessment/vulnerabilityAssessment.d.ts +104 -0
  169. package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +1 -0
  170. package/dist/security/assessment/vulnerabilityAssessment.js +315 -0
  171. package/dist/security/assessment/vulnerabilityAssessment.js.map +1 -0
  172. package/dist/security/authorization/securityAuthorization.d.ts +88 -0
  173. package/dist/security/authorization/securityAuthorization.d.ts.map +1 -0
  174. package/dist/security/authorization/securityAuthorization.js +172 -0
  175. package/dist/security/authorization/securityAuthorization.js.map +1 -0
  176. package/dist/security/comprehensive-targeting.d.ts +85 -0
  177. package/dist/security/comprehensive-targeting.d.ts.map +1 -0
  178. package/dist/security/comprehensive-targeting.js +438 -0
  179. package/dist/security/comprehensive-targeting.js.map +1 -0
  180. package/dist/security/global-security-integration.d.ts +91 -0
  181. package/dist/security/global-security-integration.d.ts.map +1 -0
  182. package/dist/security/global-security-integration.js +218 -0
  183. package/dist/security/global-security-integration.js.map +1 -0
  184. package/dist/security/index.d.ts +38 -0
  185. package/dist/security/index.d.ts.map +1 -0
  186. package/dist/security/index.js +47 -0
  187. package/dist/security/index.js.map +1 -0
  188. package/dist/security/persistence-analyzer.d.ts +56 -0
  189. package/dist/security/persistence-analyzer.d.ts.map +1 -0
  190. package/dist/security/persistence-analyzer.js +187 -0
  191. package/dist/security/persistence-analyzer.js.map +1 -0
  192. package/dist/security/persistence-cli.d.ts +36 -0
  193. package/dist/security/persistence-cli.d.ts.map +1 -0
  194. package/dist/security/persistence-cli.js +160 -0
  195. package/dist/security/persistence-cli.js.map +1 -0
  196. package/dist/security/persistence-research.d.ts +92 -0
  197. package/dist/security/persistence-research.d.ts.map +1 -0
  198. package/dist/security/persistence-research.js +364 -0
  199. package/dist/security/persistence-research.js.map +1 -0
  200. package/dist/security/research/persistenceResearch.d.ts +97 -0
  201. package/dist/security/research/persistenceResearch.d.ts.map +1 -0
  202. package/dist/security/research/persistenceResearch.js +282 -0
  203. package/dist/security/research/persistenceResearch.js.map +1 -0
  204. package/dist/security/security-integration.d.ts +74 -0
  205. package/dist/security/security-integration.d.ts.map +1 -0
  206. package/dist/security/security-integration.js +137 -0
  207. package/dist/security/security-integration.js.map +1 -0
  208. package/dist/security/security-testing-framework.d.ts +112 -0
  209. package/dist/security/security-testing-framework.d.ts.map +1 -0
  210. package/dist/security/security-testing-framework.js +364 -0
  211. package/dist/security/security-testing-framework.js.map +1 -0
  212. package/dist/security/simulation/attackSimulation.d.ts +93 -0
  213. package/dist/security/simulation/attackSimulation.d.ts.map +1 -0
  214. package/dist/security/simulation/attackSimulation.js +341 -0
  215. package/dist/security/simulation/attackSimulation.js.map +1 -0
  216. package/dist/security/strategic-operations.d.ts +100 -0
  217. package/dist/security/strategic-operations.d.ts.map +1 -0
  218. package/dist/security/strategic-operations.js +276 -0
  219. package/dist/security/strategic-operations.js.map +1 -0
  220. package/dist/security/tool-security-wrapper.d.ts +58 -0
  221. package/dist/security/tool-security-wrapper.d.ts.map +1 -0
  222. package/dist/security/tool-security-wrapper.js +156 -0
  223. package/dist/security/tool-security-wrapper.js.map +1 -0
  224. package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
  225. package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
  226. package/dist/shell/claudeCodeStreamHandler.js +322 -0
  227. package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
  228. package/dist/shell/inputQueueManager.d.ts +144 -0
  229. package/dist/shell/inputQueueManager.d.ts.map +1 -0
  230. package/dist/shell/inputQueueManager.js +290 -0
  231. package/dist/shell/inputQueueManager.js.map +1 -0
  232. package/dist/shell/interactiveShell.d.ts +7 -11
  233. package/dist/shell/interactiveShell.d.ts.map +1 -1
  234. package/dist/shell/interactiveShell.js +157 -190
  235. package/dist/shell/interactiveShell.js.map +1 -1
  236. package/dist/shell/metricsTracker.d.ts +60 -0
  237. package/dist/shell/metricsTracker.d.ts.map +1 -0
  238. package/dist/shell/metricsTracker.js +119 -0
  239. package/dist/shell/metricsTracker.js.map +1 -0
  240. package/dist/shell/shellApp.d.ts +0 -2
  241. package/dist/shell/shellApp.d.ts.map +1 -1
  242. package/dist/shell/shellApp.js +1 -36
  243. package/dist/shell/shellApp.js.map +1 -1
  244. package/dist/shell/streamingOutputManager.d.ts +115 -0
  245. package/dist/shell/streamingOutputManager.d.ts.map +1 -0
  246. package/dist/shell/streamingOutputManager.js +225 -0
  247. package/dist/shell/streamingOutputManager.js.map +1 -0
  248. package/dist/shell/systemPrompt.d.ts.map +1 -1
  249. package/dist/shell/systemPrompt.js +4 -1
  250. package/dist/shell/systemPrompt.js.map +1 -1
  251. package/dist/shell/terminalInput.d.ts +153 -67
  252. package/dist/shell/terminalInput.d.ts.map +1 -1
  253. package/dist/shell/terminalInput.js +726 -454
  254. package/dist/shell/terminalInput.js.map +1 -1
  255. package/dist/shell/terminalInputAdapter.d.ts +25 -20
  256. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  257. package/dist/shell/terminalInputAdapter.js +36 -14
  258. package/dist/shell/terminalInputAdapter.js.map +1 -1
  259. package/dist/subagents/taskRunner.d.ts +1 -7
  260. package/dist/subagents/taskRunner.d.ts.map +1 -1
  261. package/dist/subagents/taskRunner.js +47 -180
  262. package/dist/subagents/taskRunner.js.map +1 -1
  263. package/dist/tools/securityTools.d.ts +22 -0
  264. package/dist/tools/securityTools.d.ts.map +1 -0
  265. package/dist/tools/securityTools.js +448 -0
  266. package/dist/tools/securityTools.js.map +1 -0
  267. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  268. package/dist/ui/ShellUIAdapter.js +12 -13
  269. package/dist/ui/ShellUIAdapter.js.map +1 -1
  270. package/dist/ui/display.d.ts +0 -19
  271. package/dist/ui/display.d.ts.map +1 -1
  272. package/dist/ui/display.js +33 -131
  273. package/dist/ui/display.js.map +1 -1
  274. package/dist/ui/persistentPrompt.d.ts +50 -0
  275. package/dist/ui/persistentPrompt.d.ts.map +1 -0
  276. package/dist/ui/persistentPrompt.js +92 -0
  277. package/dist/ui/persistentPrompt.js.map +1 -0
  278. package/dist/ui/terminalUISchema.d.ts +195 -0
  279. package/dist/ui/terminalUISchema.d.ts.map +1 -0
  280. package/dist/ui/terminalUISchema.js +113 -0
  281. package/dist/ui/terminalUISchema.js.map +1 -0
  282. package/dist/ui/theme.d.ts.map +1 -1
  283. package/dist/ui/theme.js +8 -6
  284. package/dist/ui/theme.js.map +1 -1
  285. package/dist/ui/toolDisplay.d.ts +158 -0
  286. package/dist/ui/toolDisplay.d.ts.map +1 -1
  287. package/dist/ui/toolDisplay.js +348 -0
  288. package/dist/ui/toolDisplay.js.map +1 -1
  289. package/dist/ui/unified/layout.d.ts +0 -1
  290. package/dist/ui/unified/layout.d.ts.map +1 -1
  291. package/dist/ui/unified/layout.js +25 -15
  292. package/dist/ui/unified/layout.js.map +1 -1
  293. package/package.json +1 -1
  294. package/scripts/deploy-security-capabilities.js +178 -0
  295. package/dist/core/hooks.d.ts +0 -113
  296. package/dist/core/hooks.d.ts.map +0 -1
  297. package/dist/core/hooks.js +0 -267
  298. package/dist/core/hooks.js.map +0 -1
  299. package/dist/core/metricsTracker.d.ts +0 -122
  300. package/dist/core/metricsTracker.d.ts.map +0 -1
  301. package/dist/core/metricsTracker.js.map +0 -1
  302. package/dist/core/securityAssessment.d.ts +0 -91
  303. package/dist/core/securityAssessment.d.ts.map +0 -1
  304. package/dist/core/securityAssessment.js +0 -580
  305. package/dist/core/securityAssessment.js.map +0 -1
  306. package/dist/core/verification.d.ts +0 -137
  307. package/dist/core/verification.d.ts.map +0 -1
  308. package/dist/core/verification.js +0 -323
  309. package/dist/core/verification.js.map +0 -1
  310. package/dist/subagents/agentConfig.d.ts +0 -27
  311. package/dist/subagents/agentConfig.d.ts.map +0 -1
  312. package/dist/subagents/agentConfig.js +0 -89
  313. package/dist/subagents/agentConfig.js.map +0 -1
  314. package/dist/subagents/agentRegistry.d.ts +0 -33
  315. package/dist/subagents/agentRegistry.d.ts.map +0 -1
  316. package/dist/subagents/agentRegistry.js +0 -162
  317. package/dist/subagents/agentRegistry.js.map +0 -1
  318. package/dist/utils/frontmatter.d.ts +0 -10
  319. package/dist/utils/frontmatter.d.ts.map +0 -1
  320. package/dist/utils/frontmatter.js +0 -78
  321. 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
@@ -69,11 +67,6 @@ export class TerminalInput extends EventEmitter {
69
67
  statusMessage = null;
70
68
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
71
69
  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
70
  reservedLines = 2;
78
71
  scrollRegionActive = false;
79
72
  lastRenderContent = '';
@@ -81,35 +74,47 @@ export class TerminalInput extends EventEmitter {
81
74
  renderDirty = false;
82
75
  isRendering = false;
83
76
  pinnedTopRows = 0;
77
+ inlineAnchorRow = null;
78
+ inlineLayout = false;
79
+ anchorProvider = null;
80
+ // Flow mode: when true, renders inline after content (no absolute positioning)
81
+ flowMode = true;
82
+ flowModeRenderedLines = 0; // Track lines rendered for clearing
83
+ contentEndRow = 0; // Row where content ends (for idle mode positioning)
84
+ // Command suggestions (Claude Code style auto-complete)
85
+ commandSuggestions = [];
86
+ filteredSuggestions = [];
87
+ selectedSuggestionIndex = 0;
88
+ showSuggestions = false;
89
+ maxVisibleSuggestions = 10;
84
90
  // Lifecycle
85
91
  disposed = false;
86
92
  enabled = true;
87
93
  contextUsage = null;
88
- contextAutoCompactThreshold = 90;
89
- thinkingModeLabel = null;
90
94
  editMode = 'display-edits';
91
95
  verificationEnabled = true;
92
96
  autoContinueEnabled = false;
93
97
  verificationHotkey = 'alt+v';
94
98
  autoContinueHotkey = 'alt+c';
95
- thinkingHotkey = '/thinking';
96
- modelLabel = null;
97
- providerLabel = null;
98
99
  // Output interceptor cleanup
99
100
  outputInterceptorCleanup;
100
- // Streaming render throttle
101
- lastStreamingRender = 0;
102
- streamingRenderInterval = 250; // ms between renders during streaming
101
+ // Metrics tracking for status bar
102
+ streamingStartTime = null;
103
+ tokensUsed = 0;
104
+ thinkingEnabled = true;
105
+ modelInfo = null; // Provider · Model info
106
+ // Streaming input area render timer (updates elapsed time display)
103
107
  streamingRenderTimer = null;
104
108
  constructor(writeStream = process.stdout, config = {}) {
105
109
  super();
106
110
  this.out = writeStream;
111
+ // Use schema defaults for configuration consistency
107
112
  this.config = {
108
- maxLines: config.maxLines ?? 1000,
109
- maxLength: config.maxLength ?? 10000,
113
+ maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
114
+ maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
110
115
  maxQueueSize: config.maxQueueSize ?? 100,
111
- promptChar: config.promptChar ?? '> ',
112
- continuationChar: config.continuationChar ?? '│ ',
116
+ promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
117
+ continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
113
118
  };
114
119
  }
115
120
  // ===========================================================================
@@ -188,6 +193,11 @@ export class TerminalInput extends EventEmitter {
188
193
  if (handled)
189
194
  return;
190
195
  }
196
+ // Handle '?' for help hint (if buffer is empty)
197
+ if (str === '?' && this.buffer.length === 0) {
198
+ this.emit('showHelp');
199
+ return;
200
+ }
191
201
  // Insert printable characters
192
202
  if (str && !key?.ctrl && !key?.meta) {
193
203
  this.insertText(str);
@@ -196,38 +206,388 @@ export class TerminalInput extends EventEmitter {
196
206
  /**
197
207
  * Set the input mode
198
208
  *
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.
209
+ * Streaming mode disables scroll region and lets content flow naturally.
210
+ * The input area will be re-rendered after streaming ends at wherever
211
+ * the cursor is (below the streamed content).
201
212
  */
202
213
  setMode(mode) {
203
214
  const prevMode = this.mode;
204
215
  this.mode = mode;
205
216
  if (mode === 'streaming' && prevMode !== 'streaming') {
206
- // Keep scroll region active so status/prompt stay pinned while streaming
207
- this.resetStreamingRenderThrottle();
208
- this.enableScrollRegion();
217
+ // Track streaming start time for elapsed display
218
+ this.streamingStartTime = Date.now();
219
+ // NO scroll regions - content flows naturally to terminal scrollback
220
+ // Input area renders at absolute bottom using cursor save/restore
221
+ this.pinnedTopRows = 0;
222
+ this.reservedLines = 5; // Reserve space for input area at bottom
223
+ // Disable any existing scroll region
224
+ this.disableScrollRegion();
225
+ // Initial render of input area at bottom (with lock)
226
+ writeLock.withLock(() => {
227
+ this.renderStreamingInputArea();
228
+ }, 'terminalInput.streamingInit');
229
+ // Start timer to update streaming status and re-render input area
230
+ this.streamingRenderTimer = setInterval(() => {
231
+ if (this.mode === 'streaming') {
232
+ // Use writeLock to prevent race with streaming output
233
+ writeLock.withLock(() => {
234
+ this.updateStreamingStatus();
235
+ this.renderStreamingInputArea();
236
+ }, 'terminalInput.streamingUpdate');
237
+ }
238
+ }, 1000);
209
239
  this.renderDirty = true;
210
- this.render();
211
240
  }
212
241
  else if (mode !== 'streaming' && prevMode === 'streaming') {
213
- // Streaming ended - render the input area
214
- this.resetStreamingRenderThrottle();
215
- this.enableScrollRegion();
216
- this.forceRender();
242
+ // Stop streaming render timer
243
+ if (this.streamingRenderTimer) {
244
+ clearInterval(this.streamingRenderTimer);
245
+ this.streamingRenderTimer = null;
246
+ }
247
+ // Reset streaming time
248
+ this.streamingStartTime = null;
249
+ this.pinnedTopRows = 0;
250
+ // Ensure no scroll region is active
251
+ this.disableScrollRegion();
252
+ // Reset flow mode tracking
253
+ this.flowModeRenderedLines = 0;
254
+ // Render input area using unified method (same as streaming, but normal mode)
255
+ writeLock.withLock(() => {
256
+ this.renderPinnedInputArea();
257
+ }, 'terminalInput.streamingEnd');
258
+ }
259
+ }
260
+ /**
261
+ * Update streaming status label (called by timer)
262
+ */
263
+ updateStreamingStatus() {
264
+ if (this.mode !== 'streaming' || !this.streamingStartTime)
265
+ return;
266
+ // Calculate elapsed time
267
+ const elapsed = Date.now() - this.streamingStartTime;
268
+ const seconds = Math.floor(elapsed / 1000);
269
+ const minutes = Math.floor(seconds / 60);
270
+ const secs = seconds % 60;
271
+ // Format elapsed time
272
+ let elapsedStr;
273
+ if (minutes > 0) {
274
+ elapsedStr = `${minutes}m ${secs}s`;
275
+ }
276
+ else {
277
+ elapsedStr = `${secs}s`;
278
+ }
279
+ // Update streaming label
280
+ this.streamingLabel = `Streaming ${elapsedStr}`;
281
+ }
282
+ /**
283
+ * Render input area - unified for streaming and normal modes.
284
+ *
285
+ * In streaming mode: renders at absolute bottom, uses cursor save/restore
286
+ * In normal mode: renders right after the banner (pinnedTopRows + 1)
287
+ */
288
+ renderPinnedInputArea() {
289
+ const { rows, cols } = this.getSize();
290
+ const maxWidth = Math.max(8, cols - 4);
291
+ const divider = renderDivider(cols - 2);
292
+ const isStreaming = this.mode === 'streaming';
293
+ // Wrap buffer into display lines (multi-line support)
294
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
295
+ const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
296
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
297
+ const displayLines = Math.min(lines.length, maxVisible);
298
+ // Calculate display window (keep cursor visible)
299
+ let startLine = 0;
300
+ if (lines.length > displayLines) {
301
+ startLine = Math.max(0, cursorLine - displayLines + 1);
302
+ startLine = Math.min(startLine, lines.length - displayLines);
303
+ }
304
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
305
+ const adjustedCursorLine = cursorLine - startLine;
306
+ // Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
307
+ const hasModelInfo = !!this.modelInfo;
308
+ const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
309
+ // Save cursor position during streaming (so content flow resumes correctly)
310
+ if (isStreaming) {
311
+ this.write(ESC.SAVE);
312
+ }
313
+ this.write(ESC.HIDE);
314
+ this.write(ESC.RESET);
315
+ // Calculate start row based on mode:
316
+ // - Streaming: absolute bottom (rows - totalHeight + 1)
317
+ // - Normal: right after content (contentEndRow + 1)
318
+ let currentRow;
319
+ if (isStreaming) {
320
+ currentRow = Math.max(1, rows - totalHeight + 1);
321
+ }
322
+ else {
323
+ // In normal mode, render right after content
324
+ // Use contentEndRow if set, otherwise use pinnedTopRows
325
+ const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
326
+ currentRow = Math.max(1, contentRow + 1);
327
+ }
328
+ let finalRow = currentRow;
329
+ let finalCol = 3;
330
+ // Clear from current position to end of screen to remove any "ghost" content
331
+ this.write(ESC.TO(currentRow, 1));
332
+ this.write(ESC.CLEAR_TO_END);
333
+ // Status bar
334
+ this.write(ESC.TO(currentRow, 1));
335
+ this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
336
+ currentRow++;
337
+ // Model info line (if set) - displayed below status, above input
338
+ if (hasModelInfo) {
339
+ const { dim: DIM, reset: R } = UI_COLORS;
340
+ this.write(ESC.TO(currentRow, 1));
341
+ // Build model info with context usage
342
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
343
+ if (this.contextUsage !== null) {
344
+ const rem = Math.max(0, 100 - this.contextUsage);
345
+ if (rem < 10)
346
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
347
+ else if (rem < 25)
348
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
349
+ else
350
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
351
+ }
352
+ this.write(modelLine);
353
+ currentRow++;
354
+ }
355
+ // Top divider
356
+ this.write(ESC.TO(currentRow, 1));
357
+ this.write(divider);
358
+ currentRow++;
359
+ // Input lines with background styling
360
+ for (let i = 0; i < visibleLines.length; i++) {
361
+ this.write(ESC.TO(currentRow, 1));
362
+ const line = visibleLines[i] ?? '';
363
+ const absoluteLineIdx = startLine + i;
364
+ const isFirstLine = absoluteLineIdx === 0;
365
+ const isCursorLine = i === adjustedCursorLine;
366
+ // Background
367
+ this.write(ESC.BG_DARK);
368
+ // Prompt prefix
369
+ this.write(ESC.DIM);
370
+ this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
371
+ this.write(ESC.RESET);
372
+ this.write(ESC.BG_DARK);
373
+ if (isCursorLine) {
374
+ const col = Math.min(cursorCol, line.length);
375
+ const before = line.slice(0, col);
376
+ const at = col < line.length ? line[col] : ' ';
377
+ const after = col < line.length ? line.slice(col + 1) : '';
378
+ this.write(before);
379
+ this.write(ESC.REVERSE + ESC.BOLD);
380
+ this.write(at);
381
+ this.write(ESC.RESET + ESC.BG_DARK);
382
+ this.write(after);
383
+ finalRow = currentRow;
384
+ finalCol = this.config.promptChar.length + col + 1;
385
+ }
386
+ else {
387
+ this.write(line);
388
+ }
389
+ // Pad to edge
390
+ const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
391
+ const padding = Math.max(0, cols - lineLen - 1);
392
+ if (padding > 0)
393
+ this.write(' '.repeat(padding));
394
+ this.write(ESC.RESET);
395
+ currentRow++;
396
+ }
397
+ // Bottom divider
398
+ this.write(ESC.TO(currentRow, 1));
399
+ this.write(divider);
400
+ currentRow++;
401
+ // Mode controls line
402
+ this.write(ESC.TO(currentRow, 1));
403
+ this.write(this.buildModeControls(cols));
404
+ // Restore cursor position during streaming, or show cursor in normal mode
405
+ if (isStreaming) {
406
+ this.write(ESC.RESTORE);
407
+ }
408
+ else {
409
+ // Position cursor in input area
410
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
411
+ this.write(ESC.SHOW);
412
+ }
413
+ // Update reserved lines for scroll region calculations
414
+ this.updateReservedLines(totalHeight);
415
+ }
416
+ /**
417
+ * Render input area during streaming (alias for unified method)
418
+ */
419
+ renderStreamingInputArea() {
420
+ this.renderPinnedInputArea();
421
+ }
422
+ /**
423
+ * Enable or disable flow mode.
424
+ * In flow mode, the input renders immediately after content (wherever cursor is).
425
+ * When disabled, input renders at the absolute bottom of terminal.
426
+ */
427
+ setFlowMode(enabled) {
428
+ if (this.flowMode === enabled)
429
+ return;
430
+ this.flowMode = enabled;
431
+ this.renderDirty = true;
432
+ this.scheduleRender();
433
+ }
434
+ /**
435
+ * Check if flow mode is enabled.
436
+ */
437
+ isFlowMode() {
438
+ return this.flowMode;
439
+ }
440
+ /**
441
+ * Set the row where content ends (for idle mode positioning).
442
+ * Input area will render starting from this row + 1.
443
+ */
444
+ setContentEndRow(row) {
445
+ this.contentEndRow = Math.max(0, row);
446
+ this.renderDirty = true;
447
+ this.scheduleRender();
448
+ }
449
+ /**
450
+ * Set available slash commands for auto-complete suggestions.
451
+ */
452
+ setCommands(commands) {
453
+ this.commandSuggestions = commands;
454
+ this.updateSuggestions();
455
+ }
456
+ /**
457
+ * Update filtered suggestions based on current input.
458
+ */
459
+ updateSuggestions() {
460
+ const input = this.buffer.trim();
461
+ // Only show suggestions when input starts with "/"
462
+ if (!input.startsWith('/')) {
463
+ this.showSuggestions = false;
464
+ this.filteredSuggestions = [];
465
+ this.selectedSuggestionIndex = 0;
466
+ return;
467
+ }
468
+ const query = input.toLowerCase();
469
+ this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
470
+ cmd.command.toLowerCase().includes(query.slice(1)));
471
+ // Show suggestions if we have matches
472
+ this.showSuggestions = this.filteredSuggestions.length > 0;
473
+ // Keep selection in bounds
474
+ if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
475
+ this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
217
476
  }
218
477
  }
478
+ /**
479
+ * Select next suggestion (arrow down / tab).
480
+ */
481
+ selectNextSuggestion() {
482
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
483
+ return;
484
+ this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
485
+ this.renderDirty = true;
486
+ this.scheduleRender();
487
+ }
488
+ /**
489
+ * Select previous suggestion (arrow up / shift+tab).
490
+ */
491
+ selectPrevSuggestion() {
492
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
493
+ return;
494
+ this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
495
+ ? this.filteredSuggestions.length - 1
496
+ : this.selectedSuggestionIndex - 1;
497
+ this.renderDirty = true;
498
+ this.scheduleRender();
499
+ }
500
+ /**
501
+ * Accept current suggestion and insert into buffer.
502
+ */
503
+ acceptSuggestion() {
504
+ if (!this.showSuggestions || this.filteredSuggestions.length === 0)
505
+ return false;
506
+ const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
507
+ if (!selected)
508
+ return false;
509
+ // Replace buffer with selected command
510
+ this.buffer = selected.command + ' ';
511
+ this.cursor = this.buffer.length;
512
+ this.showSuggestions = false;
513
+ this.renderDirty = true;
514
+ this.scheduleRender();
515
+ return true;
516
+ }
517
+ /**
518
+ * Check if suggestions are visible.
519
+ */
520
+ areSuggestionsVisible() {
521
+ return this.showSuggestions && this.filteredSuggestions.length > 0;
522
+ }
523
+ /**
524
+ * Update token count for metrics display
525
+ */
526
+ setTokensUsed(tokens) {
527
+ this.tokensUsed = tokens;
528
+ }
529
+ /**
530
+ * Toggle thinking/reasoning mode
531
+ */
532
+ toggleThinking() {
533
+ this.thinkingEnabled = !this.thinkingEnabled;
534
+ this.emit('thinkingToggle', this.thinkingEnabled);
535
+ this.scheduleRender();
536
+ }
537
+ /**
538
+ * Get thinking enabled state
539
+ */
540
+ isThinkingEnabled() {
541
+ return this.thinkingEnabled;
542
+ }
219
543
  /**
220
544
  * Keep the top N rows pinned outside the scroll region (used for the launch banner).
221
545
  */
222
546
  setPinnedHeaderLines(count) {
223
- // No pinned header rows anymore; keep everything in the scroll region.
224
- if (this.pinnedTopRows !== 0) {
225
- this.pinnedTopRows = 0;
547
+ // Set pinned header rows (banner area that scroll region excludes)
548
+ if (this.pinnedTopRows !== count) {
549
+ this.pinnedTopRows = count;
226
550
  if (this.scrollRegionActive) {
227
551
  this.applyScrollRegion();
228
552
  }
229
553
  }
230
554
  }
555
+ /**
556
+ * Anchor prompt rendering near a specific row (inline layout). Pass null to
557
+ * restore the default bottom-aligned layout.
558
+ */
559
+ setInlineAnchor(row) {
560
+ if (row === null || row === undefined) {
561
+ this.inlineAnchorRow = null;
562
+ this.inlineLayout = false;
563
+ this.renderDirty = true;
564
+ this.render();
565
+ return;
566
+ }
567
+ const { rows } = this.getSize();
568
+ const clamped = Math.max(1, Math.min(Math.floor(row), rows));
569
+ this.inlineAnchorRow = clamped;
570
+ this.inlineLayout = true;
571
+ this.renderDirty = true;
572
+ this.render();
573
+ }
574
+ /**
575
+ * Provide a dynamic anchor callback. When set, the prompt will follow the
576
+ * output by re-evaluating the anchor before each render.
577
+ */
578
+ setInlineAnchorProvider(provider) {
579
+ this.anchorProvider = provider;
580
+ if (!provider) {
581
+ this.inlineLayout = false;
582
+ this.inlineAnchorRow = null;
583
+ this.renderDirty = true;
584
+ this.render();
585
+ return;
586
+ }
587
+ this.inlineLayout = true;
588
+ this.renderDirty = true;
589
+ this.render();
590
+ }
231
591
  /**
232
592
  * Get current mode
233
593
  */
@@ -337,37 +697,6 @@ export class TerminalInput extends EventEmitter {
337
697
  this.streamingLabel = next;
338
698
  this.scheduleRender();
339
699
  }
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
700
  /**
372
701
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
373
702
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -377,22 +706,26 @@ export class TerminalInput extends EventEmitter {
377
706
  const nextAutoContinue = !!options.autoContinueEnabled;
378
707
  const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
379
708
  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
709
  if (this.verificationEnabled === nextVerification &&
383
710
  this.autoContinueEnabled === nextAutoContinue &&
384
711
  this.verificationHotkey === nextVerifyHotkey &&
385
- this.autoContinueHotkey === nextAutoHotkey &&
386
- this.thinkingHotkey === nextThinkingHotkey &&
387
- this.thinkingModeLabel === nextThinkingLabel) {
712
+ this.autoContinueHotkey === nextAutoHotkey) {
388
713
  return;
389
714
  }
390
715
  this.verificationEnabled = nextVerification;
391
716
  this.autoContinueEnabled = nextAutoContinue;
392
717
  this.verificationHotkey = nextVerifyHotkey;
393
718
  this.autoContinueHotkey = nextAutoHotkey;
394
- this.thinkingHotkey = nextThinkingHotkey;
395
- this.thinkingModeLabel = nextThinkingLabel;
719
+ this.scheduleRender();
720
+ }
721
+ /**
722
+ * Set the model info string (e.g., "OpenAI · gpt-4")
723
+ * This is displayed persistently above the input area.
724
+ */
725
+ setModelInfo(info) {
726
+ if (this.modelInfo === info)
727
+ return;
728
+ this.modelInfo = info;
396
729
  this.scheduleRender();
397
730
  }
398
731
  /**
@@ -404,389 +737,297 @@ export class TerminalInput extends EventEmitter {
404
737
  this.streamingLabel = null;
405
738
  this.scheduleRender();
406
739
  }
407
- /**
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
740
  /**
421
741
  * Render the input area - Claude Code style with mode controls
422
742
  *
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.
743
+ * Same rendering for both normal and streaming modes - just different status bar.
744
+ * During streaming, uses cursor save/restore to preserve streaming position.
426
745
  */
427
746
  render() {
428
747
  if (!this.canRender())
429
748
  return;
430
749
  if (this.isRendering)
431
750
  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
751
  const shouldSkip = !this.renderDirty &&
445
752
  this.buffer === this.lastRenderContent &&
446
753
  this.cursor === this.lastRenderCursor;
447
754
  this.renderDirty = false;
448
- // Skip if nothing changed and no explicit refresh requested
755
+ // Skip if nothing changed (unless explicitly forced)
449
756
  if (shouldSkip) {
450
757
  return;
451
758
  }
452
- // If write lock is held, defer render to avoid race conditions
759
+ // If write lock is held, defer render
453
760
  if (writeLock.isLocked()) {
454
761
  writeLock.safeWrite(() => this.render());
455
762
  return;
456
763
  }
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
- // Render
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
764
+ this.isRendering = true;
765
+ writeLock.lock('terminalInput.render');
766
+ try {
767
+ // Render input area at bottom (outside scroll region)
768
+ this.renderBottomPinned();
769
+ }
770
+ finally {
771
+ writeLock.unlock();
772
+ this.isRendering = false;
773
+ }
774
+ }
775
+ /**
776
+ * Render in flow mode - delegates to bottom-pinned for stability.
777
+ *
778
+ * Flow mode attempted inline rendering but caused duplicate renders
779
+ * due to unreliable cursor position tracking. Bottom-pinned is reliable.
780
+ */
781
+ renderFlowMode() {
782
+ // Use stable bottom-pinned approach
783
+ this.renderBottomPinned();
784
+ }
785
+ /**
786
+ * Render in bottom-pinned mode - Claude Code style with suggestions
787
+ *
788
+ * Works for both normal and streaming modes:
789
+ * - During streaming: saves/restores cursor position
790
+ * - Status bar shows streaming info or "Type a message"
791
+ *
792
+ * Layout when suggestions visible:
793
+ * - Top divider
794
+ * - Input line(s)
795
+ * - Bottom divider
796
+ * - Suggestions (command list)
797
+ *
798
+ * Layout when suggestions hidden:
799
+ * - Status bar (Ready/Streaming)
800
+ * - Top divider
801
+ * - Input line(s)
802
+ * - Bottom divider
803
+ * - Mode controls
804
+ */
805
+ renderBottomPinned() {
806
+ const { rows, cols } = this.getSize();
807
+ const maxWidth = Math.max(8, cols - 4);
808
+ const isStreaming = this.mode === 'streaming';
809
+ // Use unified pinned input area (works for both streaming and normal)
810
+ // Only use complex rendering when suggestions are visible
811
+ const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
812
+ if (!hasSuggestions) {
813
+ this.renderPinnedInputArea();
814
+ return;
815
+ }
816
+ // Wrap buffer into display lines
817
+ const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
818
+ const availableForContent = Math.max(1, rows - 3);
819
+ const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
820
+ const displayLines = Math.min(lines.length, maxVisible);
821
+ // Calculate display window (keep cursor visible)
822
+ let startLine = 0;
823
+ if (lines.length > displayLines) {
824
+ startLine = Math.max(0, cursorLine - displayLines + 1);
825
+ startLine = Math.min(startLine, lines.length - displayLines);
826
+ }
827
+ const visibleLines = lines.slice(startLine, startLine + displayLines);
828
+ const adjustedCursorLine = cursorLine - startLine;
829
+ // Calculate suggestion display (not during streaming)
830
+ const suggestionsToShow = (!isStreaming && this.showSuggestions)
831
+ ? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
832
+ : [];
833
+ const suggestionLines = suggestionsToShow.length;
834
+ this.write(ESC.HIDE);
835
+ this.write(ESC.RESET);
836
+ const divider = renderDivider(cols - 2);
837
+ // Calculate positions from absolute bottom
838
+ let currentRow;
839
+ if (suggestionLines > 0) {
840
+ // With suggestions: input area + dividers + suggestions
841
+ // Layout: [topDiv] [input] [bottomDiv] [suggestions...]
842
+ const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
843
+ currentRow = Math.max(1, rows - totalHeight + 1);
844
+ this.updateReservedLines(totalHeight);
845
+ // Clear from current position to end of screen to remove any "ghost" content
846
+ this.write(ESC.TO(currentRow, 1));
847
+ this.write(ESC.CLEAR_TO_END);
848
+ // Top divider
494
849
  this.write(ESC.TO(currentRow, 1));
495
- this.write(ESC.CLEAR_LINE);
496
- const divider = renderDivider(cols - 2);
497
850
  this.write(divider);
498
- currentRow += 1;
499
- // Render input lines
851
+ currentRow++;
852
+ // Input lines
500
853
  let finalRow = currentRow;
501
854
  let finalCol = 3;
502
855
  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);
856
+ this.write(ESC.TO(currentRow, 1));
506
857
  const line = visibleLines[i] ?? '';
507
858
  const absoluteLineIdx = startLine + i;
508
859
  const isFirstLine = absoluteLineIdx === 0;
509
860
  const isCursorLine = i === adjustedCursorLine;
510
- // Background
511
- this.write(ESC.BG_DARK);
512
- // Prompt prefix
513
- this.write(ESC.DIM);
514
861
  this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
515
- this.write(ESC.RESET);
516
- this.write(ESC.BG_DARK);
517
862
  if (isCursorLine) {
518
- // Render with block cursor
519
863
  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;
864
+ this.write(line.slice(0, col));
865
+ this.write(ESC.REVERSE);
866
+ this.write(col < line.length ? line[col] : ' ');
867
+ this.write(ESC.RESET);
868
+ this.write(line.slice(col + 1));
869
+ finalRow = currentRow;
529
870
  finalCol = this.config.promptChar.length + col + 1;
530
871
  }
531
872
  else {
532
873
  this.write(line);
533
874
  }
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);
875
+ currentRow++;
540
876
  }
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
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;
877
+ // Bottom divider
878
+ this.write(ESC.TO(currentRow, 1));
879
+ this.write(divider);
880
+ currentRow++;
881
+ // Suggestions (Claude Code style)
882
+ for (let i = 0; i < suggestionsToShow.length; i++) {
883
+ this.write(ESC.TO(currentRow, 1));
884
+ const suggestion = suggestionsToShow[i];
885
+ const isSelected = i === this.selectedSuggestionIndex;
886
+ // Indent and highlight selected
887
+ this.write(' ');
888
+ if (isSelected) {
889
+ this.write(ESC.REVERSE);
890
+ this.write(ESC.BOLD);
891
+ }
892
+ this.write(suggestion.command);
893
+ if (isSelected) {
894
+ this.write(ESC.RESET);
895
+ }
896
+ // Description (dimmed)
897
+ const descSpace = cols - suggestion.command.length - 8;
898
+ if (descSpace > 10 && suggestion.description) {
899
+ const desc = suggestion.description.slice(0, descSpace);
900
+ this.write(ESC.RESET);
901
+ this.write(ESC.DIM);
902
+ this.write(' ');
903
+ this.write(desc);
904
+ this.write(ESC.RESET);
905
+ }
906
+ currentRow++;
556
907
  }
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;
908
+ // Position cursor in input area
909
+ this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
567
910
  }
911
+ this.write(ESC.SHOW);
912
+ // Update state
913
+ this.lastRenderContent = this.buffer;
914
+ this.lastRenderCursor = this.cursor;
568
915
  }
569
916
  /**
570
- * Build one or more compact meta lines above the divider (thinking, status, usage).
571
- * During streaming, shows model line pinned above streaming info.
917
+ * Build status bar for streaming mode (shows elapsed time, queue count).
572
918
  */
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
- }
919
+ buildStreamingStatusBar(cols) {
920
+ const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
921
+ // Streaming status with elapsed time
922
+ let elapsed = '0s';
923
+ if (this.streamingStartTime) {
924
+ const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
925
+ const mins = Math.floor(secs / 60);
926
+ elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
927
+ }
928
+ let status = `${GREEN}● Streaming${R} ${elapsed}`;
929
+ // Queue indicator
630
930
  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));
931
+ status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
647
932
  }
933
+ // Hint for typing
934
+ status += ` ${DIM}· type to queue message${R}`;
935
+ return status;
648
936
  }
649
937
  /**
650
- * Build Claude Code style mode controls line.
651
- * Combines streaming label + override status + main status for simultaneous display.
938
+ * Build status bar showing streaming/ready status and key info.
939
+ * This is the TOP line above the input area - minimal Claude Code style.
652
940
  */
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' });
941
+ buildStatusBar(cols) {
942
+ const maxWidth = cols - 2;
943
+ const parts = [];
944
+ // Streaming status with elapsed time (left side)
945
+ if (this.mode === 'streaming') {
946
+ let statusText = '● Streaming';
947
+ if (this.streamingStartTime) {
948
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
949
+ const mins = Math.floor(elapsed / 60);
950
+ const secs = elapsed % 60;
951
+ statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
952
+ }
953
+ parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
678
954
  }
679
- if (this.buffer.includes('\n')) {
680
- const lineCount = this.buffer.split('\n').length;
681
- leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
955
+ // Queue indicator during streaming
956
+ if (this.mode === 'streaming' && this.queue.length > 0) {
957
+ parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
682
958
  }
959
+ // Paste indicator
683
960
  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
- });
961
+ const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
962
+ parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
689
963
  }
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
- if (this.modelLabel) {
696
- const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
697
- rightParts.push({ text: modelText, tone: 'muted' });
698
- }
699
- if (contextRemaining !== null) {
700
- const tone = contextRemaining <= 10 ? 'warn' : 'muted';
701
- const label = contextRemaining === 0 && this.contextUsage !== null
702
- ? 'Context auto-compact imminent'
703
- : `Context left until auto-compact: ${contextRemaining}%`;
704
- rightParts.push({ text: label, tone });
705
- }
706
- if (!rightParts.length || width < 60) {
707
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
708
- return renderStatusLine(merged, width);
709
- }
710
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
711
- const rightWidth = Math.max(14, width - leftWidth - 1);
712
- const leftText = renderStatusLine(leftParts, leftWidth);
713
- const rightText = renderStatusLine(rightParts, rightWidth);
714
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
715
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
716
- }
717
- formatHotkey(hotkey) {
718
- const normalized = hotkey.trim().toLowerCase();
719
- if (!normalized)
720
- return hotkey;
721
- const parts = normalized.split('+').filter(Boolean);
722
- const map = {
723
- shift: '⇧',
724
- sh: '⇧',
725
- alt: '⌥',
726
- option: '⌥',
727
- opt: '⌥',
728
- ctrl: '⌃',
729
- control: '⌃',
730
- cmd: '⌘',
731
- meta: '⌘',
732
- };
733
- const formatted = parts
734
- .map((part) => {
735
- const symbol = map[part];
736
- if (symbol)
737
- return symbol;
738
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
739
- })
740
- .join('');
741
- return formatted || hotkey;
742
- }
743
- computeContextRemaining() {
744
- if (this.contextUsage === null) {
745
- return null;
746
- }
747
- return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
748
- }
749
- computeTokensRemaining() {
750
- if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
751
- return null;
752
- }
753
- const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
754
- return this.formatTokenCount(remaining);
755
- }
756
- formatElapsedLabel(seconds) {
757
- if (seconds < 60) {
758
- return `${seconds}s`;
759
- }
760
- const mins = Math.floor(seconds / 60);
761
- const secs = seconds % 60;
762
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
763
- }
764
- formatTokenCount(value) {
765
- if (!Number.isFinite(value)) {
766
- return `${value}`;
964
+ // Override/warning status
965
+ if (this.overrideStatusMessage) {
966
+ parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
967
+ }
968
+ // If idle with empty buffer, show quick shortcuts
969
+ if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
970
+ return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
767
971
  }
768
- if (value >= 1_000_000) {
769
- return `${(value / 1_000_000).toFixed(1)}M`;
972
+ // Multi-line indicator
973
+ if (this.buffer.includes('\n')) {
974
+ parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
770
975
  }
771
- if (value >= 1_000) {
772
- return `${(value / 1_000).toFixed(1)}k`;
976
+ if (parts.length === 0) {
977
+ return ''; // Empty status bar when idle
773
978
  }
774
- return `${Math.round(value)}`;
775
- }
776
- visibleLength(value) {
777
- const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
778
- return value.replace(ansiPattern, '').length;
979
+ const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
980
+ return joined.slice(0, maxWidth);
779
981
  }
780
982
  /**
781
- * Debug-only snapshot used by tests to assert rendered strings without
782
- * needing a TTY. Not used by production code.
983
+ * Build mode controls line showing toggles and context info.
984
+ * This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
985
+ *
986
+ * Layout: [toggles on left] ... [context info on right]
783
987
  */
784
- getDebugUiSnapshot(width) {
785
- const cols = Math.max(8, width ?? this.getSize().cols);
786
- return {
787
- meta: this.buildMetaLines(cols - 2),
788
- controls: this.buildModeControls(cols),
789
- };
988
+ buildModeControls(cols) {
989
+ const maxWidth = cols - 2;
990
+ // Use schema-defined colors for consistency
991
+ const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
992
+ // Mode toggles with colors (following ModeControlsSchema)
993
+ const toggles = [];
994
+ // Edit mode (green=auto, yellow=ask) - per schema.editMode
995
+ if (this.editMode === 'display-edits') {
996
+ toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
997
+ }
998
+ else {
999
+ toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
1000
+ }
1001
+ // Thinking mode (cyan when on) - per schema.thinkingMode
1002
+ toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
1003
+ // Verification (green when on) - per schema.verificationMode
1004
+ toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
1005
+ // Auto-continue (magenta when on) - per schema.autoContinueMode
1006
+ toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
1007
+ const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
1008
+ // Context usage with color - per schema.contextUsage thresholds
1009
+ let rightPart = '';
1010
+ if (this.contextUsage !== null) {
1011
+ const rem = Math.max(0, 100 - this.contextUsage);
1012
+ // Thresholds: critical < 10%, warning < 25%
1013
+ if (rem < 10)
1014
+ rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
1015
+ else if (rem < 25)
1016
+ rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
1017
+ else
1018
+ rightPart = `${DIM}ctx: ${rem}%${R}`;
1019
+ }
1020
+ // Calculate visible lengths (strip ANSI)
1021
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
1022
+ const leftLen = strip(leftPart).length;
1023
+ const rightLen = strip(rightPart).length;
1024
+ if (leftLen + rightLen < maxWidth - 4) {
1025
+ return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
1026
+ }
1027
+ if (rightLen > 0 && leftLen + 8 < maxWidth) {
1028
+ return `${leftPart} ${rightPart}`;
1029
+ }
1030
+ return leftPart;
790
1031
  }
791
1032
  /**
792
1033
  * Force a re-render
@@ -809,19 +1050,17 @@ export class TerminalInput extends EventEmitter {
809
1050
  handleResize() {
810
1051
  this.lastRenderContent = '';
811
1052
  this.lastRenderCursor = -1;
812
- this.resetStreamingRenderThrottle();
813
1053
  // Re-clamp pinned header rows to the new terminal height
814
1054
  this.setPinnedHeaderLines(this.pinnedTopRows);
815
- if (this.scrollRegionActive) {
816
- this.disableScrollRegion();
817
- this.enableScrollRegion();
818
- }
819
1055
  this.scheduleRender();
820
1056
  }
821
1057
  /**
822
1058
  * Register with display's output interceptor to position cursor correctly.
823
1059
  * When scroll region is active, output needs to go to the scroll region,
824
1060
  * not the protected bottom area where the input is rendered.
1061
+ *
1062
+ * NOTE: With scroll region properly set, content naturally stays within
1063
+ * the region boundaries - no cursor manipulation needed per-write.
825
1064
  */
826
1065
  registerOutputInterceptor(display) {
827
1066
  if (this.outputInterceptorCleanup) {
@@ -829,20 +1068,11 @@ export class TerminalInput extends EventEmitter {
829
1068
  }
830
1069
  this.outputInterceptorCleanup = display.registerOutputInterceptor({
831
1070
  beforeWrite: () => {
832
- // When the scroll region is active, temporarily move the cursor into
833
- // the scrollable area so streamed output lands above the pinned prompt.
834
- if (this.scrollRegionActive) {
835
- const { rows } = this.getSize();
836
- const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
837
- this.write(ESC.SAVE);
838
- this.write(ESC.TO(scrollBottom, 1));
839
- }
1071
+ // Scroll region handles content containment automatically
1072
+ // No per-write cursor manipulation needed
840
1073
  },
841
1074
  afterWrite: () => {
842
- // Restore cursor back to the pinned prompt after output completes.
843
- if (this.scrollRegionActive) {
844
- this.write(ESC.RESTORE);
845
- }
1075
+ // No cursor manipulation needed
846
1076
  },
847
1077
  });
848
1078
  }
@@ -852,6 +1082,11 @@ export class TerminalInput extends EventEmitter {
852
1082
  dispose() {
853
1083
  if (this.disposed)
854
1084
  return;
1085
+ // Clean up streaming render timer
1086
+ if (this.streamingRenderTimer) {
1087
+ clearInterval(this.streamingRenderTimer);
1088
+ this.streamingRenderTimer = null;
1089
+ }
855
1090
  // Clean up output interceptor
856
1091
  if (this.outputInterceptorCleanup) {
857
1092
  this.outputInterceptorCleanup();
@@ -859,7 +1094,6 @@ export class TerminalInput extends EventEmitter {
859
1094
  }
860
1095
  this.disposed = true;
861
1096
  this.enabled = false;
862
- this.resetStreamingRenderThrottle();
863
1097
  this.disableScrollRegion();
864
1098
  this.disableBracketedPaste();
865
1099
  this.buffer = '';
@@ -965,7 +1199,22 @@ export class TerminalInput extends EventEmitter {
965
1199
  this.toggleEditMode();
966
1200
  return true;
967
1201
  }
968
- this.insertText(' ');
1202
+ // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
1203
+ if (this.findPlaceholderAt(this.cursor)) {
1204
+ this.togglePasteExpansion();
1205
+ }
1206
+ else {
1207
+ this.toggleThinking();
1208
+ }
1209
+ return true;
1210
+ case 'escape':
1211
+ // Esc: interrupt if streaming, otherwise clear buffer
1212
+ if (this.mode === 'streaming') {
1213
+ this.emit('interrupt');
1214
+ }
1215
+ else if (this.buffer.length > 0) {
1216
+ this.clear();
1217
+ }
969
1218
  return true;
970
1219
  }
971
1220
  return false;
@@ -983,6 +1232,7 @@ export class TerminalInput extends EventEmitter {
983
1232
  this.insertPlainText(chunk, insertPos);
984
1233
  this.cursor = insertPos + chunk.length;
985
1234
  this.emit('change', this.buffer);
1235
+ this.updateSuggestions();
986
1236
  this.scheduleRender();
987
1237
  }
988
1238
  insertNewline() {
@@ -1007,6 +1257,7 @@ export class TerminalInput extends EventEmitter {
1007
1257
  this.cursor = Math.max(0, this.cursor - 1);
1008
1258
  }
1009
1259
  this.emit('change', this.buffer);
1260
+ this.updateSuggestions();
1010
1261
  this.scheduleRender();
1011
1262
  }
1012
1263
  deleteForward() {
@@ -1256,9 +1507,7 @@ export class TerminalInput extends EventEmitter {
1256
1507
  if (available <= 0)
1257
1508
  return;
1258
1509
  const chunk = clean.slice(0, available);
1259
- const isMultiline = isMultilinePaste(chunk);
1260
- const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1261
- if (isMultiline && !isShortMultiline) {
1510
+ if (isMultilinePaste(chunk)) {
1262
1511
  this.insertPastePlaceholder(chunk);
1263
1512
  }
1264
1513
  else {
@@ -1278,7 +1527,6 @@ export class TerminalInput extends EventEmitter {
1278
1527
  return;
1279
1528
  this.applyScrollRegion();
1280
1529
  this.scrollRegionActive = true;
1281
- this.forceRender();
1282
1530
  }
1283
1531
  disableScrollRegion() {
1284
1532
  if (!this.scrollRegionActive)
@@ -1429,19 +1677,17 @@ export class TerminalInput extends EventEmitter {
1429
1677
  this.shiftPlaceholders(position, text.length);
1430
1678
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1431
1679
  }
1432
- shouldInlineMultiline(content) {
1433
- const lines = content.split('\n').length;
1434
- const maxInlineLines = 4;
1435
- const maxInlineChars = 240;
1436
- return lines <= maxInlineLines && content.length <= maxInlineChars;
1437
- }
1438
1680
  findPlaceholderAt(position) {
1439
1681
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1440
1682
  }
1441
- buildPlaceholder(lineCount) {
1683
+ buildPlaceholder(summary) {
1442
1684
  const id = ++this.pasteCounter;
1443
- const plural = lineCount === 1 ? '' : 's';
1444
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1685
+ const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1686
+ // Show first line preview (truncated)
1687
+ const preview = summary.preview.length > 30
1688
+ ? `${summary.preview.slice(0, 30)}...`
1689
+ : summary.preview;
1690
+ const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1445
1691
  return { id, placeholder };
1446
1692
  }
1447
1693
  insertPastePlaceholder(content) {
@@ -1449,21 +1695,67 @@ export class TerminalInput extends EventEmitter {
1449
1695
  if (available <= 0)
1450
1696
  return;
1451
1697
  const cleanContent = content.slice(0, available);
1452
- const lineCount = cleanContent.split('\n').length;
1453
- const { id, placeholder } = this.buildPlaceholder(lineCount);
1698
+ const summary = generatePasteSummary(cleanContent);
1699
+ // For short pastes (< 5 lines), show full content instead of placeholder
1700
+ if (summary.lineCount < 5) {
1701
+ const placeholder = this.findPlaceholderAt(this.cursor);
1702
+ const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1703
+ this.insertPlainText(cleanContent, insertPos);
1704
+ this.cursor = insertPos + cleanContent.length;
1705
+ return;
1706
+ }
1707
+ const { id, placeholder } = this.buildPlaceholder(summary);
1454
1708
  const insertPos = this.cursor;
1455
1709
  this.shiftPlaceholders(insertPos, placeholder.length);
1456
1710
  this.pastePlaceholders.push({
1457
1711
  id,
1458
1712
  content: cleanContent,
1459
- lineCount,
1713
+ lineCount: summary.lineCount,
1460
1714
  placeholder,
1461
1715
  start: insertPos,
1462
1716
  end: insertPos + placeholder.length,
1717
+ summary,
1718
+ expanded: false,
1463
1719
  });
1464
1720
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1465
1721
  this.cursor = insertPos + placeholder.length;
1466
1722
  }
1723
+ /**
1724
+ * Toggle expansion of a paste placeholder at the current cursor position.
1725
+ * When expanded, shows first 3 and last 2 lines of the content.
1726
+ */
1727
+ togglePasteExpansion() {
1728
+ const placeholder = this.findPlaceholderAt(this.cursor);
1729
+ if (!placeholder)
1730
+ return false;
1731
+ placeholder.expanded = !placeholder.expanded;
1732
+ // Update the placeholder text in buffer
1733
+ const newPlaceholder = placeholder.expanded
1734
+ ? this.buildExpandedPlaceholder(placeholder)
1735
+ : this.buildPlaceholder(placeholder.summary).placeholder;
1736
+ const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1737
+ // Update buffer
1738
+ this.buffer =
1739
+ this.buffer.slice(0, placeholder.start) +
1740
+ newPlaceholder +
1741
+ this.buffer.slice(placeholder.end);
1742
+ // Update placeholder tracking
1743
+ placeholder.placeholder = newPlaceholder;
1744
+ placeholder.end = placeholder.start + newPlaceholder.length;
1745
+ // Shift other placeholders
1746
+ this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1747
+ this.scheduleRender();
1748
+ return true;
1749
+ }
1750
+ buildExpandedPlaceholder(ph) {
1751
+ const lines = ph.content.split('\n');
1752
+ const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1753
+ const lastLines = lines.length > 5
1754
+ ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1755
+ : '';
1756
+ const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1757
+ return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1758
+ }
1467
1759
  deletePlaceholder(placeholder) {
1468
1760
  const length = placeholder.end - placeholder.start;
1469
1761
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
@@ -1471,11 +1763,7 @@ export class TerminalInput extends EventEmitter {
1471
1763
  this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
1472
1764
  this.cursor = placeholder.start;
1473
1765
  }
1474
- updateContextUsage(value, autoCompactThreshold) {
1475
- if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
1476
- const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
1477
- this.contextAutoCompactThreshold = boundedThreshold;
1478
- }
1766
+ updateContextUsage(value) {
1479
1767
  if (value === null || !Number.isFinite(value)) {
1480
1768
  this.contextUsage = null;
1481
1769
  }
@@ -1502,22 +1790,6 @@ export class TerminalInput extends EventEmitter {
1502
1790
  const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
1503
1791
  this.setEditMode(next);
1504
1792
  }
1505
- scheduleStreamingRender(delayMs) {
1506
- if (this.streamingRenderTimer)
1507
- return;
1508
- const wait = Math.max(16, delayMs);
1509
- this.streamingRenderTimer = setTimeout(() => {
1510
- this.streamingRenderTimer = null;
1511
- this.render();
1512
- }, wait);
1513
- }
1514
- resetStreamingRenderThrottle() {
1515
- if (this.streamingRenderTimer) {
1516
- clearTimeout(this.streamingRenderTimer);
1517
- this.streamingRenderTimer = null;
1518
- }
1519
- this.lastStreamingRender = 0;
1520
- }
1521
1793
  scheduleRender() {
1522
1794
  if (!this.canRender())
1523
1795
  return;