erosolar-cli 1.7.260 → 1.7.262
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 +22 -148
- 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/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.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/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/shell/interactiveShell.d.ts +10 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +182 -36
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +68 -140
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +424 -685
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +20 -15
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +14 -22
- package/dist/shell/terminalInputAdapter.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 +19 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +131 -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/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/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/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/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/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/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,45 +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
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
84
|
-
commandSuggestions = [];
|
|
85
|
-
filteredSuggestions = [];
|
|
86
|
-
selectedSuggestionIndex = 0;
|
|
87
|
-
showSuggestions = false;
|
|
88
|
-
maxVisibleSuggestions = 10;
|
|
89
84
|
// Lifecycle
|
|
90
85
|
disposed = false;
|
|
91
86
|
enabled = true;
|
|
92
87
|
contextUsage = null;
|
|
88
|
+
contextAutoCompactThreshold = 90;
|
|
89
|
+
thinkingModeLabel = null;
|
|
93
90
|
editMode = 'display-edits';
|
|
94
91
|
verificationEnabled = true;
|
|
95
92
|
autoContinueEnabled = false;
|
|
96
93
|
verificationHotkey = 'alt+v';
|
|
97
94
|
autoContinueHotkey = 'alt+c';
|
|
95
|
+
thinkingHotkey = '/thinking';
|
|
96
|
+
modelLabel = null;
|
|
97
|
+
providerLabel = null;
|
|
98
98
|
// Output interceptor cleanup
|
|
99
99
|
outputInterceptorCleanup;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
thinkingEnabled = true;
|
|
104
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
100
|
+
// Streaming render throttle
|
|
101
|
+
lastStreamingRender = 0;
|
|
102
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
105
103
|
streamingRenderTimer = null;
|
|
106
104
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
105
|
super();
|
|
108
106
|
this.out = writeStream;
|
|
109
|
-
// Use schema defaults for configuration consistency
|
|
110
107
|
this.config = {
|
|
111
|
-
maxLines: config.maxLines ??
|
|
112
|
-
maxLength: config.maxLength ??
|
|
108
|
+
maxLines: config.maxLines ?? 1000,
|
|
109
|
+
maxLength: config.maxLength ?? 10000,
|
|
113
110
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
114
|
-
promptChar: config.promptChar ??
|
|
115
|
-
continuationChar: config.continuationChar ??
|
|
111
|
+
promptChar: config.promptChar ?? '> ',
|
|
112
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
116
113
|
};
|
|
117
114
|
}
|
|
118
115
|
// ===========================================================================
|
|
@@ -191,11 +188,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
191
188
|
if (handled)
|
|
192
189
|
return;
|
|
193
190
|
}
|
|
194
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
195
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
196
|
-
this.emit('showHelp');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
191
|
// Insert printable characters
|
|
200
192
|
if (str && !key?.ctrl && !key?.meta) {
|
|
201
193
|
this.insertText(str);
|
|
@@ -204,312 +196,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
204
196
|
/**
|
|
205
197
|
* Set the input mode
|
|
206
198
|
*
|
|
207
|
-
* Streaming
|
|
208
|
-
*
|
|
209
|
-
* 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.
|
|
210
201
|
*/
|
|
211
202
|
setMode(mode) {
|
|
212
203
|
const prevMode = this.mode;
|
|
213
204
|
this.mode = mode;
|
|
214
205
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
// Input area renders at absolute bottom using cursor save/restore
|
|
219
|
-
this.pinnedTopRows = 0;
|
|
220
|
-
this.reservedLines = 5; // Reserve space for input area at bottom
|
|
221
|
-
// Disable any existing scroll region
|
|
222
|
-
this.disableScrollRegion();
|
|
223
|
-
// Initial render of input area at bottom
|
|
224
|
-
this.renderStreamingInputArea();
|
|
225
|
-
// Start timer to update streaming status and re-render input area
|
|
226
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
227
|
-
if (this.mode === 'streaming') {
|
|
228
|
-
this.updateStreamingStatus();
|
|
229
|
-
this.renderStreamingInputArea();
|
|
230
|
-
}
|
|
231
|
-
}, 1000);
|
|
206
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
207
|
+
this.resetStreamingRenderThrottle();
|
|
208
|
+
this.enableScrollRegion();
|
|
232
209
|
this.renderDirty = true;
|
|
210
|
+
this.render();
|
|
233
211
|
}
|
|
234
212
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
// Reset streaming time
|
|
241
|
-
this.streamingStartTime = null;
|
|
242
|
-
this.pinnedTopRows = 0;
|
|
243
|
-
// Ensure no scroll region is active
|
|
244
|
-
this.disableScrollRegion();
|
|
245
|
-
// Reset flow mode tracking
|
|
246
|
-
this.flowModeRenderedLines = 0;
|
|
247
|
-
// Render input area using unified method (same as streaming, but normal mode)
|
|
248
|
-
this.renderPinnedInputArea();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Update streaming status label (called by timer)
|
|
253
|
-
*/
|
|
254
|
-
updateStreamingStatus() {
|
|
255
|
-
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
256
|
-
return;
|
|
257
|
-
// Calculate elapsed time
|
|
258
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
259
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
260
|
-
const minutes = Math.floor(seconds / 60);
|
|
261
|
-
const secs = seconds % 60;
|
|
262
|
-
// Format elapsed time
|
|
263
|
-
let elapsedStr;
|
|
264
|
-
if (minutes > 0) {
|
|
265
|
-
elapsedStr = `${minutes}m ${secs}s`;
|
|
213
|
+
// Streaming ended - render the input area
|
|
214
|
+
this.resetStreamingRenderThrottle();
|
|
215
|
+
this.enableScrollRegion();
|
|
216
|
+
this.forceRender();
|
|
266
217
|
}
|
|
267
|
-
else {
|
|
268
|
-
elapsedStr = `${secs}s`;
|
|
269
|
-
}
|
|
270
|
-
// Update streaming label
|
|
271
|
-
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Render input area at absolute bottom - unified for streaming and normal modes.
|
|
275
|
-
* Uses cursor save/restore during streaming so content flow is not disrupted.
|
|
276
|
-
* In normal mode, cursor is positioned in the input area.
|
|
277
|
-
*/
|
|
278
|
-
renderPinnedInputArea() {
|
|
279
|
-
const { rows, cols } = this.getSize();
|
|
280
|
-
const divider = renderDivider(cols - 2);
|
|
281
|
-
const isStreaming = this.mode === 'streaming';
|
|
282
|
-
// Build status text based on mode
|
|
283
|
-
let statusText;
|
|
284
|
-
if (isStreaming && this.streamingStartTime) {
|
|
285
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
286
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
287
|
-
const minutes = Math.floor(seconds / 60);
|
|
288
|
-
const secs = seconds % 60;
|
|
289
|
-
const elapsedStr = minutes > 0 ? `${minutes}m ${secs}s` : `${secs}s`;
|
|
290
|
-
statusText = `${UI_COLORS.dim}● Streaming ${elapsedStr} · type to queue message${UI_COLORS.reset}`;
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
statusText = `${UI_COLORS.dim}Type a message...${UI_COLORS.reset}`;
|
|
294
|
-
}
|
|
295
|
-
// Save cursor position during streaming (so content flow resumes correctly)
|
|
296
|
-
if (isStreaming) {
|
|
297
|
-
this.write(ESC.SAVE);
|
|
298
|
-
}
|
|
299
|
-
this.write(ESC.HIDE);
|
|
300
|
-
// Input area: 5 lines from bottom
|
|
301
|
-
// Row layout (from bottom): controls | bottomDiv | input | topDiv | status
|
|
302
|
-
const controlsRow = rows;
|
|
303
|
-
const bottomDivRow = rows - 1;
|
|
304
|
-
const inputRow = rows - 2;
|
|
305
|
-
const topDivRow = rows - 3;
|
|
306
|
-
const statusRow = rows - 4;
|
|
307
|
-
// Status bar
|
|
308
|
-
this.write(ESC.TO(statusRow, 1));
|
|
309
|
-
this.write(ESC.CLEAR_LINE);
|
|
310
|
-
this.write(statusText);
|
|
311
|
-
// Top divider
|
|
312
|
-
this.write(ESC.TO(topDivRow, 1));
|
|
313
|
-
this.write(ESC.CLEAR_LINE);
|
|
314
|
-
this.write(divider);
|
|
315
|
-
// Input line with buffer content and cursor
|
|
316
|
-
this.write(ESC.TO(inputRow, 1));
|
|
317
|
-
this.write(ESC.CLEAR_LINE);
|
|
318
|
-
const maxInputWidth = cols - 4;
|
|
319
|
-
const inputDisplay = this.buffer.slice(0, maxInputWidth);
|
|
320
|
-
const cursorPos = Math.min(this.cursor, maxInputWidth);
|
|
321
|
-
// Render with cursor highlight
|
|
322
|
-
this.write(this.config.promptChar);
|
|
323
|
-
this.write(inputDisplay.slice(0, cursorPos));
|
|
324
|
-
this.write(ESC.REVERSE);
|
|
325
|
-
this.write(cursorPos < inputDisplay.length ? inputDisplay[cursorPos] : ' ');
|
|
326
|
-
this.write(ESC.RESET);
|
|
327
|
-
this.write(inputDisplay.slice(cursorPos + 1));
|
|
328
|
-
// Bottom divider
|
|
329
|
-
this.write(ESC.TO(bottomDivRow, 1));
|
|
330
|
-
this.write(ESC.CLEAR_LINE);
|
|
331
|
-
this.write(divider);
|
|
332
|
-
// Mode controls line
|
|
333
|
-
this.write(ESC.TO(controlsRow, 1));
|
|
334
|
-
this.write(ESC.CLEAR_LINE);
|
|
335
|
-
this.write(this.buildModeControls(cols));
|
|
336
|
-
// Restore cursor position during streaming, or show cursor in normal mode
|
|
337
|
-
if (isStreaming) {
|
|
338
|
-
this.write(ESC.RESTORE);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
// Position cursor in input area
|
|
342
|
-
const cursorCol = this.config.promptChar.length + cursorPos + 1;
|
|
343
|
-
this.write(ESC.TO(inputRow, cursorCol));
|
|
344
|
-
this.write(ESC.SHOW);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Render input area during streaming (alias for unified method)
|
|
349
|
-
*/
|
|
350
|
-
renderStreamingInputArea() {
|
|
351
|
-
this.renderPinnedInputArea();
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Enable or disable flow mode.
|
|
355
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
356
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
357
|
-
*/
|
|
358
|
-
setFlowMode(enabled) {
|
|
359
|
-
if (this.flowMode === enabled)
|
|
360
|
-
return;
|
|
361
|
-
this.flowMode = enabled;
|
|
362
|
-
this.renderDirty = true;
|
|
363
|
-
this.scheduleRender();
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Check if flow mode is enabled.
|
|
367
|
-
*/
|
|
368
|
-
isFlowMode() {
|
|
369
|
-
return this.flowMode;
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Set available slash commands for auto-complete suggestions.
|
|
373
|
-
*/
|
|
374
|
-
setCommands(commands) {
|
|
375
|
-
this.commandSuggestions = commands;
|
|
376
|
-
this.updateSuggestions();
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Update filtered suggestions based on current input.
|
|
380
|
-
*/
|
|
381
|
-
updateSuggestions() {
|
|
382
|
-
const input = this.buffer.trim();
|
|
383
|
-
// Only show suggestions when input starts with "/"
|
|
384
|
-
if (!input.startsWith('/')) {
|
|
385
|
-
this.showSuggestions = false;
|
|
386
|
-
this.filteredSuggestions = [];
|
|
387
|
-
this.selectedSuggestionIndex = 0;
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
const query = input.toLowerCase();
|
|
391
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
392
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
393
|
-
// Show suggestions if we have matches
|
|
394
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
395
|
-
// Keep selection in bounds
|
|
396
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
397
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Select next suggestion (arrow down / tab).
|
|
402
|
-
*/
|
|
403
|
-
selectNextSuggestion() {
|
|
404
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
405
|
-
return;
|
|
406
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
407
|
-
this.renderDirty = true;
|
|
408
|
-
this.scheduleRender();
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
412
|
-
*/
|
|
413
|
-
selectPrevSuggestion() {
|
|
414
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
415
|
-
return;
|
|
416
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
417
|
-
? this.filteredSuggestions.length - 1
|
|
418
|
-
: this.selectedSuggestionIndex - 1;
|
|
419
|
-
this.renderDirty = true;
|
|
420
|
-
this.scheduleRender();
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Accept current suggestion and insert into buffer.
|
|
424
|
-
*/
|
|
425
|
-
acceptSuggestion() {
|
|
426
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
427
|
-
return false;
|
|
428
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
429
|
-
if (!selected)
|
|
430
|
-
return false;
|
|
431
|
-
// Replace buffer with selected command
|
|
432
|
-
this.buffer = selected.command + ' ';
|
|
433
|
-
this.cursor = this.buffer.length;
|
|
434
|
-
this.showSuggestions = false;
|
|
435
|
-
this.renderDirty = true;
|
|
436
|
-
this.scheduleRender();
|
|
437
|
-
return true;
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Check if suggestions are visible.
|
|
441
|
-
*/
|
|
442
|
-
areSuggestionsVisible() {
|
|
443
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Update token count for metrics display
|
|
447
|
-
*/
|
|
448
|
-
setTokensUsed(tokens) {
|
|
449
|
-
this.tokensUsed = tokens;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Toggle thinking/reasoning mode
|
|
453
|
-
*/
|
|
454
|
-
toggleThinking() {
|
|
455
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
456
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
457
|
-
this.scheduleRender();
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Get thinking enabled state
|
|
461
|
-
*/
|
|
462
|
-
isThinkingEnabled() {
|
|
463
|
-
return this.thinkingEnabled;
|
|
464
218
|
}
|
|
465
219
|
/**
|
|
466
220
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
467
221
|
*/
|
|
468
222
|
setPinnedHeaderLines(count) {
|
|
469
|
-
//
|
|
470
|
-
if (this.pinnedTopRows !==
|
|
471
|
-
this.pinnedTopRows =
|
|
223
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
224
|
+
if (this.pinnedTopRows !== 0) {
|
|
225
|
+
this.pinnedTopRows = 0;
|
|
472
226
|
if (this.scrollRegionActive) {
|
|
473
227
|
this.applyScrollRegion();
|
|
474
228
|
}
|
|
475
229
|
}
|
|
476
230
|
}
|
|
477
|
-
/**
|
|
478
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
479
|
-
* restore the default bottom-aligned layout.
|
|
480
|
-
*/
|
|
481
|
-
setInlineAnchor(row) {
|
|
482
|
-
if (row === null || row === undefined) {
|
|
483
|
-
this.inlineAnchorRow = null;
|
|
484
|
-
this.inlineLayout = false;
|
|
485
|
-
this.renderDirty = true;
|
|
486
|
-
this.render();
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const { rows } = this.getSize();
|
|
490
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
491
|
-
this.inlineAnchorRow = clamped;
|
|
492
|
-
this.inlineLayout = true;
|
|
493
|
-
this.renderDirty = true;
|
|
494
|
-
this.render();
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
498
|
-
* output by re-evaluating the anchor before each render.
|
|
499
|
-
*/
|
|
500
|
-
setInlineAnchorProvider(provider) {
|
|
501
|
-
this.anchorProvider = provider;
|
|
502
|
-
if (!provider) {
|
|
503
|
-
this.inlineLayout = false;
|
|
504
|
-
this.inlineAnchorRow = null;
|
|
505
|
-
this.renderDirty = true;
|
|
506
|
-
this.render();
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
this.inlineLayout = true;
|
|
510
|
-
this.renderDirty = true;
|
|
511
|
-
this.render();
|
|
512
|
-
}
|
|
513
231
|
/**
|
|
514
232
|
* Get current mode
|
|
515
233
|
*/
|
|
@@ -619,6 +337,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
619
337
|
this.streamingLabel = next;
|
|
620
338
|
this.scheduleRender();
|
|
621
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
|
+
}
|
|
622
371
|
/**
|
|
623
372
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
624
373
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -628,16 +377,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
628
377
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
629
378
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
630
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);
|
|
631
382
|
if (this.verificationEnabled === nextVerification &&
|
|
632
383
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
633
384
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
634
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
385
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
386
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
387
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
635
388
|
return;
|
|
636
389
|
}
|
|
637
390
|
this.verificationEnabled = nextVerification;
|
|
638
391
|
this.autoContinueEnabled = nextAutoContinue;
|
|
639
392
|
this.verificationHotkey = nextVerifyHotkey;
|
|
640
393
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
394
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
395
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
641
396
|
this.scheduleRender();
|
|
642
397
|
}
|
|
643
398
|
/**
|
|
@@ -649,198 +404,104 @@ export class TerminalInput extends EventEmitter {
|
|
|
649
404
|
this.streamingLabel = null;
|
|
650
405
|
this.scheduleRender();
|
|
651
406
|
}
|
|
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
|
+
}
|
|
652
420
|
/**
|
|
653
421
|
* Render the input area - Claude Code style with mode controls
|
|
654
422
|
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
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.
|
|
657
426
|
*/
|
|
658
427
|
render() {
|
|
659
428
|
if (!this.canRender())
|
|
660
429
|
return;
|
|
661
430
|
if (this.isRendering)
|
|
662
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
|
+
}
|
|
663
444
|
const shouldSkip = !this.renderDirty &&
|
|
664
445
|
this.buffer === this.lastRenderContent &&
|
|
665
446
|
this.cursor === this.lastRenderCursor;
|
|
666
447
|
this.renderDirty = false;
|
|
667
|
-
// Skip if nothing changed
|
|
448
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
668
449
|
if (shouldSkip) {
|
|
669
450
|
return;
|
|
670
451
|
}
|
|
671
|
-
// If write lock is held, defer render
|
|
452
|
+
// If write lock is held, defer render to avoid race conditions
|
|
672
453
|
if (writeLock.isLocked()) {
|
|
673
454
|
writeLock.safeWrite(() => this.render());
|
|
674
455
|
return;
|
|
675
456
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// Render input area at bottom (outside scroll region)
|
|
680
|
-
this.renderBottomPinned();
|
|
681
|
-
}
|
|
682
|
-
finally {
|
|
683
|
-
writeLock.unlock();
|
|
684
|
-
this.isRendering = false;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
689
|
-
*
|
|
690
|
-
* Flow mode attempted inline rendering but caused duplicate renders
|
|
691
|
-
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
692
|
-
*/
|
|
693
|
-
renderFlowMode() {
|
|
694
|
-
// Use stable bottom-pinned approach
|
|
695
|
-
this.renderBottomPinned();
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
699
|
-
*
|
|
700
|
-
* Works for both normal and streaming modes:
|
|
701
|
-
* - During streaming: saves/restores cursor position
|
|
702
|
-
* - Status bar shows streaming info or "Type a message"
|
|
703
|
-
*
|
|
704
|
-
* Layout when suggestions visible:
|
|
705
|
-
* - Top divider
|
|
706
|
-
* - Input line(s)
|
|
707
|
-
* - Bottom divider
|
|
708
|
-
* - Suggestions (command list)
|
|
709
|
-
*
|
|
710
|
-
* Layout when suggestions hidden:
|
|
711
|
-
* - Status bar (Ready/Streaming)
|
|
712
|
-
* - Top divider
|
|
713
|
-
* - Input line(s)
|
|
714
|
-
* - Bottom divider
|
|
715
|
-
* - Mode controls
|
|
716
|
-
*/
|
|
717
|
-
renderBottomPinned() {
|
|
718
|
-
const { rows, cols } = this.getSize();
|
|
719
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
720
|
-
const isStreaming = this.mode === 'streaming';
|
|
721
|
-
// Use unified pinned input area (works for both streaming and normal)
|
|
722
|
-
// Only use complex rendering when suggestions are visible
|
|
723
|
-
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
724
|
-
if (!hasSuggestions) {
|
|
725
|
-
this.renderPinnedInputArea();
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
// Wrap buffer into display lines
|
|
729
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
730
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
731
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
732
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
733
|
-
// Calculate display window (keep cursor visible)
|
|
734
|
-
let startLine = 0;
|
|
735
|
-
if (lines.length > displayLines) {
|
|
736
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
737
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
738
|
-
}
|
|
739
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
740
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
741
|
-
// Calculate suggestion display (not during streaming)
|
|
742
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
743
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
744
|
-
: [];
|
|
745
|
-
const suggestionLines = suggestionsToShow.length;
|
|
746
|
-
this.write(ESC.HIDE);
|
|
747
|
-
this.write(ESC.RESET);
|
|
748
|
-
const divider = renderDivider(cols - 2);
|
|
749
|
-
// Calculate positions from absolute bottom
|
|
750
|
-
let currentRow;
|
|
751
|
-
if (suggestionLines > 0) {
|
|
752
|
-
// With suggestions: input area + dividers + suggestions
|
|
753
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
754
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
755
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
756
|
-
this.updateReservedLines(totalHeight);
|
|
757
|
-
// Top divider
|
|
758
|
-
this.write(ESC.TO(currentRow, 1));
|
|
759
|
-
this.write(ESC.CLEAR_LINE);
|
|
760
|
-
this.write(divider);
|
|
761
|
-
currentRow++;
|
|
762
|
-
// Input lines
|
|
763
|
-
let finalRow = currentRow;
|
|
764
|
-
let finalCol = 3;
|
|
765
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
766
|
-
this.write(ESC.TO(currentRow, 1));
|
|
767
|
-
this.write(ESC.CLEAR_LINE);
|
|
768
|
-
const line = visibleLines[i] ?? '';
|
|
769
|
-
const absoluteLineIdx = startLine + i;
|
|
770
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
771
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
772
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
773
|
-
if (isCursorLine) {
|
|
774
|
-
const col = Math.min(cursorCol, line.length);
|
|
775
|
-
this.write(line.slice(0, col));
|
|
776
|
-
this.write(ESC.REVERSE);
|
|
777
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
778
|
-
this.write(ESC.RESET);
|
|
779
|
-
this.write(line.slice(col + 1));
|
|
780
|
-
finalRow = currentRow;
|
|
781
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
this.write(line);
|
|
785
|
-
}
|
|
786
|
-
currentRow++;
|
|
457
|
+
const performRender = () => {
|
|
458
|
+
if (!this.scrollRegionActive) {
|
|
459
|
+
this.enableScrollRegion();
|
|
787
460
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
this.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
461
|
+
const { rows, cols } = this.getSize();
|
|
462
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
463
|
+
// Wrap buffer into display lines
|
|
464
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
465
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
466
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
467
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
468
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
469
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
470
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
471
|
+
// Calculate display window (keep cursor visible)
|
|
472
|
+
let startLine = 0;
|
|
473
|
+
if (lines.length > displayLines) {
|
|
474
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
475
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
476
|
+
}
|
|
477
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
478
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
479
|
+
// Render
|
|
480
|
+
this.write(ESC.HIDE);
|
|
481
|
+
this.write(ESC.RESET);
|
|
482
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
483
|
+
let currentRow = startRow;
|
|
484
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
485
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
486
|
+
// Meta/status header (elapsed, tokens/context)
|
|
487
|
+
for (const metaLine of metaLines) {
|
|
795
488
|
this.write(ESC.TO(currentRow, 1));
|
|
796
489
|
this.write(ESC.CLEAR_LINE);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
// Indent and highlight selected
|
|
800
|
-
this.write(' ');
|
|
801
|
-
if (isSelected) {
|
|
802
|
-
this.write(ESC.REVERSE);
|
|
803
|
-
this.write(ESC.BOLD);
|
|
804
|
-
}
|
|
805
|
-
this.write(suggestion.command);
|
|
806
|
-
if (isSelected) {
|
|
807
|
-
this.write(ESC.RESET);
|
|
808
|
-
}
|
|
809
|
-
// Description (dimmed)
|
|
810
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
811
|
-
if (descSpace > 10 && suggestion.description) {
|
|
812
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
813
|
-
this.write(ESC.RESET);
|
|
814
|
-
this.write(ESC.DIM);
|
|
815
|
-
this.write(' ');
|
|
816
|
-
this.write(desc);
|
|
817
|
-
this.write(ESC.RESET);
|
|
818
|
-
}
|
|
819
|
-
currentRow++;
|
|
490
|
+
this.write(metaLine);
|
|
491
|
+
currentRow += 1;
|
|
820
492
|
}
|
|
821
|
-
//
|
|
822
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
823
|
-
}
|
|
824
|
-
else {
|
|
825
|
-
// Without suggestions: normal layout with status bar and controls
|
|
826
|
-
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
827
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
828
|
-
this.updateReservedLines(totalHeight);
|
|
829
|
-
// Status bar (streaming or normal)
|
|
830
|
-
this.write(ESC.TO(currentRow, 1));
|
|
831
|
-
this.write(ESC.CLEAR_LINE);
|
|
832
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
833
|
-
currentRow++;
|
|
834
|
-
// Top divider
|
|
493
|
+
// Separator line
|
|
835
494
|
this.write(ESC.TO(currentRow, 1));
|
|
836
495
|
this.write(ESC.CLEAR_LINE);
|
|
496
|
+
const divider = renderDivider(cols - 2);
|
|
837
497
|
this.write(divider);
|
|
838
|
-
currentRow
|
|
839
|
-
//
|
|
498
|
+
currentRow += 1;
|
|
499
|
+
// Render input lines
|
|
840
500
|
let finalRow = currentRow;
|
|
841
501
|
let finalCol = 3;
|
|
842
502
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
843
|
-
|
|
503
|
+
const rowNum = currentRow + i;
|
|
504
|
+
this.write(ESC.TO(rowNum, 1));
|
|
844
505
|
this.write(ESC.CLEAR_LINE);
|
|
845
506
|
const line = visibleLines[i] ?? '';
|
|
846
507
|
const absoluteLineIdx = startLine + i;
|
|
@@ -854,6 +515,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
854
515
|
this.write(ESC.RESET);
|
|
855
516
|
this.write(ESC.BG_DARK);
|
|
856
517
|
if (isCursorLine) {
|
|
518
|
+
// Render with block cursor
|
|
857
519
|
const col = Math.min(cursorCol, line.length);
|
|
858
520
|
const before = line.slice(0, col);
|
|
859
521
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -863,157 +525,265 @@ export class TerminalInput extends EventEmitter {
|
|
|
863
525
|
this.write(at);
|
|
864
526
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
865
527
|
this.write(after);
|
|
866
|
-
finalRow =
|
|
528
|
+
finalRow = rowNum;
|
|
867
529
|
finalCol = this.config.promptChar.length + col + 1;
|
|
868
530
|
}
|
|
869
531
|
else {
|
|
870
532
|
this.write(line);
|
|
871
533
|
}
|
|
872
|
-
// Pad to edge
|
|
534
|
+
// Pad to edge for clean look
|
|
873
535
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
874
536
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
875
537
|
if (padding > 0)
|
|
876
538
|
this.write(' '.repeat(padding));
|
|
877
539
|
this.write(ESC.RESET);
|
|
878
|
-
currentRow++;
|
|
879
540
|
}
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
this.write(ESC.
|
|
883
|
-
this.write(divider);
|
|
884
|
-
currentRow++;
|
|
885
|
-
// Mode controls
|
|
886
|
-
this.write(ESC.TO(currentRow, 1));
|
|
541
|
+
// Mode controls line (Claude Code style)
|
|
542
|
+
const controlRow = currentRow + visibleLines.length;
|
|
543
|
+
this.write(ESC.TO(controlRow, 1));
|
|
887
544
|
this.write(ESC.CLEAR_LINE);
|
|
888
545
|
this.write(this.buildModeControls(cols));
|
|
889
|
-
// Position cursor
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
546
|
+
// Position cursor
|
|
547
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
548
|
+
this.write(ESC.SHOW);
|
|
549
|
+
// Update state
|
|
550
|
+
this.lastRenderContent = this.buffer;
|
|
551
|
+
this.lastRenderCursor = this.cursor;
|
|
552
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
553
|
+
if (this.streamingRenderTimer) {
|
|
554
|
+
clearTimeout(this.streamingRenderTimer);
|
|
555
|
+
this.streamingRenderTimer = null;
|
|
895
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;
|
|
896
567
|
}
|
|
897
|
-
this.write(ESC.SHOW);
|
|
898
|
-
// Update state
|
|
899
|
-
this.lastRenderContent = this.buffer;
|
|
900
|
-
this.lastRenderCursor = this.cursor;
|
|
901
568
|
}
|
|
902
569
|
/**
|
|
903
|
-
* Build
|
|
570
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
571
|
+
* During streaming, consolidates into a single line to minimize cursor repositioning
|
|
572
|
+
* and prevent escape code interleaving with streamed content.
|
|
904
573
|
*/
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
574
|
+
buildMetaLines(width) {
|
|
575
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
576
|
+
// During streaming, consolidate everything into a single line to reduce escape codes
|
|
577
|
+
if (streamingActive) {
|
|
578
|
+
const parts = [];
|
|
579
|
+
// Essential streaming info only
|
|
580
|
+
if (this.metaThinkingMs !== null) {
|
|
581
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
582
|
+
}
|
|
583
|
+
if (this.metaElapsedSeconds !== null) {
|
|
584
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
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
|
+
}
|
|
916
627
|
if (this.queue.length > 0) {
|
|
917
|
-
|
|
628
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
918
629
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
630
|
+
if (usageParts.length) {
|
|
631
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
632
|
+
}
|
|
633
|
+
return lines;
|
|
922
634
|
}
|
|
923
635
|
/**
|
|
924
|
-
*
|
|
925
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
636
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
926
637
|
*/
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (this.streamingStartTime) {
|
|
934
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
935
|
-
const mins = Math.floor(elapsed / 60);
|
|
936
|
-
const secs = elapsed % 60;
|
|
937
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
938
|
-
}
|
|
939
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
940
|
-
}
|
|
941
|
-
// Queue indicator during streaming
|
|
942
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
943
|
-
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
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));
|
|
944
644
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Build Claude Code style mode controls line.
|
|
648
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
649
|
+
*/
|
|
650
|
+
buildModeControls(cols) {
|
|
651
|
+
const width = Math.max(8, cols - 2);
|
|
652
|
+
const leftParts = [];
|
|
653
|
+
const rightParts = [];
|
|
654
|
+
if (this.streamingLabel) {
|
|
655
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
949
656
|
}
|
|
950
|
-
// Override/warning status
|
|
951
657
|
if (this.overrideStatusMessage) {
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
658
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
659
|
+
}
|
|
660
|
+
if (this.statusMessage) {
|
|
661
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
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' });
|
|
957
675
|
}
|
|
958
|
-
// Multi-line indicator
|
|
959
676
|
if (this.buffer.includes('\n')) {
|
|
960
|
-
|
|
677
|
+
const lineCount = this.buffer.split('\n').length;
|
|
678
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
961
679
|
}
|
|
962
|
-
if (
|
|
963
|
-
|
|
680
|
+
if (this.pastePlaceholders.length > 0) {
|
|
681
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
682
|
+
leftParts.push({
|
|
683
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
684
|
+
tone: 'info',
|
|
685
|
+
});
|
|
964
686
|
}
|
|
965
|
-
const
|
|
966
|
-
|
|
687
|
+
const contextRemaining = this.computeContextRemaining();
|
|
688
|
+
if (this.thinkingModeLabel) {
|
|
689
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
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}`;
|
|
764
|
+
}
|
|
765
|
+
if (value >= 1_000_000) {
|
|
766
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
767
|
+
}
|
|
768
|
+
if (value >= 1_000) {
|
|
769
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
770
|
+
}
|
|
771
|
+
return `${Math.round(value)}`;
|
|
772
|
+
}
|
|
773
|
+
visibleLength(value) {
|
|
774
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
775
|
+
return value.replace(ansiPattern, '').length;
|
|
967
776
|
}
|
|
968
777
|
/**
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
778
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
779
|
+
* needing a TTY. Not used by production code.
|
|
973
780
|
*/
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
981
|
-
if (this.editMode === 'display-edits') {
|
|
982
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
983
|
-
}
|
|
984
|
-
else {
|
|
985
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
986
|
-
}
|
|
987
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
988
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
989
|
-
// Verification (green when on) - per schema.verificationMode
|
|
990
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
991
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
992
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
993
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
994
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
995
|
-
let rightPart = '';
|
|
996
|
-
if (this.contextUsage !== null) {
|
|
997
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
998
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
999
|
-
if (rem < 10)
|
|
1000
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1001
|
-
else if (rem < 25)
|
|
1002
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1003
|
-
else
|
|
1004
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1005
|
-
}
|
|
1006
|
-
// Calculate visible lengths (strip ANSI)
|
|
1007
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1008
|
-
const leftLen = strip(leftPart).length;
|
|
1009
|
-
const rightLen = strip(rightPart).length;
|
|
1010
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
1011
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1012
|
-
}
|
|
1013
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1014
|
-
return `${leftPart} ${rightPart}`;
|
|
1015
|
-
}
|
|
1016
|
-
return leftPart;
|
|
781
|
+
getDebugUiSnapshot(width) {
|
|
782
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
783
|
+
return {
|
|
784
|
+
meta: this.buildMetaLines(cols - 2),
|
|
785
|
+
controls: this.buildModeControls(cols),
|
|
786
|
+
};
|
|
1017
787
|
}
|
|
1018
788
|
/**
|
|
1019
789
|
* Force a re-render
|
|
@@ -1036,17 +806,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1036
806
|
handleResize() {
|
|
1037
807
|
this.lastRenderContent = '';
|
|
1038
808
|
this.lastRenderCursor = -1;
|
|
809
|
+
this.resetStreamingRenderThrottle();
|
|
1039
810
|
// Re-clamp pinned header rows to the new terminal height
|
|
1040
811
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
812
|
+
if (this.scrollRegionActive) {
|
|
813
|
+
this.disableScrollRegion();
|
|
814
|
+
this.enableScrollRegion();
|
|
815
|
+
}
|
|
1041
816
|
this.scheduleRender();
|
|
1042
817
|
}
|
|
1043
818
|
/**
|
|
1044
819
|
* Register with display's output interceptor to position cursor correctly.
|
|
1045
820
|
* When scroll region is active, output needs to go to the scroll region,
|
|
1046
821
|
* not the protected bottom area where the input is rendered.
|
|
1047
|
-
*
|
|
1048
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
1049
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
1050
822
|
*/
|
|
1051
823
|
registerOutputInterceptor(display) {
|
|
1052
824
|
if (this.outputInterceptorCleanup) {
|
|
@@ -1054,11 +826,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
1054
826
|
}
|
|
1055
827
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
1056
828
|
beforeWrite: () => {
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
829
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
830
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
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
|
+
}
|
|
1059
837
|
},
|
|
1060
838
|
afterWrite: () => {
|
|
1061
|
-
//
|
|
839
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
840
|
+
if (this.scrollRegionActive) {
|
|
841
|
+
this.write(ESC.RESTORE);
|
|
842
|
+
}
|
|
1062
843
|
},
|
|
1063
844
|
});
|
|
1064
845
|
}
|
|
@@ -1068,11 +849,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1068
849
|
dispose() {
|
|
1069
850
|
if (this.disposed)
|
|
1070
851
|
return;
|
|
1071
|
-
// Clean up streaming render timer
|
|
1072
|
-
if (this.streamingRenderTimer) {
|
|
1073
|
-
clearInterval(this.streamingRenderTimer);
|
|
1074
|
-
this.streamingRenderTimer = null;
|
|
1075
|
-
}
|
|
1076
852
|
// Clean up output interceptor
|
|
1077
853
|
if (this.outputInterceptorCleanup) {
|
|
1078
854
|
this.outputInterceptorCleanup();
|
|
@@ -1080,6 +856,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1080
856
|
}
|
|
1081
857
|
this.disposed = true;
|
|
1082
858
|
this.enabled = false;
|
|
859
|
+
this.resetStreamingRenderThrottle();
|
|
1083
860
|
this.disableScrollRegion();
|
|
1084
861
|
this.disableBracketedPaste();
|
|
1085
862
|
this.buffer = '';
|
|
@@ -1185,22 +962,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1185
962
|
this.toggleEditMode();
|
|
1186
963
|
return true;
|
|
1187
964
|
}
|
|
1188
|
-
|
|
1189
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1190
|
-
this.togglePasteExpansion();
|
|
1191
|
-
}
|
|
1192
|
-
else {
|
|
1193
|
-
this.toggleThinking();
|
|
1194
|
-
}
|
|
1195
|
-
return true;
|
|
1196
|
-
case 'escape':
|
|
1197
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1198
|
-
if (this.mode === 'streaming') {
|
|
1199
|
-
this.emit('interrupt');
|
|
1200
|
-
}
|
|
1201
|
-
else if (this.buffer.length > 0) {
|
|
1202
|
-
this.clear();
|
|
1203
|
-
}
|
|
965
|
+
this.insertText(' ');
|
|
1204
966
|
return true;
|
|
1205
967
|
}
|
|
1206
968
|
return false;
|
|
@@ -1218,7 +980,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1218
980
|
this.insertPlainText(chunk, insertPos);
|
|
1219
981
|
this.cursor = insertPos + chunk.length;
|
|
1220
982
|
this.emit('change', this.buffer);
|
|
1221
|
-
this.updateSuggestions();
|
|
1222
983
|
this.scheduleRender();
|
|
1223
984
|
}
|
|
1224
985
|
insertNewline() {
|
|
@@ -1243,7 +1004,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1243
1004
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1244
1005
|
}
|
|
1245
1006
|
this.emit('change', this.buffer);
|
|
1246
|
-
this.updateSuggestions();
|
|
1247
1007
|
this.scheduleRender();
|
|
1248
1008
|
}
|
|
1249
1009
|
deleteForward() {
|
|
@@ -1493,7 +1253,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1493
1253
|
if (available <= 0)
|
|
1494
1254
|
return;
|
|
1495
1255
|
const chunk = clean.slice(0, available);
|
|
1496
|
-
|
|
1256
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1257
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1258
|
+
if (isMultiline && !isShortMultiline) {
|
|
1497
1259
|
this.insertPastePlaceholder(chunk);
|
|
1498
1260
|
}
|
|
1499
1261
|
else {
|
|
@@ -1513,6 +1275,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1513
1275
|
return;
|
|
1514
1276
|
this.applyScrollRegion();
|
|
1515
1277
|
this.scrollRegionActive = true;
|
|
1278
|
+
this.forceRender();
|
|
1516
1279
|
}
|
|
1517
1280
|
disableScrollRegion() {
|
|
1518
1281
|
if (!this.scrollRegionActive)
|
|
@@ -1663,17 +1426,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1663
1426
|
this.shiftPlaceholders(position, text.length);
|
|
1664
1427
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1665
1428
|
}
|
|
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
|
+
}
|
|
1666
1435
|
findPlaceholderAt(position) {
|
|
1667
1436
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1668
1437
|
}
|
|
1669
|
-
buildPlaceholder(
|
|
1438
|
+
buildPlaceholder(lineCount) {
|
|
1670
1439
|
const id = ++this.pasteCounter;
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
const preview = summary.preview.length > 30
|
|
1674
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1675
|
-
: summary.preview;
|
|
1676
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1440
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1441
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1677
1442
|
return { id, placeholder };
|
|
1678
1443
|
}
|
|
1679
1444
|
insertPastePlaceholder(content) {
|
|
@@ -1681,67 +1446,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1681
1446
|
if (available <= 0)
|
|
1682
1447
|
return;
|
|
1683
1448
|
const cleanContent = content.slice(0, available);
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
if (summary.lineCount < 5) {
|
|
1687
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1688
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1689
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1690
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1449
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1450
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1694
1451
|
const insertPos = this.cursor;
|
|
1695
1452
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1696
1453
|
this.pastePlaceholders.push({
|
|
1697
1454
|
id,
|
|
1698
1455
|
content: cleanContent,
|
|
1699
|
-
lineCount
|
|
1456
|
+
lineCount,
|
|
1700
1457
|
placeholder,
|
|
1701
1458
|
start: insertPos,
|
|
1702
1459
|
end: insertPos + placeholder.length,
|
|
1703
|
-
summary,
|
|
1704
|
-
expanded: false,
|
|
1705
1460
|
});
|
|
1706
1461
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1707
1462
|
this.cursor = insertPos + placeholder.length;
|
|
1708
1463
|
}
|
|
1709
|
-
/**
|
|
1710
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1711
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1712
|
-
*/
|
|
1713
|
-
togglePasteExpansion() {
|
|
1714
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1715
|
-
if (!placeholder)
|
|
1716
|
-
return false;
|
|
1717
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1718
|
-
// Update the placeholder text in buffer
|
|
1719
|
-
const newPlaceholder = placeholder.expanded
|
|
1720
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1721
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1722
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1723
|
-
// Update buffer
|
|
1724
|
-
this.buffer =
|
|
1725
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1726
|
-
newPlaceholder +
|
|
1727
|
-
this.buffer.slice(placeholder.end);
|
|
1728
|
-
// Update placeholder tracking
|
|
1729
|
-
placeholder.placeholder = newPlaceholder;
|
|
1730
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1731
|
-
// Shift other placeholders
|
|
1732
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1733
|
-
this.scheduleRender();
|
|
1734
|
-
return true;
|
|
1735
|
-
}
|
|
1736
|
-
buildExpandedPlaceholder(ph) {
|
|
1737
|
-
const lines = ph.content.split('\n');
|
|
1738
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1739
|
-
const lastLines = lines.length > 5
|
|
1740
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1741
|
-
: '';
|
|
1742
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1743
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1744
|
-
}
|
|
1745
1464
|
deletePlaceholder(placeholder) {
|
|
1746
1465
|
const length = placeholder.end - placeholder.start;
|
|
1747
1466
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1749,7 +1468,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1749
1468
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1750
1469
|
this.cursor = placeholder.start;
|
|
1751
1470
|
}
|
|
1752
|
-
updateContextUsage(value) {
|
|
1471
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
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
|
+
}
|
|
1753
1476
|
if (value === null || !Number.isFinite(value)) {
|
|
1754
1477
|
this.contextUsage = null;
|
|
1755
1478
|
}
|
|
@@ -1776,6 +1499,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1776
1499
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1777
1500
|
this.setEditMode(next);
|
|
1778
1501
|
}
|
|
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
|
+
}
|
|
1779
1518
|
scheduleRender() {
|
|
1780
1519
|
if (!this.canRender())
|
|
1781
1520
|
return;
|