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