erosolar-cli 1.7.354 → 1.7.356
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.
- package/README.md +148 -24
- package/dist/alpha-zero/agentWrapper.d.ts +84 -0
- package/dist/alpha-zero/agentWrapper.d.ts.map +1 -0
- package/dist/alpha-zero/agentWrapper.js +171 -0
- package/dist/alpha-zero/agentWrapper.js.map +1 -0
- package/dist/alpha-zero/codeEvaluator.d.ts +25 -0
- package/dist/alpha-zero/codeEvaluator.d.ts.map +1 -0
- package/dist/alpha-zero/codeEvaluator.js +273 -0
- package/dist/alpha-zero/codeEvaluator.js.map +1 -0
- package/dist/alpha-zero/competitiveRunner.d.ts +66 -0
- package/dist/alpha-zero/competitiveRunner.d.ts.map +1 -0
- package/dist/alpha-zero/competitiveRunner.js +224 -0
- package/dist/alpha-zero/competitiveRunner.js.map +1 -0
- package/dist/alpha-zero/index.d.ts +67 -0
- package/dist/alpha-zero/index.d.ts.map +1 -0
- package/dist/alpha-zero/index.js +99 -0
- package/dist/alpha-zero/index.js.map +1 -0
- package/dist/alpha-zero/introspection.d.ts +128 -0
- package/dist/alpha-zero/introspection.d.ts.map +1 -0
- package/dist/alpha-zero/introspection.js +300 -0
- package/dist/alpha-zero/introspection.js.map +1 -0
- package/dist/alpha-zero/metricsTracker.d.ts +71 -0
- package/dist/alpha-zero/metricsTracker.d.ts.map +1 -0
- package/dist/{core → alpha-zero}/metricsTracker.js +5 -2
- package/dist/alpha-zero/metricsTracker.js.map +1 -0
- package/dist/alpha-zero/security/core.d.ts +125 -0
- package/dist/alpha-zero/security/core.d.ts.map +1 -0
- package/dist/alpha-zero/security/core.js +271 -0
- package/dist/alpha-zero/security/core.js.map +1 -0
- package/dist/alpha-zero/security/google.d.ts +125 -0
- package/dist/alpha-zero/security/google.d.ts.map +1 -0
- package/dist/alpha-zero/security/google.js +311 -0
- package/dist/alpha-zero/security/google.js.map +1 -0
- package/dist/alpha-zero/security/googleLoader.d.ts +17 -0
- package/dist/alpha-zero/security/googleLoader.d.ts.map +1 -0
- package/dist/alpha-zero/security/googleLoader.js +41 -0
- package/dist/alpha-zero/security/googleLoader.js.map +1 -0
- package/dist/alpha-zero/security/index.d.ts +29 -0
- package/dist/alpha-zero/security/index.d.ts.map +1 -0
- package/dist/alpha-zero/security/index.js +32 -0
- package/dist/alpha-zero/security/index.js.map +1 -0
- package/dist/alpha-zero/security/simulation.d.ts +124 -0
- package/dist/alpha-zero/security/simulation.d.ts.map +1 -0
- package/dist/alpha-zero/security/simulation.js +277 -0
- package/dist/alpha-zero/security/simulation.js.map +1 -0
- package/dist/alpha-zero/selfModification.d.ts +109 -0
- package/dist/alpha-zero/selfModification.d.ts.map +1 -0
- package/dist/alpha-zero/selfModification.js +233 -0
- package/dist/alpha-zero/selfModification.js.map +1 -0
- package/dist/alpha-zero/types.d.ts +170 -0
- package/dist/alpha-zero/types.d.ts.map +1 -0
- package/dist/alpha-zero/types.js +31 -0
- package/dist/alpha-zero/types.js.map +1 -0
- package/dist/bin/erosolar.js +21 -5
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +31 -56
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/capabilities/securityTestingCapability.d.ts +13 -0
- package/dist/capabilities/securityTestingCapability.d.ts.map +1 -0
- package/dist/capabilities/securityTestingCapability.js +25 -0
- package/dist/capabilities/securityTestingCapability.js.map +1 -0
- package/dist/contracts/agent-schemas.json +15 -0
- package/dist/contracts/tools.schema.json +9 -0
- package/dist/core/agent.d.ts +2 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js.map +1 -1
- package/dist/core/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/customCommands.d.ts +0 -1
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +0 -3
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/core/hooksSystem.d.ts +65 -0
- package/dist/core/hooksSystem.d.ts.map +1 -0
- package/dist/core/hooksSystem.js +273 -0
- package/dist/core/hooksSystem.js.map +1 -0
- package/dist/core/memorySystem.d.ts +48 -0
- package/dist/core/memorySystem.d.ts.map +1 -0
- package/dist/core/memorySystem.js +271 -0
- package/dist/core/memorySystem.js.map +1 -0
- package/dist/core/sessionStore.d.ts +0 -2
- package/dist/core/sessionStore.d.ts.map +1 -1
- package/dist/core/sessionStore.js +0 -1
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +14 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +1 -22
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +5 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +3 -14
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/unified/errors.d.ts +189 -0
- package/dist/core/unified/errors.d.ts.map +1 -0
- package/dist/core/unified/errors.js +497 -0
- package/dist/core/unified/errors.js.map +1 -0
- package/dist/core/unified/index.d.ts +19 -0
- package/dist/core/unified/index.d.ts.map +1 -0
- package/dist/core/unified/index.js +68 -0
- package/dist/core/unified/index.js.map +1 -0
- package/dist/core/unified/schema.d.ts +101 -0
- package/dist/core/unified/schema.d.ts.map +1 -0
- package/dist/core/unified/schema.js +350 -0
- package/dist/core/unified/schema.js.map +1 -0
- package/dist/core/unified/toolRuntime.d.ts +179 -0
- package/dist/core/unified/toolRuntime.d.ts.map +1 -0
- package/dist/core/unified/toolRuntime.js +517 -0
- package/dist/core/unified/toolRuntime.js.map +1 -0
- package/dist/core/unified/tools.d.ts +127 -0
- package/dist/core/unified/tools.d.ts.map +1 -0
- package/dist/core/unified/tools.js +1333 -0
- package/dist/core/unified/tools.js.map +1 -0
- package/dist/core/unified/types.d.ts +352 -0
- package/dist/core/unified/types.d.ts.map +1 -0
- package/dist/core/unified/types.js +12 -0
- package/dist/core/unified/types.js.map +1 -0
- package/dist/core/unified/version.d.ts +209 -0
- package/dist/core/unified/version.d.ts.map +1 -0
- package/dist/core/unified/version.js +454 -0
- package/dist/core/unified/version.js.map +1 -0
- package/dist/core/validationRunner.d.ts +3 -1
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -21
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +3 -0
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +1 -0
- package/dist/plugins/tools/security/securityPlugin.js +12 -0
- package/dist/plugins/tools/security/securityPlugin.js.map +1 -0
- package/dist/runtime/agentSession.d.ts +2 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/security/active-stack-security.d.ts +112 -0
- package/dist/security/active-stack-security.d.ts.map +1 -0
- package/dist/security/active-stack-security.js +296 -0
- package/dist/security/active-stack-security.js.map +1 -0
- package/dist/security/advanced-persistence-research.d.ts +92 -0
- package/dist/security/advanced-persistence-research.d.ts.map +1 -0
- package/dist/security/advanced-persistence-research.js +195 -0
- package/dist/security/advanced-persistence-research.js.map +1 -0
- package/dist/security/advanced-targeting.d.ts +119 -0
- package/dist/security/advanced-targeting.d.ts.map +1 -0
- package/dist/security/advanced-targeting.js +233 -0
- package/dist/security/advanced-targeting.js.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +104 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.js +315 -0
- package/dist/security/assessment/vulnerabilityAssessment.js.map +1 -0
- package/dist/security/authorization/securityAuthorization.d.ts +88 -0
- package/dist/security/authorization/securityAuthorization.d.ts.map +1 -0
- package/dist/security/authorization/securityAuthorization.js +172 -0
- package/dist/security/authorization/securityAuthorization.js.map +1 -0
- package/dist/security/comprehensive-targeting.d.ts +85 -0
- package/dist/security/comprehensive-targeting.d.ts.map +1 -0
- package/dist/security/comprehensive-targeting.js +438 -0
- package/dist/security/comprehensive-targeting.js.map +1 -0
- package/dist/security/global-security-integration.d.ts +91 -0
- package/dist/security/global-security-integration.d.ts.map +1 -0
- package/dist/security/global-security-integration.js +218 -0
- package/dist/security/global-security-integration.js.map +1 -0
- package/dist/security/index.d.ts +38 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +47 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/persistence-analyzer.d.ts +56 -0
- package/dist/security/persistence-analyzer.d.ts.map +1 -0
- package/dist/security/persistence-analyzer.js +187 -0
- package/dist/security/persistence-analyzer.js.map +1 -0
- package/dist/security/persistence-cli.d.ts +36 -0
- package/dist/security/persistence-cli.d.ts.map +1 -0
- package/dist/security/persistence-cli.js +160 -0
- package/dist/security/persistence-cli.js.map +1 -0
- package/dist/security/persistence-research.d.ts +92 -0
- package/dist/security/persistence-research.d.ts.map +1 -0
- package/dist/security/persistence-research.js +364 -0
- package/dist/security/persistence-research.js.map +1 -0
- package/dist/security/research/persistenceResearch.d.ts +97 -0
- package/dist/security/research/persistenceResearch.d.ts.map +1 -0
- package/dist/security/research/persistenceResearch.js +282 -0
- package/dist/security/research/persistenceResearch.js.map +1 -0
- package/dist/security/security-integration.d.ts +74 -0
- package/dist/security/security-integration.d.ts.map +1 -0
- package/dist/security/security-integration.js +137 -0
- package/dist/security/security-integration.js.map +1 -0
- package/dist/security/security-testing-framework.d.ts +112 -0
- package/dist/security/security-testing-framework.d.ts.map +1 -0
- package/dist/security/security-testing-framework.js +364 -0
- package/dist/security/security-testing-framework.js.map +1 -0
- package/dist/security/simulation/attackSimulation.d.ts +93 -0
- package/dist/security/simulation/attackSimulation.d.ts.map +1 -0
- package/dist/security/simulation/attackSimulation.js +341 -0
- package/dist/security/simulation/attackSimulation.js.map +1 -0
- package/dist/security/strategic-operations.d.ts +100 -0
- package/dist/security/strategic-operations.d.ts.map +1 -0
- package/dist/security/strategic-operations.js +276 -0
- package/dist/security/strategic-operations.js.map +1 -0
- package/dist/security/tool-security-wrapper.d.ts +58 -0
- package/dist/security/tool-security-wrapper.d.ts.map +1 -0
- package/dist/security/tool-security-wrapper.js +156 -0
- package/dist/security/tool-security-wrapper.js.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +7 -43
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +166 -417
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/metricsTracker.d.ts +60 -0
- package/dist/shell/metricsTracker.d.ts.map +1 -0
- package/dist/shell/metricsTracker.js +119 -0
- package/dist/shell/metricsTracker.js.map +1 -0
- package/dist/shell/shellApp.d.ts +0 -2
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +9 -82
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +125 -250
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +612 -1071
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +24 -106
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +30 -139
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/taskRunner.d.ts +1 -7
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +49 -200
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/securityTools.d.ts +22 -0
- package/dist/tools/securityTools.d.ts.map +1 -0
- package/dist/tools/securityTools.js +448 -0
- package/dist/tools/securityTools.js.map +1 -0
- package/dist/ui/ShellUIAdapter.d.ts +1 -7
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +18 -42
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +45 -24
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +274 -148
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/terminalUISchema.d.ts +195 -0
- package/dist/ui/terminalUISchema.d.ts.map +1 -0
- package/dist/ui/terminalUISchema.js +113 -0
- package/dist/ui/terminalUISchema.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +158 -0
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +348 -0
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +0 -20
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +216 -105
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +4 -4
- package/scripts/deploy-security-capabilities.js +178 -0
- package/dist/core/hooks.d.ts +0 -113
- package/dist/core/hooks.d.ts.map +0 -1
- package/dist/core/hooks.js +0 -267
- package/dist/core/hooks.js.map +0 -1
- package/dist/core/metricsTracker.d.ts +0 -122
- package/dist/core/metricsTracker.d.ts.map +0 -1
- package/dist/core/metricsTracker.js.map +0 -1
- package/dist/core/securityAssessment.d.ts +0 -91
- package/dist/core/securityAssessment.d.ts.map +0 -1
- package/dist/core/securityAssessment.js +0 -580
- package/dist/core/securityAssessment.js.map +0 -1
- package/dist/core/verification.d.ts +0 -137
- package/dist/core/verification.d.ts.map +0 -1
- package/dist/core/verification.js +0 -323
- package/dist/core/verification.js.map +0 -1
- package/dist/subagents/agentConfig.d.ts +0 -27
- package/dist/subagents/agentConfig.d.ts.map +0 -1
- package/dist/subagents/agentConfig.js +0 -89
- package/dist/subagents/agentConfig.js.map +0 -1
- package/dist/subagents/agentRegistry.d.ts +0 -33
- package/dist/subagents/agentRegistry.d.ts.map +0 -1
- package/dist/subagents/agentRegistry.js +0 -162
- package/dist/subagents/agentRegistry.js.map +0 -1
- package/dist/utils/frontmatter.d.ts +0 -10
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js +0 -78
- package/dist/utils/frontmatter.js.map +0 -1
|
@@ -3,24 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
-
* - Hybrid floating/scroll approach:
|
|
7
|
-
* - Initially: chat box floats below content
|
|
8
|
-
* - When terminal fills: scroll region activates, chat box pins to bottom
|
|
9
6
|
* - Native bracketed paste support (no heuristics)
|
|
10
7
|
* - Clean cursor model with render-time wrapping
|
|
11
8
|
* - State machine for different input modes
|
|
12
9
|
* - No readline dependency for display
|
|
13
|
-
* - Text selection enabled: mouse tracking disabled by default to preserve
|
|
14
|
-
* native terminal text selection and copy/paste functionality
|
|
15
|
-
* - Scrollback navigation via keyboard: PageUp/PageDown, Ctrl+Home/End
|
|
16
10
|
*/
|
|
17
11
|
import { EventEmitter } from 'node:events';
|
|
18
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
19
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
20
|
-
import {
|
|
21
|
-
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
22
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
23
|
-
import { theme } from '../ui/theme.js';
|
|
14
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
24
15
|
// ANSI escape codes
|
|
25
16
|
const ESC = {
|
|
26
17
|
// Cursor control
|
|
@@ -30,18 +21,14 @@ const ESC = {
|
|
|
30
21
|
SHOW: '\x1b[?25h',
|
|
31
22
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
32
23
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
+
// Screen control
|
|
25
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
+
HOME: '\x1b[H',
|
|
27
|
+
ALT_SCREEN_ENTER: '\x1b[?1049h', // Enter alternate screen buffer
|
|
28
|
+
ALT_SCREEN_EXIT: '\x1b[?1049l', // Exit alternate screen buffer
|
|
33
29
|
// Line control
|
|
34
30
|
CLEAR_LINE: '\x1b[2K',
|
|
35
31
|
CLEAR_TO_END: '\x1b[0J',
|
|
36
|
-
// Screen control
|
|
37
|
-
HOME: '\x1b[H',
|
|
38
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
39
|
-
// Alternate screen buffer (like vim/tmux)
|
|
40
|
-
ENTER_ALT_SCREEN: '\x1b[?1049h',
|
|
41
|
-
EXIT_ALT_SCREEN: '\x1b[?1049l',
|
|
42
|
-
// Scroll region
|
|
43
|
-
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
44
|
-
RESET_SCROLL: '\x1b[r',
|
|
45
32
|
// Style
|
|
46
33
|
RESET: '\x1b[0m',
|
|
47
34
|
DIM: '\x1b[2m',
|
|
@@ -53,12 +40,6 @@ const ESC = {
|
|
|
53
40
|
PASTE_DISABLE: '\x1b[?2004l',
|
|
54
41
|
PASTE_START: '\x1b[200~',
|
|
55
42
|
PASTE_END: '\x1b[201~',
|
|
56
|
-
// Mouse tracking - Button events only (allows Shift+drag for text selection)
|
|
57
|
-
// Mode 1000: Track button presses/releases (wheel events included)
|
|
58
|
-
// Mode 1006: SGR extended format for better coordinate handling
|
|
59
|
-
// Note: NOT using mode 1003 (all motion) to preserve terminal text selection
|
|
60
|
-
MOUSE_ENABLE: '\x1b[?1000h\x1b[?1006h', // Enable button tracking + SGR mode
|
|
61
|
-
MOUSE_DISABLE: '\x1b[?1006l\x1b[?1000l', // Disable mouse tracking
|
|
62
43
|
};
|
|
63
44
|
/**
|
|
64
45
|
* Unified terminal input handler
|
|
@@ -87,54 +68,49 @@ export class TerminalInput extends EventEmitter {
|
|
|
87
68
|
statusMessage = null;
|
|
88
69
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
89
70
|
streamingLabel = null; // Streaming progress indicator
|
|
90
|
-
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
91
|
-
metaTokensUsed = null; // Optional token usage
|
|
92
|
-
metaTokenLimit = null; // Optional token window
|
|
93
|
-
metaThinkingMs = null; // Optional thinking duration
|
|
94
|
-
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
95
71
|
lastRenderContent = '';
|
|
96
72
|
lastRenderCursor = -1;
|
|
97
73
|
renderDirty = false;
|
|
98
74
|
isRendering = false;
|
|
75
|
+
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
76
|
+
inputAreaStartRow = 0; // Track absolute row position of input area
|
|
77
|
+
contentEndRow = 0; // Row where content ends (chat box renders below this)
|
|
78
|
+
// Command suggestions (Claude Code style auto-complete)
|
|
79
|
+
commandSuggestions = [];
|
|
80
|
+
filteredSuggestions = [];
|
|
81
|
+
selectedSuggestionIndex = 0;
|
|
82
|
+
showSuggestions = false;
|
|
99
83
|
// Lifecycle
|
|
100
84
|
disposed = false;
|
|
101
85
|
enabled = true;
|
|
102
86
|
contextUsage = null;
|
|
103
|
-
contextAutoCompactThreshold = 90;
|
|
104
|
-
// Track current content row (starts at top, moves down)
|
|
105
|
-
contentRow = 1;
|
|
106
|
-
// Track if scroll region is currently active
|
|
107
|
-
scrollRegionActive = false;
|
|
108
|
-
thinkingModeLabel = null;
|
|
109
|
-
// Scrollback buffer
|
|
110
|
-
scrollbackBuffer = [];
|
|
111
|
-
maxScrollbackLines = 10000;
|
|
112
|
-
scrollbackOffset = 0; // 0 = at bottom (live), > 0 = scrolled up
|
|
113
|
-
isInScrollbackMode = false;
|
|
114
|
-
scrollIndicatorFrame = 0; // For animated scroll indicator
|
|
115
|
-
// Alternate screen state
|
|
116
|
-
alternateScreenActive = false;
|
|
117
87
|
editMode = 'display-edits';
|
|
118
88
|
verificationEnabled = true;
|
|
119
89
|
autoContinueEnabled = false;
|
|
120
90
|
verificationHotkey = 'alt+v';
|
|
121
91
|
autoContinueHotkey = 'alt+c';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
92
|
+
// Output interceptor cleanup
|
|
93
|
+
outputInterceptorCleanup;
|
|
94
|
+
// Metrics tracking for status bar
|
|
95
|
+
streamingStartTime = null;
|
|
96
|
+
thinkingEnabled = true;
|
|
97
|
+
modelInfo = null; // Provider · Model info
|
|
98
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
128
99
|
streamingRenderTimer = null;
|
|
100
|
+
// Reference to display module for getting line counts during streaming
|
|
101
|
+
displayRef = null;
|
|
102
|
+
// Unified UI initialization flag
|
|
103
|
+
unifiedUIInitialized = false;
|
|
129
104
|
constructor(writeStream = process.stdout, config = {}) {
|
|
130
105
|
super();
|
|
131
106
|
this.out = writeStream;
|
|
107
|
+
// Use schema defaults for configuration consistency
|
|
132
108
|
this.config = {
|
|
133
|
-
maxLines: config.maxLines ??
|
|
134
|
-
maxLength: config.maxLength ??
|
|
109
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
110
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
135
111
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
136
|
-
promptChar: config.promptChar ??
|
|
137
|
-
continuationChar: config.continuationChar ??
|
|
112
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
113
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
138
114
|
};
|
|
139
115
|
}
|
|
140
116
|
// ===========================================================================
|
|
@@ -157,47 +133,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
135
|
/**
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
enableMouseTracking() {
|
|
163
|
-
if (this.isTTY()) {
|
|
164
|
-
this.write(ESC.MOUSE_ENABLE);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Disable mouse tracking
|
|
169
|
-
*/
|
|
170
|
-
disableMouseTracking() {
|
|
171
|
-
if (this.isTTY()) {
|
|
172
|
-
this.write(ESC.MOUSE_DISABLE);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Process raw terminal data (handles bracketed paste sequences, mouse events, and escape sequences)
|
|
177
|
-
* Returns true if the data was consumed (paste sequence, mouse event, etc.)
|
|
136
|
+
* Process raw terminal data (handles bracketed paste sequences)
|
|
137
|
+
* Returns true if the data was consumed (paste sequence)
|
|
178
138
|
*/
|
|
179
139
|
processRawData(data) {
|
|
180
|
-
// Check for mouse events (SGR mode: \x1b[<button;x;yM or m)
|
|
181
|
-
const mouseMatch = data.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
182
|
-
if (mouseMatch) {
|
|
183
|
-
const button = parseInt(mouseMatch[1], 10);
|
|
184
|
-
const x = parseInt(mouseMatch[2], 10);
|
|
185
|
-
const y = parseInt(mouseMatch[3], 10);
|
|
186
|
-
const action = mouseMatch[4]; // 'M' = press, 'm' = release
|
|
187
|
-
this.handleMouseEvent(button, x, y, action);
|
|
188
|
-
// Remove mouse event from data and return remaining
|
|
189
|
-
const mouseEventEnd = data.indexOf(action) + 1;
|
|
190
|
-
const remaining = data.slice(mouseEventEnd);
|
|
191
|
-
return { consumed: true, passthrough: remaining };
|
|
192
|
-
}
|
|
193
|
-
// Filter out arrow key escape sequences (they're handled by keypress events)
|
|
194
|
-
// Arrow keys: \x1b[A (up), \x1b[B (down), \x1b[C (right), \x1b[D (left)
|
|
195
|
-
const arrowMatch = data.match(/\x1b\[[ABCD]/);
|
|
196
|
-
if (arrowMatch) {
|
|
197
|
-
// Arrow keys should be handled by keypress handler, strip them from passthrough
|
|
198
|
-
const filtered = data.replace(/\x1b\[[ABCD]/g, '');
|
|
199
|
-
return { consumed: filtered.length !== data.length, passthrough: filtered };
|
|
200
|
-
}
|
|
201
140
|
// Check for paste start
|
|
202
141
|
if (data.includes(ESC.PASTE_START)) {
|
|
203
142
|
this.isPasting = true;
|
|
@@ -228,21 +167,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
228
167
|
}
|
|
229
168
|
return { consumed: false, passthrough: data };
|
|
230
169
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Handle mouse events (button, x, y coordinates, action)
|
|
233
|
-
*/
|
|
234
|
-
handleMouseEvent(button, _x, _y, _action) {
|
|
235
|
-
// Mouse wheel events: button 64 = scroll up, button 65 = scroll down
|
|
236
|
-
if (button === 64) {
|
|
237
|
-
// Scroll up (3 lines per wheel tick)
|
|
238
|
-
this.scrollUp(3);
|
|
239
|
-
}
|
|
240
|
-
else if (button === 65) {
|
|
241
|
-
// Scroll down (3 lines per wheel tick)
|
|
242
|
-
this.scrollDown(3);
|
|
243
|
-
}
|
|
244
|
-
// Ignore other mouse events (clicks, drags, etc.) for now
|
|
245
|
-
}
|
|
246
170
|
/**
|
|
247
171
|
* Handle a keypress event
|
|
248
172
|
*/
|
|
@@ -265,36 +189,258 @@ export class TerminalInput extends EventEmitter {
|
|
|
265
189
|
if (handled)
|
|
266
190
|
return;
|
|
267
191
|
}
|
|
192
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
193
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
194
|
+
this.emit('showHelp');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
268
197
|
// Insert printable characters
|
|
269
198
|
if (str && !key?.ctrl && !key?.meta) {
|
|
270
199
|
this.insertText(str);
|
|
271
200
|
}
|
|
272
201
|
}
|
|
202
|
+
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
203
|
+
bannerContent = null;
|
|
204
|
+
/**
|
|
205
|
+
* Set banner content to be written when unified UI initializes.
|
|
206
|
+
*/
|
|
207
|
+
setBannerContent(content) {
|
|
208
|
+
this.bannerContent = content;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Initialize the unified UI system with BOTTOM PINNED chat box.
|
|
212
|
+
*
|
|
213
|
+
* Layout:
|
|
214
|
+
* 1. Clear screen
|
|
215
|
+
* 2. Write banner at top
|
|
216
|
+
* 3. Set content cursor row after banner
|
|
217
|
+
* 4. Render chat box at bottom (sets up scroll region)
|
|
218
|
+
*/
|
|
219
|
+
initializeUnifiedUI() {
|
|
220
|
+
if (this.unifiedUIInitialized) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Enter alternate screen buffer for complete terminal control
|
|
224
|
+
this.write(ESC.ALT_SCREEN_ENTER);
|
|
225
|
+
// Hide cursor during setup
|
|
226
|
+
this.write(ESC.HIDE);
|
|
227
|
+
// Clear screen and go home (in alternate buffer)
|
|
228
|
+
this.write(ESC.HOME);
|
|
229
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
230
|
+
// Write banner at top
|
|
231
|
+
let bannerLines = 0;
|
|
232
|
+
if (this.bannerContent) {
|
|
233
|
+
const lines = this.bannerContent.split('\n');
|
|
234
|
+
bannerLines = lines.length + 2; // +2 for trailing \n\n
|
|
235
|
+
process.stdout.write(this.bannerContent + '\n\n');
|
|
236
|
+
}
|
|
237
|
+
// Set content cursor row after banner
|
|
238
|
+
this.contentCursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
|
|
239
|
+
// Content ends at same row initially (no content yet)
|
|
240
|
+
this.contentEndRow = this.contentCursorRow - 1;
|
|
241
|
+
// Mark initialized
|
|
242
|
+
this.unifiedUIInitialized = true;
|
|
243
|
+
// Render floating chat box below content
|
|
244
|
+
this.renderFloatingInputArea();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Clear the input area at its tracked position.
|
|
248
|
+
* Returns true if something was cleared.
|
|
249
|
+
*/
|
|
250
|
+
clearInputArea() {
|
|
251
|
+
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
252
|
+
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
253
|
+
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
254
|
+
this.write(ESC.CLEAR_LINE);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Reset input area tracking state.
|
|
262
|
+
*/
|
|
263
|
+
resetInputAreaTracking() {
|
|
264
|
+
this.inputAreaStartRow = 0;
|
|
265
|
+
this.flowModeRenderedLines = 0;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Render chat box - FLOATING below content (no scroll regions).
|
|
269
|
+
* Chat box appears right after content and moves down as content grows.
|
|
270
|
+
* Content and banner scroll naturally off the top of the screen.
|
|
271
|
+
*/
|
|
272
|
+
renderFloatingInputArea() {
|
|
273
|
+
const { cols, rows } = this.getSize();
|
|
274
|
+
const layout = this.buildInputLayout(cols);
|
|
275
|
+
// Clamp start row so the full UI stays visible, even when content exceeds viewport
|
|
276
|
+
const chatBoxHeight = layout.lines.length;
|
|
277
|
+
const chatBoxStartRow = Math.max(1, Math.min(this.contentEndRow + 1, rows - chatBoxHeight + 1));
|
|
278
|
+
// Save cursor position (content writer), render, then restore to avoid disrupting output
|
|
279
|
+
this.write(ESC.SAVE);
|
|
280
|
+
this.write(ESC.HIDE);
|
|
281
|
+
this.inputAreaStartRow = chatBoxStartRow;
|
|
282
|
+
let currentRow = chatBoxStartRow;
|
|
283
|
+
// Render each line with clearing to avoid ghosting
|
|
284
|
+
for (const line of layout.lines) {
|
|
285
|
+
this.write(ESC.TO(currentRow, 1));
|
|
286
|
+
this.write(ESC.CLEAR_LINE);
|
|
287
|
+
this.write(this.fitToWidth(line, cols));
|
|
288
|
+
currentRow++;
|
|
289
|
+
}
|
|
290
|
+
this.flowModeRenderedLines = chatBoxHeight;
|
|
291
|
+
this.lastRenderContent = this.buffer;
|
|
292
|
+
this.lastRenderCursor = this.cursor;
|
|
293
|
+
// Restore cursor for content writes; caret is rendered virtually inside the prompt
|
|
294
|
+
this.write(ESC.RESTORE);
|
|
295
|
+
this.write(ESC.SHOW);
|
|
296
|
+
}
|
|
273
297
|
/**
|
|
274
298
|
* Set the input mode
|
|
275
299
|
*
|
|
276
|
-
*
|
|
300
|
+
* BOTTOM PINNED with SSE: Chat box stays at terminal bottom.
|
|
301
|
+
* Scroll region protects chat box, content scrolls above it.
|
|
277
302
|
*/
|
|
278
303
|
setMode(mode) {
|
|
279
304
|
const prevMode = this.mode;
|
|
280
305
|
this.mode = mode;
|
|
281
306
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
282
|
-
|
|
307
|
+
// Track streaming start time for elapsed display
|
|
308
|
+
this.streamingStartTime = Date.now();
|
|
309
|
+
// Ensure unified UI is initialized
|
|
310
|
+
if (!this.unifiedUIInitialized) {
|
|
311
|
+
this.initializeUnifiedUI();
|
|
312
|
+
}
|
|
313
|
+
// Start periodic render timer to keep chat box updated during streaming
|
|
314
|
+
// This updates contentEndRow from display and re-renders chat box
|
|
315
|
+
if (!this.streamingRenderTimer) {
|
|
316
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
317
|
+
// Update contentEndRow from display's line count
|
|
318
|
+
if (this.displayRef?.getTotalWrittenLines) {
|
|
319
|
+
const next = this.displayRef.getTotalWrittenLines();
|
|
320
|
+
if (typeof next === 'number' && Number.isFinite(next) && next !== this.contentEndRow) {
|
|
321
|
+
this.contentEndRow = next;
|
|
322
|
+
this.renderDirty = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Re-render chat box at updated position
|
|
326
|
+
if (this.renderDirty) {
|
|
327
|
+
this.render();
|
|
328
|
+
}
|
|
329
|
+
}, 120); // Update periodically without overwhelming output
|
|
330
|
+
}
|
|
331
|
+
// Initial render
|
|
283
332
|
this.renderDirty = true;
|
|
284
|
-
this.
|
|
333
|
+
this.scheduleRender();
|
|
285
334
|
}
|
|
286
335
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
287
|
-
//
|
|
288
|
-
this.
|
|
289
|
-
|
|
336
|
+
// Stop streaming render timer
|
|
337
|
+
if (this.streamingRenderTimer) {
|
|
338
|
+
clearInterval(this.streamingRenderTimer);
|
|
339
|
+
this.streamingRenderTimer = null;
|
|
340
|
+
}
|
|
341
|
+
// Reset streaming time
|
|
342
|
+
this.streamingStartTime = null;
|
|
343
|
+
// Final render with accurate position
|
|
344
|
+
this.renderDirty = true;
|
|
345
|
+
this.scheduleRender();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Set the row where content ends (for idle mode positioning).
|
|
350
|
+
* Input area will render starting from this row + 1.
|
|
351
|
+
*/
|
|
352
|
+
setContentEndRow(row) {
|
|
353
|
+
this.contentEndRow = Math.max(0, row);
|
|
354
|
+
this.renderDirty = true;
|
|
355
|
+
this.scheduleRender();
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Set available slash commands for auto-complete suggestions.
|
|
359
|
+
*/
|
|
360
|
+
setCommands(commands) {
|
|
361
|
+
this.commandSuggestions = commands;
|
|
362
|
+
this.updateSuggestions();
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Update filtered suggestions based on current input.
|
|
366
|
+
*/
|
|
367
|
+
updateSuggestions() {
|
|
368
|
+
const input = this.buffer.trim();
|
|
369
|
+
// Only show suggestions when input starts with "/"
|
|
370
|
+
if (!input.startsWith('/')) {
|
|
371
|
+
this.showSuggestions = false;
|
|
372
|
+
this.filteredSuggestions = [];
|
|
373
|
+
this.selectedSuggestionIndex = 0;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const query = input.toLowerCase();
|
|
377
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
378
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
379
|
+
// Show suggestions if we have matches
|
|
380
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
381
|
+
// Keep selection in bounds
|
|
382
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
383
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
290
384
|
}
|
|
291
385
|
}
|
|
292
386
|
/**
|
|
293
|
-
*
|
|
294
|
-
|
|
387
|
+
* Select next suggestion (arrow down / tab).
|
|
388
|
+
*/
|
|
389
|
+
selectNextSuggestion() {
|
|
390
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
391
|
+
return;
|
|
392
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
393
|
+
this.renderDirty = true;
|
|
394
|
+
this.scheduleRender();
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
398
|
+
*/
|
|
399
|
+
selectPrevSuggestion() {
|
|
400
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
401
|
+
return;
|
|
402
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
403
|
+
? this.filteredSuggestions.length - 1
|
|
404
|
+
: this.selectedSuggestionIndex - 1;
|
|
405
|
+
this.renderDirty = true;
|
|
406
|
+
this.scheduleRender();
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Accept current suggestion and insert into buffer.
|
|
410
|
+
*/
|
|
411
|
+
acceptSuggestion() {
|
|
412
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
413
|
+
return false;
|
|
414
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
415
|
+
if (!selected)
|
|
416
|
+
return false;
|
|
417
|
+
// Replace buffer with selected command
|
|
418
|
+
this.buffer = selected.command + ' ';
|
|
419
|
+
this.cursor = this.buffer.length;
|
|
420
|
+
this.showSuggestions = false;
|
|
421
|
+
this.renderDirty = true;
|
|
422
|
+
this.scheduleRender();
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if suggestions are visible.
|
|
427
|
+
*/
|
|
428
|
+
areSuggestionsVisible() {
|
|
429
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Toggle thinking/reasoning mode
|
|
433
|
+
*/
|
|
434
|
+
toggleThinking() {
|
|
435
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
436
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
437
|
+
this.scheduleRender();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get thinking enabled state
|
|
295
441
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
442
|
+
isThinkingEnabled() {
|
|
443
|
+
return this.thinkingEnabled;
|
|
298
444
|
}
|
|
299
445
|
/**
|
|
300
446
|
* Get current mode
|
|
@@ -327,14 +473,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
327
473
|
}
|
|
328
474
|
/**
|
|
329
475
|
* Clear the buffer
|
|
476
|
+
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
330
477
|
*/
|
|
331
|
-
clear() {
|
|
478
|
+
clear(skipRender = false) {
|
|
332
479
|
this.buffer = '';
|
|
333
480
|
this.cursor = 0;
|
|
334
481
|
this.historyIndex = -1;
|
|
335
482
|
this.tempInput = '';
|
|
336
483
|
this.pastePlaceholders = [];
|
|
337
|
-
|
|
484
|
+
if (!skipRender) {
|
|
485
|
+
this.scheduleRender();
|
|
486
|
+
}
|
|
338
487
|
}
|
|
339
488
|
/**
|
|
340
489
|
* Get queued inputs
|
|
@@ -405,37 +554,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
405
554
|
this.streamingLabel = next;
|
|
406
555
|
this.scheduleRender();
|
|
407
556
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
410
|
-
*/
|
|
411
|
-
setMetaStatus(meta) {
|
|
412
|
-
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
413
|
-
? Math.floor(meta.elapsedSeconds)
|
|
414
|
-
: null;
|
|
415
|
-
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
416
|
-
? Math.floor(meta.tokensUsed)
|
|
417
|
-
: null;
|
|
418
|
-
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
419
|
-
? Math.floor(meta.tokenLimit)
|
|
420
|
-
: null;
|
|
421
|
-
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
422
|
-
? Math.floor(meta.thinkingMs)
|
|
423
|
-
: null;
|
|
424
|
-
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
425
|
-
if (this.metaElapsedSeconds === nextElapsed &&
|
|
426
|
-
this.metaTokensUsed === nextTokens &&
|
|
427
|
-
this.metaTokenLimit === nextLimit &&
|
|
428
|
-
this.metaThinkingMs === nextThinking &&
|
|
429
|
-
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
this.metaElapsedSeconds = nextElapsed;
|
|
433
|
-
this.metaTokensUsed = nextTokens;
|
|
434
|
-
this.metaTokenLimit = nextLimit;
|
|
435
|
-
this.metaThinkingMs = nextThinking;
|
|
436
|
-
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
437
|
-
this.scheduleRender();
|
|
438
|
-
}
|
|
439
557
|
/**
|
|
440
558
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
441
559
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -445,22 +563,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
445
563
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
446
564
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
447
565
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
448
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
449
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
450
566
|
if (this.verificationEnabled === nextVerification &&
|
|
451
567
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
452
568
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
453
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
454
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
455
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
569
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
456
570
|
return;
|
|
457
571
|
}
|
|
458
572
|
this.verificationEnabled = nextVerification;
|
|
459
573
|
this.autoContinueEnabled = nextAutoContinue;
|
|
460
574
|
this.verificationHotkey = nextVerifyHotkey;
|
|
461
575
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
462
|
-
this.
|
|
463
|
-
|
|
576
|
+
this.scheduleRender();
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
580
|
+
* This is displayed persistently above the input area.
|
|
581
|
+
*/
|
|
582
|
+
setModelInfo(info) {
|
|
583
|
+
if (this.modelInfo === info)
|
|
584
|
+
return;
|
|
585
|
+
this.modelInfo = info;
|
|
464
586
|
this.scheduleRender();
|
|
465
587
|
}
|
|
466
588
|
/**
|
|
@@ -473,174 +595,33 @@ export class TerminalInput extends EventEmitter {
|
|
|
473
595
|
this.scheduleRender();
|
|
474
596
|
}
|
|
475
597
|
/**
|
|
476
|
-
*
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const nextModel = options.model?.trim() || null;
|
|
480
|
-
const nextProvider = options.provider?.trim() || null;
|
|
481
|
-
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
this.modelLabel = nextModel;
|
|
485
|
-
this.providerLabel = nextProvider;
|
|
486
|
-
this.scheduleRender();
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Render the floating input area at contentRow.
|
|
490
|
-
*
|
|
491
|
-
* The chat box "floats" - it renders right below the last streamed content.
|
|
492
|
-
* As content is added, contentRow advances, and the chat box moves down.
|
|
493
|
-
* No scroll regions - pure floating behavior.
|
|
598
|
+
* Render the input area.
|
|
599
|
+
* During streaming: renders at terminal bottom (with scroll region)
|
|
600
|
+
* After streaming: renders floating below content
|
|
494
601
|
*/
|
|
495
602
|
render() {
|
|
496
603
|
if (!this.canRender())
|
|
497
604
|
return;
|
|
498
605
|
if (this.isRendering)
|
|
499
606
|
return;
|
|
500
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
501
|
-
// During streaming, throttle re-renders
|
|
502
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
503
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
504
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
505
|
-
if (waitMs > 0) {
|
|
506
|
-
this.renderDirty = true;
|
|
507
|
-
this.scheduleStreamingRender(waitMs);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
607
|
const shouldSkip = !this.renderDirty &&
|
|
512
608
|
this.buffer === this.lastRenderContent &&
|
|
513
609
|
this.cursor === this.lastRenderCursor;
|
|
514
610
|
this.renderDirty = false;
|
|
611
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
515
612
|
if (shouldSkip) {
|
|
516
613
|
return;
|
|
517
614
|
}
|
|
615
|
+
// If write lock is held, defer render
|
|
518
616
|
if (writeLock.isLocked()) {
|
|
519
617
|
writeLock.safeWrite(() => this.render());
|
|
520
618
|
return;
|
|
521
619
|
}
|
|
522
|
-
this.renderPinnedChatBox();
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Unified scroll region renderer.
|
|
526
|
-
* Chat box is ALWAYS pinned at the bottom of the terminal.
|
|
527
|
-
* Content scrolls in the region above the chat box.
|
|
528
|
-
*/
|
|
529
|
-
renderPinnedChatBox() {
|
|
530
|
-
const { rows, cols } = this.getSize();
|
|
531
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
532
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
533
|
-
// Wrap buffer into display lines
|
|
534
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
535
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
536
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
537
|
-
const metaLines = this.buildMetaLines(cols - 2);
|
|
538
|
-
// Calculate display window (keep cursor visible)
|
|
539
|
-
let startLine = 0;
|
|
540
|
-
if (lines.length > displayLines) {
|
|
541
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
542
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
543
|
-
}
|
|
544
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
545
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
546
|
-
// Chat box height
|
|
547
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
548
|
-
// ALWAYS pin chat box at absolute bottom
|
|
549
|
-
const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
|
|
550
|
-
const scrollEnd = chatBoxStartRow - 1;
|
|
551
|
-
writeLock.lock('terminalInput.renderPinned');
|
|
552
620
|
this.isRendering = true;
|
|
621
|
+
writeLock.lock('terminalInput.render');
|
|
553
622
|
try {
|
|
554
|
-
|
|
555
|
-
this.
|
|
556
|
-
// Temporarily reset scroll region to write chat box cleanly
|
|
557
|
-
if (this.scrollRegionActive) {
|
|
558
|
-
this.write(ESC.RESET_SCROLL);
|
|
559
|
-
}
|
|
560
|
-
// Clear the chat box area
|
|
561
|
-
for (let i = 0; i < chatBoxHeight; i++) {
|
|
562
|
-
const row = chatBoxStartRow + i;
|
|
563
|
-
if (row <= rows) {
|
|
564
|
-
this.write(ESC.TO(row, 1));
|
|
565
|
-
this.write(ESC.CLEAR_LINE);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
let currentRow = chatBoxStartRow;
|
|
569
|
-
// Render scroll/status indicator on the left (Claude Code style)
|
|
570
|
-
const scrollIndicator = this.buildScrollIndicator();
|
|
571
|
-
// Meta/status header with scroll indicator
|
|
572
|
-
for (const metaLine of metaLines) {
|
|
573
|
-
this.write(ESC.TO(currentRow, 1));
|
|
574
|
-
this.write(metaLine);
|
|
575
|
-
currentRow += 1;
|
|
576
|
-
}
|
|
577
|
-
// Separator line with scroll status
|
|
578
|
-
this.write(ESC.TO(currentRow, 1));
|
|
579
|
-
const dividerLabel = scrollIndicator || undefined;
|
|
580
|
-
this.write(renderDivider(cols - 2, dividerLabel));
|
|
581
|
-
currentRow += 1;
|
|
582
|
-
// Render input lines
|
|
583
|
-
let finalRow = currentRow;
|
|
584
|
-
let finalCol = 3;
|
|
585
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
586
|
-
const rowNum = currentRow + i;
|
|
587
|
-
this.write(ESC.TO(rowNum, 1));
|
|
588
|
-
const line = visibleLines[i] ?? '';
|
|
589
|
-
const isFirstLine = (startLine + i) === 0;
|
|
590
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
591
|
-
this.write(ESC.BG_DARK);
|
|
592
|
-
this.write(ESC.DIM);
|
|
593
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
594
|
-
this.write(ESC.RESET);
|
|
595
|
-
this.write(ESC.BG_DARK);
|
|
596
|
-
if (isCursorLine) {
|
|
597
|
-
const col = Math.min(cursorCol, line.length);
|
|
598
|
-
const before = line.slice(0, col);
|
|
599
|
-
const at = col < line.length ? line[col] : ' ';
|
|
600
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
601
|
-
this.write(before);
|
|
602
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
603
|
-
this.write(at);
|
|
604
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
605
|
-
this.write(after);
|
|
606
|
-
finalRow = rowNum;
|
|
607
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
this.write(line);
|
|
611
|
-
}
|
|
612
|
-
// Pad to edge
|
|
613
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
614
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
615
|
-
if (padding > 0)
|
|
616
|
-
this.write(' '.repeat(padding));
|
|
617
|
-
this.write(ESC.RESET);
|
|
618
|
-
}
|
|
619
|
-
// Mode controls line with all keyboard shortcuts
|
|
620
|
-
const controlRow = currentRow + visibleLines.length;
|
|
621
|
-
this.write(ESC.TO(controlRow, 1));
|
|
622
|
-
this.write(this.buildModeControls(cols));
|
|
623
|
-
// Restore scroll region and cursor
|
|
624
|
-
if (this.scrollRegionActive) {
|
|
625
|
-
// Restore scroll region
|
|
626
|
-
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
627
|
-
// Restore cursor to where it was before rendering (preserves column position)
|
|
628
|
-
this.write(ESC.RESTORE);
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
// Not streaming - position cursor in input box
|
|
632
|
-
this.write(ESC.RESTORE);
|
|
633
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
634
|
-
}
|
|
635
|
-
this.write(ESC.SHOW);
|
|
636
|
-
// Update state
|
|
637
|
-
this.lastRenderContent = this.buffer;
|
|
638
|
-
this.lastRenderCursor = this.cursor;
|
|
639
|
-
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
640
|
-
if (this.streamingRenderTimer) {
|
|
641
|
-
clearTimeout(this.streamingRenderTimer);
|
|
642
|
-
this.streamingRenderTimer = null;
|
|
643
|
-
}
|
|
623
|
+
// Always render floating right after content (no wasted space)
|
|
624
|
+
this.renderFloatingInputArea();
|
|
644
625
|
}
|
|
645
626
|
finally {
|
|
646
627
|
writeLock.unlock();
|
|
@@ -648,179 +629,211 @@ export class TerminalInput extends EventEmitter {
|
|
|
648
629
|
}
|
|
649
630
|
}
|
|
650
631
|
/**
|
|
651
|
-
* Build
|
|
652
|
-
* Shows model/provider and key metrics in a single line.
|
|
632
|
+
* Build the structured input layout according to the UI schema.
|
|
653
633
|
*/
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
634
|
+
buildInputLayout(cols) {
|
|
635
|
+
const width = Math.min(cols, Math.max(DEFAULT_UI_CONFIG.banner.minWidth, Math.min(DEFAULT_UI_CONFIG.banner.maxWidth, cols)));
|
|
636
|
+
const lines = [];
|
|
637
|
+
lines.push(this.buildStatusBar(width));
|
|
638
|
+
lines.push(this.buildDivider(width));
|
|
639
|
+
lines.push(...this.buildInputLines(width));
|
|
640
|
+
const suggestions = this.buildSuggestions(width);
|
|
641
|
+
if (suggestions.length > 0) {
|
|
642
|
+
lines.push(...suggestions);
|
|
643
|
+
}
|
|
644
|
+
lines.push(this.buildDivider(width));
|
|
645
|
+
lines.push(this.buildModeControls(width));
|
|
646
|
+
return { lines };
|
|
647
|
+
}
|
|
648
|
+
buildDivider(width) {
|
|
649
|
+
const lineWidth = Math.max(8, Math.min(width, DEFAULT_UI_CONFIG.banner.maxWidth));
|
|
650
|
+
return `${UI_COLORS.dim}${DEFAULT_UI_CONFIG.divider.char.repeat(lineWidth)}${UI_COLORS.reset}`;
|
|
651
|
+
}
|
|
652
|
+
buildInputLines(width) {
|
|
653
|
+
const prompt = `${UI_COLORS.dim}${this.config.promptChar}${UI_COLORS.reset}`;
|
|
654
|
+
const continuation = `${UI_COLORS.dim}${this.config.continuationChar}${UI_COLORS.reset}`;
|
|
655
|
+
const promptWidth = this.visibleLength(prompt);
|
|
656
|
+
const textWidth = Math.max(1, width - promptWidth);
|
|
657
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(textWidth);
|
|
658
|
+
const maxLines = Math.max(1, this.config.maxLines);
|
|
659
|
+
const startLine = Math.max(0, lines.length - maxLines);
|
|
660
|
+
const visibleLines = lines.slice(startLine);
|
|
661
|
+
const caretLine = Math.max(0, cursorLine - startLine);
|
|
662
|
+
if (!visibleLines.length) {
|
|
663
|
+
visibleLines.push('');
|
|
664
|
+
}
|
|
665
|
+
return visibleLines.map((lineText, index) => {
|
|
666
|
+
const hasCursor = caretLine === index;
|
|
667
|
+
const cursorIndex = hasCursor ? cursorCol : null;
|
|
668
|
+
const prefix = index === 0 ? prompt : continuation;
|
|
669
|
+
return this.renderPromptLine(prefix, lineText, cursorIndex, textWidth, width);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
buildSuggestions(width) {
|
|
673
|
+
if (!this.areSuggestionsVisible()) {
|
|
685
674
|
return [];
|
|
686
675
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
676
|
+
const lines = [];
|
|
677
|
+
const maxSuggestions = Math.min(3, this.filteredSuggestions.length);
|
|
678
|
+
lines.push(this.fitToWidth(`${UI_COLORS.dim}Suggestions${UI_COLORS.reset}`, width));
|
|
679
|
+
for (let i = 0; i < maxSuggestions; i++) {
|
|
680
|
+
const suggestion = this.filteredSuggestions[i];
|
|
681
|
+
if (!suggestion)
|
|
682
|
+
continue;
|
|
683
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
684
|
+
const bullet = isSelected ? `${UI_COLORS.cyan}›${UI_COLORS.reset}` : `${UI_COLORS.dim}·${UI_COLORS.reset}`;
|
|
685
|
+
const body = `${suggestion.command} ${UI_COLORS.dim}${suggestion.description}${UI_COLORS.reset}`.trim();
|
|
686
|
+
lines.push(this.fitToWidth(`${bullet} ${body}`, width));
|
|
687
|
+
}
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
renderPromptLine(prefix, text, cursorCol, textWidth, totalWidth) {
|
|
691
|
+
const visibleTextWidth = Math.max(1, textWidth);
|
|
692
|
+
const padded = (text ?? '').slice(0, visibleTextWidth).padEnd(visibleTextWidth, ' ');
|
|
693
|
+
let rendered = padded;
|
|
694
|
+
if (cursorCol !== null && cursorCol >= 0) {
|
|
695
|
+
const safeCol = Math.max(0, Math.min(cursorCol, Math.max(0, visibleTextWidth - 1)));
|
|
696
|
+
const before = padded.slice(0, safeCol);
|
|
697
|
+
const caretChar = padded[safeCol] ?? ' ';
|
|
698
|
+
const after = padded.slice(safeCol + 1);
|
|
699
|
+
rendered = `${before}${UI_COLORS.reverse}${caretChar || ' '}${UI_COLORS.reset}${after}`;
|
|
700
|
+
}
|
|
701
|
+
return this.fitToWidth(`${prefix}${rendered}`, totalWidth);
|
|
697
702
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
* Compact single line with essential info.
|
|
701
|
-
*/
|
|
702
|
-
buildModeControls(cols) {
|
|
703
|
-
const width = Math.max(8, cols - 2);
|
|
704
|
-
const parts = [];
|
|
705
|
-
// Streaming indicator
|
|
706
|
-
if (this.streamingLabel) {
|
|
707
|
-
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
708
|
-
}
|
|
709
|
-
// Interrupt hint (during streaming)
|
|
710
|
-
if (this.mode === 'streaming' || this.scrollRegionActive) {
|
|
711
|
-
parts.push({ text: `[Esc] stop`, tone: 'warn' });
|
|
712
|
-
}
|
|
713
|
-
// Scrollback indicator
|
|
714
|
-
if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
|
|
715
|
-
parts.push({ text: `↕${this.scrollbackOffset}L`, tone: 'info' });
|
|
716
|
-
}
|
|
717
|
-
else if (this.scrollbackBuffer.length > 50) {
|
|
718
|
-
parts.push({ text: `↕${this.scrollbackBuffer.length}L`, tone: 'muted' });
|
|
719
|
-
}
|
|
720
|
-
// Toggle states (compact)
|
|
721
|
-
const editIcon = this.editMode === 'display-edits' ? '✓' : '?';
|
|
722
|
-
const verifyIcon = this.verificationEnabled ? '✓' : '○';
|
|
723
|
-
const autoIcon = this.autoContinueEnabled ? '✓' : '○';
|
|
724
|
-
parts.push({
|
|
725
|
-
text: `edits${editIcon} verify${verifyIcon} auto${autoIcon}`,
|
|
726
|
-
tone: 'muted'
|
|
727
|
-
});
|
|
728
|
-
// Context remaining
|
|
729
|
-
const contextRemaining = this.computeContextRemaining();
|
|
730
|
-
if (contextRemaining !== null && contextRemaining <= 50) {
|
|
731
|
-
const tone = contextRemaining <= 10 ? 'warn' : 'info';
|
|
732
|
-
parts.push({ text: `${contextRemaining}%`, tone });
|
|
733
|
-
}
|
|
734
|
-
// Model info (when not streaming)
|
|
735
|
-
if (this.modelLabel && !this.streamingLabel) {
|
|
736
|
-
const shortModel = this.modelLabel.length > 20 ? this.modelLabel.slice(0, 18) + '..' : this.modelLabel;
|
|
737
|
-
parts.push({ text: shortModel, tone: 'muted' });
|
|
738
|
-
}
|
|
739
|
-
return renderStatusLine(parts, width);
|
|
740
|
-
}
|
|
741
|
-
formatHotkey(hotkey) {
|
|
742
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
743
|
-
if (!normalized)
|
|
744
|
-
return hotkey;
|
|
745
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
746
|
-
// Use readable key names instead of symbols for better terminal compatibility
|
|
747
|
-
const map = {
|
|
748
|
-
shift: 'Shift',
|
|
749
|
-
sh: 'Shift',
|
|
750
|
-
alt: 'Alt',
|
|
751
|
-
option: 'Alt',
|
|
752
|
-
opt: 'Alt',
|
|
753
|
-
ctrl: 'Ctrl',
|
|
754
|
-
control: 'Ctrl',
|
|
755
|
-
cmd: 'Cmd',
|
|
756
|
-
meta: 'Cmd',
|
|
757
|
-
esc: 'Esc',
|
|
758
|
-
escape: 'Esc',
|
|
759
|
-
tab: 'Tab',
|
|
760
|
-
return: 'Enter',
|
|
761
|
-
enter: 'Enter',
|
|
762
|
-
pageup: 'PgUp',
|
|
763
|
-
pagedown: 'PgDn',
|
|
764
|
-
home: 'Home',
|
|
765
|
-
end: 'End',
|
|
766
|
-
};
|
|
767
|
-
const formatted = parts
|
|
768
|
-
.map((part) => {
|
|
769
|
-
const label = map[part];
|
|
770
|
-
if (label)
|
|
771
|
-
return label;
|
|
772
|
-
return part.length === 1 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1);
|
|
773
|
-
})
|
|
774
|
-
.join('+');
|
|
775
|
-
return `[${formatted}]`;
|
|
703
|
+
visibleLength(value) {
|
|
704
|
+
return value.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
776
705
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
706
|
+
fitToWidth(value, width) {
|
|
707
|
+
const target = Math.max(1, width);
|
|
708
|
+
const visible = this.visibleLength(value);
|
|
709
|
+
if (visible === target) {
|
|
710
|
+
return value;
|
|
780
711
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
computeTokensRemaining() {
|
|
784
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
785
|
-
return null;
|
|
712
|
+
if (visible < target) {
|
|
713
|
+
return `${value}${' '.repeat(target - visible)}`;
|
|
786
714
|
}
|
|
787
|
-
|
|
788
|
-
return this.formatTokenCount(remaining);
|
|
715
|
+
return this.truncateToWidth(value, target);
|
|
789
716
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
717
|
+
truncateToWidth(value, width) {
|
|
718
|
+
const target = Math.max(0, width);
|
|
719
|
+
let visible = 0;
|
|
720
|
+
let result = '';
|
|
721
|
+
for (let i = 0; i < value.length && visible < target; i++) {
|
|
722
|
+
const char = value[i];
|
|
723
|
+
if (char === '\x1b') {
|
|
724
|
+
const match = value.slice(i).match(/^\x1b\[[0-9;]*[A-Za-z]/);
|
|
725
|
+
if (match) {
|
|
726
|
+
result += match[0];
|
|
727
|
+
i += match[0].length - 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
result += char;
|
|
732
|
+
visible += 1;
|
|
793
733
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
734
|
+
if (value.includes('\x1b') && !result.endsWith(UI_COLORS.reset)) {
|
|
735
|
+
result += UI_COLORS.reset;
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
797
738
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
739
|
+
/**
|
|
740
|
+
* Build status bar showing streaming/ready status and key info.
|
|
741
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
742
|
+
*/
|
|
743
|
+
buildStatusBar(width) {
|
|
744
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
745
|
+
const segments = [];
|
|
746
|
+
// Streaming status with elapsed time
|
|
747
|
+
if (this.mode === 'streaming' || this.streamingLabel) {
|
|
748
|
+
let label = this.streamingLabel || 'Streaming';
|
|
749
|
+
if (this.streamingStartTime) {
|
|
750
|
+
const elapsed = Math.max(0, Math.floor((Date.now() - this.streamingStartTime) / 1000));
|
|
751
|
+
const mins = Math.floor(elapsed / 60);
|
|
752
|
+
const secs = elapsed % 60;
|
|
753
|
+
const elapsedLabel = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
754
|
+
label = `${label} ${elapsedLabel}`;
|
|
755
|
+
}
|
|
756
|
+
segments.push(`${GREEN}● ${label}${R}`);
|
|
801
757
|
}
|
|
802
|
-
|
|
803
|
-
|
|
758
|
+
// Override/warning status
|
|
759
|
+
if (this.overrideStatusMessage) {
|
|
760
|
+
segments.push(`${YELLOW}⚠ ${this.overrideStatusMessage}${R}`);
|
|
804
761
|
}
|
|
805
|
-
|
|
806
|
-
|
|
762
|
+
// Primary status message
|
|
763
|
+
if (this.statusMessage) {
|
|
764
|
+
segments.push(`${CYAN}${this.statusMessage}${R}`);
|
|
807
765
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
766
|
+
// Queue + paste indicators
|
|
767
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
768
|
+
segments.push(`${CYAN}queued ${this.queue.length}${R}`);
|
|
769
|
+
}
|
|
770
|
+
if (this.pastePlaceholders.length > 0) {
|
|
771
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
772
|
+
segments.push(`${CYAN}📋 ${totalLines}L${R}`);
|
|
773
|
+
}
|
|
774
|
+
// Model info for quick glance
|
|
775
|
+
if (this.modelInfo) {
|
|
776
|
+
segments.push(`${DIM}${this.modelInfo}${R}`);
|
|
777
|
+
}
|
|
778
|
+
// Default hint when idle
|
|
779
|
+
if (!segments.length) {
|
|
780
|
+
segments.push(`${DIM}Type a message or / for commands${R}`);
|
|
781
|
+
}
|
|
782
|
+
// Multi-line indicator
|
|
783
|
+
if (this.buffer.includes('\n')) {
|
|
784
|
+
segments.push(`${DIM}${this.buffer.split('\n').length}L${R}`);
|
|
785
|
+
}
|
|
786
|
+
const joined = segments.join(`${DIM} · ${R}`);
|
|
787
|
+
return this.fitToWidth(joined, width);
|
|
813
788
|
}
|
|
814
789
|
/**
|
|
815
|
-
*
|
|
816
|
-
*
|
|
790
|
+
* Build mode controls line showing toggles and context info.
|
|
791
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
792
|
+
*
|
|
793
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
817
794
|
*/
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
795
|
+
buildModeControls(cols) {
|
|
796
|
+
const maxWidth = Math.max(10, cols - 2);
|
|
797
|
+
// Use schema-defined colors for consistency
|
|
798
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
799
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
800
|
+
const toggles = [];
|
|
801
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
802
|
+
if (this.editMode === 'display-edits') {
|
|
803
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
807
|
+
}
|
|
808
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
809
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
810
|
+
// Verification (green when on) - per schema.verificationMode
|
|
811
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
812
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
813
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
814
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
815
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
816
|
+
const rightParts = [];
|
|
817
|
+
if (this.contextUsage !== null) {
|
|
818
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
819
|
+
if (rem < 10)
|
|
820
|
+
rightParts.push(`${RED}⚠ ctx: ${rem}%${R}`);
|
|
821
|
+
else if (rem < 25)
|
|
822
|
+
rightParts.push(`${YELLOW}! ctx: ${rem}%${R}`);
|
|
823
|
+
else
|
|
824
|
+
rightParts.push(`${DIM}ctx: ${rem}%${R}`);
|
|
825
|
+
}
|
|
826
|
+
if (this.modelInfo) {
|
|
827
|
+
rightParts.push(`${DIM}${this.modelInfo}${R}`);
|
|
828
|
+
}
|
|
829
|
+
const rightPart = rightParts.join(`${DIM} · ${R}`);
|
|
830
|
+
const leftLen = this.visibleLength(leftPart);
|
|
831
|
+
const rightLen = this.visibleLength(rightPart);
|
|
832
|
+
if (rightPart && leftLen + rightLen < maxWidth - 2) {
|
|
833
|
+
return `${leftPart}${' '.repeat(Math.max(1, maxWidth - leftLen - rightLen))}${rightPart}`;
|
|
834
|
+
}
|
|
835
|
+
const combined = rightPart ? `${leftPart}${DIM} · ${R}${rightPart}` : leftPart;
|
|
836
|
+
return this.fitToWidth(combined, maxWidth);
|
|
824
837
|
}
|
|
825
838
|
/**
|
|
826
839
|
* Force a re-render
|
|
@@ -843,213 +856,40 @@ export class TerminalInput extends EventEmitter {
|
|
|
843
856
|
handleResize() {
|
|
844
857
|
this.lastRenderContent = '';
|
|
845
858
|
this.lastRenderCursor = -1;
|
|
846
|
-
this.
|
|
847
|
-
// If in scrollback mode, re-render the scrollback view with new dimensions
|
|
848
|
-
if (this.isInScrollbackMode) {
|
|
849
|
-
this.renderScrollbackView();
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
this.scheduleRender();
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Enter streaming mode with scroll region.
|
|
857
|
-
* Sets up terminal scroll region to exclude chat box.
|
|
858
|
-
*/
|
|
859
|
-
enterStreamingScrollRegion() {
|
|
860
|
-
const { rows } = this.getSize();
|
|
861
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
862
|
-
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
863
|
-
writeLock.lock('enterStreamingScrollRegion');
|
|
864
|
-
try {
|
|
865
|
-
// Set scroll region for content area (above chat box)
|
|
866
|
-
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
867
|
-
// Position cursor at current content row
|
|
868
|
-
this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
|
|
869
|
-
this.scrollRegionActive = true;
|
|
870
|
-
this.setStatusMessage('esc to interrupt');
|
|
871
|
-
}
|
|
872
|
-
finally {
|
|
873
|
-
writeLock.unlock();
|
|
874
|
-
}
|
|
875
|
-
// Render pinned chat box at bottom
|
|
876
|
-
this.forceRender();
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Exit streaming mode and restore normal operation.
|
|
880
|
-
*/
|
|
881
|
-
exitStreamingScrollRegion() {
|
|
882
|
-
writeLock.lock('exitStreamingScrollRegion');
|
|
883
|
-
try {
|
|
884
|
-
// Reset scroll region to full terminal
|
|
885
|
-
this.write(ESC.RESET_SCROLL);
|
|
886
|
-
this.scrollRegionActive = false;
|
|
887
|
-
this.setStatusMessage('Ready for prompts');
|
|
888
|
-
}
|
|
889
|
-
finally {
|
|
890
|
-
writeLock.unlock();
|
|
891
|
-
}
|
|
892
|
-
// Final render
|
|
893
|
-
this.forceRender();
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* Render chat box at bottom - now uses unified renderer.
|
|
897
|
-
* @deprecated Use renderPinnedChatBox() directly via render()/forceRender()
|
|
898
|
-
*/
|
|
899
|
-
renderChatBoxAtBottom() {
|
|
900
|
-
this.renderPinnedChatBox();
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Stream content within the scroll region.
|
|
904
|
-
* Content is written directly and scrolls naturally.
|
|
905
|
-
*/
|
|
906
|
-
streamContent(content) {
|
|
907
|
-
if (!content)
|
|
908
|
-
return;
|
|
909
|
-
// Capture content in scrollback buffer
|
|
910
|
-
this.addToScrollback(content);
|
|
911
|
-
writeLock.lock('streamContent');
|
|
912
|
-
try {
|
|
913
|
-
// Write content - scroll region handles scrolling
|
|
914
|
-
this.write(content);
|
|
915
|
-
// Track newlines
|
|
916
|
-
const newlines = (content.match(/\n/g) || []).length;
|
|
917
|
-
this.contentRow += newlines;
|
|
918
|
-
}
|
|
919
|
-
finally {
|
|
920
|
-
writeLock.unlock();
|
|
921
|
-
}
|
|
922
|
-
// Throttle chat box updates during streaming
|
|
923
|
-
this.scheduleStreamingRender(200);
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Enable scroll region (no-op in floating mode).
|
|
927
|
-
*/
|
|
928
|
-
enableScrollRegion() {
|
|
929
|
-
// No-op: using pure floating approach
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Disable scroll region (no-op in floating mode).
|
|
933
|
-
*/
|
|
934
|
-
disableScrollRegion() {
|
|
935
|
-
// No-op: using pure floating approach
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Calculate chat box height.
|
|
939
|
-
*/
|
|
940
|
-
getChatBoxHeight() {
|
|
941
|
-
return 5; // Fixed: divider + input + status + buffer
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* @deprecated Use streamContent() instead
|
|
945
|
-
* Register with display's output interceptor - kept for backwards compatibility
|
|
946
|
-
*/
|
|
947
|
-
registerOutputInterceptor(_display) {
|
|
948
|
-
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Write content above the floating chat box.
|
|
952
|
-
* Works both during streaming and when idle.
|
|
953
|
-
*/
|
|
954
|
-
writeToScrollRegion(content) {
|
|
955
|
-
if (!content)
|
|
956
|
-
return;
|
|
957
|
-
// Capture content in scrollback buffer
|
|
958
|
-
this.addToScrollback(content);
|
|
959
|
-
writeLock.lock('writeToScrollRegion');
|
|
960
|
-
try {
|
|
961
|
-
// Position cursor at content row and write
|
|
962
|
-
this.write(ESC.TO(this.contentRow, 1));
|
|
963
|
-
this.write(content);
|
|
964
|
-
// Track newlines
|
|
965
|
-
const newlines = (content.match(/\n/g) || []).length;
|
|
966
|
-
this.contentRow += newlines;
|
|
967
|
-
}
|
|
968
|
-
finally {
|
|
969
|
-
writeLock.unlock();
|
|
970
|
-
}
|
|
971
|
-
// Re-render chat box below new content (only when not streaming)
|
|
972
|
-
if (!this.scrollRegionActive) {
|
|
973
|
-
this.forceRender();
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Enter alternate screen buffer and clear it.
|
|
978
|
-
* This gives us full control over the terminal without affecting user's history.
|
|
979
|
-
*/
|
|
980
|
-
enterAlternateScreen() {
|
|
981
|
-
writeLock.lock('enterAltScreen');
|
|
982
|
-
try {
|
|
983
|
-
this.write(ESC.ENTER_ALT_SCREEN);
|
|
984
|
-
this.write(ESC.HOME);
|
|
985
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
986
|
-
this.contentRow = 1;
|
|
987
|
-
this.alternateScreenActive = true;
|
|
988
|
-
}
|
|
989
|
-
finally {
|
|
990
|
-
writeLock.unlock();
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Exit alternate screen buffer.
|
|
995
|
-
* Restores the user's previous terminal content.
|
|
996
|
-
*/
|
|
997
|
-
exitAlternateScreen() {
|
|
998
|
-
writeLock.lock('exitAltScreen');
|
|
999
|
-
try {
|
|
1000
|
-
this.write(ESC.EXIT_ALT_SCREEN);
|
|
1001
|
-
this.alternateScreenActive = false;
|
|
1002
|
-
}
|
|
1003
|
-
finally {
|
|
1004
|
-
writeLock.unlock();
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Check if alternate screen buffer is currently active.
|
|
1009
|
-
*/
|
|
1010
|
-
isAlternateScreenActive() {
|
|
1011
|
-
return this.alternateScreenActive;
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Get a snapshot of the scrollback buffer (for display on exit).
|
|
1015
|
-
*/
|
|
1016
|
-
getScrollbackSnapshot() {
|
|
1017
|
-
return [...this.scrollbackBuffer];
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Clear the entire terminal screen and reset content position.
|
|
1021
|
-
* This removes all content including the launching command.
|
|
1022
|
-
*/
|
|
1023
|
-
clearScreen() {
|
|
1024
|
-
writeLock.lock('clearScreen');
|
|
1025
|
-
try {
|
|
1026
|
-
this.write(ESC.HOME);
|
|
1027
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
1028
|
-
this.contentRow = 1;
|
|
1029
|
-
}
|
|
1030
|
-
finally {
|
|
1031
|
-
writeLock.unlock();
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Reset content position to row 1.
|
|
1036
|
-
* Does NOT clear the terminal - content starts from current position.
|
|
1037
|
-
*/
|
|
1038
|
-
resetContentPosition() {
|
|
1039
|
-
this.contentRow = 1;
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Set the content row explicitly (used after banner is written).
|
|
1043
|
-
* This tells the input where content should start flowing from.
|
|
1044
|
-
*/
|
|
1045
|
-
setContentRow(row) {
|
|
1046
|
-
this.contentRow = Math.max(1, row);
|
|
859
|
+
this.scheduleRender();
|
|
1047
860
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
861
|
+
// Track current content row for writing
|
|
862
|
+
contentCursorRow = 1;
|
|
863
|
+
/**
|
|
864
|
+
* Register with display's output interceptor.
|
|
865
|
+
* Clears chat box before writes, re-renders after with updated position.
|
|
866
|
+
*/
|
|
867
|
+
registerOutputInterceptor(display) {
|
|
868
|
+
if (this.outputInterceptorCleanup) {
|
|
869
|
+
this.outputInterceptorCleanup();
|
|
870
|
+
}
|
|
871
|
+
// Store display reference for streaming timer to use
|
|
872
|
+
this.displayRef = display;
|
|
873
|
+
// Clear chat box before writes to make room for content
|
|
874
|
+
// Re-render is done via periodic timer during streaming, or setContentEndRow after
|
|
875
|
+
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
876
|
+
beforeWrite: () => {
|
|
877
|
+
// Clear chat box to make room for content
|
|
878
|
+
this.clearInputArea();
|
|
879
|
+
},
|
|
880
|
+
afterWrite: () => {
|
|
881
|
+
if (this.mode !== 'streaming') {
|
|
882
|
+
if (this.displayRef?.getTotalWrittenLines) {
|
|
883
|
+
const next = this.displayRef.getTotalWrittenLines();
|
|
884
|
+
if (typeof next === 'number' && Number.isFinite(next)) {
|
|
885
|
+
this.contentEndRow = next;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
this.renderDirty = true;
|
|
889
|
+
queueMicrotask(() => this.render());
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
});
|
|
1053
893
|
}
|
|
1054
894
|
/**
|
|
1055
895
|
* Dispose and clean up
|
|
@@ -1057,10 +897,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
1057
897
|
dispose() {
|
|
1058
898
|
if (this.disposed)
|
|
1059
899
|
return;
|
|
900
|
+
// Clean up streaming render timer
|
|
901
|
+
if (this.streamingRenderTimer) {
|
|
902
|
+
clearInterval(this.streamingRenderTimer);
|
|
903
|
+
this.streamingRenderTimer = null;
|
|
904
|
+
}
|
|
905
|
+
// Clean up output interceptor
|
|
906
|
+
if (this.outputInterceptorCleanup) {
|
|
907
|
+
this.outputInterceptorCleanup();
|
|
908
|
+
this.outputInterceptorCleanup = undefined;
|
|
909
|
+
}
|
|
910
|
+
// Reset scroll region
|
|
911
|
+
this.write('\x1b[r');
|
|
912
|
+
// Exit alternate screen buffer (restores main terminal)
|
|
913
|
+
if (this.unifiedUIInitialized) {
|
|
914
|
+
this.write(ESC.ALT_SCREEN_EXIT);
|
|
915
|
+
}
|
|
1060
916
|
this.disposed = true;
|
|
1061
917
|
this.enabled = false;
|
|
1062
|
-
this.disableScrollRegion();
|
|
1063
|
-
this.resetStreamingRenderThrottle();
|
|
1064
918
|
this.disableBracketedPaste();
|
|
1065
919
|
this.buffer = '';
|
|
1066
920
|
this.queue = [];
|
|
@@ -1102,13 +956,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
1102
956
|
break;
|
|
1103
957
|
}
|
|
1104
958
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Handle Alt/Meta key combinations for mode toggles and navigation.
|
|
1107
|
-
* All major erosolar-cli features accessible via keyboard shortcuts.
|
|
1108
|
-
*/
|
|
1109
959
|
handleMetaKey(key) {
|
|
1110
960
|
switch (key.name) {
|
|
1111
|
-
// === CURSOR MOVEMENT ===
|
|
1112
961
|
case 'left':
|
|
1113
962
|
case 'b':
|
|
1114
963
|
this.moveCursorWordLeft();
|
|
@@ -1123,96 +972,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
1123
972
|
case 'return':
|
|
1124
973
|
this.insertNewline();
|
|
1125
974
|
break;
|
|
1126
|
-
// === MODE TOGGLES ===
|
|
1127
975
|
case 'v':
|
|
1128
|
-
// Alt+V: Toggle verification mode (auto-tests after edits)
|
|
1129
976
|
this.emit('toggleVerify');
|
|
1130
977
|
break;
|
|
1131
978
|
case 'c':
|
|
1132
|
-
// Alt+C: Toggle auto-continue mode
|
|
1133
979
|
this.emit('toggleAutoContinue');
|
|
1134
980
|
break;
|
|
1135
|
-
case 't':
|
|
1136
|
-
// Alt+T: Toggle/cycle thinking mode
|
|
1137
|
-
this.emit('toggleThinking');
|
|
1138
|
-
break;
|
|
1139
|
-
case 'e':
|
|
1140
|
-
// Alt+E: Toggle edit permission mode (ask/auto)
|
|
1141
|
-
this.toggleEditMode();
|
|
1142
|
-
this.emit('toggleEditMode');
|
|
1143
|
-
break;
|
|
1144
|
-
case 'x':
|
|
1145
|
-
// Alt+X: Clear/compact context
|
|
1146
|
-
this.emit('clearContext');
|
|
1147
|
-
break;
|
|
1148
|
-
// === SCROLLBACK NAVIGATION ===
|
|
1149
|
-
case 's':
|
|
1150
|
-
// Alt+S: Toggle scrollback mode
|
|
1151
|
-
this.toggleScrollbackMode();
|
|
1152
|
-
break;
|
|
1153
|
-
case 'up':
|
|
1154
|
-
// Alt+Up: Quick scroll up into history
|
|
1155
|
-
if (!this.isInScrollbackMode) {
|
|
1156
|
-
this.scrollUp(10);
|
|
1157
|
-
}
|
|
1158
|
-
else {
|
|
1159
|
-
this.scrollUp(1);
|
|
1160
|
-
}
|
|
1161
|
-
break;
|
|
1162
|
-
case 'down':
|
|
1163
|
-
// Alt+Down: Quick scroll down
|
|
1164
|
-
if (this.isInScrollbackMode) {
|
|
1165
|
-
this.scrollDown(1);
|
|
1166
|
-
}
|
|
1167
|
-
break;
|
|
1168
|
-
case 'pageup':
|
|
1169
|
-
// Alt+PageUp: Page up in scrollback
|
|
1170
|
-
this.scrollUp(this.getPageSize());
|
|
1171
|
-
break;
|
|
1172
|
-
case 'pagedown':
|
|
1173
|
-
// Alt+PageDown: Page down in scrollback
|
|
1174
|
-
this.scrollDown(this.getPageSize());
|
|
1175
|
-
break;
|
|
1176
|
-
case 'home':
|
|
1177
|
-
// Alt+Home: Jump to top of scrollback
|
|
1178
|
-
this.scrollToTop();
|
|
1179
|
-
break;
|
|
1180
|
-
case 'end':
|
|
1181
|
-
// Alt+End: Jump to bottom (live)
|
|
1182
|
-
this.scrollToBottom();
|
|
1183
|
-
break;
|
|
1184
981
|
}
|
|
1185
982
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Get page size for scrollback navigation.
|
|
1188
|
-
*/
|
|
1189
|
-
getPageSize() {
|
|
1190
|
-
const { rows } = this.getSize();
|
|
1191
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1192
|
-
return Math.max(5, rows - chatBoxHeight - 2);
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Build scroll indicator for the divider line (Claude Code style).
|
|
1196
|
-
* Shows scroll position when in scrollback mode, or history size hint when idle.
|
|
1197
|
-
*/
|
|
1198
|
-
buildScrollIndicator() {
|
|
1199
|
-
const bufferSize = this.scrollbackBuffer.length;
|
|
1200
|
-
// In scrollback mode - show position
|
|
1201
|
-
if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
|
|
1202
|
-
const { rows } = this.getSize();
|
|
1203
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1204
|
-
const viewportHeight = Math.max(1, rows - chatBoxHeight);
|
|
1205
|
-
const currentPos = Math.max(0, bufferSize - this.scrollbackOffset - viewportHeight);
|
|
1206
|
-
const pct = bufferSize > 0 ? Math.round((currentPos / bufferSize) * 100) : 0;
|
|
1207
|
-
return `↑${this.scrollbackOffset} · ${pct}% · PgUp/Dn`;
|
|
1208
|
-
}
|
|
1209
|
-
// Not in scrollback - show hint if there's history
|
|
1210
|
-
if (bufferSize > 20) {
|
|
1211
|
-
const sizeLabel = bufferSize >= 1000 ? `${Math.floor(bufferSize / 1000)}k` : `${bufferSize}`;
|
|
1212
|
-
return `↕${sizeLabel}L · PgUp`;
|
|
1213
|
-
}
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
983
|
handleSpecialKey(_str, key) {
|
|
1217
984
|
switch (key.name) {
|
|
1218
985
|
case 'return':
|
|
@@ -1236,53 +1003,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
1236
1003
|
this.moveCursorRight();
|
|
1237
1004
|
return true;
|
|
1238
1005
|
case 'up':
|
|
1239
|
-
|
|
1240
|
-
if ((key.ctrl && key.shift) || key.shift) {
|
|
1241
|
-
this.scrollUp(5);
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
this.handleUp();
|
|
1245
|
-
}
|
|
1006
|
+
this.handleUp();
|
|
1246
1007
|
return true;
|
|
1247
1008
|
case 'down':
|
|
1248
|
-
|
|
1249
|
-
if ((key.ctrl && key.shift) || key.shift) {
|
|
1250
|
-
this.scrollDown(5);
|
|
1251
|
-
}
|
|
1252
|
-
else {
|
|
1253
|
-
this.handleDown();
|
|
1254
|
-
}
|
|
1009
|
+
this.handleDown();
|
|
1255
1010
|
return true;
|
|
1256
1011
|
case 'home':
|
|
1257
|
-
|
|
1258
|
-
if (key.ctrl || this.isInScrollbackMode) {
|
|
1259
|
-
this.scrollToTop();
|
|
1260
|
-
}
|
|
1261
|
-
else {
|
|
1262
|
-
this.moveCursorToLineStart();
|
|
1263
|
-
}
|
|
1012
|
+
this.moveCursorToLineStart();
|
|
1264
1013
|
return true;
|
|
1265
1014
|
case 'end':
|
|
1266
|
-
|
|
1267
|
-
if (key.ctrl || this.isInScrollbackMode) {
|
|
1268
|
-
this.scrollToBottom();
|
|
1269
|
-
}
|
|
1270
|
-
else {
|
|
1271
|
-
this.moveCursorToLineEnd();
|
|
1272
|
-
}
|
|
1273
|
-
return true;
|
|
1274
|
-
case 'pageup':
|
|
1275
|
-
this.scrollUp(20); // Scroll up by 20 lines
|
|
1276
|
-
return true;
|
|
1277
|
-
case 'pagedown':
|
|
1278
|
-
this.scrollDown(20); // Scroll down by 20 lines
|
|
1015
|
+
this.moveCursorToLineEnd();
|
|
1279
1016
|
return true;
|
|
1280
1017
|
case 'tab':
|
|
1281
1018
|
if (key.shift) {
|
|
1282
1019
|
this.toggleEditMode();
|
|
1283
1020
|
return true;
|
|
1284
1021
|
}
|
|
1285
|
-
|
|
1022
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1023
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1024
|
+
this.togglePasteExpansion();
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
this.toggleThinking();
|
|
1028
|
+
}
|
|
1029
|
+
return true;
|
|
1030
|
+
case 'escape':
|
|
1031
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1032
|
+
if (this.mode === 'streaming') {
|
|
1033
|
+
this.emit('interrupt');
|
|
1034
|
+
}
|
|
1035
|
+
else if (this.buffer.length > 0) {
|
|
1036
|
+
this.clear();
|
|
1037
|
+
}
|
|
1286
1038
|
return true;
|
|
1287
1039
|
}
|
|
1288
1040
|
return false;
|
|
@@ -1300,6 +1052,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1300
1052
|
this.insertPlainText(chunk, insertPos);
|
|
1301
1053
|
this.cursor = insertPos + chunk.length;
|
|
1302
1054
|
this.emit('change', this.buffer);
|
|
1055
|
+
this.updateSuggestions();
|
|
1303
1056
|
this.scheduleRender();
|
|
1304
1057
|
}
|
|
1305
1058
|
insertNewline() {
|
|
@@ -1324,6 +1077,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1324
1077
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1325
1078
|
}
|
|
1326
1079
|
this.emit('change', this.buffer);
|
|
1080
|
+
this.updateSuggestions();
|
|
1327
1081
|
this.scheduleRender();
|
|
1328
1082
|
}
|
|
1329
1083
|
deleteForward() {
|
|
@@ -1551,12 +1305,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1551
1305
|
timestamp: Date.now(),
|
|
1552
1306
|
});
|
|
1553
1307
|
this.emit('queue', text);
|
|
1554
|
-
this.clear(); // Clear immediately for queued input
|
|
1308
|
+
this.clear(); // Clear immediately for queued input, re-render to update queue display
|
|
1555
1309
|
}
|
|
1556
1310
|
else {
|
|
1557
|
-
// In idle mode, clear the input
|
|
1558
|
-
// The
|
|
1559
|
-
|
|
1311
|
+
// In idle mode, clear the input WITHOUT rendering.
|
|
1312
|
+
// The caller will display the user message and start streaming.
|
|
1313
|
+
// We'll render the input area again after streaming ends.
|
|
1314
|
+
this.clear(true); // Skip render - streaming will handle display
|
|
1560
1315
|
this.emit('submit', text);
|
|
1561
1316
|
}
|
|
1562
1317
|
}
|
|
@@ -1573,9 +1328,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1573
1328
|
if (available <= 0)
|
|
1574
1329
|
return;
|
|
1575
1330
|
const chunk = clean.slice(0, available);
|
|
1576
|
-
|
|
1577
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1578
|
-
if (isMultiline && !isShortMultiline) {
|
|
1331
|
+
if (isMultilinePaste(chunk)) {
|
|
1579
1332
|
this.insertPastePlaceholder(chunk);
|
|
1580
1333
|
}
|
|
1581
1334
|
else {
|
|
@@ -1639,236 +1392,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1639
1392
|
return { lines, cursorLine, cursorCol };
|
|
1640
1393
|
}
|
|
1641
1394
|
// ===========================================================================
|
|
1642
|
-
// SCROLLBACK BUFFER
|
|
1643
|
-
// ===========================================================================
|
|
1644
|
-
/**
|
|
1645
|
-
* Add content to the scrollback buffer for history retention
|
|
1646
|
-
*/
|
|
1647
|
-
addToScrollback(content) {
|
|
1648
|
-
if (!content)
|
|
1649
|
-
return;
|
|
1650
|
-
// Split content into lines and add to buffer
|
|
1651
|
-
const lines = content.split('\n');
|
|
1652
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1653
|
-
const line = lines[i];
|
|
1654
|
-
if (line !== undefined) {
|
|
1655
|
-
// Only add non-empty lines or preserve newlines between content
|
|
1656
|
-
if (i < lines.length - 1 || line.length > 0) {
|
|
1657
|
-
this.scrollbackBuffer.push(line);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
// Trim buffer if it exceeds max size
|
|
1662
|
-
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1663
|
-
this.scrollbackBuffer.shift();
|
|
1664
|
-
}
|
|
1665
|
-
// If we're in live mode (not scrolled up), keep offset at 0
|
|
1666
|
-
if (this.scrollbackOffset === 0) {
|
|
1667
|
-
this.isInScrollbackMode = false;
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
/**
|
|
1671
|
-
* Scroll up by a number of lines (PageUp)
|
|
1672
|
-
*/
|
|
1673
|
-
scrollUp(lines = 10) {
|
|
1674
|
-
const { rows } = this.getSize();
|
|
1675
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1676
|
-
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1677
|
-
// Calculate max scroll offset
|
|
1678
|
-
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1679
|
-
this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
|
|
1680
|
-
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1681
|
-
if (this.isInScrollbackMode) {
|
|
1682
|
-
this.renderScrollbackView();
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
/**
|
|
1686
|
-
* Scroll down by a number of lines (PageDown)
|
|
1687
|
-
*/
|
|
1688
|
-
scrollDown(lines = 10) {
|
|
1689
|
-
this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
|
|
1690
|
-
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1691
|
-
if (this.isInScrollbackMode) {
|
|
1692
|
-
this.renderScrollbackView();
|
|
1693
|
-
}
|
|
1694
|
-
else {
|
|
1695
|
-
// Returned to live mode - force re-render
|
|
1696
|
-
this.forceRender();
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
/**
|
|
1700
|
-
* Jump to the top of scrollback buffer
|
|
1701
|
-
*/
|
|
1702
|
-
scrollToTop() {
|
|
1703
|
-
const { rows } = this.getSize();
|
|
1704
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1705
|
-
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1706
|
-
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1707
|
-
this.scrollbackOffset = maxOffset;
|
|
1708
|
-
this.isInScrollbackMode = true;
|
|
1709
|
-
this.renderScrollbackView();
|
|
1710
|
-
}
|
|
1711
|
-
/**
|
|
1712
|
-
* Jump to the bottom (live mode)
|
|
1713
|
-
*/
|
|
1714
|
-
scrollToBottom() {
|
|
1715
|
-
this.scrollbackOffset = 0;
|
|
1716
|
-
this.isInScrollbackMode = false;
|
|
1717
|
-
this.forceRender();
|
|
1718
|
-
}
|
|
1719
|
-
/**
|
|
1720
|
-
* Toggle scrollback mode on/off (Alt+S hotkey)
|
|
1721
|
-
*/
|
|
1722
|
-
toggleScrollbackMode() {
|
|
1723
|
-
if (this.isInScrollbackMode) {
|
|
1724
|
-
this.scrollToBottom();
|
|
1725
|
-
}
|
|
1726
|
-
else if (this.scrollbackBuffer.length > 0) {
|
|
1727
|
-
this.scrollUp(20);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
/**
|
|
1731
|
-
* Render the scrollback buffer view with enhanced visuals
|
|
1732
|
-
* Features:
|
|
1733
|
-
* - Visual scroll position indicator
|
|
1734
|
-
* - Progress bar showing position in history
|
|
1735
|
-
* - Keyboard navigation hints
|
|
1736
|
-
* - Animated indicators
|
|
1737
|
-
*/
|
|
1738
|
-
renderScrollbackView() {
|
|
1739
|
-
const { rows, cols } = this.getSize();
|
|
1740
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1741
|
-
const contentHeight = Math.max(1, rows - chatBoxHeight);
|
|
1742
|
-
writeLock.lock('renderScrollback');
|
|
1743
|
-
try {
|
|
1744
|
-
this.write(ESC.SAVE);
|
|
1745
|
-
this.write(ESC.HIDE);
|
|
1746
|
-
// Clear content area
|
|
1747
|
-
for (let i = 1; i <= contentHeight; i++) {
|
|
1748
|
-
this.write(ESC.TO(i, 1));
|
|
1749
|
-
this.write(ESC.CLEAR_LINE);
|
|
1750
|
-
}
|
|
1751
|
-
// Calculate which lines to show
|
|
1752
|
-
const totalLines = this.scrollbackBuffer.length;
|
|
1753
|
-
const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
|
|
1754
|
-
const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
|
|
1755
|
-
const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
|
|
1756
|
-
// Build header bar with navigation hints
|
|
1757
|
-
const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
|
|
1758
|
-
this.write(ESC.TO(1, 1));
|
|
1759
|
-
this.write(headerInfo);
|
|
1760
|
-
// Render visible lines with line numbers and visual guides
|
|
1761
|
-
const lineNumWidth = String(totalLines).length + 1;
|
|
1762
|
-
const contentStart = 2; // Start after header
|
|
1763
|
-
for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
|
|
1764
|
-
const line = visibleLines[i] ?? '';
|
|
1765
|
-
const lineNum = startIdx + i + 1;
|
|
1766
|
-
this.write(ESC.TO(contentStart + i, 1));
|
|
1767
|
-
// Line number gutter
|
|
1768
|
-
const numStr = String(lineNum).padStart(lineNumWidth, ' ');
|
|
1769
|
-
this.write(theme.ui.muted(`${numStr} │ `));
|
|
1770
|
-
// Content with truncation
|
|
1771
|
-
const gutterWidth = lineNumWidth + 4;
|
|
1772
|
-
const maxLen = cols - gutterWidth - 2;
|
|
1773
|
-
const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
|
|
1774
|
-
this.write(displayLine);
|
|
1775
|
-
}
|
|
1776
|
-
// Add visual scroll track on the right edge
|
|
1777
|
-
this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
|
|
1778
|
-
this.write(ESC.RESTORE);
|
|
1779
|
-
this.write(ESC.SHOW);
|
|
1780
|
-
}
|
|
1781
|
-
finally {
|
|
1782
|
-
writeLock.unlock();
|
|
1783
|
-
}
|
|
1784
|
-
// Re-render chat box
|
|
1785
|
-
this.forceRender();
|
|
1786
|
-
}
|
|
1787
|
-
/**
|
|
1788
|
-
* Build scrollback header with navigation hints
|
|
1789
|
-
*/
|
|
1790
|
-
buildScrollbackHeader(cols, totalLines, startIdx, endIdx) {
|
|
1791
|
-
const percentage = Math.round((endIdx / totalLines) * 100);
|
|
1792
|
-
// Animated scroll indicator
|
|
1793
|
-
const scrollFrames = ['◆', '◇', '◆', '◈'];
|
|
1794
|
-
this.scrollIndicatorFrame = (this.scrollIndicatorFrame + 1) % scrollFrames.length;
|
|
1795
|
-
const indicator = scrollFrames[this.scrollIndicatorFrame];
|
|
1796
|
-
// Build header parts
|
|
1797
|
-
const leftPart = theme.info(`${indicator} SCROLLBACK`) +
|
|
1798
|
-
theme.ui.muted(` [${startIdx + 1}-${endIdx} of ${totalLines}]`);
|
|
1799
|
-
const progressBar = this.buildProgressBar(percentage, 15);
|
|
1800
|
-
const rightPart = progressBar +
|
|
1801
|
-
theme.ui.muted(` ${percentage}%`) +
|
|
1802
|
-
theme.ui.muted(' │ ') +
|
|
1803
|
-
theme.primary('PgUp') + theme.ui.muted('/') + theme.primary('PgDn') +
|
|
1804
|
-
theme.ui.muted(' scroll · ') +
|
|
1805
|
-
theme.primary('End') + theme.ui.muted(' exit');
|
|
1806
|
-
const leftLen = this.visibleLength(leftPart);
|
|
1807
|
-
const rightLen = this.visibleLength(rightPart);
|
|
1808
|
-
const padding = Math.max(1, cols - leftLen - rightLen - 2);
|
|
1809
|
-
return `${leftPart}${' '.repeat(padding)}${rightPart}`;
|
|
1810
|
-
}
|
|
1811
|
-
/**
|
|
1812
|
-
* Render visual scroll track on the right side
|
|
1813
|
-
*/
|
|
1814
|
-
renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx) {
|
|
1815
|
-
if (totalLines <= contentHeight || cols < 40)
|
|
1816
|
-
return;
|
|
1817
|
-
const trackHeight = contentHeight - 1; // Exclude header
|
|
1818
|
-
const viewportRatio = (endIdx - startIdx) / totalLines;
|
|
1819
|
-
const positionRatio = startIdx / Math.max(1, totalLines - (endIdx - startIdx));
|
|
1820
|
-
// Calculate thumb size and position
|
|
1821
|
-
const thumbSize = Math.max(1, Math.round(viewportRatio * trackHeight));
|
|
1822
|
-
const thumbStart = Math.round(positionRatio * (trackHeight - thumbSize));
|
|
1823
|
-
// Render track on right edge
|
|
1824
|
-
for (let i = 0; i < trackHeight; i++) {
|
|
1825
|
-
const row = 2 + i; // Start after header
|
|
1826
|
-
this.write(ESC.TO(row, cols));
|
|
1827
|
-
if (i >= thumbStart && i < thumbStart + thumbSize) {
|
|
1828
|
-
// Thumb (viewport indicator)
|
|
1829
|
-
this.write(theme.accent('█'));
|
|
1830
|
-
}
|
|
1831
|
-
else {
|
|
1832
|
-
// Track background
|
|
1833
|
-
this.write(theme.ui.muted('░'));
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Build a visual progress bar
|
|
1839
|
-
*/
|
|
1840
|
-
buildProgressBar(percentage, width = 10) {
|
|
1841
|
-
const filled = Math.round((percentage / 100) * width);
|
|
1842
|
-
const empty = width - filled;
|
|
1843
|
-
const bar = theme.accent('█'.repeat(filled)) +
|
|
1844
|
-
theme.ui.muted('░'.repeat(empty));
|
|
1845
|
-
return `${theme.ui.muted('[')}${bar}${theme.ui.muted(']')}`;
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* Get scrollback buffer content (for persistence)
|
|
1849
|
-
*/
|
|
1850
|
-
getScrollbackBuffer() {
|
|
1851
|
-
return [...this.scrollbackBuffer];
|
|
1852
|
-
}
|
|
1853
|
-
/**
|
|
1854
|
-
* Load scrollback buffer (for restoration)
|
|
1855
|
-
*/
|
|
1856
|
-
loadScrollbackBuffer(lines) {
|
|
1857
|
-
this.scrollbackBuffer = [...lines];
|
|
1858
|
-
// Trim if necessary
|
|
1859
|
-
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1860
|
-
this.scrollbackBuffer.shift();
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Clear scrollback buffer
|
|
1865
|
-
*/
|
|
1866
|
-
clearScrollbackBuffer() {
|
|
1867
|
-
this.scrollbackBuffer = [];
|
|
1868
|
-
this.scrollbackOffset = 0;
|
|
1869
|
-
this.isInScrollbackMode = false;
|
|
1870
|
-
}
|
|
1871
|
-
// ===========================================================================
|
|
1872
1395
|
// UTILITIES
|
|
1873
1396
|
// ===========================================================================
|
|
1874
1397
|
getComposedLength() {
|
|
@@ -1941,19 +1464,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1941
1464
|
this.shiftPlaceholders(position, text.length);
|
|
1942
1465
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1943
1466
|
}
|
|
1944
|
-
shouldInlineMultiline(content) {
|
|
1945
|
-
const lines = content.split('\n').length;
|
|
1946
|
-
const maxInlineLines = 4;
|
|
1947
|
-
const maxInlineChars = 240;
|
|
1948
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1949
|
-
}
|
|
1950
1467
|
findPlaceholderAt(position) {
|
|
1951
1468
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1952
1469
|
}
|
|
1953
|
-
buildPlaceholder(
|
|
1470
|
+
buildPlaceholder(summary) {
|
|
1954
1471
|
const id = ++this.pasteCounter;
|
|
1955
|
-
const
|
|
1956
|
-
|
|
1472
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1473
|
+
// Show first line preview (truncated)
|
|
1474
|
+
const preview = summary.preview.length > 30
|
|
1475
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1476
|
+
: summary.preview;
|
|
1477
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1957
1478
|
return { id, placeholder };
|
|
1958
1479
|
}
|
|
1959
1480
|
insertPastePlaceholder(content) {
|
|
@@ -1961,21 +1482,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1961
1482
|
if (available <= 0)
|
|
1962
1483
|
return;
|
|
1963
1484
|
const cleanContent = content.slice(0, available);
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1485
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1486
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1487
|
+
if (summary.lineCount < 5) {
|
|
1488
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1489
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1490
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1491
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1966
1495
|
const insertPos = this.cursor;
|
|
1967
1496
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1968
1497
|
this.pastePlaceholders.push({
|
|
1969
1498
|
id,
|
|
1970
1499
|
content: cleanContent,
|
|
1971
|
-
lineCount,
|
|
1500
|
+
lineCount: summary.lineCount,
|
|
1972
1501
|
placeholder,
|
|
1973
1502
|
start: insertPos,
|
|
1974
1503
|
end: insertPos + placeholder.length,
|
|
1504
|
+
summary,
|
|
1505
|
+
expanded: false,
|
|
1975
1506
|
});
|
|
1976
1507
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1977
1508
|
this.cursor = insertPos + placeholder.length;
|
|
1978
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1512
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1513
|
+
*/
|
|
1514
|
+
togglePasteExpansion() {
|
|
1515
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1516
|
+
if (!placeholder)
|
|
1517
|
+
return false;
|
|
1518
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1519
|
+
// Update the placeholder text in buffer
|
|
1520
|
+
const newPlaceholder = placeholder.expanded
|
|
1521
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1522
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1523
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1524
|
+
// Update buffer
|
|
1525
|
+
this.buffer =
|
|
1526
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1527
|
+
newPlaceholder +
|
|
1528
|
+
this.buffer.slice(placeholder.end);
|
|
1529
|
+
// Update placeholder tracking
|
|
1530
|
+
placeholder.placeholder = newPlaceholder;
|
|
1531
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1532
|
+
// Shift other placeholders
|
|
1533
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1534
|
+
this.scheduleRender();
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
buildExpandedPlaceholder(ph) {
|
|
1538
|
+
const lines = ph.content.split('\n');
|
|
1539
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1540
|
+
const lastLines = lines.length > 5
|
|
1541
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1542
|
+
: '';
|
|
1543
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1544
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1545
|
+
}
|
|
1979
1546
|
deletePlaceholder(placeholder) {
|
|
1980
1547
|
const length = placeholder.end - placeholder.start;
|
|
1981
1548
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1983,11 +1550,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1983
1550
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1984
1551
|
this.cursor = placeholder.start;
|
|
1985
1552
|
}
|
|
1986
|
-
updateContextUsage(value
|
|
1987
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1988
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1989
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1990
|
-
}
|
|
1553
|
+
updateContextUsage(value) {
|
|
1991
1554
|
if (value === null || !Number.isFinite(value)) {
|
|
1992
1555
|
this.contextUsage = null;
|
|
1993
1556
|
}
|
|
@@ -2014,28 +1577,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
2014
1577
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
2015
1578
|
this.setEditMode(next);
|
|
2016
1579
|
}
|
|
2017
|
-
scheduleStreamingRender(delayMs) {
|
|
2018
|
-
if (this.streamingRenderTimer)
|
|
2019
|
-
return;
|
|
2020
|
-
const wait = Math.max(16, delayMs);
|
|
2021
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
2022
|
-
this.streamingRenderTimer = null;
|
|
2023
|
-
// During streaming, only update chat box (not full render)
|
|
2024
|
-
if (this.scrollRegionActive) {
|
|
2025
|
-
this.renderChatBoxAtBottom();
|
|
2026
|
-
}
|
|
2027
|
-
else {
|
|
2028
|
-
this.render();
|
|
2029
|
-
}
|
|
2030
|
-
}, wait);
|
|
2031
|
-
}
|
|
2032
|
-
resetStreamingRenderThrottle() {
|
|
2033
|
-
if (this.streamingRenderTimer) {
|
|
2034
|
-
clearTimeout(this.streamingRenderTimer);
|
|
2035
|
-
this.streamingRenderTimer = null;
|
|
2036
|
-
}
|
|
2037
|
-
this.lastStreamingRender = 0;
|
|
2038
|
-
}
|
|
2039
1580
|
scheduleRender() {
|
|
2040
1581
|
if (!this.canRender())
|
|
2041
1582
|
return;
|