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