erosolar-cli 1.7.262 → 1.7.263
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 -22
- 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/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/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/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.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/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/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 -11
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +153 -190
- 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 +0 -16
- 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 +147 -68
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +689 -451
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +20 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +29 -14
- package/dist/shell/terminalInputAdapter.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.map +1 -1
- package/dist/ui/ShellUIAdapter.js +12 -13
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +0 -19
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +33 -131
- 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 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +25 -15
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- 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
|
@@ -3,18 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
-
* - One bottom-pinned chat box for the entire session (no inline anchors)
|
|
7
6
|
* - Native bracketed paste support (no heuristics)
|
|
8
7
|
* - Clean cursor model with render-time wrapping
|
|
9
8
|
* - State machine for different input modes
|
|
10
9
|
* - No readline dependency for display
|
|
11
10
|
*/
|
|
12
11
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
14
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
15
|
-
import { renderDivider
|
|
16
|
-
import {
|
|
17
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
14
|
+
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
18
16
|
// ANSI escape codes
|
|
19
17
|
const ESC = {
|
|
20
18
|
// Cursor control
|
|
@@ -69,11 +67,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
67
|
statusMessage = null;
|
|
70
68
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
69
|
streamingLabel = null; // Streaming progress indicator
|
|
72
|
-
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
73
|
-
metaTokensUsed = null; // Optional token usage
|
|
74
|
-
metaTokenLimit = null; // Optional token window
|
|
75
|
-
metaThinkingMs = null; // Optional thinking duration
|
|
76
|
-
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
77
70
|
reservedLines = 2;
|
|
78
71
|
scrollRegionActive = false;
|
|
79
72
|
lastRenderContent = '';
|
|
@@ -81,35 +74,46 @@ export class TerminalInput extends EventEmitter {
|
|
|
81
74
|
renderDirty = false;
|
|
82
75
|
isRendering = false;
|
|
83
76
|
pinnedTopRows = 0;
|
|
77
|
+
inlineAnchorRow = null;
|
|
78
|
+
inlineLayout = false;
|
|
79
|
+
anchorProvider = null;
|
|
80
|
+
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
81
|
+
flowMode = true;
|
|
82
|
+
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
83
|
+
contentEndRow = 0; // Row where content ends (for idle mode positioning)
|
|
84
|
+
// Command suggestions (Claude Code style auto-complete)
|
|
85
|
+
commandSuggestions = [];
|
|
86
|
+
filteredSuggestions = [];
|
|
87
|
+
selectedSuggestionIndex = 0;
|
|
88
|
+
showSuggestions = false;
|
|
89
|
+
maxVisibleSuggestions = 10;
|
|
84
90
|
// Lifecycle
|
|
85
91
|
disposed = false;
|
|
86
92
|
enabled = true;
|
|
87
93
|
contextUsage = null;
|
|
88
|
-
contextAutoCompactThreshold = 90;
|
|
89
|
-
thinkingModeLabel = null;
|
|
90
94
|
editMode = 'display-edits';
|
|
91
95
|
verificationEnabled = true;
|
|
92
96
|
autoContinueEnabled = false;
|
|
93
97
|
verificationHotkey = 'alt+v';
|
|
94
98
|
autoContinueHotkey = 'alt+c';
|
|
95
|
-
thinkingHotkey = '/thinking';
|
|
96
|
-
modelLabel = null;
|
|
97
|
-
providerLabel = null;
|
|
98
99
|
// Output interceptor cleanup
|
|
99
100
|
outputInterceptorCleanup;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Metrics tracking for status bar
|
|
102
|
+
streamingStartTime = null;
|
|
103
|
+
tokensUsed = 0;
|
|
104
|
+
thinkingEnabled = true;
|
|
105
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
103
106
|
streamingRenderTimer = null;
|
|
104
107
|
constructor(writeStream = process.stdout, config = {}) {
|
|
105
108
|
super();
|
|
106
109
|
this.out = writeStream;
|
|
110
|
+
// Use schema defaults for configuration consistency
|
|
107
111
|
this.config = {
|
|
108
|
-
maxLines: config.maxLines ??
|
|
109
|
-
maxLength: config.maxLength ??
|
|
112
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
113
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
110
114
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
111
|
-
promptChar: config.promptChar ??
|
|
112
|
-
continuationChar: config.continuationChar ??
|
|
115
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
116
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
// ===========================================================================
|
|
@@ -188,6 +192,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
188
192
|
if (handled)
|
|
189
193
|
return;
|
|
190
194
|
}
|
|
195
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
196
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
197
|
+
this.emit('showHelp');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
191
200
|
// Insert printable characters
|
|
192
201
|
if (str && !key?.ctrl && !key?.meta) {
|
|
193
202
|
this.insertText(str);
|
|
@@ -196,38 +205,362 @@ export class TerminalInput extends EventEmitter {
|
|
|
196
205
|
/**
|
|
197
206
|
* Set the input mode
|
|
198
207
|
*
|
|
199
|
-
* Streaming
|
|
200
|
-
*
|
|
208
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
209
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
210
|
+
* the cursor is (below the streamed content).
|
|
201
211
|
*/
|
|
202
212
|
setMode(mode) {
|
|
203
213
|
const prevMode = this.mode;
|
|
204
214
|
this.mode = mode;
|
|
205
215
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
206
|
-
//
|
|
207
|
-
this.
|
|
208
|
-
|
|
216
|
+
// Track streaming start time for elapsed display
|
|
217
|
+
this.streamingStartTime = Date.now();
|
|
218
|
+
// NO scroll regions - content flows naturally to terminal scrollback
|
|
219
|
+
// Input area renders at absolute bottom using cursor save/restore
|
|
220
|
+
this.pinnedTopRows = 0;
|
|
221
|
+
this.reservedLines = 5; // Reserve space for input area at bottom
|
|
222
|
+
// Disable any existing scroll region
|
|
223
|
+
this.disableScrollRegion();
|
|
224
|
+
// Initial render of input area at bottom
|
|
225
|
+
this.renderStreamingInputArea();
|
|
226
|
+
// Start timer to update streaming status and re-render input area
|
|
227
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
228
|
+
if (this.mode === 'streaming') {
|
|
229
|
+
this.updateStreamingStatus();
|
|
230
|
+
this.renderStreamingInputArea();
|
|
231
|
+
}
|
|
232
|
+
}, 1000);
|
|
209
233
|
this.renderDirty = true;
|
|
210
|
-
this.render();
|
|
211
234
|
}
|
|
212
235
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
213
|
-
//
|
|
214
|
-
this.
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
// Stop streaming render timer
|
|
237
|
+
if (this.streamingRenderTimer) {
|
|
238
|
+
clearInterval(this.streamingRenderTimer);
|
|
239
|
+
this.streamingRenderTimer = null;
|
|
240
|
+
}
|
|
241
|
+
// Reset streaming time
|
|
242
|
+
this.streamingStartTime = null;
|
|
243
|
+
this.pinnedTopRows = 0;
|
|
244
|
+
// Ensure no scroll region is active
|
|
245
|
+
this.disableScrollRegion();
|
|
246
|
+
// Reset flow mode tracking
|
|
247
|
+
this.flowModeRenderedLines = 0;
|
|
248
|
+
// Render input area using unified method (same as streaming, but normal mode)
|
|
249
|
+
this.renderPinnedInputArea();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Update streaming status label (called by timer)
|
|
254
|
+
*/
|
|
255
|
+
updateStreamingStatus() {
|
|
256
|
+
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
257
|
+
return;
|
|
258
|
+
// Calculate elapsed time
|
|
259
|
+
const elapsed = Date.now() - this.streamingStartTime;
|
|
260
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
261
|
+
const minutes = Math.floor(seconds / 60);
|
|
262
|
+
const secs = seconds % 60;
|
|
263
|
+
// Format elapsed time
|
|
264
|
+
let elapsedStr;
|
|
265
|
+
if (minutes > 0) {
|
|
266
|
+
elapsedStr = `${minutes}m ${secs}s`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
elapsedStr = `${secs}s`;
|
|
270
|
+
}
|
|
271
|
+
// Update streaming label
|
|
272
|
+
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Render input area - unified for streaming and normal modes.
|
|
276
|
+
*
|
|
277
|
+
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
278
|
+
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
279
|
+
*/
|
|
280
|
+
renderPinnedInputArea() {
|
|
281
|
+
const { rows, cols } = this.getSize();
|
|
282
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
283
|
+
const divider = renderDivider(cols - 2);
|
|
284
|
+
const isStreaming = this.mode === 'streaming';
|
|
285
|
+
// Wrap buffer into display lines (multi-line support)
|
|
286
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
287
|
+
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
288
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
289
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
290
|
+
// Calculate display window (keep cursor visible)
|
|
291
|
+
let startLine = 0;
|
|
292
|
+
if (lines.length > displayLines) {
|
|
293
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
294
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
295
|
+
}
|
|
296
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
297
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
298
|
+
// Calculate total height: status + topDiv + input lines + bottomDiv + controls
|
|
299
|
+
const totalHeight = 4 + visibleLines.length;
|
|
300
|
+
// Save cursor position during streaming (so content flow resumes correctly)
|
|
301
|
+
if (isStreaming) {
|
|
302
|
+
this.write(ESC.SAVE);
|
|
303
|
+
}
|
|
304
|
+
this.write(ESC.HIDE);
|
|
305
|
+
this.write(ESC.RESET);
|
|
306
|
+
// Calculate start row based on mode:
|
|
307
|
+
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
308
|
+
// - Normal: right after content (contentEndRow + 1)
|
|
309
|
+
let currentRow;
|
|
310
|
+
if (isStreaming) {
|
|
311
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// In normal mode, render right after content
|
|
315
|
+
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
316
|
+
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
317
|
+
currentRow = Math.max(1, contentRow + 1);
|
|
318
|
+
}
|
|
319
|
+
let finalRow = currentRow;
|
|
320
|
+
let finalCol = 3;
|
|
321
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
322
|
+
this.write(ESC.TO(currentRow, 1));
|
|
323
|
+
this.write(ESC.CLEAR_TO_END);
|
|
324
|
+
// Status bar
|
|
325
|
+
this.write(ESC.TO(currentRow, 1));
|
|
326
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
327
|
+
currentRow++;
|
|
328
|
+
// Top divider
|
|
329
|
+
this.write(ESC.TO(currentRow, 1));
|
|
330
|
+
this.write(divider);
|
|
331
|
+
currentRow++;
|
|
332
|
+
// Input lines with background styling
|
|
333
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
334
|
+
this.write(ESC.TO(currentRow, 1));
|
|
335
|
+
const line = visibleLines[i] ?? '';
|
|
336
|
+
const absoluteLineIdx = startLine + i;
|
|
337
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
338
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
339
|
+
// Background
|
|
340
|
+
this.write(ESC.BG_DARK);
|
|
341
|
+
// Prompt prefix
|
|
342
|
+
this.write(ESC.DIM);
|
|
343
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
344
|
+
this.write(ESC.RESET);
|
|
345
|
+
this.write(ESC.BG_DARK);
|
|
346
|
+
if (isCursorLine) {
|
|
347
|
+
const col = Math.min(cursorCol, line.length);
|
|
348
|
+
const before = line.slice(0, col);
|
|
349
|
+
const at = col < line.length ? line[col] : ' ';
|
|
350
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
351
|
+
this.write(before);
|
|
352
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
353
|
+
this.write(at);
|
|
354
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
355
|
+
this.write(after);
|
|
356
|
+
finalRow = currentRow;
|
|
357
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.write(line);
|
|
361
|
+
}
|
|
362
|
+
// Pad to edge
|
|
363
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
364
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
365
|
+
if (padding > 0)
|
|
366
|
+
this.write(' '.repeat(padding));
|
|
367
|
+
this.write(ESC.RESET);
|
|
368
|
+
currentRow++;
|
|
369
|
+
}
|
|
370
|
+
// Bottom divider
|
|
371
|
+
this.write(ESC.TO(currentRow, 1));
|
|
372
|
+
this.write(divider);
|
|
373
|
+
currentRow++;
|
|
374
|
+
// Mode controls line
|
|
375
|
+
this.write(ESC.TO(currentRow, 1));
|
|
376
|
+
this.write(this.buildModeControls(cols));
|
|
377
|
+
// Restore cursor position during streaming, or show cursor in normal mode
|
|
378
|
+
if (isStreaming) {
|
|
379
|
+
this.write(ESC.RESTORE);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Position cursor in input area
|
|
383
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
384
|
+
this.write(ESC.SHOW);
|
|
385
|
+
}
|
|
386
|
+
// Update reserved lines for scroll region calculations
|
|
387
|
+
this.updateReservedLines(totalHeight);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Render input area during streaming (alias for unified method)
|
|
391
|
+
*/
|
|
392
|
+
renderStreamingInputArea() {
|
|
393
|
+
this.renderPinnedInputArea();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Enable or disable flow mode.
|
|
397
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
398
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
399
|
+
*/
|
|
400
|
+
setFlowMode(enabled) {
|
|
401
|
+
if (this.flowMode === enabled)
|
|
402
|
+
return;
|
|
403
|
+
this.flowMode = enabled;
|
|
404
|
+
this.renderDirty = true;
|
|
405
|
+
this.scheduleRender();
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Check if flow mode is enabled.
|
|
409
|
+
*/
|
|
410
|
+
isFlowMode() {
|
|
411
|
+
return this.flowMode;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Set the row where content ends (for idle mode positioning).
|
|
415
|
+
* Input area will render starting from this row + 1.
|
|
416
|
+
*/
|
|
417
|
+
setContentEndRow(row) {
|
|
418
|
+
this.contentEndRow = Math.max(0, row);
|
|
419
|
+
this.renderDirty = true;
|
|
420
|
+
this.scheduleRender();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Set available slash commands for auto-complete suggestions.
|
|
424
|
+
*/
|
|
425
|
+
setCommands(commands) {
|
|
426
|
+
this.commandSuggestions = commands;
|
|
427
|
+
this.updateSuggestions();
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Update filtered suggestions based on current input.
|
|
431
|
+
*/
|
|
432
|
+
updateSuggestions() {
|
|
433
|
+
const input = this.buffer.trim();
|
|
434
|
+
// Only show suggestions when input starts with "/"
|
|
435
|
+
if (!input.startsWith('/')) {
|
|
436
|
+
this.showSuggestions = false;
|
|
437
|
+
this.filteredSuggestions = [];
|
|
438
|
+
this.selectedSuggestionIndex = 0;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const query = input.toLowerCase();
|
|
442
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
443
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
444
|
+
// Show suggestions if we have matches
|
|
445
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
446
|
+
// Keep selection in bounds
|
|
447
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
448
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
217
449
|
}
|
|
218
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Select next suggestion (arrow down / tab).
|
|
453
|
+
*/
|
|
454
|
+
selectNextSuggestion() {
|
|
455
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
456
|
+
return;
|
|
457
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
458
|
+
this.renderDirty = true;
|
|
459
|
+
this.scheduleRender();
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
463
|
+
*/
|
|
464
|
+
selectPrevSuggestion() {
|
|
465
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
466
|
+
return;
|
|
467
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
468
|
+
? this.filteredSuggestions.length - 1
|
|
469
|
+
: this.selectedSuggestionIndex - 1;
|
|
470
|
+
this.renderDirty = true;
|
|
471
|
+
this.scheduleRender();
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Accept current suggestion and insert into buffer.
|
|
475
|
+
*/
|
|
476
|
+
acceptSuggestion() {
|
|
477
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
478
|
+
return false;
|
|
479
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
480
|
+
if (!selected)
|
|
481
|
+
return false;
|
|
482
|
+
// Replace buffer with selected command
|
|
483
|
+
this.buffer = selected.command + ' ';
|
|
484
|
+
this.cursor = this.buffer.length;
|
|
485
|
+
this.showSuggestions = false;
|
|
486
|
+
this.renderDirty = true;
|
|
487
|
+
this.scheduleRender();
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Check if suggestions are visible.
|
|
492
|
+
*/
|
|
493
|
+
areSuggestionsVisible() {
|
|
494
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Update token count for metrics display
|
|
498
|
+
*/
|
|
499
|
+
setTokensUsed(tokens) {
|
|
500
|
+
this.tokensUsed = tokens;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Toggle thinking/reasoning mode
|
|
504
|
+
*/
|
|
505
|
+
toggleThinking() {
|
|
506
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
507
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
508
|
+
this.scheduleRender();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get thinking enabled state
|
|
512
|
+
*/
|
|
513
|
+
isThinkingEnabled() {
|
|
514
|
+
return this.thinkingEnabled;
|
|
515
|
+
}
|
|
219
516
|
/**
|
|
220
517
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
221
518
|
*/
|
|
222
519
|
setPinnedHeaderLines(count) {
|
|
223
|
-
//
|
|
224
|
-
if (this.pinnedTopRows !==
|
|
225
|
-
this.pinnedTopRows =
|
|
520
|
+
// Set pinned header rows (banner area that scroll region excludes)
|
|
521
|
+
if (this.pinnedTopRows !== count) {
|
|
522
|
+
this.pinnedTopRows = count;
|
|
226
523
|
if (this.scrollRegionActive) {
|
|
227
524
|
this.applyScrollRegion();
|
|
228
525
|
}
|
|
229
526
|
}
|
|
230
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
530
|
+
* restore the default bottom-aligned layout.
|
|
531
|
+
*/
|
|
532
|
+
setInlineAnchor(row) {
|
|
533
|
+
if (row === null || row === undefined) {
|
|
534
|
+
this.inlineAnchorRow = null;
|
|
535
|
+
this.inlineLayout = false;
|
|
536
|
+
this.renderDirty = true;
|
|
537
|
+
this.render();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const { rows } = this.getSize();
|
|
541
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
542
|
+
this.inlineAnchorRow = clamped;
|
|
543
|
+
this.inlineLayout = true;
|
|
544
|
+
this.renderDirty = true;
|
|
545
|
+
this.render();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
549
|
+
* output by re-evaluating the anchor before each render.
|
|
550
|
+
*/
|
|
551
|
+
setInlineAnchorProvider(provider) {
|
|
552
|
+
this.anchorProvider = provider;
|
|
553
|
+
if (!provider) {
|
|
554
|
+
this.inlineLayout = false;
|
|
555
|
+
this.inlineAnchorRow = null;
|
|
556
|
+
this.renderDirty = true;
|
|
557
|
+
this.render();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.inlineLayout = true;
|
|
561
|
+
this.renderDirty = true;
|
|
562
|
+
this.render();
|
|
563
|
+
}
|
|
231
564
|
/**
|
|
232
565
|
* Get current mode
|
|
233
566
|
*/
|
|
@@ -337,37 +670,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
337
670
|
this.streamingLabel = next;
|
|
338
671
|
this.scheduleRender();
|
|
339
672
|
}
|
|
340
|
-
/**
|
|
341
|
-
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
342
|
-
*/
|
|
343
|
-
setMetaStatus(meta) {
|
|
344
|
-
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
345
|
-
? Math.floor(meta.elapsedSeconds)
|
|
346
|
-
: null;
|
|
347
|
-
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
348
|
-
? Math.floor(meta.tokensUsed)
|
|
349
|
-
: null;
|
|
350
|
-
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
351
|
-
? Math.floor(meta.tokenLimit)
|
|
352
|
-
: null;
|
|
353
|
-
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
354
|
-
? Math.floor(meta.thinkingMs)
|
|
355
|
-
: null;
|
|
356
|
-
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
357
|
-
if (this.metaElapsedSeconds === nextElapsed &&
|
|
358
|
-
this.metaTokensUsed === nextTokens &&
|
|
359
|
-
this.metaTokenLimit === nextLimit &&
|
|
360
|
-
this.metaThinkingMs === nextThinking &&
|
|
361
|
-
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
this.metaElapsedSeconds = nextElapsed;
|
|
365
|
-
this.metaTokensUsed = nextTokens;
|
|
366
|
-
this.metaTokenLimit = nextLimit;
|
|
367
|
-
this.metaThinkingMs = nextThinking;
|
|
368
|
-
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
369
|
-
this.scheduleRender();
|
|
370
|
-
}
|
|
371
673
|
/**
|
|
372
674
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
373
675
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -377,22 +679,16 @@ export class TerminalInput extends EventEmitter {
|
|
|
377
679
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
378
680
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
379
681
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
380
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
381
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
382
682
|
if (this.verificationEnabled === nextVerification &&
|
|
383
683
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
384
684
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
385
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
386
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
387
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
685
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
388
686
|
return;
|
|
389
687
|
}
|
|
390
688
|
this.verificationEnabled = nextVerification;
|
|
391
689
|
this.autoContinueEnabled = nextAutoContinue;
|
|
392
690
|
this.verificationHotkey = nextVerifyHotkey;
|
|
393
691
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
394
|
-
this.thinkingHotkey = nextThinkingHotkey;
|
|
395
|
-
this.thinkingModeLabel = nextThinkingLabel;
|
|
396
692
|
this.scheduleRender();
|
|
397
693
|
}
|
|
398
694
|
/**
|
|
@@ -404,386 +700,297 @@ export class TerminalInput extends EventEmitter {
|
|
|
404
700
|
this.streamingLabel = null;
|
|
405
701
|
this.scheduleRender();
|
|
406
702
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Surface model/provider context in the controls bar.
|
|
409
|
-
*/
|
|
410
|
-
setModelContext(options) {
|
|
411
|
-
const nextModel = options.model?.trim() || null;
|
|
412
|
-
const nextProvider = options.provider?.trim() || null;
|
|
413
|
-
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
this.modelLabel = nextModel;
|
|
417
|
-
this.providerLabel = nextProvider;
|
|
418
|
-
this.scheduleRender();
|
|
419
|
-
}
|
|
420
703
|
/**
|
|
421
704
|
* Render the input area - Claude Code style with mode controls
|
|
422
705
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* naturally above while elapsed time and status stay fresh.
|
|
706
|
+
* Same rendering for both normal and streaming modes - just different status bar.
|
|
707
|
+
* During streaming, uses cursor save/restore to preserve streaming position.
|
|
426
708
|
*/
|
|
427
709
|
render() {
|
|
428
710
|
if (!this.canRender())
|
|
429
711
|
return;
|
|
430
712
|
if (this.isRendering)
|
|
431
713
|
return;
|
|
432
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
433
|
-
// During streaming we still render the pinned input/status region, but throttle
|
|
434
|
-
// to avoid fighting with the streamed content flow.
|
|
435
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
436
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
437
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
438
|
-
if (waitMs > 0) {
|
|
439
|
-
this.renderDirty = true;
|
|
440
|
-
this.scheduleStreamingRender(waitMs);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
714
|
const shouldSkip = !this.renderDirty &&
|
|
445
715
|
this.buffer === this.lastRenderContent &&
|
|
446
716
|
this.cursor === this.lastRenderCursor;
|
|
447
717
|
this.renderDirty = false;
|
|
448
|
-
// Skip if nothing changed
|
|
718
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
449
719
|
if (shouldSkip) {
|
|
450
720
|
return;
|
|
451
721
|
}
|
|
452
|
-
// If write lock is held, defer render
|
|
722
|
+
// If write lock is held, defer render
|
|
453
723
|
if (writeLock.isLocked()) {
|
|
454
724
|
writeLock.safeWrite(() => this.render());
|
|
455
725
|
return;
|
|
456
726
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
727
|
+
this.isRendering = true;
|
|
728
|
+
writeLock.lock('terminalInput.render');
|
|
729
|
+
try {
|
|
730
|
+
// Render input area at bottom (outside scroll region)
|
|
731
|
+
this.renderBottomPinned();
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
writeLock.unlock();
|
|
735
|
+
this.isRendering = false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
740
|
+
*
|
|
741
|
+
* Flow mode attempted inline rendering but caused duplicate renders
|
|
742
|
+
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
743
|
+
*/
|
|
744
|
+
renderFlowMode() {
|
|
745
|
+
// Use stable bottom-pinned approach
|
|
746
|
+
this.renderBottomPinned();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
750
|
+
*
|
|
751
|
+
* Works for both normal and streaming modes:
|
|
752
|
+
* - During streaming: saves/restores cursor position
|
|
753
|
+
* - Status bar shows streaming info or "Type a message"
|
|
754
|
+
*
|
|
755
|
+
* Layout when suggestions visible:
|
|
756
|
+
* - Top divider
|
|
757
|
+
* - Input line(s)
|
|
758
|
+
* - Bottom divider
|
|
759
|
+
* - Suggestions (command list)
|
|
760
|
+
*
|
|
761
|
+
* Layout when suggestions hidden:
|
|
762
|
+
* - Status bar (Ready/Streaming)
|
|
763
|
+
* - Top divider
|
|
764
|
+
* - Input line(s)
|
|
765
|
+
* - Bottom divider
|
|
766
|
+
* - Mode controls
|
|
767
|
+
*/
|
|
768
|
+
renderBottomPinned() {
|
|
769
|
+
const { rows, cols } = this.getSize();
|
|
770
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
771
|
+
const isStreaming = this.mode === 'streaming';
|
|
772
|
+
// Use unified pinned input area (works for both streaming and normal)
|
|
773
|
+
// Only use complex rendering when suggestions are visible
|
|
774
|
+
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
775
|
+
if (!hasSuggestions) {
|
|
776
|
+
this.renderPinnedInputArea();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
// Wrap buffer into display lines
|
|
780
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
781
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
782
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
783
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
784
|
+
// Calculate display window (keep cursor visible)
|
|
785
|
+
let startLine = 0;
|
|
786
|
+
if (lines.length > displayLines) {
|
|
787
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
788
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
789
|
+
}
|
|
790
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
791
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
792
|
+
// Calculate suggestion display (not during streaming)
|
|
793
|
+
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
794
|
+
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
795
|
+
: [];
|
|
796
|
+
const suggestionLines = suggestionsToShow.length;
|
|
797
|
+
this.write(ESC.HIDE);
|
|
798
|
+
this.write(ESC.RESET);
|
|
799
|
+
const divider = renderDivider(cols - 2);
|
|
800
|
+
// Calculate positions from absolute bottom
|
|
801
|
+
let currentRow;
|
|
802
|
+
if (suggestionLines > 0) {
|
|
803
|
+
// With suggestions: input area + dividers + suggestions
|
|
804
|
+
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
805
|
+
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
806
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
807
|
+
this.updateReservedLines(totalHeight);
|
|
808
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
809
|
+
this.write(ESC.TO(currentRow, 1));
|
|
810
|
+
this.write(ESC.CLEAR_TO_END);
|
|
811
|
+
// Top divider
|
|
494
812
|
this.write(ESC.TO(currentRow, 1));
|
|
495
|
-
this.write(ESC.CLEAR_LINE);
|
|
496
|
-
const divider = renderDivider(cols - 2);
|
|
497
813
|
this.write(divider);
|
|
498
|
-
currentRow
|
|
499
|
-
//
|
|
814
|
+
currentRow++;
|
|
815
|
+
// Input lines
|
|
500
816
|
let finalRow = currentRow;
|
|
501
817
|
let finalCol = 3;
|
|
502
818
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
503
|
-
|
|
504
|
-
this.write(ESC.TO(rowNum, 1));
|
|
505
|
-
this.write(ESC.CLEAR_LINE);
|
|
819
|
+
this.write(ESC.TO(currentRow, 1));
|
|
506
820
|
const line = visibleLines[i] ?? '';
|
|
507
821
|
const absoluteLineIdx = startLine + i;
|
|
508
822
|
const isFirstLine = absoluteLineIdx === 0;
|
|
509
823
|
const isCursorLine = i === adjustedCursorLine;
|
|
510
|
-
// Background
|
|
511
|
-
this.write(ESC.BG_DARK);
|
|
512
|
-
// Prompt prefix
|
|
513
|
-
this.write(ESC.DIM);
|
|
514
824
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
515
|
-
this.write(ESC.RESET);
|
|
516
|
-
this.write(ESC.BG_DARK);
|
|
517
825
|
if (isCursorLine) {
|
|
518
|
-
// Render with block cursor
|
|
519
826
|
const col = Math.min(cursorCol, line.length);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.write(
|
|
524
|
-
this.write(
|
|
525
|
-
|
|
526
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
527
|
-
this.write(after);
|
|
528
|
-
finalRow = rowNum;
|
|
827
|
+
this.write(line.slice(0, col));
|
|
828
|
+
this.write(ESC.REVERSE);
|
|
829
|
+
this.write(col < line.length ? line[col] : ' ');
|
|
830
|
+
this.write(ESC.RESET);
|
|
831
|
+
this.write(line.slice(col + 1));
|
|
832
|
+
finalRow = currentRow;
|
|
529
833
|
finalCol = this.config.promptChar.length + col + 1;
|
|
530
834
|
}
|
|
531
835
|
else {
|
|
532
836
|
this.write(line);
|
|
533
837
|
}
|
|
534
|
-
|
|
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);
|
|
838
|
+
currentRow++;
|
|
540
839
|
}
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
this.write(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
840
|
+
// Bottom divider
|
|
841
|
+
this.write(ESC.TO(currentRow, 1));
|
|
842
|
+
this.write(divider);
|
|
843
|
+
currentRow++;
|
|
844
|
+
// Suggestions (Claude Code style)
|
|
845
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
846
|
+
this.write(ESC.TO(currentRow, 1));
|
|
847
|
+
const suggestion = suggestionsToShow[i];
|
|
848
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
849
|
+
// Indent and highlight selected
|
|
850
|
+
this.write(' ');
|
|
851
|
+
if (isSelected) {
|
|
852
|
+
this.write(ESC.REVERSE);
|
|
853
|
+
this.write(ESC.BOLD);
|
|
854
|
+
}
|
|
855
|
+
this.write(suggestion.command);
|
|
856
|
+
if (isSelected) {
|
|
857
|
+
this.write(ESC.RESET);
|
|
858
|
+
}
|
|
859
|
+
// Description (dimmed)
|
|
860
|
+
const descSpace = cols - suggestion.command.length - 8;
|
|
861
|
+
if (descSpace > 10 && suggestion.description) {
|
|
862
|
+
const desc = suggestion.description.slice(0, descSpace);
|
|
863
|
+
this.write(ESC.RESET);
|
|
864
|
+
this.write(ESC.DIM);
|
|
865
|
+
this.write(' ');
|
|
866
|
+
this.write(desc);
|
|
867
|
+
this.write(ESC.RESET);
|
|
868
|
+
}
|
|
869
|
+
currentRow++;
|
|
556
870
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
writeLock.lock('terminalInput.render');
|
|
560
|
-
this.isRendering = true;
|
|
561
|
-
try {
|
|
562
|
-
performRender();
|
|
563
|
-
}
|
|
564
|
-
finally {
|
|
565
|
-
writeLock.unlock();
|
|
566
|
-
this.isRendering = false;
|
|
871
|
+
// Position cursor in input area
|
|
872
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
567
873
|
}
|
|
874
|
+
this.write(ESC.SHOW);
|
|
875
|
+
// Update state
|
|
876
|
+
this.lastRenderContent = this.buffer;
|
|
877
|
+
this.lastRenderCursor = this.cursor;
|
|
568
878
|
}
|
|
569
879
|
/**
|
|
570
|
-
* Build
|
|
571
|
-
* During streaming, consolidates into a single line to minimize cursor repositioning
|
|
572
|
-
* and prevent escape code interleaving with streamed content.
|
|
880
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
573
881
|
*/
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
587
|
-
return parts.length ? [renderStatusLine(parts, width)] : [];
|
|
588
|
-
}
|
|
589
|
-
// Non-streaming: show full status info
|
|
590
|
-
const lines = [];
|
|
591
|
-
if (this.metaThinkingMs !== null) {
|
|
592
|
-
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
593
|
-
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
594
|
-
}
|
|
595
|
-
if (this.modelLabel) {
|
|
596
|
-
const modelText = this.providerLabel
|
|
597
|
-
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
598
|
-
: this.modelLabel;
|
|
599
|
-
lines.push(renderStatusLine([{ text: `model ${modelText}`, tone: 'muted' }], width));
|
|
600
|
-
}
|
|
601
|
-
const statusParts = [];
|
|
602
|
-
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
603
|
-
if (statusLabel) {
|
|
604
|
-
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
605
|
-
}
|
|
606
|
-
if (this.metaElapsedSeconds !== null) {
|
|
607
|
-
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
608
|
-
}
|
|
609
|
-
const tokensRemaining = this.computeTokensRemaining();
|
|
610
|
-
if (tokensRemaining !== null) {
|
|
611
|
-
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
612
|
-
}
|
|
613
|
-
if (statusParts.length) {
|
|
614
|
-
lines.push(renderStatusLine(statusParts, width));
|
|
615
|
-
}
|
|
616
|
-
const usageParts = [];
|
|
617
|
-
if (this.metaTokensUsed !== null) {
|
|
618
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
619
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
620
|
-
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
621
|
-
}
|
|
622
|
-
if (this.contextUsage !== null) {
|
|
623
|
-
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
624
|
-
const left = Math.max(0, 100 - this.contextUsage);
|
|
625
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
626
|
-
}
|
|
882
|
+
buildStreamingStatusBar(cols) {
|
|
883
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
884
|
+
// Streaming status with elapsed time
|
|
885
|
+
let elapsed = '0s';
|
|
886
|
+
if (this.streamingStartTime) {
|
|
887
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
888
|
+
const mins = Math.floor(secs / 60);
|
|
889
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
890
|
+
}
|
|
891
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
892
|
+
// Queue indicator
|
|
627
893
|
if (this.queue.length > 0) {
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
if (usageParts.length) {
|
|
631
|
-
lines.push(renderStatusLine(usageParts, width));
|
|
632
|
-
}
|
|
633
|
-
return lines;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
637
|
-
*/
|
|
638
|
-
clearReservedArea(startRow, reservedLines, cols) {
|
|
639
|
-
const width = Math.max(1, cols);
|
|
640
|
-
for (let i = 0; i < reservedLines; i++) {
|
|
641
|
-
const row = startRow + i;
|
|
642
|
-
this.write(ESC.TO(row, 1));
|
|
643
|
-
this.write(' '.repeat(width));
|
|
894
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
644
895
|
}
|
|
896
|
+
// Hint for typing
|
|
897
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
898
|
+
return status;
|
|
645
899
|
}
|
|
646
900
|
/**
|
|
647
|
-
* Build
|
|
648
|
-
*
|
|
901
|
+
* Build status bar showing streaming/ready status and key info.
|
|
902
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
649
903
|
*/
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
if (this.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const editHotkey = this.formatHotkey('shift+tab');
|
|
664
|
-
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
665
|
-
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
666
|
-
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
667
|
-
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
668
|
-
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
669
|
-
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
670
|
-
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
671
|
-
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
672
|
-
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
673
|
-
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
674
|
-
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
904
|
+
buildStatusBar(cols) {
|
|
905
|
+
const maxWidth = cols - 2;
|
|
906
|
+
const parts = [];
|
|
907
|
+
// Streaming status with elapsed time (left side)
|
|
908
|
+
if (this.mode === 'streaming') {
|
|
909
|
+
let statusText = '● Streaming';
|
|
910
|
+
if (this.streamingStartTime) {
|
|
911
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
912
|
+
const mins = Math.floor(elapsed / 60);
|
|
913
|
+
const secs = elapsed % 60;
|
|
914
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
915
|
+
}
|
|
916
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
675
917
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
918
|
+
// Queue indicator during streaming
|
|
919
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
920
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
679
921
|
}
|
|
922
|
+
// Paste indicator
|
|
680
923
|
if (this.pastePlaceholders.length > 0) {
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
684
|
-
tone: 'info',
|
|
685
|
-
});
|
|
924
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
925
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
686
926
|
}
|
|
687
|
-
|
|
688
|
-
if (this.
|
|
689
|
-
|
|
690
|
-
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
691
|
-
}
|
|
692
|
-
if (this.modelLabel) {
|
|
693
|
-
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
694
|
-
rightParts.push({ text: modelText, tone: 'muted' });
|
|
695
|
-
}
|
|
696
|
-
if (contextRemaining !== null) {
|
|
697
|
-
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
698
|
-
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
699
|
-
? 'Context auto-compact imminent'
|
|
700
|
-
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
701
|
-
rightParts.push({ text: label, tone });
|
|
702
|
-
}
|
|
703
|
-
if (!rightParts.length || width < 60) {
|
|
704
|
-
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
705
|
-
return renderStatusLine(merged, width);
|
|
706
|
-
}
|
|
707
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
708
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
709
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
710
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
711
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
712
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
713
|
-
}
|
|
714
|
-
formatHotkey(hotkey) {
|
|
715
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
716
|
-
if (!normalized)
|
|
717
|
-
return hotkey;
|
|
718
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
719
|
-
const map = {
|
|
720
|
-
shift: '⇧',
|
|
721
|
-
sh: '⇧',
|
|
722
|
-
alt: '⌥',
|
|
723
|
-
option: '⌥',
|
|
724
|
-
opt: '⌥',
|
|
725
|
-
ctrl: '⌃',
|
|
726
|
-
control: '⌃',
|
|
727
|
-
cmd: '⌘',
|
|
728
|
-
meta: '⌘',
|
|
729
|
-
};
|
|
730
|
-
const formatted = parts
|
|
731
|
-
.map((part) => {
|
|
732
|
-
const symbol = map[part];
|
|
733
|
-
if (symbol)
|
|
734
|
-
return symbol;
|
|
735
|
-
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
736
|
-
})
|
|
737
|
-
.join('');
|
|
738
|
-
return formatted || hotkey;
|
|
739
|
-
}
|
|
740
|
-
computeContextRemaining() {
|
|
741
|
-
if (this.contextUsage === null) {
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
745
|
-
}
|
|
746
|
-
computeTokensRemaining() {
|
|
747
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
751
|
-
return this.formatTokenCount(remaining);
|
|
752
|
-
}
|
|
753
|
-
formatElapsedLabel(seconds) {
|
|
754
|
-
if (seconds < 60) {
|
|
755
|
-
return `${seconds}s`;
|
|
756
|
-
}
|
|
757
|
-
const mins = Math.floor(seconds / 60);
|
|
758
|
-
const secs = seconds % 60;
|
|
759
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
760
|
-
}
|
|
761
|
-
formatTokenCount(value) {
|
|
762
|
-
if (!Number.isFinite(value)) {
|
|
763
|
-
return `${value}`;
|
|
927
|
+
// Override/warning status
|
|
928
|
+
if (this.overrideStatusMessage) {
|
|
929
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
764
930
|
}
|
|
765
|
-
|
|
766
|
-
|
|
931
|
+
// If idle with empty buffer, show quick shortcuts
|
|
932
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
933
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
767
934
|
}
|
|
768
|
-
|
|
769
|
-
|
|
935
|
+
// Multi-line indicator
|
|
936
|
+
if (this.buffer.includes('\n')) {
|
|
937
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
770
938
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
return
|
|
939
|
+
if (parts.length === 0) {
|
|
940
|
+
return ''; // Empty status bar when idle
|
|
941
|
+
}
|
|
942
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
943
|
+
return joined.slice(0, maxWidth);
|
|
776
944
|
}
|
|
777
945
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
946
|
+
* Build mode controls line showing toggles and context info.
|
|
947
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
948
|
+
*
|
|
949
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
780
950
|
*/
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
951
|
+
buildModeControls(cols) {
|
|
952
|
+
const maxWidth = cols - 2;
|
|
953
|
+
// Use schema-defined colors for consistency
|
|
954
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
955
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
956
|
+
const toggles = [];
|
|
957
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
958
|
+
if (this.editMode === 'display-edits') {
|
|
959
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
963
|
+
}
|
|
964
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
965
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
966
|
+
// Verification (green when on) - per schema.verificationMode
|
|
967
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
968
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
969
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
970
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
971
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
972
|
+
let rightPart = '';
|
|
973
|
+
if (this.contextUsage !== null) {
|
|
974
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
975
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
976
|
+
if (rem < 10)
|
|
977
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
978
|
+
else if (rem < 25)
|
|
979
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
980
|
+
else
|
|
981
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
982
|
+
}
|
|
983
|
+
// Calculate visible lengths (strip ANSI)
|
|
984
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
985
|
+
const leftLen = strip(leftPart).length;
|
|
986
|
+
const rightLen = strip(rightPart).length;
|
|
987
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
988
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
989
|
+
}
|
|
990
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
991
|
+
return `${leftPart} ${rightPart}`;
|
|
992
|
+
}
|
|
993
|
+
return leftPart;
|
|
787
994
|
}
|
|
788
995
|
/**
|
|
789
996
|
* Force a re-render
|
|
@@ -806,19 +1013,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
806
1013
|
handleResize() {
|
|
807
1014
|
this.lastRenderContent = '';
|
|
808
1015
|
this.lastRenderCursor = -1;
|
|
809
|
-
this.resetStreamingRenderThrottle();
|
|
810
1016
|
// Re-clamp pinned header rows to the new terminal height
|
|
811
1017
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
812
|
-
if (this.scrollRegionActive) {
|
|
813
|
-
this.disableScrollRegion();
|
|
814
|
-
this.enableScrollRegion();
|
|
815
|
-
}
|
|
816
1018
|
this.scheduleRender();
|
|
817
1019
|
}
|
|
818
1020
|
/**
|
|
819
1021
|
* Register with display's output interceptor to position cursor correctly.
|
|
820
1022
|
* When scroll region is active, output needs to go to the scroll region,
|
|
821
1023
|
* not the protected bottom area where the input is rendered.
|
|
1024
|
+
*
|
|
1025
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
1026
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
822
1027
|
*/
|
|
823
1028
|
registerOutputInterceptor(display) {
|
|
824
1029
|
if (this.outputInterceptorCleanup) {
|
|
@@ -826,20 +1031,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
826
1031
|
}
|
|
827
1032
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
828
1033
|
beforeWrite: () => {
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
if (this.scrollRegionActive) {
|
|
832
|
-
const { rows } = this.getSize();
|
|
833
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
834
|
-
this.write(ESC.SAVE);
|
|
835
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
836
|
-
}
|
|
1034
|
+
// Scroll region handles content containment automatically
|
|
1035
|
+
// No per-write cursor manipulation needed
|
|
837
1036
|
},
|
|
838
1037
|
afterWrite: () => {
|
|
839
|
-
//
|
|
840
|
-
if (this.scrollRegionActive) {
|
|
841
|
-
this.write(ESC.RESTORE);
|
|
842
|
-
}
|
|
1038
|
+
// No cursor manipulation needed
|
|
843
1039
|
},
|
|
844
1040
|
});
|
|
845
1041
|
}
|
|
@@ -849,6 +1045,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
849
1045
|
dispose() {
|
|
850
1046
|
if (this.disposed)
|
|
851
1047
|
return;
|
|
1048
|
+
// Clean up streaming render timer
|
|
1049
|
+
if (this.streamingRenderTimer) {
|
|
1050
|
+
clearInterval(this.streamingRenderTimer);
|
|
1051
|
+
this.streamingRenderTimer = null;
|
|
1052
|
+
}
|
|
852
1053
|
// Clean up output interceptor
|
|
853
1054
|
if (this.outputInterceptorCleanup) {
|
|
854
1055
|
this.outputInterceptorCleanup();
|
|
@@ -856,7 +1057,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
856
1057
|
}
|
|
857
1058
|
this.disposed = true;
|
|
858
1059
|
this.enabled = false;
|
|
859
|
-
this.resetStreamingRenderThrottle();
|
|
860
1060
|
this.disableScrollRegion();
|
|
861
1061
|
this.disableBracketedPaste();
|
|
862
1062
|
this.buffer = '';
|
|
@@ -962,7 +1162,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
962
1162
|
this.toggleEditMode();
|
|
963
1163
|
return true;
|
|
964
1164
|
}
|
|
965
|
-
|
|
1165
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1166
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1167
|
+
this.togglePasteExpansion();
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
this.toggleThinking();
|
|
1171
|
+
}
|
|
1172
|
+
return true;
|
|
1173
|
+
case 'escape':
|
|
1174
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1175
|
+
if (this.mode === 'streaming') {
|
|
1176
|
+
this.emit('interrupt');
|
|
1177
|
+
}
|
|
1178
|
+
else if (this.buffer.length > 0) {
|
|
1179
|
+
this.clear();
|
|
1180
|
+
}
|
|
966
1181
|
return true;
|
|
967
1182
|
}
|
|
968
1183
|
return false;
|
|
@@ -980,6 +1195,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
980
1195
|
this.insertPlainText(chunk, insertPos);
|
|
981
1196
|
this.cursor = insertPos + chunk.length;
|
|
982
1197
|
this.emit('change', this.buffer);
|
|
1198
|
+
this.updateSuggestions();
|
|
983
1199
|
this.scheduleRender();
|
|
984
1200
|
}
|
|
985
1201
|
insertNewline() {
|
|
@@ -1004,6 +1220,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1004
1220
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1005
1221
|
}
|
|
1006
1222
|
this.emit('change', this.buffer);
|
|
1223
|
+
this.updateSuggestions();
|
|
1007
1224
|
this.scheduleRender();
|
|
1008
1225
|
}
|
|
1009
1226
|
deleteForward() {
|
|
@@ -1253,9 +1470,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1253
1470
|
if (available <= 0)
|
|
1254
1471
|
return;
|
|
1255
1472
|
const chunk = clean.slice(0, available);
|
|
1256
|
-
|
|
1257
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1258
|
-
if (isMultiline && !isShortMultiline) {
|
|
1473
|
+
if (isMultilinePaste(chunk)) {
|
|
1259
1474
|
this.insertPastePlaceholder(chunk);
|
|
1260
1475
|
}
|
|
1261
1476
|
else {
|
|
@@ -1275,7 +1490,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1275
1490
|
return;
|
|
1276
1491
|
this.applyScrollRegion();
|
|
1277
1492
|
this.scrollRegionActive = true;
|
|
1278
|
-
this.forceRender();
|
|
1279
1493
|
}
|
|
1280
1494
|
disableScrollRegion() {
|
|
1281
1495
|
if (!this.scrollRegionActive)
|
|
@@ -1426,19 +1640,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1426
1640
|
this.shiftPlaceholders(position, text.length);
|
|
1427
1641
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1428
1642
|
}
|
|
1429
|
-
shouldInlineMultiline(content) {
|
|
1430
|
-
const lines = content.split('\n').length;
|
|
1431
|
-
const maxInlineLines = 4;
|
|
1432
|
-
const maxInlineChars = 240;
|
|
1433
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1434
|
-
}
|
|
1435
1643
|
findPlaceholderAt(position) {
|
|
1436
1644
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1437
1645
|
}
|
|
1438
|
-
buildPlaceholder(
|
|
1646
|
+
buildPlaceholder(summary) {
|
|
1439
1647
|
const id = ++this.pasteCounter;
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1648
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1649
|
+
// Show first line preview (truncated)
|
|
1650
|
+
const preview = summary.preview.length > 30
|
|
1651
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1652
|
+
: summary.preview;
|
|
1653
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1442
1654
|
return { id, placeholder };
|
|
1443
1655
|
}
|
|
1444
1656
|
insertPastePlaceholder(content) {
|
|
@@ -1446,21 +1658,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1446
1658
|
if (available <= 0)
|
|
1447
1659
|
return;
|
|
1448
1660
|
const cleanContent = content.slice(0, available);
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1661
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1662
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1663
|
+
if (summary.lineCount < 5) {
|
|
1664
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1665
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1666
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1667
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1451
1671
|
const insertPos = this.cursor;
|
|
1452
1672
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1453
1673
|
this.pastePlaceholders.push({
|
|
1454
1674
|
id,
|
|
1455
1675
|
content: cleanContent,
|
|
1456
|
-
lineCount,
|
|
1676
|
+
lineCount: summary.lineCount,
|
|
1457
1677
|
placeholder,
|
|
1458
1678
|
start: insertPos,
|
|
1459
1679
|
end: insertPos + placeholder.length,
|
|
1680
|
+
summary,
|
|
1681
|
+
expanded: false,
|
|
1460
1682
|
});
|
|
1461
1683
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1462
1684
|
this.cursor = insertPos + placeholder.length;
|
|
1463
1685
|
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1688
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1689
|
+
*/
|
|
1690
|
+
togglePasteExpansion() {
|
|
1691
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1692
|
+
if (!placeholder)
|
|
1693
|
+
return false;
|
|
1694
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1695
|
+
// Update the placeholder text in buffer
|
|
1696
|
+
const newPlaceholder = placeholder.expanded
|
|
1697
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1698
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1699
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1700
|
+
// Update buffer
|
|
1701
|
+
this.buffer =
|
|
1702
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1703
|
+
newPlaceholder +
|
|
1704
|
+
this.buffer.slice(placeholder.end);
|
|
1705
|
+
// Update placeholder tracking
|
|
1706
|
+
placeholder.placeholder = newPlaceholder;
|
|
1707
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1708
|
+
// Shift other placeholders
|
|
1709
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1710
|
+
this.scheduleRender();
|
|
1711
|
+
return true;
|
|
1712
|
+
}
|
|
1713
|
+
buildExpandedPlaceholder(ph) {
|
|
1714
|
+
const lines = ph.content.split('\n');
|
|
1715
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1716
|
+
const lastLines = lines.length > 5
|
|
1717
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1718
|
+
: '';
|
|
1719
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1720
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1721
|
+
}
|
|
1464
1722
|
deletePlaceholder(placeholder) {
|
|
1465
1723
|
const length = placeholder.end - placeholder.start;
|
|
1466
1724
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1468,11 +1726,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1468
1726
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1469
1727
|
this.cursor = placeholder.start;
|
|
1470
1728
|
}
|
|
1471
|
-
updateContextUsage(value
|
|
1472
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1473
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1474
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1475
|
-
}
|
|
1729
|
+
updateContextUsage(value) {
|
|
1476
1730
|
if (value === null || !Number.isFinite(value)) {
|
|
1477
1731
|
this.contextUsage = null;
|
|
1478
1732
|
}
|
|
@@ -1499,22 +1753,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1499
1753
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1500
1754
|
this.setEditMode(next);
|
|
1501
1755
|
}
|
|
1502
|
-
scheduleStreamingRender(delayMs) {
|
|
1503
|
-
if (this.streamingRenderTimer)
|
|
1504
|
-
return;
|
|
1505
|
-
const wait = Math.max(16, delayMs);
|
|
1506
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1507
|
-
this.streamingRenderTimer = null;
|
|
1508
|
-
this.render();
|
|
1509
|
-
}, wait);
|
|
1510
|
-
}
|
|
1511
|
-
resetStreamingRenderThrottle() {
|
|
1512
|
-
if (this.streamingRenderTimer) {
|
|
1513
|
-
clearTimeout(this.streamingRenderTimer);
|
|
1514
|
-
this.streamingRenderTimer = null;
|
|
1515
|
-
}
|
|
1516
|
-
this.lastStreamingRender = 0;
|
|
1517
|
-
}
|
|
1518
1756
|
scheduleRender() {
|
|
1519
1757
|
if (!this.canRender())
|
|
1520
1758
|
return;
|