erosolar-cli 1.7.261 → 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 +448 -667
- 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,343 +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();
|
|
213
|
+
// Streaming ended - render the input area
|
|
214
|
+
this.resetStreamingRenderThrottle();
|
|
215
|
+
this.enableScrollRegion();
|
|
216
|
+
this.forceRender();
|
|
249
217
|
}
|
|
250
218
|
}
|
|
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`;
|
|
266
|
-
}
|
|
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
|
-
* Supports multi-line input, background styling, and cursor positioning.
|
|
276
|
-
* Uses cursor save/restore during streaming so content flow is not disrupted.
|
|
277
|
-
*/
|
|
278
|
-
renderPinnedInputArea() {
|
|
279
|
-
const { rows, cols } = this.getSize();
|
|
280
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
281
|
-
const divider = renderDivider(cols - 2);
|
|
282
|
-
const isStreaming = this.mode === 'streaming';
|
|
283
|
-
// Wrap buffer into display lines (multi-line support)
|
|
284
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
285
|
-
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
286
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
287
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
288
|
-
// Calculate display window (keep cursor visible)
|
|
289
|
-
let startLine = 0;
|
|
290
|
-
if (lines.length > displayLines) {
|
|
291
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
292
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
293
|
-
}
|
|
294
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
295
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
296
|
-
// Calculate total height: status + topDiv + input lines + bottomDiv + controls
|
|
297
|
-
const totalHeight = 4 + visibleLines.length;
|
|
298
|
-
// Save cursor position during streaming (so content flow resumes correctly)
|
|
299
|
-
if (isStreaming) {
|
|
300
|
-
this.write(ESC.SAVE);
|
|
301
|
-
}
|
|
302
|
-
this.write(ESC.HIDE);
|
|
303
|
-
this.write(ESC.RESET);
|
|
304
|
-
// Position from absolute bottom
|
|
305
|
-
let currentRow = Math.max(1, rows - totalHeight + 1);
|
|
306
|
-
let finalRow = currentRow;
|
|
307
|
-
let finalCol = 3;
|
|
308
|
-
// Status bar
|
|
309
|
-
this.write(ESC.TO(currentRow, 1));
|
|
310
|
-
this.write(ESC.CLEAR_LINE);
|
|
311
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
312
|
-
currentRow++;
|
|
313
|
-
// Top divider
|
|
314
|
-
this.write(ESC.TO(currentRow, 1));
|
|
315
|
-
this.write(ESC.CLEAR_LINE);
|
|
316
|
-
this.write(divider);
|
|
317
|
-
currentRow++;
|
|
318
|
-
// Input lines with background styling
|
|
319
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
320
|
-
this.write(ESC.TO(currentRow, 1));
|
|
321
|
-
this.write(ESC.CLEAR_LINE);
|
|
322
|
-
const line = visibleLines[i] ?? '';
|
|
323
|
-
const absoluteLineIdx = startLine + i;
|
|
324
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
325
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
326
|
-
// Background
|
|
327
|
-
this.write(ESC.BG_DARK);
|
|
328
|
-
// Prompt prefix
|
|
329
|
-
this.write(ESC.DIM);
|
|
330
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
331
|
-
this.write(ESC.RESET);
|
|
332
|
-
this.write(ESC.BG_DARK);
|
|
333
|
-
if (isCursorLine) {
|
|
334
|
-
const col = Math.min(cursorCol, line.length);
|
|
335
|
-
const before = line.slice(0, col);
|
|
336
|
-
const at = col < line.length ? line[col] : ' ';
|
|
337
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
338
|
-
this.write(before);
|
|
339
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
340
|
-
this.write(at);
|
|
341
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
342
|
-
this.write(after);
|
|
343
|
-
finalRow = currentRow;
|
|
344
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
this.write(line);
|
|
348
|
-
}
|
|
349
|
-
// Pad to edge
|
|
350
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
351
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
352
|
-
if (padding > 0)
|
|
353
|
-
this.write(' '.repeat(padding));
|
|
354
|
-
this.write(ESC.RESET);
|
|
355
|
-
currentRow++;
|
|
356
|
-
}
|
|
357
|
-
// Bottom divider
|
|
358
|
-
this.write(ESC.TO(currentRow, 1));
|
|
359
|
-
this.write(ESC.CLEAR_LINE);
|
|
360
|
-
this.write(divider);
|
|
361
|
-
currentRow++;
|
|
362
|
-
// Mode controls line
|
|
363
|
-
this.write(ESC.TO(currentRow, 1));
|
|
364
|
-
this.write(ESC.CLEAR_LINE);
|
|
365
|
-
this.write(this.buildModeControls(cols));
|
|
366
|
-
// Restore cursor position during streaming, or show cursor in normal mode
|
|
367
|
-
if (isStreaming) {
|
|
368
|
-
this.write(ESC.RESTORE);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
// Position cursor in input area
|
|
372
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
373
|
-
this.write(ESC.SHOW);
|
|
374
|
-
}
|
|
375
|
-
// Update reserved lines for scroll region calculations
|
|
376
|
-
this.updateReservedLines(totalHeight);
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Render input area during streaming (alias for unified method)
|
|
380
|
-
*/
|
|
381
|
-
renderStreamingInputArea() {
|
|
382
|
-
this.renderPinnedInputArea();
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Enable or disable flow mode.
|
|
386
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
387
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
388
|
-
*/
|
|
389
|
-
setFlowMode(enabled) {
|
|
390
|
-
if (this.flowMode === enabled)
|
|
391
|
-
return;
|
|
392
|
-
this.flowMode = enabled;
|
|
393
|
-
this.renderDirty = true;
|
|
394
|
-
this.scheduleRender();
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Check if flow mode is enabled.
|
|
398
|
-
*/
|
|
399
|
-
isFlowMode() {
|
|
400
|
-
return this.flowMode;
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Set available slash commands for auto-complete suggestions.
|
|
404
|
-
*/
|
|
405
|
-
setCommands(commands) {
|
|
406
|
-
this.commandSuggestions = commands;
|
|
407
|
-
this.updateSuggestions();
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Update filtered suggestions based on current input.
|
|
411
|
-
*/
|
|
412
|
-
updateSuggestions() {
|
|
413
|
-
const input = this.buffer.trim();
|
|
414
|
-
// Only show suggestions when input starts with "/"
|
|
415
|
-
if (!input.startsWith('/')) {
|
|
416
|
-
this.showSuggestions = false;
|
|
417
|
-
this.filteredSuggestions = [];
|
|
418
|
-
this.selectedSuggestionIndex = 0;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
const query = input.toLowerCase();
|
|
422
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
423
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
424
|
-
// Show suggestions if we have matches
|
|
425
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
426
|
-
// Keep selection in bounds
|
|
427
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
428
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Select next suggestion (arrow down / tab).
|
|
433
|
-
*/
|
|
434
|
-
selectNextSuggestion() {
|
|
435
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
436
|
-
return;
|
|
437
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
438
|
-
this.renderDirty = true;
|
|
439
|
-
this.scheduleRender();
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
443
|
-
*/
|
|
444
|
-
selectPrevSuggestion() {
|
|
445
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
446
|
-
return;
|
|
447
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
448
|
-
? this.filteredSuggestions.length - 1
|
|
449
|
-
: this.selectedSuggestionIndex - 1;
|
|
450
|
-
this.renderDirty = true;
|
|
451
|
-
this.scheduleRender();
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Accept current suggestion and insert into buffer.
|
|
455
|
-
*/
|
|
456
|
-
acceptSuggestion() {
|
|
457
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
458
|
-
return false;
|
|
459
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
460
|
-
if (!selected)
|
|
461
|
-
return false;
|
|
462
|
-
// Replace buffer with selected command
|
|
463
|
-
this.buffer = selected.command + ' ';
|
|
464
|
-
this.cursor = this.buffer.length;
|
|
465
|
-
this.showSuggestions = false;
|
|
466
|
-
this.renderDirty = true;
|
|
467
|
-
this.scheduleRender();
|
|
468
|
-
return true;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Check if suggestions are visible.
|
|
472
|
-
*/
|
|
473
|
-
areSuggestionsVisible() {
|
|
474
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Update token count for metrics display
|
|
478
|
-
*/
|
|
479
|
-
setTokensUsed(tokens) {
|
|
480
|
-
this.tokensUsed = tokens;
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* Toggle thinking/reasoning mode
|
|
484
|
-
*/
|
|
485
|
-
toggleThinking() {
|
|
486
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
487
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
488
|
-
this.scheduleRender();
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Get thinking enabled state
|
|
492
|
-
*/
|
|
493
|
-
isThinkingEnabled() {
|
|
494
|
-
return this.thinkingEnabled;
|
|
495
|
-
}
|
|
496
219
|
/**
|
|
497
220
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
498
221
|
*/
|
|
499
222
|
setPinnedHeaderLines(count) {
|
|
500
|
-
//
|
|
501
|
-
if (this.pinnedTopRows !==
|
|
502
|
-
this.pinnedTopRows =
|
|
223
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
224
|
+
if (this.pinnedTopRows !== 0) {
|
|
225
|
+
this.pinnedTopRows = 0;
|
|
503
226
|
if (this.scrollRegionActive) {
|
|
504
227
|
this.applyScrollRegion();
|
|
505
228
|
}
|
|
506
229
|
}
|
|
507
230
|
}
|
|
508
|
-
/**
|
|
509
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
510
|
-
* restore the default bottom-aligned layout.
|
|
511
|
-
*/
|
|
512
|
-
setInlineAnchor(row) {
|
|
513
|
-
if (row === null || row === undefined) {
|
|
514
|
-
this.inlineAnchorRow = null;
|
|
515
|
-
this.inlineLayout = false;
|
|
516
|
-
this.renderDirty = true;
|
|
517
|
-
this.render();
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
const { rows } = this.getSize();
|
|
521
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
522
|
-
this.inlineAnchorRow = clamped;
|
|
523
|
-
this.inlineLayout = true;
|
|
524
|
-
this.renderDirty = true;
|
|
525
|
-
this.render();
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
529
|
-
* output by re-evaluating the anchor before each render.
|
|
530
|
-
*/
|
|
531
|
-
setInlineAnchorProvider(provider) {
|
|
532
|
-
this.anchorProvider = provider;
|
|
533
|
-
if (!provider) {
|
|
534
|
-
this.inlineLayout = false;
|
|
535
|
-
this.inlineAnchorRow = null;
|
|
536
|
-
this.renderDirty = true;
|
|
537
|
-
this.render();
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
this.inlineLayout = true;
|
|
541
|
-
this.renderDirty = true;
|
|
542
|
-
this.render();
|
|
543
|
-
}
|
|
544
231
|
/**
|
|
545
232
|
* Get current mode
|
|
546
233
|
*/
|
|
@@ -650,6 +337,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
650
337
|
this.streamingLabel = next;
|
|
651
338
|
this.scheduleRender();
|
|
652
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
|
+
}
|
|
653
371
|
/**
|
|
654
372
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
655
373
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -659,16 +377,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
659
377
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
660
378
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
661
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);
|
|
662
382
|
if (this.verificationEnabled === nextVerification &&
|
|
663
383
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
664
384
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
665
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
385
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
386
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
387
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
666
388
|
return;
|
|
667
389
|
}
|
|
668
390
|
this.verificationEnabled = nextVerification;
|
|
669
391
|
this.autoContinueEnabled = nextAutoContinue;
|
|
670
392
|
this.verificationHotkey = nextVerifyHotkey;
|
|
671
393
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
394
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
395
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
672
396
|
this.scheduleRender();
|
|
673
397
|
}
|
|
674
398
|
/**
|
|
@@ -680,298 +404,386 @@ export class TerminalInput extends EventEmitter {
|
|
|
680
404
|
this.streamingLabel = null;
|
|
681
405
|
this.scheduleRender();
|
|
682
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
|
+
}
|
|
683
420
|
/**
|
|
684
421
|
* Render the input area - Claude Code style with mode controls
|
|
685
422
|
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
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.
|
|
688
426
|
*/
|
|
689
427
|
render() {
|
|
690
428
|
if (!this.canRender())
|
|
691
429
|
return;
|
|
692
430
|
if (this.isRendering)
|
|
693
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
|
+
}
|
|
694
444
|
const shouldSkip = !this.renderDirty &&
|
|
695
445
|
this.buffer === this.lastRenderContent &&
|
|
696
446
|
this.cursor === this.lastRenderCursor;
|
|
697
447
|
this.renderDirty = false;
|
|
698
|
-
// Skip if nothing changed
|
|
448
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
699
449
|
if (shouldSkip) {
|
|
700
450
|
return;
|
|
701
451
|
}
|
|
702
|
-
// If write lock is held, defer render
|
|
452
|
+
// If write lock is held, defer render to avoid race conditions
|
|
703
453
|
if (writeLock.isLocked()) {
|
|
704
454
|
writeLock.safeWrite(() => this.render());
|
|
705
455
|
return;
|
|
706
456
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
this.
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
* - Input line(s)
|
|
745
|
-
* - Bottom divider
|
|
746
|
-
* - Mode controls
|
|
747
|
-
*/
|
|
748
|
-
renderBottomPinned() {
|
|
749
|
-
const { rows, cols } = this.getSize();
|
|
750
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
751
|
-
const isStreaming = this.mode === 'streaming';
|
|
752
|
-
// Use unified pinned input area (works for both streaming and normal)
|
|
753
|
-
// Only use complex rendering when suggestions are visible
|
|
754
|
-
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
755
|
-
if (!hasSuggestions) {
|
|
756
|
-
this.renderPinnedInputArea();
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
// Wrap buffer into display lines
|
|
760
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
761
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
762
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
763
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
764
|
-
// Calculate display window (keep cursor visible)
|
|
765
|
-
let startLine = 0;
|
|
766
|
-
if (lines.length > displayLines) {
|
|
767
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
768
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
769
|
-
}
|
|
770
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
771
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
772
|
-
// Calculate suggestion display (not during streaming)
|
|
773
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
774
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
775
|
-
: [];
|
|
776
|
-
const suggestionLines = suggestionsToShow.length;
|
|
777
|
-
this.write(ESC.HIDE);
|
|
778
|
-
this.write(ESC.RESET);
|
|
779
|
-
const divider = renderDivider(cols - 2);
|
|
780
|
-
// Calculate positions from absolute bottom
|
|
781
|
-
let currentRow;
|
|
782
|
-
if (suggestionLines > 0) {
|
|
783
|
-
// With suggestions: input area + dividers + suggestions
|
|
784
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
785
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
786
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
787
|
-
this.updateReservedLines(totalHeight);
|
|
788
|
-
// Top divider
|
|
457
|
+
const performRender = () => {
|
|
458
|
+
if (!this.scrollRegionActive) {
|
|
459
|
+
this.enableScrollRegion();
|
|
460
|
+
}
|
|
461
|
+
const { rows, cols } = this.getSize();
|
|
462
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
463
|
+
// Wrap buffer into display lines
|
|
464
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
465
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
466
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
467
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
468
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
469
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
470
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
471
|
+
// Calculate display window (keep cursor visible)
|
|
472
|
+
let startLine = 0;
|
|
473
|
+
if (lines.length > displayLines) {
|
|
474
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
475
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
476
|
+
}
|
|
477
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
478
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
479
|
+
// Render
|
|
480
|
+
this.write(ESC.HIDE);
|
|
481
|
+
this.write(ESC.RESET);
|
|
482
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
483
|
+
let currentRow = startRow;
|
|
484
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
485
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
486
|
+
// Meta/status header (elapsed, tokens/context)
|
|
487
|
+
for (const metaLine of metaLines) {
|
|
488
|
+
this.write(ESC.TO(currentRow, 1));
|
|
489
|
+
this.write(ESC.CLEAR_LINE);
|
|
490
|
+
this.write(metaLine);
|
|
491
|
+
currentRow += 1;
|
|
492
|
+
}
|
|
493
|
+
// Separator line
|
|
789
494
|
this.write(ESC.TO(currentRow, 1));
|
|
790
495
|
this.write(ESC.CLEAR_LINE);
|
|
496
|
+
const divider = renderDivider(cols - 2);
|
|
791
497
|
this.write(divider);
|
|
792
|
-
currentRow
|
|
793
|
-
//
|
|
498
|
+
currentRow += 1;
|
|
499
|
+
// Render input lines
|
|
794
500
|
let finalRow = currentRow;
|
|
795
501
|
let finalCol = 3;
|
|
796
502
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
797
|
-
|
|
503
|
+
const rowNum = currentRow + i;
|
|
504
|
+
this.write(ESC.TO(rowNum, 1));
|
|
798
505
|
this.write(ESC.CLEAR_LINE);
|
|
799
506
|
const line = visibleLines[i] ?? '';
|
|
800
507
|
const absoluteLineIdx = startLine + i;
|
|
801
508
|
const isFirstLine = absoluteLineIdx === 0;
|
|
802
509
|
const isCursorLine = i === adjustedCursorLine;
|
|
510
|
+
// Background
|
|
511
|
+
this.write(ESC.BG_DARK);
|
|
512
|
+
// Prompt prefix
|
|
513
|
+
this.write(ESC.DIM);
|
|
803
514
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
515
|
+
this.write(ESC.RESET);
|
|
516
|
+
this.write(ESC.BG_DARK);
|
|
804
517
|
if (isCursorLine) {
|
|
518
|
+
// Render with block cursor
|
|
805
519
|
const col = Math.min(cursorCol, line.length);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
this.write(
|
|
810
|
-
this.write(
|
|
811
|
-
|
|
520
|
+
const before = line.slice(0, col);
|
|
521
|
+
const at = col < line.length ? line[col] : ' ';
|
|
522
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
523
|
+
this.write(before);
|
|
524
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
525
|
+
this.write(at);
|
|
526
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
527
|
+
this.write(after);
|
|
528
|
+
finalRow = rowNum;
|
|
812
529
|
finalCol = this.config.promptChar.length + col + 1;
|
|
813
530
|
}
|
|
814
531
|
else {
|
|
815
532
|
this.write(line);
|
|
816
533
|
}
|
|
817
|
-
|
|
534
|
+
// Pad to edge for clean look
|
|
535
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
536
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
537
|
+
if (padding > 0)
|
|
538
|
+
this.write(' '.repeat(padding));
|
|
539
|
+
this.write(ESC.RESET);
|
|
818
540
|
}
|
|
819
|
-
//
|
|
820
|
-
|
|
541
|
+
// Mode controls line (Claude Code style)
|
|
542
|
+
const controlRow = currentRow + visibleLines.length;
|
|
543
|
+
this.write(ESC.TO(controlRow, 1));
|
|
821
544
|
this.write(ESC.CLEAR_LINE);
|
|
822
|
-
this.write(
|
|
823
|
-
|
|
824
|
-
// Suggestions (Claude Code style)
|
|
825
|
-
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
826
|
-
this.write(ESC.TO(currentRow, 1));
|
|
827
|
-
this.write(ESC.CLEAR_LINE);
|
|
828
|
-
const suggestion = suggestionsToShow[i];
|
|
829
|
-
const isSelected = i === this.selectedSuggestionIndex;
|
|
830
|
-
// Indent and highlight selected
|
|
831
|
-
this.write(' ');
|
|
832
|
-
if (isSelected) {
|
|
833
|
-
this.write(ESC.REVERSE);
|
|
834
|
-
this.write(ESC.BOLD);
|
|
835
|
-
}
|
|
836
|
-
this.write(suggestion.command);
|
|
837
|
-
if (isSelected) {
|
|
838
|
-
this.write(ESC.RESET);
|
|
839
|
-
}
|
|
840
|
-
// Description (dimmed)
|
|
841
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
842
|
-
if (descSpace > 10 && suggestion.description) {
|
|
843
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
844
|
-
this.write(ESC.RESET);
|
|
845
|
-
this.write(ESC.DIM);
|
|
846
|
-
this.write(' ');
|
|
847
|
-
this.write(desc);
|
|
848
|
-
this.write(ESC.RESET);
|
|
849
|
-
}
|
|
850
|
-
currentRow++;
|
|
851
|
-
}
|
|
852
|
-
// Position cursor in input area
|
|
545
|
+
this.write(this.buildModeControls(cols));
|
|
546
|
+
// Position cursor
|
|
853
547
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
548
|
+
this.write(ESC.SHOW);
|
|
549
|
+
// Update state
|
|
550
|
+
this.lastRenderContent = this.buffer;
|
|
551
|
+
this.lastRenderCursor = this.cursor;
|
|
552
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
553
|
+
if (this.streamingRenderTimer) {
|
|
554
|
+
clearTimeout(this.streamingRenderTimer);
|
|
555
|
+
this.streamingRenderTimer = null;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
// Use write lock during render to prevent interleaved output
|
|
559
|
+
writeLock.lock('terminalInput.render');
|
|
560
|
+
this.isRendering = true;
|
|
561
|
+
try {
|
|
562
|
+
performRender();
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
writeLock.unlock();
|
|
566
|
+
this.isRendering = false;
|
|
854
567
|
}
|
|
855
|
-
this.write(ESC.SHOW);
|
|
856
|
-
// Update state
|
|
857
|
-
this.lastRenderContent = this.buffer;
|
|
858
|
-
this.lastRenderCursor = this.cursor;
|
|
859
568
|
}
|
|
860
569
|
/**
|
|
861
|
-
* 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.
|
|
862
573
|
*/
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
}
|
|
874
627
|
if (this.queue.length > 0) {
|
|
875
|
-
|
|
628
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
629
|
+
}
|
|
630
|
+
if (usageParts.length) {
|
|
631
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
876
632
|
}
|
|
877
|
-
|
|
878
|
-
status += ` ${DIM}· type to queue message${R}`;
|
|
879
|
-
return status;
|
|
633
|
+
return lines;
|
|
880
634
|
}
|
|
881
635
|
/**
|
|
882
|
-
*
|
|
883
|
-
* 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.
|
|
884
637
|
*/
|
|
885
|
-
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (this.streamingStartTime) {
|
|
892
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
893
|
-
const mins = Math.floor(elapsed / 60);
|
|
894
|
-
const secs = elapsed % 60;
|
|
895
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
896
|
-
}
|
|
897
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
898
|
-
}
|
|
899
|
-
// Queue indicator during streaming
|
|
900
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
901
|
-
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));
|
|
902
644
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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' });
|
|
907
656
|
}
|
|
908
|
-
// Override/warning status
|
|
909
657
|
if (this.overrideStatusMessage) {
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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' });
|
|
915
675
|
}
|
|
916
|
-
// Multi-line indicator
|
|
917
676
|
if (this.buffer.includes('\n')) {
|
|
918
|
-
|
|
677
|
+
const lineCount = this.buffer.split('\n').length;
|
|
678
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
679
|
+
}
|
|
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
|
+
});
|
|
686
|
+
}
|
|
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}`;
|
|
919
764
|
}
|
|
920
|
-
if (
|
|
921
|
-
return
|
|
765
|
+
if (value >= 1_000_000) {
|
|
766
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
922
767
|
}
|
|
923
|
-
|
|
924
|
-
|
|
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;
|
|
925
776
|
}
|
|
926
777
|
/**
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
* 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.
|
|
931
780
|
*/
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
939
|
-
if (this.editMode === 'display-edits') {
|
|
940
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
941
|
-
}
|
|
942
|
-
else {
|
|
943
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
944
|
-
}
|
|
945
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
946
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
947
|
-
// Verification (green when on) - per schema.verificationMode
|
|
948
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
949
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
950
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
951
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
952
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
953
|
-
let rightPart = '';
|
|
954
|
-
if (this.contextUsage !== null) {
|
|
955
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
956
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
957
|
-
if (rem < 10)
|
|
958
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
959
|
-
else if (rem < 25)
|
|
960
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
961
|
-
else
|
|
962
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
963
|
-
}
|
|
964
|
-
// Calculate visible lengths (strip ANSI)
|
|
965
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
966
|
-
const leftLen = strip(leftPart).length;
|
|
967
|
-
const rightLen = strip(rightPart).length;
|
|
968
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
969
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
970
|
-
}
|
|
971
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
972
|
-
return `${leftPart} ${rightPart}`;
|
|
973
|
-
}
|
|
974
|
-
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
|
+
};
|
|
975
787
|
}
|
|
976
788
|
/**
|
|
977
789
|
* Force a re-render
|
|
@@ -994,17 +806,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
994
806
|
handleResize() {
|
|
995
807
|
this.lastRenderContent = '';
|
|
996
808
|
this.lastRenderCursor = -1;
|
|
809
|
+
this.resetStreamingRenderThrottle();
|
|
997
810
|
// Re-clamp pinned header rows to the new terminal height
|
|
998
811
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
812
|
+
if (this.scrollRegionActive) {
|
|
813
|
+
this.disableScrollRegion();
|
|
814
|
+
this.enableScrollRegion();
|
|
815
|
+
}
|
|
999
816
|
this.scheduleRender();
|
|
1000
817
|
}
|
|
1001
818
|
/**
|
|
1002
819
|
* Register with display's output interceptor to position cursor correctly.
|
|
1003
820
|
* When scroll region is active, output needs to go to the scroll region,
|
|
1004
821
|
* not the protected bottom area where the input is rendered.
|
|
1005
|
-
*
|
|
1006
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
1007
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
1008
822
|
*/
|
|
1009
823
|
registerOutputInterceptor(display) {
|
|
1010
824
|
if (this.outputInterceptorCleanup) {
|
|
@@ -1012,11 +826,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
1012
826
|
}
|
|
1013
827
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
1014
828
|
beforeWrite: () => {
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
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
|
+
}
|
|
1017
837
|
},
|
|
1018
838
|
afterWrite: () => {
|
|
1019
|
-
//
|
|
839
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
840
|
+
if (this.scrollRegionActive) {
|
|
841
|
+
this.write(ESC.RESTORE);
|
|
842
|
+
}
|
|
1020
843
|
},
|
|
1021
844
|
});
|
|
1022
845
|
}
|
|
@@ -1026,11 +849,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1026
849
|
dispose() {
|
|
1027
850
|
if (this.disposed)
|
|
1028
851
|
return;
|
|
1029
|
-
// Clean up streaming render timer
|
|
1030
|
-
if (this.streamingRenderTimer) {
|
|
1031
|
-
clearInterval(this.streamingRenderTimer);
|
|
1032
|
-
this.streamingRenderTimer = null;
|
|
1033
|
-
}
|
|
1034
852
|
// Clean up output interceptor
|
|
1035
853
|
if (this.outputInterceptorCleanup) {
|
|
1036
854
|
this.outputInterceptorCleanup();
|
|
@@ -1038,6 +856,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1038
856
|
}
|
|
1039
857
|
this.disposed = true;
|
|
1040
858
|
this.enabled = false;
|
|
859
|
+
this.resetStreamingRenderThrottle();
|
|
1041
860
|
this.disableScrollRegion();
|
|
1042
861
|
this.disableBracketedPaste();
|
|
1043
862
|
this.buffer = '';
|
|
@@ -1143,22 +962,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1143
962
|
this.toggleEditMode();
|
|
1144
963
|
return true;
|
|
1145
964
|
}
|
|
1146
|
-
|
|
1147
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1148
|
-
this.togglePasteExpansion();
|
|
1149
|
-
}
|
|
1150
|
-
else {
|
|
1151
|
-
this.toggleThinking();
|
|
1152
|
-
}
|
|
1153
|
-
return true;
|
|
1154
|
-
case 'escape':
|
|
1155
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1156
|
-
if (this.mode === 'streaming') {
|
|
1157
|
-
this.emit('interrupt');
|
|
1158
|
-
}
|
|
1159
|
-
else if (this.buffer.length > 0) {
|
|
1160
|
-
this.clear();
|
|
1161
|
-
}
|
|
965
|
+
this.insertText(' ');
|
|
1162
966
|
return true;
|
|
1163
967
|
}
|
|
1164
968
|
return false;
|
|
@@ -1176,7 +980,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1176
980
|
this.insertPlainText(chunk, insertPos);
|
|
1177
981
|
this.cursor = insertPos + chunk.length;
|
|
1178
982
|
this.emit('change', this.buffer);
|
|
1179
|
-
this.updateSuggestions();
|
|
1180
983
|
this.scheduleRender();
|
|
1181
984
|
}
|
|
1182
985
|
insertNewline() {
|
|
@@ -1201,7 +1004,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1201
1004
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1202
1005
|
}
|
|
1203
1006
|
this.emit('change', this.buffer);
|
|
1204
|
-
this.updateSuggestions();
|
|
1205
1007
|
this.scheduleRender();
|
|
1206
1008
|
}
|
|
1207
1009
|
deleteForward() {
|
|
@@ -1451,7 +1253,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1451
1253
|
if (available <= 0)
|
|
1452
1254
|
return;
|
|
1453
1255
|
const chunk = clean.slice(0, available);
|
|
1454
|
-
|
|
1256
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1257
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1258
|
+
if (isMultiline && !isShortMultiline) {
|
|
1455
1259
|
this.insertPastePlaceholder(chunk);
|
|
1456
1260
|
}
|
|
1457
1261
|
else {
|
|
@@ -1471,6 +1275,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1471
1275
|
return;
|
|
1472
1276
|
this.applyScrollRegion();
|
|
1473
1277
|
this.scrollRegionActive = true;
|
|
1278
|
+
this.forceRender();
|
|
1474
1279
|
}
|
|
1475
1280
|
disableScrollRegion() {
|
|
1476
1281
|
if (!this.scrollRegionActive)
|
|
@@ -1621,17 +1426,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1621
1426
|
this.shiftPlaceholders(position, text.length);
|
|
1622
1427
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1623
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
|
+
}
|
|
1624
1435
|
findPlaceholderAt(position) {
|
|
1625
1436
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1626
1437
|
}
|
|
1627
|
-
buildPlaceholder(
|
|
1438
|
+
buildPlaceholder(lineCount) {
|
|
1628
1439
|
const id = ++this.pasteCounter;
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
const preview = summary.preview.length > 30
|
|
1632
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1633
|
-
: summary.preview;
|
|
1634
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1440
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1441
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1635
1442
|
return { id, placeholder };
|
|
1636
1443
|
}
|
|
1637
1444
|
insertPastePlaceholder(content) {
|
|
@@ -1639,67 +1446,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1639
1446
|
if (available <= 0)
|
|
1640
1447
|
return;
|
|
1641
1448
|
const cleanContent = content.slice(0, available);
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
if (summary.lineCount < 5) {
|
|
1645
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1646
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1647
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1648
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1449
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1450
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1652
1451
|
const insertPos = this.cursor;
|
|
1653
1452
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1654
1453
|
this.pastePlaceholders.push({
|
|
1655
1454
|
id,
|
|
1656
1455
|
content: cleanContent,
|
|
1657
|
-
lineCount
|
|
1456
|
+
lineCount,
|
|
1658
1457
|
placeholder,
|
|
1659
1458
|
start: insertPos,
|
|
1660
1459
|
end: insertPos + placeholder.length,
|
|
1661
|
-
summary,
|
|
1662
|
-
expanded: false,
|
|
1663
1460
|
});
|
|
1664
1461
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1665
1462
|
this.cursor = insertPos + placeholder.length;
|
|
1666
1463
|
}
|
|
1667
|
-
/**
|
|
1668
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1669
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1670
|
-
*/
|
|
1671
|
-
togglePasteExpansion() {
|
|
1672
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1673
|
-
if (!placeholder)
|
|
1674
|
-
return false;
|
|
1675
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1676
|
-
// Update the placeholder text in buffer
|
|
1677
|
-
const newPlaceholder = placeholder.expanded
|
|
1678
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1679
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1680
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1681
|
-
// Update buffer
|
|
1682
|
-
this.buffer =
|
|
1683
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1684
|
-
newPlaceholder +
|
|
1685
|
-
this.buffer.slice(placeholder.end);
|
|
1686
|
-
// Update placeholder tracking
|
|
1687
|
-
placeholder.placeholder = newPlaceholder;
|
|
1688
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1689
|
-
// Shift other placeholders
|
|
1690
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1691
|
-
this.scheduleRender();
|
|
1692
|
-
return true;
|
|
1693
|
-
}
|
|
1694
|
-
buildExpandedPlaceholder(ph) {
|
|
1695
|
-
const lines = ph.content.split('\n');
|
|
1696
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1697
|
-
const lastLines = lines.length > 5
|
|
1698
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1699
|
-
: '';
|
|
1700
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1701
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1702
|
-
}
|
|
1703
1464
|
deletePlaceholder(placeholder) {
|
|
1704
1465
|
const length = placeholder.end - placeholder.start;
|
|
1705
1466
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1707,7 +1468,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1707
1468
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1708
1469
|
this.cursor = placeholder.start;
|
|
1709
1470
|
}
|
|
1710
|
-
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
|
+
}
|
|
1711
1476
|
if (value === null || !Number.isFinite(value)) {
|
|
1712
1477
|
this.contextUsage = null;
|
|
1713
1478
|
}
|
|
@@ -1734,6 +1499,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1734
1499
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1735
1500
|
this.setEditMode(next);
|
|
1736
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
|
+
}
|
|
1737
1518
|
scheduleRender() {
|
|
1738
1519
|
if (!this.canRender())
|
|
1739
1520
|
return;
|