erosolar-cli 1.7.266 → 1.7.267
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 -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 +77 -153
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +490 -726
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +20 -25
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +14 -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 +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,47 +81,38 @@ 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 where content cursor is in scroll region (for streaming)
|
|
90
|
+
contentCursorRow = 1;
|
|
91
|
+
contentCursorCol = 1;
|
|
92
|
+
thinkingModeLabel = null;
|
|
94
93
|
editMode = 'display-edits';
|
|
95
94
|
verificationEnabled = true;
|
|
96
95
|
autoContinueEnabled = false;
|
|
97
96
|
verificationHotkey = 'alt+v';
|
|
98
97
|
autoContinueHotkey = 'alt+c';
|
|
98
|
+
thinkingHotkey = '/thinking';
|
|
99
|
+
modelLabel = null;
|
|
100
|
+
providerLabel = null;
|
|
99
101
|
// Output interceptor cleanup
|
|
100
102
|
outputInterceptorCleanup;
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
thinkingEnabled = true;
|
|
105
|
-
modelInfo = null; // Provider · Model info
|
|
106
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
103
|
+
// Streaming render throttle
|
|
104
|
+
lastStreamingRender = 0;
|
|
105
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
107
106
|
streamingRenderTimer = null;
|
|
108
107
|
constructor(writeStream = process.stdout, config = {}) {
|
|
109
108
|
super();
|
|
110
109
|
this.out = writeStream;
|
|
111
|
-
// Use schema defaults for configuration consistency
|
|
112
110
|
this.config = {
|
|
113
|
-
maxLines: config.maxLines ??
|
|
114
|
-
maxLength: config.maxLength ??
|
|
111
|
+
maxLines: config.maxLines ?? 1000,
|
|
112
|
+
maxLength: config.maxLength ?? 10000,
|
|
115
113
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
116
|
-
promptChar: config.promptChar ??
|
|
117
|
-
continuationChar: config.continuationChar ??
|
|
114
|
+
promptChar: config.promptChar ?? '> ',
|
|
115
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
118
116
|
};
|
|
119
117
|
}
|
|
120
118
|
// ===========================================================================
|
|
@@ -193,11 +191,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
193
191
|
if (handled)
|
|
194
192
|
return;
|
|
195
193
|
}
|
|
196
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
197
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
198
|
-
this.emit('showHelp');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
194
|
// Insert printable characters
|
|
202
195
|
if (str && !key?.ctrl && !key?.meta) {
|
|
203
196
|
this.insertText(str);
|
|
@@ -206,388 +199,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
206
199
|
/**
|
|
207
200
|
* Set the input mode
|
|
208
201
|
*
|
|
209
|
-
* Streaming
|
|
210
|
-
*
|
|
211
|
-
* the cursor is (below the streamed content).
|
|
202
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
203
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
212
204
|
*/
|
|
213
205
|
setMode(mode) {
|
|
214
206
|
const prevMode = this.mode;
|
|
215
207
|
this.mode = mode;
|
|
216
208
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
217
|
-
//
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
// Input area renders at absolute bottom using cursor save/restore
|
|
221
|
-
this.pinnedTopRows = 0;
|
|
222
|
-
this.reservedLines = 5; // Reserve space for input area at bottom
|
|
223
|
-
// Disable any existing scroll region
|
|
224
|
-
this.disableScrollRegion();
|
|
225
|
-
// Initial render of input area at bottom (with lock)
|
|
226
|
-
writeLock.withLock(() => {
|
|
227
|
-
this.renderStreamingInputArea();
|
|
228
|
-
}, 'terminalInput.streamingInit');
|
|
229
|
-
// Start timer to update streaming status and re-render input area
|
|
230
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
231
|
-
if (this.mode === 'streaming') {
|
|
232
|
-
// Use writeLock to prevent race with streaming output
|
|
233
|
-
writeLock.withLock(() => {
|
|
234
|
-
this.updateStreamingStatus();
|
|
235
|
-
this.renderStreamingInputArea();
|
|
236
|
-
}, 'terminalInput.streamingUpdate');
|
|
237
|
-
}
|
|
238
|
-
}, 1000);
|
|
209
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
210
|
+
this.resetStreamingRenderThrottle();
|
|
211
|
+
this.enableScrollRegion();
|
|
239
212
|
this.renderDirty = true;
|
|
213
|
+
this.render();
|
|
240
214
|
}
|
|
241
215
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
// Reset streaming time
|
|
248
|
-
this.streamingStartTime = null;
|
|
249
|
-
this.pinnedTopRows = 0;
|
|
250
|
-
// Ensure no scroll region is active
|
|
251
|
-
this.disableScrollRegion();
|
|
252
|
-
// Reset flow mode tracking
|
|
253
|
-
this.flowModeRenderedLines = 0;
|
|
254
|
-
// Render input area using unified method (same as streaming, but normal mode)
|
|
255
|
-
writeLock.withLock(() => {
|
|
256
|
-
this.renderPinnedInputArea();
|
|
257
|
-
}, 'terminalInput.streamingEnd');
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Update streaming status label (called by timer)
|
|
262
|
-
*/
|
|
263
|
-
updateStreamingStatus() {
|
|
264
|
-
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
265
|
-
return;
|
|
266
|
-
// Calculate elapsed time
|
|
267
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
268
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
269
|
-
const minutes = Math.floor(seconds / 60);
|
|
270
|
-
const secs = seconds % 60;
|
|
271
|
-
// Format elapsed time
|
|
272
|
-
let elapsedStr;
|
|
273
|
-
if (minutes > 0) {
|
|
274
|
-
elapsedStr = `${minutes}m ${secs}s`;
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
elapsedStr = `${secs}s`;
|
|
278
|
-
}
|
|
279
|
-
// Update streaming label
|
|
280
|
-
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Render input area - unified for streaming and normal modes.
|
|
284
|
-
*
|
|
285
|
-
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
286
|
-
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
287
|
-
*/
|
|
288
|
-
renderPinnedInputArea() {
|
|
289
|
-
const { rows, cols } = this.getSize();
|
|
290
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
291
|
-
const divider = renderDivider(cols - 2);
|
|
292
|
-
const isStreaming = this.mode === 'streaming';
|
|
293
|
-
// Wrap buffer into display lines (multi-line support)
|
|
294
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
295
|
-
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
296
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
297
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
298
|
-
// Calculate display window (keep cursor visible)
|
|
299
|
-
let startLine = 0;
|
|
300
|
-
if (lines.length > displayLines) {
|
|
301
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
302
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
303
|
-
}
|
|
304
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
305
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
306
|
-
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
307
|
-
const hasModelInfo = !!this.modelInfo;
|
|
308
|
-
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
309
|
-
// Save cursor position during streaming (so content flow resumes correctly)
|
|
310
|
-
if (isStreaming) {
|
|
311
|
-
this.write(ESC.SAVE);
|
|
312
|
-
}
|
|
313
|
-
this.write(ESC.HIDE);
|
|
314
|
-
this.write(ESC.RESET);
|
|
315
|
-
// Calculate start row based on mode:
|
|
316
|
-
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
317
|
-
// - Normal: right after content (contentEndRow + 1)
|
|
318
|
-
let currentRow;
|
|
319
|
-
if (isStreaming) {
|
|
320
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
// In normal mode, render right after content
|
|
324
|
-
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
325
|
-
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
326
|
-
currentRow = Math.max(1, contentRow + 1);
|
|
327
|
-
}
|
|
328
|
-
let finalRow = currentRow;
|
|
329
|
-
let finalCol = 3;
|
|
330
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
331
|
-
this.write(ESC.TO(currentRow, 1));
|
|
332
|
-
this.write(ESC.CLEAR_TO_END);
|
|
333
|
-
// Status bar
|
|
334
|
-
this.write(ESC.TO(currentRow, 1));
|
|
335
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
336
|
-
currentRow++;
|
|
337
|
-
// Model info line (if set) - displayed below status, above input
|
|
338
|
-
if (hasModelInfo) {
|
|
339
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
340
|
-
this.write(ESC.TO(currentRow, 1));
|
|
341
|
-
// Build model info with context usage
|
|
342
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
343
|
-
if (this.contextUsage !== null) {
|
|
344
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
345
|
-
if (rem < 10)
|
|
346
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
347
|
-
else if (rem < 25)
|
|
348
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
349
|
-
else
|
|
350
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
351
|
-
}
|
|
352
|
-
this.write(modelLine);
|
|
353
|
-
currentRow++;
|
|
354
|
-
}
|
|
355
|
-
// Top divider
|
|
356
|
-
this.write(ESC.TO(currentRow, 1));
|
|
357
|
-
this.write(divider);
|
|
358
|
-
currentRow++;
|
|
359
|
-
// Input lines with background styling
|
|
360
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
361
|
-
this.write(ESC.TO(currentRow, 1));
|
|
362
|
-
const line = visibleLines[i] ?? '';
|
|
363
|
-
const absoluteLineIdx = startLine + i;
|
|
364
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
365
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
366
|
-
// Background
|
|
367
|
-
this.write(ESC.BG_DARK);
|
|
368
|
-
// Prompt prefix
|
|
369
|
-
this.write(ESC.DIM);
|
|
370
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
371
|
-
this.write(ESC.RESET);
|
|
372
|
-
this.write(ESC.BG_DARK);
|
|
373
|
-
if (isCursorLine) {
|
|
374
|
-
const col = Math.min(cursorCol, line.length);
|
|
375
|
-
const before = line.slice(0, col);
|
|
376
|
-
const at = col < line.length ? line[col] : ' ';
|
|
377
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
378
|
-
this.write(before);
|
|
379
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
380
|
-
this.write(at);
|
|
381
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
382
|
-
this.write(after);
|
|
383
|
-
finalRow = currentRow;
|
|
384
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
this.write(line);
|
|
388
|
-
}
|
|
389
|
-
// Pad to edge
|
|
390
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
391
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
392
|
-
if (padding > 0)
|
|
393
|
-
this.write(' '.repeat(padding));
|
|
394
|
-
this.write(ESC.RESET);
|
|
395
|
-
currentRow++;
|
|
396
|
-
}
|
|
397
|
-
// Bottom divider
|
|
398
|
-
this.write(ESC.TO(currentRow, 1));
|
|
399
|
-
this.write(divider);
|
|
400
|
-
currentRow++;
|
|
401
|
-
// Mode controls line
|
|
402
|
-
this.write(ESC.TO(currentRow, 1));
|
|
403
|
-
this.write(this.buildModeControls(cols));
|
|
404
|
-
// Restore cursor position during streaming, or show cursor in normal mode
|
|
405
|
-
if (isStreaming) {
|
|
406
|
-
this.write(ESC.RESTORE);
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
// Position cursor in input area
|
|
410
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
411
|
-
this.write(ESC.SHOW);
|
|
216
|
+
// Streaming ended - render the input area
|
|
217
|
+
this.resetStreamingRenderThrottle();
|
|
218
|
+
this.enableScrollRegion();
|
|
219
|
+
this.forceRender();
|
|
412
220
|
}
|
|
413
|
-
// Update reserved lines for scroll region calculations
|
|
414
|
-
this.updateReservedLines(totalHeight);
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Render input area during streaming (alias for unified method)
|
|
418
|
-
*/
|
|
419
|
-
renderStreamingInputArea() {
|
|
420
|
-
this.renderPinnedInputArea();
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Enable or disable flow mode.
|
|
424
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
425
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
426
|
-
*/
|
|
427
|
-
setFlowMode(enabled) {
|
|
428
|
-
if (this.flowMode === enabled)
|
|
429
|
-
return;
|
|
430
|
-
this.flowMode = enabled;
|
|
431
|
-
this.renderDirty = true;
|
|
432
|
-
this.scheduleRender();
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Check if flow mode is enabled.
|
|
436
|
-
*/
|
|
437
|
-
isFlowMode() {
|
|
438
|
-
return this.flowMode;
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Set the row where content ends (for idle mode positioning).
|
|
442
|
-
* Input area will render starting from this row + 1.
|
|
443
|
-
*/
|
|
444
|
-
setContentEndRow(row) {
|
|
445
|
-
this.contentEndRow = Math.max(0, row);
|
|
446
|
-
this.renderDirty = true;
|
|
447
|
-
this.scheduleRender();
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Set available slash commands for auto-complete suggestions.
|
|
451
|
-
*/
|
|
452
|
-
setCommands(commands) {
|
|
453
|
-
this.commandSuggestions = commands;
|
|
454
|
-
this.updateSuggestions();
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Update filtered suggestions based on current input.
|
|
458
|
-
*/
|
|
459
|
-
updateSuggestions() {
|
|
460
|
-
const input = this.buffer.trim();
|
|
461
|
-
// Only show suggestions when input starts with "/"
|
|
462
|
-
if (!input.startsWith('/')) {
|
|
463
|
-
this.showSuggestions = false;
|
|
464
|
-
this.filteredSuggestions = [];
|
|
465
|
-
this.selectedSuggestionIndex = 0;
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const query = input.toLowerCase();
|
|
469
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
470
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
471
|
-
// Show suggestions if we have matches
|
|
472
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
473
|
-
// Keep selection in bounds
|
|
474
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
475
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Select next suggestion (arrow down / tab).
|
|
480
|
-
*/
|
|
481
|
-
selectNextSuggestion() {
|
|
482
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
483
|
-
return;
|
|
484
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
485
|
-
this.renderDirty = true;
|
|
486
|
-
this.scheduleRender();
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
490
|
-
*/
|
|
491
|
-
selectPrevSuggestion() {
|
|
492
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
493
|
-
return;
|
|
494
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
495
|
-
? this.filteredSuggestions.length - 1
|
|
496
|
-
: this.selectedSuggestionIndex - 1;
|
|
497
|
-
this.renderDirty = true;
|
|
498
|
-
this.scheduleRender();
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Accept current suggestion and insert into buffer.
|
|
502
|
-
*/
|
|
503
|
-
acceptSuggestion() {
|
|
504
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
505
|
-
return false;
|
|
506
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
507
|
-
if (!selected)
|
|
508
|
-
return false;
|
|
509
|
-
// Replace buffer with selected command
|
|
510
|
-
this.buffer = selected.command + ' ';
|
|
511
|
-
this.cursor = this.buffer.length;
|
|
512
|
-
this.showSuggestions = false;
|
|
513
|
-
this.renderDirty = true;
|
|
514
|
-
this.scheduleRender();
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Check if suggestions are visible.
|
|
519
|
-
*/
|
|
520
|
-
areSuggestionsVisible() {
|
|
521
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* Update token count for metrics display
|
|
525
|
-
*/
|
|
526
|
-
setTokensUsed(tokens) {
|
|
527
|
-
this.tokensUsed = tokens;
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Toggle thinking/reasoning mode
|
|
531
|
-
*/
|
|
532
|
-
toggleThinking() {
|
|
533
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
534
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
535
|
-
this.scheduleRender();
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Get thinking enabled state
|
|
539
|
-
*/
|
|
540
|
-
isThinkingEnabled() {
|
|
541
|
-
return this.thinkingEnabled;
|
|
542
221
|
}
|
|
543
222
|
/**
|
|
544
223
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
545
224
|
*/
|
|
546
225
|
setPinnedHeaderLines(count) {
|
|
547
|
-
//
|
|
548
|
-
if (this.pinnedTopRows !==
|
|
549
|
-
this.pinnedTopRows =
|
|
226
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
227
|
+
if (this.pinnedTopRows !== 0) {
|
|
228
|
+
this.pinnedTopRows = 0;
|
|
550
229
|
if (this.scrollRegionActive) {
|
|
551
230
|
this.applyScrollRegion();
|
|
552
231
|
}
|
|
553
232
|
}
|
|
554
233
|
}
|
|
555
|
-
/**
|
|
556
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
557
|
-
* restore the default bottom-aligned layout.
|
|
558
|
-
*/
|
|
559
|
-
setInlineAnchor(row) {
|
|
560
|
-
if (row === null || row === undefined) {
|
|
561
|
-
this.inlineAnchorRow = null;
|
|
562
|
-
this.inlineLayout = false;
|
|
563
|
-
this.renderDirty = true;
|
|
564
|
-
this.render();
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const { rows } = this.getSize();
|
|
568
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
569
|
-
this.inlineAnchorRow = clamped;
|
|
570
|
-
this.inlineLayout = true;
|
|
571
|
-
this.renderDirty = true;
|
|
572
|
-
this.render();
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
576
|
-
* output by re-evaluating the anchor before each render.
|
|
577
|
-
*/
|
|
578
|
-
setInlineAnchorProvider(provider) {
|
|
579
|
-
this.anchorProvider = provider;
|
|
580
|
-
if (!provider) {
|
|
581
|
-
this.inlineLayout = false;
|
|
582
|
-
this.inlineAnchorRow = null;
|
|
583
|
-
this.renderDirty = true;
|
|
584
|
-
this.render();
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
this.inlineLayout = true;
|
|
588
|
-
this.renderDirty = true;
|
|
589
|
-
this.render();
|
|
590
|
-
}
|
|
591
234
|
/**
|
|
592
235
|
* Get current mode
|
|
593
236
|
*/
|
|
@@ -697,6 +340,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
697
340
|
this.streamingLabel = next;
|
|
698
341
|
this.scheduleRender();
|
|
699
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
345
|
+
*/
|
|
346
|
+
setMetaStatus(meta) {
|
|
347
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
348
|
+
? Math.floor(meta.elapsedSeconds)
|
|
349
|
+
: null;
|
|
350
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
351
|
+
? Math.floor(meta.tokensUsed)
|
|
352
|
+
: null;
|
|
353
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
354
|
+
? Math.floor(meta.tokenLimit)
|
|
355
|
+
: null;
|
|
356
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
357
|
+
? Math.floor(meta.thinkingMs)
|
|
358
|
+
: null;
|
|
359
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
360
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
361
|
+
this.metaTokensUsed === nextTokens &&
|
|
362
|
+
this.metaTokenLimit === nextLimit &&
|
|
363
|
+
this.metaThinkingMs === nextThinking &&
|
|
364
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
368
|
+
this.metaTokensUsed = nextTokens;
|
|
369
|
+
this.metaTokenLimit = nextLimit;
|
|
370
|
+
this.metaThinkingMs = nextThinking;
|
|
371
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
372
|
+
this.scheduleRender();
|
|
373
|
+
}
|
|
700
374
|
/**
|
|
701
375
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
702
376
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -706,26 +380,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
706
380
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
707
381
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
708
382
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
383
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
384
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
709
385
|
if (this.verificationEnabled === nextVerification &&
|
|
710
386
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
711
387
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
712
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
388
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
389
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
390
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
713
391
|
return;
|
|
714
392
|
}
|
|
715
393
|
this.verificationEnabled = nextVerification;
|
|
716
394
|
this.autoContinueEnabled = nextAutoContinue;
|
|
717
395
|
this.verificationHotkey = nextVerifyHotkey;
|
|
718
396
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
719
|
-
this.
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
723
|
-
* This is displayed persistently above the input area.
|
|
724
|
-
*/
|
|
725
|
-
setModelInfo(info) {
|
|
726
|
-
if (this.modelInfo === info)
|
|
727
|
-
return;
|
|
728
|
-
this.modelInfo = info;
|
|
397
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
398
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
729
399
|
this.scheduleRender();
|
|
730
400
|
}
|
|
731
401
|
/**
|
|
@@ -737,297 +407,400 @@ export class TerminalInput extends EventEmitter {
|
|
|
737
407
|
this.streamingLabel = null;
|
|
738
408
|
this.scheduleRender();
|
|
739
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Surface model/provider context in the controls bar.
|
|
412
|
+
*/
|
|
413
|
+
setModelContext(options) {
|
|
414
|
+
const nextModel = options.model?.trim() || null;
|
|
415
|
+
const nextProvider = options.provider?.trim() || null;
|
|
416
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.modelLabel = nextModel;
|
|
420
|
+
this.providerLabel = nextProvider;
|
|
421
|
+
this.scheduleRender();
|
|
422
|
+
}
|
|
740
423
|
/**
|
|
741
424
|
* Render the input area - Claude Code style with mode controls
|
|
742
425
|
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
426
|
+
* During streaming we keep the scroll region active and repaint only the
|
|
427
|
+
* pinned status/input block (throttled) so streamed content can scroll
|
|
428
|
+
* naturally above while elapsed time and status stay fresh.
|
|
745
429
|
*/
|
|
746
430
|
render() {
|
|
747
431
|
if (!this.canRender())
|
|
748
432
|
return;
|
|
749
433
|
if (this.isRendering)
|
|
750
434
|
return;
|
|
435
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
436
|
+
// During streaming we still render the pinned input/status region, but throttle
|
|
437
|
+
// to avoid fighting with the streamed content flow.
|
|
438
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
439
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
440
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
441
|
+
if (waitMs > 0) {
|
|
442
|
+
this.renderDirty = true;
|
|
443
|
+
this.scheduleStreamingRender(waitMs);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
751
447
|
const shouldSkip = !this.renderDirty &&
|
|
752
448
|
this.buffer === this.lastRenderContent &&
|
|
753
449
|
this.cursor === this.lastRenderCursor;
|
|
754
450
|
this.renderDirty = false;
|
|
755
|
-
// Skip if nothing changed
|
|
451
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
756
452
|
if (shouldSkip) {
|
|
757
453
|
return;
|
|
758
454
|
}
|
|
759
|
-
// If write lock is held, defer render
|
|
455
|
+
// If write lock is held, defer render to avoid race conditions
|
|
760
456
|
if (writeLock.isLocked()) {
|
|
761
457
|
writeLock.safeWrite(() => this.render());
|
|
762
458
|
return;
|
|
763
459
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
* - Input line(s)
|
|
802
|
-
* - Bottom divider
|
|
803
|
-
* - Mode controls
|
|
804
|
-
*/
|
|
805
|
-
renderBottomPinned() {
|
|
806
|
-
const { rows, cols } = this.getSize();
|
|
807
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
808
|
-
const isStreaming = this.mode === 'streaming';
|
|
809
|
-
// Use unified pinned input area (works for both streaming and normal)
|
|
810
|
-
// Only use complex rendering when suggestions are visible
|
|
811
|
-
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
812
|
-
if (!hasSuggestions) {
|
|
813
|
-
this.renderPinnedInputArea();
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
// Wrap buffer into display lines
|
|
817
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
818
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
819
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
820
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
821
|
-
// Calculate display window (keep cursor visible)
|
|
822
|
-
let startLine = 0;
|
|
823
|
-
if (lines.length > displayLines) {
|
|
824
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
825
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
826
|
-
}
|
|
827
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
828
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
829
|
-
// Calculate suggestion display (not during streaming)
|
|
830
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
831
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
832
|
-
: [];
|
|
833
|
-
const suggestionLines = suggestionsToShow.length;
|
|
834
|
-
this.write(ESC.HIDE);
|
|
835
|
-
this.write(ESC.RESET);
|
|
836
|
-
const divider = renderDivider(cols - 2);
|
|
837
|
-
// Calculate positions from absolute bottom
|
|
838
|
-
let currentRow;
|
|
839
|
-
if (suggestionLines > 0) {
|
|
840
|
-
// With suggestions: input area + dividers + suggestions
|
|
841
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
842
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
843
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
844
|
-
this.updateReservedLines(totalHeight);
|
|
845
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
846
|
-
this.write(ESC.TO(currentRow, 1));
|
|
847
|
-
this.write(ESC.CLEAR_TO_END);
|
|
848
|
-
// Top divider
|
|
460
|
+
const performRender = () => {
|
|
461
|
+
if (!this.scrollRegionActive) {
|
|
462
|
+
this.enableScrollRegion();
|
|
463
|
+
}
|
|
464
|
+
const { rows, cols } = this.getSize();
|
|
465
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
466
|
+
// Wrap buffer into display lines
|
|
467
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
468
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
469
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
470
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
471
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
472
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
473
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
474
|
+
// Calculate display window (keep cursor visible)
|
|
475
|
+
let startLine = 0;
|
|
476
|
+
if (lines.length > displayLines) {
|
|
477
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
478
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
479
|
+
}
|
|
480
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
481
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
482
|
+
// Hide cursor during render to prevent flicker
|
|
483
|
+
this.write(ESC.HIDE);
|
|
484
|
+
this.write(ESC.RESET);
|
|
485
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
486
|
+
let currentRow = startRow;
|
|
487
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
488
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
489
|
+
// Meta/status header (elapsed, tokens/context)
|
|
490
|
+
for (const metaLine of metaLines) {
|
|
491
|
+
this.write(ESC.TO(currentRow, 1));
|
|
492
|
+
this.write(ESC.CLEAR_LINE);
|
|
493
|
+
this.write(metaLine);
|
|
494
|
+
currentRow += 1;
|
|
495
|
+
}
|
|
496
|
+
// Separator line
|
|
849
497
|
this.write(ESC.TO(currentRow, 1));
|
|
498
|
+
this.write(ESC.CLEAR_LINE);
|
|
499
|
+
const divider = renderDivider(cols - 2);
|
|
850
500
|
this.write(divider);
|
|
851
|
-
currentRow
|
|
852
|
-
//
|
|
501
|
+
currentRow += 1;
|
|
502
|
+
// Render input lines
|
|
853
503
|
let finalRow = currentRow;
|
|
854
504
|
let finalCol = 3;
|
|
855
505
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
856
|
-
|
|
506
|
+
const rowNum = currentRow + i;
|
|
507
|
+
this.write(ESC.TO(rowNum, 1));
|
|
508
|
+
this.write(ESC.CLEAR_LINE);
|
|
857
509
|
const line = visibleLines[i] ?? '';
|
|
858
510
|
const absoluteLineIdx = startLine + i;
|
|
859
511
|
const isFirstLine = absoluteLineIdx === 0;
|
|
860
512
|
const isCursorLine = i === adjustedCursorLine;
|
|
513
|
+
// Background
|
|
514
|
+
this.write(ESC.BG_DARK);
|
|
515
|
+
// Prompt prefix
|
|
516
|
+
this.write(ESC.DIM);
|
|
861
517
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
518
|
+
this.write(ESC.RESET);
|
|
519
|
+
this.write(ESC.BG_DARK);
|
|
862
520
|
if (isCursorLine) {
|
|
521
|
+
// Render with block cursor
|
|
863
522
|
const col = Math.min(cursorCol, line.length);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this.write(
|
|
868
|
-
this.write(
|
|
869
|
-
|
|
523
|
+
const before = line.slice(0, col);
|
|
524
|
+
const at = col < line.length ? line[col] : ' ';
|
|
525
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
526
|
+
this.write(before);
|
|
527
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
528
|
+
this.write(at);
|
|
529
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
530
|
+
this.write(after);
|
|
531
|
+
finalRow = rowNum;
|
|
870
532
|
finalCol = this.config.promptChar.length + col + 1;
|
|
871
533
|
}
|
|
872
534
|
else {
|
|
873
535
|
this.write(line);
|
|
874
536
|
}
|
|
875
|
-
|
|
537
|
+
// Pad to edge for clean look
|
|
538
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
539
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
540
|
+
if (padding > 0)
|
|
541
|
+
this.write(' '.repeat(padding));
|
|
542
|
+
this.write(ESC.RESET);
|
|
876
543
|
}
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
this.write(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
this.write(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
this.write(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
this.write(ESC.DIM);
|
|
902
|
-
this.write(' ');
|
|
903
|
-
this.write(desc);
|
|
904
|
-
this.write(ESC.RESET);
|
|
905
|
-
}
|
|
906
|
-
currentRow++;
|
|
544
|
+
// Mode controls line (Claude Code style)
|
|
545
|
+
const controlRow = currentRow + visibleLines.length;
|
|
546
|
+
this.write(ESC.TO(controlRow, 1));
|
|
547
|
+
this.write(ESC.CLEAR_LINE);
|
|
548
|
+
this.write(this.buildModeControls(cols));
|
|
549
|
+
// During streaming, position cursor back at content location (interceptor tracks this).
|
|
550
|
+
// When not streaming, position cursor in the input box for user editing.
|
|
551
|
+
if (streamingActive) {
|
|
552
|
+
// Move cursor back to scroll region where content continues
|
|
553
|
+
this.write(ESC.TO(this.contentCursorRow, this.contentCursorCol));
|
|
554
|
+
this.write(ESC.SHOW);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
// Position cursor in the input box
|
|
558
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
559
|
+
this.write(ESC.SHOW);
|
|
560
|
+
}
|
|
561
|
+
// Update state
|
|
562
|
+
this.lastRenderContent = this.buffer;
|
|
563
|
+
this.lastRenderCursor = this.cursor;
|
|
564
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
565
|
+
if (this.streamingRenderTimer) {
|
|
566
|
+
clearTimeout(this.streamingRenderTimer);
|
|
567
|
+
this.streamingRenderTimer = null;
|
|
907
568
|
}
|
|
908
|
-
|
|
909
|
-
|
|
569
|
+
};
|
|
570
|
+
// Use write lock during render to prevent interleaved output
|
|
571
|
+
writeLock.lock('terminalInput.render');
|
|
572
|
+
this.isRendering = true;
|
|
573
|
+
try {
|
|
574
|
+
performRender();
|
|
575
|
+
}
|
|
576
|
+
finally {
|
|
577
|
+
writeLock.unlock();
|
|
578
|
+
this.isRendering = false;
|
|
910
579
|
}
|
|
911
|
-
this.write(ESC.SHOW);
|
|
912
|
-
// Update state
|
|
913
|
-
this.lastRenderContent = this.buffer;
|
|
914
|
-
this.lastRenderCursor = this.cursor;
|
|
915
580
|
}
|
|
916
581
|
/**
|
|
917
|
-
* Build
|
|
582
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
583
|
+
* During streaming, shows model line pinned above streaming info.
|
|
918
584
|
*/
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (this.
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
//
|
|
585
|
+
buildMetaLines(width) {
|
|
586
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
587
|
+
const lines = [];
|
|
588
|
+
// Model line should ALWAYS be shown (pinned above streaming content)
|
|
589
|
+
if (this.modelLabel) {
|
|
590
|
+
const modelText = this.providerLabel
|
|
591
|
+
? `model ${this.modelLabel} @ ${this.providerLabel}`
|
|
592
|
+
: `model ${this.modelLabel}`;
|
|
593
|
+
lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
|
|
594
|
+
}
|
|
595
|
+
// During streaming, add a compact status line with essential info
|
|
596
|
+
if (streamingActive) {
|
|
597
|
+
const parts = [];
|
|
598
|
+
// Essential streaming info
|
|
599
|
+
if (this.metaThinkingMs !== null) {
|
|
600
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
601
|
+
}
|
|
602
|
+
if (this.metaElapsedSeconds !== null) {
|
|
603
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
604
|
+
}
|
|
605
|
+
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
606
|
+
if (parts.length) {
|
|
607
|
+
lines.push(renderStatusLine(parts, width));
|
|
608
|
+
}
|
|
609
|
+
return lines;
|
|
610
|
+
}
|
|
611
|
+
// Non-streaming: show full status info (model line already added above)
|
|
612
|
+
if (this.metaThinkingMs !== null) {
|
|
613
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
614
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
615
|
+
}
|
|
616
|
+
const statusParts = [];
|
|
617
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
618
|
+
if (statusLabel) {
|
|
619
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
620
|
+
}
|
|
621
|
+
if (this.metaElapsedSeconds !== null) {
|
|
622
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
623
|
+
}
|
|
624
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
625
|
+
if (tokensRemaining !== null) {
|
|
626
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
627
|
+
}
|
|
628
|
+
if (statusParts.length) {
|
|
629
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
630
|
+
}
|
|
631
|
+
const usageParts = [];
|
|
632
|
+
if (this.metaTokensUsed !== null) {
|
|
633
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
634
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
635
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
636
|
+
}
|
|
637
|
+
if (this.contextUsage !== null) {
|
|
638
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
639
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
640
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
641
|
+
}
|
|
930
642
|
if (this.queue.length > 0) {
|
|
931
|
-
|
|
643
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
644
|
+
}
|
|
645
|
+
if (usageParts.length) {
|
|
646
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
932
647
|
}
|
|
933
|
-
|
|
934
|
-
status += ` ${DIM}· type to queue message${R}`;
|
|
935
|
-
return status;
|
|
648
|
+
return lines;
|
|
936
649
|
}
|
|
937
650
|
/**
|
|
938
|
-
*
|
|
939
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
651
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
940
652
|
*/
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (this.streamingStartTime) {
|
|
948
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
949
|
-
const mins = Math.floor(elapsed / 60);
|
|
950
|
-
const secs = elapsed % 60;
|
|
951
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
952
|
-
}
|
|
953
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
653
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
654
|
+
const width = Math.max(1, cols);
|
|
655
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
656
|
+
const row = startRow + i;
|
|
657
|
+
this.write(ESC.TO(row, 1));
|
|
658
|
+
this.write(' '.repeat(width));
|
|
954
659
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Build Claude Code style mode controls line.
|
|
663
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
664
|
+
*/
|
|
665
|
+
buildModeControls(cols) {
|
|
666
|
+
const width = Math.max(8, cols - 2);
|
|
667
|
+
const leftParts = [];
|
|
668
|
+
const rightParts = [];
|
|
669
|
+
if (this.streamingLabel) {
|
|
670
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
963
671
|
}
|
|
964
|
-
// Override/warning status
|
|
965
672
|
if (this.overrideStatusMessage) {
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
673
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
674
|
+
}
|
|
675
|
+
if (this.statusMessage) {
|
|
676
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
677
|
+
}
|
|
678
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
679
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
680
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
681
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
682
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
683
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
684
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
685
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
686
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
687
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
688
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
689
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
971
690
|
}
|
|
972
|
-
// Multi-line indicator
|
|
973
691
|
if (this.buffer.includes('\n')) {
|
|
974
|
-
|
|
692
|
+
const lineCount = this.buffer.split('\n').length;
|
|
693
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
975
694
|
}
|
|
976
|
-
if (
|
|
977
|
-
|
|
695
|
+
if (this.pastePlaceholders.length > 0) {
|
|
696
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
697
|
+
leftParts.push({
|
|
698
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
699
|
+
tone: 'info',
|
|
700
|
+
});
|
|
978
701
|
}
|
|
979
|
-
const
|
|
980
|
-
|
|
702
|
+
const contextRemaining = this.computeContextRemaining();
|
|
703
|
+
if (this.thinkingModeLabel) {
|
|
704
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
705
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
706
|
+
}
|
|
707
|
+
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
708
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
709
|
+
if (this.modelLabel && !streamingActive) {
|
|
710
|
+
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
711
|
+
rightParts.push({ text: modelText, tone: 'muted' });
|
|
712
|
+
}
|
|
713
|
+
if (contextRemaining !== null) {
|
|
714
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
715
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
716
|
+
? 'Context auto-compact imminent'
|
|
717
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
718
|
+
rightParts.push({ text: label, tone });
|
|
719
|
+
}
|
|
720
|
+
if (!rightParts.length || width < 60) {
|
|
721
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
722
|
+
return renderStatusLine(merged, width);
|
|
723
|
+
}
|
|
724
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
725
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
726
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
727
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
728
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
729
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
730
|
+
}
|
|
731
|
+
formatHotkey(hotkey) {
|
|
732
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
733
|
+
if (!normalized)
|
|
734
|
+
return hotkey;
|
|
735
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
736
|
+
const map = {
|
|
737
|
+
shift: '⇧',
|
|
738
|
+
sh: '⇧',
|
|
739
|
+
alt: '⌥',
|
|
740
|
+
option: '⌥',
|
|
741
|
+
opt: '⌥',
|
|
742
|
+
ctrl: '⌃',
|
|
743
|
+
control: '⌃',
|
|
744
|
+
cmd: '⌘',
|
|
745
|
+
meta: '⌘',
|
|
746
|
+
};
|
|
747
|
+
const formatted = parts
|
|
748
|
+
.map((part) => {
|
|
749
|
+
const symbol = map[part];
|
|
750
|
+
if (symbol)
|
|
751
|
+
return symbol;
|
|
752
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
753
|
+
})
|
|
754
|
+
.join('');
|
|
755
|
+
return formatted || hotkey;
|
|
756
|
+
}
|
|
757
|
+
computeContextRemaining() {
|
|
758
|
+
if (this.contextUsage === null) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
762
|
+
}
|
|
763
|
+
computeTokensRemaining() {
|
|
764
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
768
|
+
return this.formatTokenCount(remaining);
|
|
769
|
+
}
|
|
770
|
+
formatElapsedLabel(seconds) {
|
|
771
|
+
if (seconds < 60) {
|
|
772
|
+
return `${seconds}s`;
|
|
773
|
+
}
|
|
774
|
+
const mins = Math.floor(seconds / 60);
|
|
775
|
+
const secs = seconds % 60;
|
|
776
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
777
|
+
}
|
|
778
|
+
formatTokenCount(value) {
|
|
779
|
+
if (!Number.isFinite(value)) {
|
|
780
|
+
return `${value}`;
|
|
781
|
+
}
|
|
782
|
+
if (value >= 1_000_000) {
|
|
783
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
784
|
+
}
|
|
785
|
+
if (value >= 1_000) {
|
|
786
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
787
|
+
}
|
|
788
|
+
return `${Math.round(value)}`;
|
|
789
|
+
}
|
|
790
|
+
visibleLength(value) {
|
|
791
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
792
|
+
return value.replace(ansiPattern, '').length;
|
|
981
793
|
}
|
|
982
794
|
/**
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
795
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
796
|
+
* needing a TTY. Not used by production code.
|
|
987
797
|
*/
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
995
|
-
if (this.editMode === 'display-edits') {
|
|
996
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
1000
|
-
}
|
|
1001
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
1002
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
1003
|
-
// Verification (green when on) - per schema.verificationMode
|
|
1004
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
1005
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
1006
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
1007
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
1008
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
1009
|
-
let rightPart = '';
|
|
1010
|
-
if (this.contextUsage !== null) {
|
|
1011
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
1012
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
1013
|
-
if (rem < 10)
|
|
1014
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1015
|
-
else if (rem < 25)
|
|
1016
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1017
|
-
else
|
|
1018
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1019
|
-
}
|
|
1020
|
-
// Calculate visible lengths (strip ANSI)
|
|
1021
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1022
|
-
const leftLen = strip(leftPart).length;
|
|
1023
|
-
const rightLen = strip(rightPart).length;
|
|
1024
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
1025
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1026
|
-
}
|
|
1027
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1028
|
-
return `${leftPart} ${rightPart}`;
|
|
1029
|
-
}
|
|
1030
|
-
return leftPart;
|
|
798
|
+
getDebugUiSnapshot(width) {
|
|
799
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
800
|
+
return {
|
|
801
|
+
meta: this.buildMetaLines(cols - 2),
|
|
802
|
+
controls: this.buildModeControls(cols),
|
|
803
|
+
};
|
|
1031
804
|
}
|
|
1032
805
|
/**
|
|
1033
806
|
* Force a re-render
|
|
@@ -1050,17 +823,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1050
823
|
handleResize() {
|
|
1051
824
|
this.lastRenderContent = '';
|
|
1052
825
|
this.lastRenderCursor = -1;
|
|
826
|
+
this.resetStreamingRenderThrottle();
|
|
1053
827
|
// Re-clamp pinned header rows to the new terminal height
|
|
1054
828
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
829
|
+
if (this.scrollRegionActive) {
|
|
830
|
+
this.disableScrollRegion();
|
|
831
|
+
this.enableScrollRegion();
|
|
832
|
+
}
|
|
1055
833
|
this.scheduleRender();
|
|
1056
834
|
}
|
|
1057
835
|
/**
|
|
1058
836
|
* Register with display's output interceptor to position cursor correctly.
|
|
1059
837
|
* When scroll region is active, output needs to go to the scroll region,
|
|
1060
838
|
* not the protected bottom area where the input is rendered.
|
|
1061
|
-
*
|
|
1062
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
1063
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
1064
839
|
*/
|
|
1065
840
|
registerOutputInterceptor(display) {
|
|
1066
841
|
if (this.outputInterceptorCleanup) {
|
|
@@ -1068,25 +843,51 @@ export class TerminalInput extends EventEmitter {
|
|
|
1068
843
|
}
|
|
1069
844
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
1070
845
|
beforeWrite: () => {
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
846
|
+
// Move cursor to where content should continue in the scroll region.
|
|
847
|
+
// This ensures streaming content goes above the pinned input area.
|
|
848
|
+
if (this.scrollRegionActive) {
|
|
849
|
+
this.write(ESC.TO(this.contentCursorRow, this.contentCursorCol));
|
|
850
|
+
}
|
|
1073
851
|
},
|
|
1074
852
|
afterWrite: () => {
|
|
1075
|
-
//
|
|
853
|
+
// After writing, advance the content cursor.
|
|
854
|
+
// We track row advancement; terminal handles column naturally.
|
|
855
|
+
// Assume each write ends with cursor ready for next content.
|
|
856
|
+
if (this.scrollRegionActive) {
|
|
857
|
+
const { rows } = this.getSize();
|
|
858
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
859
|
+
// Content scrolls when we hit bottom, so clamp to scrollBottom
|
|
860
|
+
if (this.contentCursorRow < scrollBottom) {
|
|
861
|
+
this.contentCursorRow++;
|
|
862
|
+
}
|
|
863
|
+
// Reset column to 1 (assuming newline at end of content)
|
|
864
|
+
this.contentCursorCol = 1;
|
|
865
|
+
}
|
|
1076
866
|
},
|
|
1077
867
|
});
|
|
1078
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Reset content cursor to just below the banner (start of scroll region).
|
|
871
|
+
*/
|
|
872
|
+
resetContentCursor() {
|
|
873
|
+
this.contentCursorRow = Math.max(1, this.pinnedTopRows + 1);
|
|
874
|
+
this.contentCursorCol = 1;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Position content cursor at the bottom of scroll region (for initial streaming).
|
|
878
|
+
*/
|
|
879
|
+
positionContentCursorAtBottom() {
|
|
880
|
+
const { rows } = this.getSize();
|
|
881
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
882
|
+
this.contentCursorRow = scrollBottom;
|
|
883
|
+
this.contentCursorCol = 1;
|
|
884
|
+
}
|
|
1079
885
|
/**
|
|
1080
886
|
* Dispose and clean up
|
|
1081
887
|
*/
|
|
1082
888
|
dispose() {
|
|
1083
889
|
if (this.disposed)
|
|
1084
890
|
return;
|
|
1085
|
-
// Clean up streaming render timer
|
|
1086
|
-
if (this.streamingRenderTimer) {
|
|
1087
|
-
clearInterval(this.streamingRenderTimer);
|
|
1088
|
-
this.streamingRenderTimer = null;
|
|
1089
|
-
}
|
|
1090
891
|
// Clean up output interceptor
|
|
1091
892
|
if (this.outputInterceptorCleanup) {
|
|
1092
893
|
this.outputInterceptorCleanup();
|
|
@@ -1094,6 +895,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1094
895
|
}
|
|
1095
896
|
this.disposed = true;
|
|
1096
897
|
this.enabled = false;
|
|
898
|
+
this.resetStreamingRenderThrottle();
|
|
1097
899
|
this.disableScrollRegion();
|
|
1098
900
|
this.disableBracketedPaste();
|
|
1099
901
|
this.buffer = '';
|
|
@@ -1199,22 +1001,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1199
1001
|
this.toggleEditMode();
|
|
1200
1002
|
return true;
|
|
1201
1003
|
}
|
|
1202
|
-
|
|
1203
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1204
|
-
this.togglePasteExpansion();
|
|
1205
|
-
}
|
|
1206
|
-
else {
|
|
1207
|
-
this.toggleThinking();
|
|
1208
|
-
}
|
|
1209
|
-
return true;
|
|
1210
|
-
case 'escape':
|
|
1211
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1212
|
-
if (this.mode === 'streaming') {
|
|
1213
|
-
this.emit('interrupt');
|
|
1214
|
-
}
|
|
1215
|
-
else if (this.buffer.length > 0) {
|
|
1216
|
-
this.clear();
|
|
1217
|
-
}
|
|
1004
|
+
this.insertText(' ');
|
|
1218
1005
|
return true;
|
|
1219
1006
|
}
|
|
1220
1007
|
return false;
|
|
@@ -1232,7 +1019,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1232
1019
|
this.insertPlainText(chunk, insertPos);
|
|
1233
1020
|
this.cursor = insertPos + chunk.length;
|
|
1234
1021
|
this.emit('change', this.buffer);
|
|
1235
|
-
this.updateSuggestions();
|
|
1236
1022
|
this.scheduleRender();
|
|
1237
1023
|
}
|
|
1238
1024
|
insertNewline() {
|
|
@@ -1257,7 +1043,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1257
1043
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1258
1044
|
}
|
|
1259
1045
|
this.emit('change', this.buffer);
|
|
1260
|
-
this.updateSuggestions();
|
|
1261
1046
|
this.scheduleRender();
|
|
1262
1047
|
}
|
|
1263
1048
|
deleteForward() {
|
|
@@ -1507,7 +1292,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1507
1292
|
if (available <= 0)
|
|
1508
1293
|
return;
|
|
1509
1294
|
const chunk = clean.slice(0, available);
|
|
1510
|
-
|
|
1295
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1296
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1297
|
+
if (isMultiline && !isShortMultiline) {
|
|
1511
1298
|
this.insertPastePlaceholder(chunk);
|
|
1512
1299
|
}
|
|
1513
1300
|
else {
|
|
@@ -1527,6 +1314,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1527
1314
|
return;
|
|
1528
1315
|
this.applyScrollRegion();
|
|
1529
1316
|
this.scrollRegionActive = true;
|
|
1317
|
+
this.forceRender();
|
|
1530
1318
|
}
|
|
1531
1319
|
disableScrollRegion() {
|
|
1532
1320
|
if (!this.scrollRegionActive)
|
|
@@ -1677,17 +1465,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1677
1465
|
this.shiftPlaceholders(position, text.length);
|
|
1678
1466
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1679
1467
|
}
|
|
1468
|
+
shouldInlineMultiline(content) {
|
|
1469
|
+
const lines = content.split('\n').length;
|
|
1470
|
+
const maxInlineLines = 4;
|
|
1471
|
+
const maxInlineChars = 240;
|
|
1472
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1473
|
+
}
|
|
1680
1474
|
findPlaceholderAt(position) {
|
|
1681
1475
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1682
1476
|
}
|
|
1683
|
-
buildPlaceholder(
|
|
1477
|
+
buildPlaceholder(lineCount) {
|
|
1684
1478
|
const id = ++this.pasteCounter;
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
const preview = summary.preview.length > 30
|
|
1688
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1689
|
-
: summary.preview;
|
|
1690
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1479
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1480
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1691
1481
|
return { id, placeholder };
|
|
1692
1482
|
}
|
|
1693
1483
|
insertPastePlaceholder(content) {
|
|
@@ -1695,67 +1485,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1695
1485
|
if (available <= 0)
|
|
1696
1486
|
return;
|
|
1697
1487
|
const cleanContent = content.slice(0, available);
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
if (summary.lineCount < 5) {
|
|
1701
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1702
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1703
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1704
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1488
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1489
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1708
1490
|
const insertPos = this.cursor;
|
|
1709
1491
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1710
1492
|
this.pastePlaceholders.push({
|
|
1711
1493
|
id,
|
|
1712
1494
|
content: cleanContent,
|
|
1713
|
-
lineCount
|
|
1495
|
+
lineCount,
|
|
1714
1496
|
placeholder,
|
|
1715
1497
|
start: insertPos,
|
|
1716
1498
|
end: insertPos + placeholder.length,
|
|
1717
|
-
summary,
|
|
1718
|
-
expanded: false,
|
|
1719
1499
|
});
|
|
1720
1500
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1721
1501
|
this.cursor = insertPos + placeholder.length;
|
|
1722
1502
|
}
|
|
1723
|
-
/**
|
|
1724
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1725
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1726
|
-
*/
|
|
1727
|
-
togglePasteExpansion() {
|
|
1728
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1729
|
-
if (!placeholder)
|
|
1730
|
-
return false;
|
|
1731
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1732
|
-
// Update the placeholder text in buffer
|
|
1733
|
-
const newPlaceholder = placeholder.expanded
|
|
1734
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1735
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1736
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1737
|
-
// Update buffer
|
|
1738
|
-
this.buffer =
|
|
1739
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1740
|
-
newPlaceholder +
|
|
1741
|
-
this.buffer.slice(placeholder.end);
|
|
1742
|
-
// Update placeholder tracking
|
|
1743
|
-
placeholder.placeholder = newPlaceholder;
|
|
1744
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1745
|
-
// Shift other placeholders
|
|
1746
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1747
|
-
this.scheduleRender();
|
|
1748
|
-
return true;
|
|
1749
|
-
}
|
|
1750
|
-
buildExpandedPlaceholder(ph) {
|
|
1751
|
-
const lines = ph.content.split('\n');
|
|
1752
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1753
|
-
const lastLines = lines.length > 5
|
|
1754
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1755
|
-
: '';
|
|
1756
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1757
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1758
|
-
}
|
|
1759
1503
|
deletePlaceholder(placeholder) {
|
|
1760
1504
|
const length = placeholder.end - placeholder.start;
|
|
1761
1505
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1763,7 +1507,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1763
1507
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1764
1508
|
this.cursor = placeholder.start;
|
|
1765
1509
|
}
|
|
1766
|
-
updateContextUsage(value) {
|
|
1510
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1511
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1512
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1513
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1514
|
+
}
|
|
1767
1515
|
if (value === null || !Number.isFinite(value)) {
|
|
1768
1516
|
this.contextUsage = null;
|
|
1769
1517
|
}
|
|
@@ -1790,6 +1538,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1790
1538
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1791
1539
|
this.setEditMode(next);
|
|
1792
1540
|
}
|
|
1541
|
+
scheduleStreamingRender(delayMs) {
|
|
1542
|
+
if (this.streamingRenderTimer)
|
|
1543
|
+
return;
|
|
1544
|
+
const wait = Math.max(16, delayMs);
|
|
1545
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1546
|
+
this.streamingRenderTimer = null;
|
|
1547
|
+
this.render();
|
|
1548
|
+
}, wait);
|
|
1549
|
+
}
|
|
1550
|
+
resetStreamingRenderThrottle() {
|
|
1551
|
+
if (this.streamingRenderTimer) {
|
|
1552
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1553
|
+
this.streamingRenderTimer = null;
|
|
1554
|
+
}
|
|
1555
|
+
this.lastStreamingRender = 0;
|
|
1556
|
+
}
|
|
1793
1557
|
scheduleRender() {
|
|
1794
1558
|
if (!this.canRender())
|
|
1795
1559
|
return;
|