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