erosolar-cli 1.7.286 → 1.7.289
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 +148 -24
- package/dist/alpha-zero/agentWrapper.d.ts +84 -0
- package/dist/alpha-zero/agentWrapper.d.ts.map +1 -0
- package/dist/alpha-zero/agentWrapper.js +171 -0
- package/dist/alpha-zero/agentWrapper.js.map +1 -0
- package/dist/alpha-zero/codeEvaluator.d.ts +25 -0
- package/dist/alpha-zero/codeEvaluator.d.ts.map +1 -0
- package/dist/alpha-zero/codeEvaluator.js +273 -0
- package/dist/alpha-zero/codeEvaluator.js.map +1 -0
- package/dist/alpha-zero/competitiveRunner.d.ts +66 -0
- package/dist/alpha-zero/competitiveRunner.d.ts.map +1 -0
- package/dist/alpha-zero/competitiveRunner.js +224 -0
- package/dist/alpha-zero/competitiveRunner.js.map +1 -0
- package/dist/alpha-zero/index.d.ts +67 -0
- package/dist/alpha-zero/index.d.ts.map +1 -0
- package/dist/alpha-zero/index.js +99 -0
- package/dist/alpha-zero/index.js.map +1 -0
- package/dist/alpha-zero/introspection.d.ts +128 -0
- package/dist/alpha-zero/introspection.d.ts.map +1 -0
- package/dist/alpha-zero/introspection.js +300 -0
- package/dist/alpha-zero/introspection.js.map +1 -0
- package/dist/alpha-zero/metricsTracker.d.ts +71 -0
- package/dist/alpha-zero/metricsTracker.d.ts.map +1 -0
- package/dist/{core → alpha-zero}/metricsTracker.js +5 -2
- package/dist/alpha-zero/metricsTracker.js.map +1 -0
- package/dist/alpha-zero/security/core.d.ts +125 -0
- package/dist/alpha-zero/security/core.d.ts.map +1 -0
- package/dist/alpha-zero/security/core.js +271 -0
- package/dist/alpha-zero/security/core.js.map +1 -0
- package/dist/alpha-zero/security/google.d.ts +125 -0
- package/dist/alpha-zero/security/google.d.ts.map +1 -0
- package/dist/alpha-zero/security/google.js +311 -0
- package/dist/alpha-zero/security/google.js.map +1 -0
- package/dist/alpha-zero/security/googleLoader.d.ts +17 -0
- package/dist/alpha-zero/security/googleLoader.d.ts.map +1 -0
- package/dist/alpha-zero/security/googleLoader.js +41 -0
- package/dist/alpha-zero/security/googleLoader.js.map +1 -0
- package/dist/alpha-zero/security/index.d.ts +29 -0
- package/dist/alpha-zero/security/index.d.ts.map +1 -0
- package/dist/alpha-zero/security/index.js +32 -0
- package/dist/alpha-zero/security/index.js.map +1 -0
- package/dist/alpha-zero/security/simulation.d.ts +124 -0
- package/dist/alpha-zero/security/simulation.d.ts.map +1 -0
- package/dist/alpha-zero/security/simulation.js +277 -0
- package/dist/alpha-zero/security/simulation.js.map +1 -0
- package/dist/alpha-zero/selfModification.d.ts +109 -0
- package/dist/alpha-zero/selfModification.d.ts.map +1 -0
- package/dist/alpha-zero/selfModification.js +233 -0
- package/dist/alpha-zero/selfModification.js.map +1 -0
- package/dist/alpha-zero/types.d.ts +170 -0
- package/dist/alpha-zero/types.d.ts.map +1 -0
- package/dist/alpha-zero/types.js +31 -0
- package/dist/alpha-zero/types.js.map +1 -0
- package/dist/bin/erosolar.js +0 -1
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +31 -56
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/capabilities/securityTestingCapability.d.ts +13 -0
- package/dist/capabilities/securityTestingCapability.d.ts.map +1 -0
- package/dist/capabilities/securityTestingCapability.js +25 -0
- package/dist/capabilities/securityTestingCapability.js.map +1 -0
- package/dist/contracts/agent-schemas.json +15 -0
- package/dist/contracts/tools.schema.json +9 -0
- 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/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/customCommands.d.ts +0 -1
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +0 -3
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/core/hooksSystem.d.ts +65 -0
- package/dist/core/hooksSystem.d.ts.map +1 -0
- package/dist/core/hooksSystem.js +273 -0
- package/dist/core/hooksSystem.js.map +1 -0
- package/dist/core/memorySystem.d.ts +48 -0
- package/dist/core/memorySystem.d.ts.map +1 -0
- package/dist/core/memorySystem.js +271 -0
- package/dist/core/memorySystem.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +14 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +1 -22
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +5 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +3 -14
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/unified/errors.d.ts +189 -0
- package/dist/core/unified/errors.d.ts.map +1 -0
- package/dist/core/unified/errors.js +497 -0
- package/dist/core/unified/errors.js.map +1 -0
- package/dist/core/unified/index.d.ts +19 -0
- package/dist/core/unified/index.d.ts.map +1 -0
- package/dist/core/unified/index.js +68 -0
- package/dist/core/unified/index.js.map +1 -0
- package/dist/core/unified/schema.d.ts +101 -0
- package/dist/core/unified/schema.d.ts.map +1 -0
- package/dist/core/unified/schema.js +350 -0
- package/dist/core/unified/schema.js.map +1 -0
- package/dist/core/unified/toolRuntime.d.ts +179 -0
- package/dist/core/unified/toolRuntime.d.ts.map +1 -0
- package/dist/core/unified/toolRuntime.js +517 -0
- package/dist/core/unified/toolRuntime.js.map +1 -0
- package/dist/core/unified/tools.d.ts +127 -0
- package/dist/core/unified/tools.d.ts.map +1 -0
- package/dist/core/unified/tools.js +1333 -0
- package/dist/core/unified/tools.js.map +1 -0
- package/dist/core/unified/types.d.ts +352 -0
- package/dist/core/unified/types.d.ts.map +1 -0
- package/dist/core/unified/types.js +12 -0
- package/dist/core/unified/types.js.map +1 -0
- package/dist/core/unified/version.d.ts +209 -0
- package/dist/core/unified/version.d.ts.map +1 -0
- package/dist/core/unified/version.js +454 -0
- package/dist/core/unified/version.js.map +1 -0
- package/dist/core/validationRunner.d.ts +3 -1
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -21
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- 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 +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +3 -0
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +1 -0
- package/dist/plugins/tools/security/securityPlugin.js +12 -0
- package/dist/plugins/tools/security/securityPlugin.js.map +1 -0
- 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/security/active-stack-security.d.ts +112 -0
- package/dist/security/active-stack-security.d.ts.map +1 -0
- package/dist/security/active-stack-security.js +296 -0
- package/dist/security/active-stack-security.js.map +1 -0
- package/dist/security/advanced-persistence-research.d.ts +92 -0
- package/dist/security/advanced-persistence-research.d.ts.map +1 -0
- package/dist/security/advanced-persistence-research.js +195 -0
- package/dist/security/advanced-persistence-research.js.map +1 -0
- package/dist/security/advanced-targeting.d.ts +119 -0
- package/dist/security/advanced-targeting.d.ts.map +1 -0
- package/dist/security/advanced-targeting.js +233 -0
- package/dist/security/advanced-targeting.js.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +104 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.js +315 -0
- package/dist/security/assessment/vulnerabilityAssessment.js.map +1 -0
- package/dist/security/authorization/securityAuthorization.d.ts +88 -0
- package/dist/security/authorization/securityAuthorization.d.ts.map +1 -0
- package/dist/security/authorization/securityAuthorization.js +172 -0
- package/dist/security/authorization/securityAuthorization.js.map +1 -0
- package/dist/security/comprehensive-targeting.d.ts +85 -0
- package/dist/security/comprehensive-targeting.d.ts.map +1 -0
- package/dist/security/comprehensive-targeting.js +438 -0
- package/dist/security/comprehensive-targeting.js.map +1 -0
- package/dist/security/global-security-integration.d.ts +91 -0
- package/dist/security/global-security-integration.d.ts.map +1 -0
- package/dist/security/global-security-integration.js +218 -0
- package/dist/security/global-security-integration.js.map +1 -0
- package/dist/security/index.d.ts +38 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +47 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/persistence-analyzer.d.ts +56 -0
- package/dist/security/persistence-analyzer.d.ts.map +1 -0
- package/dist/security/persistence-analyzer.js +187 -0
- package/dist/security/persistence-analyzer.js.map +1 -0
- package/dist/security/persistence-cli.d.ts +36 -0
- package/dist/security/persistence-cli.d.ts.map +1 -0
- package/dist/security/persistence-cli.js +160 -0
- package/dist/security/persistence-cli.js.map +1 -0
- package/dist/security/persistence-research.d.ts +92 -0
- package/dist/security/persistence-research.d.ts.map +1 -0
- package/dist/security/persistence-research.js +364 -0
- package/dist/security/persistence-research.js.map +1 -0
- package/dist/security/research/persistenceResearch.d.ts +97 -0
- package/dist/security/research/persistenceResearch.d.ts.map +1 -0
- package/dist/security/research/persistenceResearch.js +282 -0
- package/dist/security/research/persistenceResearch.js.map +1 -0
- package/dist/security/security-integration.d.ts +74 -0
- package/dist/security/security-integration.d.ts.map +1 -0
- package/dist/security/security-integration.js +137 -0
- package/dist/security/security-integration.js.map +1 -0
- package/dist/security/security-testing-framework.d.ts +112 -0
- package/dist/security/security-testing-framework.d.ts.map +1 -0
- package/dist/security/security-testing-framework.js +364 -0
- package/dist/security/security-testing-framework.js.map +1 -0
- package/dist/security/simulation/attackSimulation.d.ts +93 -0
- package/dist/security/simulation/attackSimulation.d.ts.map +1 -0
- package/dist/security/simulation/attackSimulation.js +341 -0
- package/dist/security/simulation/attackSimulation.js.map +1 -0
- package/dist/security/strategic-operations.d.ts +100 -0
- package/dist/security/strategic-operations.d.ts.map +1 -0
- package/dist/security/strategic-operations.js +276 -0
- package/dist/security/strategic-operations.js.map +1 -0
- package/dist/security/tool-security-wrapper.d.ts +58 -0
- package/dist/security/tool-security-wrapper.d.ts.map +1 -0
- package/dist/security/tool-security-wrapper.js +156 -0
- package/dist/security/tool-security-wrapper.js.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +7 -22
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +159 -229
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/metricsTracker.d.ts +60 -0
- package/dist/shell/metricsTracker.d.ts.map +1 -0
- package/dist/shell/metricsTracker.js +119 -0
- package/dist/shell/metricsTracker.js.map +1 -0
- package/dist/shell/shellApp.d.ts +0 -2
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +9 -40
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +137 -88
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +550 -557
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +35 -28
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +50 -26
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/taskRunner.d.ts +1 -7
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +47 -180
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/securityTools.d.ts +22 -0
- package/dist/tools/securityTools.d.ts.map +1 -0
- package/dist/tools/securityTools.js +448 -0
- package/dist/tools/securityTools.js.map +1 -0
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +12 -13
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +45 -24
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +259 -140
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/terminalUISchema.d.ts +195 -0
- package/dist/ui/terminalUISchema.d.ts.map +1 -0
- package/dist/ui/terminalUISchema.js +113 -0
- package/dist/ui/terminalUISchema.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +158 -0
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +348 -0
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +0 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +25 -15
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- package/scripts/deploy-security-capabilities.js +178 -0
- package/dist/core/hooks.d.ts +0 -113
- package/dist/core/hooks.d.ts.map +0 -1
- package/dist/core/hooks.js +0 -267
- package/dist/core/hooks.js.map +0 -1
- package/dist/core/metricsTracker.d.ts +0 -122
- package/dist/core/metricsTracker.d.ts.map +0 -1
- package/dist/core/metricsTracker.js.map +0 -1
- package/dist/core/securityAssessment.d.ts +0 -91
- package/dist/core/securityAssessment.d.ts.map +0 -1
- package/dist/core/securityAssessment.js +0 -580
- package/dist/core/securityAssessment.js.map +0 -1
- package/dist/core/verification.d.ts +0 -137
- package/dist/core/verification.d.ts.map +0 -1
- package/dist/core/verification.js +0 -323
- package/dist/core/verification.js.map +0 -1
- package/dist/subagents/agentConfig.d.ts +0 -27
- package/dist/subagents/agentConfig.d.ts.map +0 -1
- package/dist/subagents/agentConfig.js +0 -89
- package/dist/subagents/agentConfig.js.map +0 -1
- package/dist/subagents/agentRegistry.d.ts +0 -33
- package/dist/subagents/agentRegistry.d.ts.map +0 -1
- package/dist/subagents/agentRegistry.js +0 -162
- package/dist/subagents/agentRegistry.js.map +0 -1
- package/dist/utils/frontmatter.d.ts +0 -10
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js +0 -78
- package/dist/utils/frontmatter.js.map +0 -1
|
@@ -3,18 +3,15 @@
|
|
|
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)
|
|
7
6
|
* - Native bracketed paste support (no heuristics)
|
|
8
7
|
* - Clean cursor model with render-time wrapping
|
|
9
8
|
* - State machine for different input modes
|
|
10
9
|
* - No readline dependency for display
|
|
11
10
|
*/
|
|
12
11
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
14
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
15
|
-
import {
|
|
16
|
-
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
17
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
14
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
18
15
|
// ANSI escape codes
|
|
19
16
|
const ESC = {
|
|
20
17
|
// Cursor control
|
|
@@ -24,6 +21,9 @@ const ESC = {
|
|
|
24
21
|
SHOW: '\x1b[?25h',
|
|
25
22
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
26
23
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
+
// Screen control
|
|
25
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
+
HOME: '\x1b[H',
|
|
27
27
|
// Line control
|
|
28
28
|
CLEAR_LINE: '\x1b[2K',
|
|
29
29
|
CLEAR_TO_END: '\x1b[0J',
|
|
@@ -69,49 +69,55 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
69
|
statusMessage = null;
|
|
70
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
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
|
|
77
72
|
reservedLines = 2;
|
|
78
|
-
scrollRegionActive = false;
|
|
79
73
|
lastRenderContent = '';
|
|
80
74
|
lastRenderCursor = -1;
|
|
81
75
|
renderDirty = false;
|
|
82
76
|
isRendering = false;
|
|
83
77
|
pinnedTopRows = 0;
|
|
78
|
+
inlineAnchorRow = null;
|
|
79
|
+
inlineLayout = false;
|
|
80
|
+
anchorProvider = null;
|
|
81
|
+
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
82
|
+
flowMode = true;
|
|
83
|
+
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
84
|
+
contentEndRow = 0; // Row where content ends (for idle mode positioning)
|
|
85
|
+
// Command suggestions (Claude Code style auto-complete)
|
|
86
|
+
commandSuggestions = [];
|
|
87
|
+
filteredSuggestions = [];
|
|
88
|
+
selectedSuggestionIndex = 0;
|
|
89
|
+
showSuggestions = false;
|
|
90
|
+
maxVisibleSuggestions = 10;
|
|
84
91
|
// Lifecycle
|
|
85
92
|
disposed = false;
|
|
86
93
|
enabled = true;
|
|
87
94
|
contextUsage = null;
|
|
88
|
-
contextAutoCompactThreshold = 90;
|
|
89
|
-
// Track current content row in scroll region (starts at top, moves down)
|
|
90
|
-
contentRow = 1;
|
|
91
|
-
thinkingModeLabel = null;
|
|
92
95
|
editMode = 'display-edits';
|
|
93
96
|
verificationEnabled = true;
|
|
94
97
|
autoContinueEnabled = false;
|
|
95
98
|
verificationHotkey = 'alt+v';
|
|
96
99
|
autoContinueHotkey = 'alt+c';
|
|
97
|
-
thinkingHotkey = '/thinking';
|
|
98
|
-
modelLabel = null;
|
|
99
|
-
providerLabel = null;
|
|
100
100
|
// Output interceptor cleanup
|
|
101
101
|
outputInterceptorCleanup;
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// Metrics tracking for status bar
|
|
103
|
+
streamingStartTime = null;
|
|
104
|
+
tokensUsed = 0;
|
|
105
|
+
thinkingEnabled = true;
|
|
106
|
+
modelInfo = null; // Provider · Model info
|
|
107
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
105
108
|
streamingRenderTimer = null;
|
|
109
|
+
// Unified UI initialization flag
|
|
110
|
+
unifiedUIInitialized = false;
|
|
106
111
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
112
|
super();
|
|
108
113
|
this.out = writeStream;
|
|
114
|
+
// Use schema defaults for configuration consistency
|
|
109
115
|
this.config = {
|
|
110
|
-
maxLines: config.maxLines ??
|
|
111
|
-
maxLength: config.maxLength ??
|
|
116
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
117
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
112
118
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
113
|
-
promptChar: config.promptChar ??
|
|
114
|
-
continuationChar: config.continuationChar ??
|
|
119
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
120
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
// ===========================================================================
|
|
@@ -190,45 +196,345 @@ export class TerminalInput extends EventEmitter {
|
|
|
190
196
|
if (handled)
|
|
191
197
|
return;
|
|
192
198
|
}
|
|
199
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
200
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
201
|
+
this.emit('showHelp');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
193
204
|
// Insert printable characters
|
|
194
205
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
206
|
this.insertText(str);
|
|
196
207
|
}
|
|
197
208
|
}
|
|
209
|
+
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
210
|
+
bannerContent = null;
|
|
211
|
+
/**
|
|
212
|
+
* Set banner content to be written when unified UI initializes.
|
|
213
|
+
*/
|
|
214
|
+
setBannerContent(content) {
|
|
215
|
+
this.bannerContent = content;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Initialize the unified UI system immediately.
|
|
219
|
+
* Clears screen, writes banner, renders input area immediately below.
|
|
220
|
+
* This creates a compact layout on launch (no empty space).
|
|
221
|
+
* Scroll region is set up later when streaming starts.
|
|
222
|
+
*/
|
|
223
|
+
initializeUnifiedUI() {
|
|
224
|
+
if (this.unifiedUIInitialized) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Reserve lines for input area (used later when scroll region is set up)
|
|
228
|
+
this.pinnedTopRows = 0;
|
|
229
|
+
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
230
|
+
// Hide cursor during setup
|
|
231
|
+
this.write(ESC.HIDE);
|
|
232
|
+
// Clear screen
|
|
233
|
+
this.write(ESC.HOME);
|
|
234
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
235
|
+
// Position cursor at row 1
|
|
236
|
+
this.write(ESC.TO(1, 1));
|
|
237
|
+
// Write banner as first content
|
|
238
|
+
let currentRow = 1;
|
|
239
|
+
if (this.bannerContent) {
|
|
240
|
+
process.stdout.write(this.bannerContent + '\n');
|
|
241
|
+
// Count banner lines
|
|
242
|
+
currentRow += this.bannerContent.split('\n').length;
|
|
243
|
+
}
|
|
244
|
+
// Mark unified UI as initialized
|
|
245
|
+
this.unifiedUIInitialized = true;
|
|
246
|
+
// Render input area immediately after banner (not at bottom)
|
|
247
|
+
this.renderInlineInputArea(currentRow);
|
|
248
|
+
// Show cursor
|
|
249
|
+
this.write(ESC.SHOW);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Render input area at a specific row (inline, not pinned to bottom).
|
|
253
|
+
* Used on launch for compact layout.
|
|
254
|
+
*/
|
|
255
|
+
renderInlineInputArea(startRow) {
|
|
256
|
+
const { cols } = this.getSize();
|
|
257
|
+
const divider = '─'.repeat(cols - 1);
|
|
258
|
+
// Move to start row
|
|
259
|
+
this.write(ESC.TO(startRow, 1));
|
|
260
|
+
// Status bar
|
|
261
|
+
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
262
|
+
// Model info line (if set)
|
|
263
|
+
if (this.modelInfo) {
|
|
264
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
265
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
266
|
+
if (this.contextUsage !== null) {
|
|
267
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
268
|
+
if (rem < 10)
|
|
269
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
270
|
+
else if (rem < 25)
|
|
271
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
272
|
+
else
|
|
273
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
274
|
+
}
|
|
275
|
+
process.stdout.write(modelLine + '\n');
|
|
276
|
+
}
|
|
277
|
+
// Top divider
|
|
278
|
+
process.stdout.write(divider + '\n');
|
|
279
|
+
// Input line with prompt
|
|
280
|
+
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
281
|
+
process.stdout.write(ESC.BG_DARK + ' '.repeat(Math.max(0, cols - this.config.promptChar.length - 1)) + ESC.RESET + '\n');
|
|
282
|
+
// Bottom divider
|
|
283
|
+
process.stdout.write(divider + '\n');
|
|
284
|
+
// Mode controls
|
|
285
|
+
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
286
|
+
// Position cursor in input area
|
|
287
|
+
this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Render input area at current cursor position (inline, not pinned).
|
|
291
|
+
* Used after streaming ends - renders input below the streamed content.
|
|
292
|
+
*/
|
|
293
|
+
renderInlineInputAreaAtCursor() {
|
|
294
|
+
const { cols } = this.getSize();
|
|
295
|
+
const divider = '─'.repeat(cols - 1);
|
|
296
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
297
|
+
// Status bar - shows "Type a message" hint
|
|
298
|
+
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
299
|
+
// Model info line (if set)
|
|
300
|
+
if (this.modelInfo) {
|
|
301
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
302
|
+
if (this.contextUsage !== null) {
|
|
303
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
304
|
+
if (rem < 10)
|
|
305
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
306
|
+
else if (rem < 25)
|
|
307
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
308
|
+
else
|
|
309
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
310
|
+
}
|
|
311
|
+
process.stdout.write(modelLine + '\n');
|
|
312
|
+
}
|
|
313
|
+
// Top divider
|
|
314
|
+
process.stdout.write(divider + '\n');
|
|
315
|
+
// Input line with prompt and any buffer content
|
|
316
|
+
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
317
|
+
const displayLine = lines[0] ?? '';
|
|
318
|
+
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
319
|
+
process.stdout.write(ESC.BG_DARK + displayLine);
|
|
320
|
+
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
321
|
+
if (padding > 0)
|
|
322
|
+
process.stdout.write(' '.repeat(padding));
|
|
323
|
+
process.stdout.write(ESC.RESET + '\n');
|
|
324
|
+
// Bottom divider
|
|
325
|
+
process.stdout.write(divider + '\n');
|
|
326
|
+
// Mode controls
|
|
327
|
+
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
328
|
+
// Show cursor
|
|
329
|
+
this.write(ESC.SHOW);
|
|
330
|
+
// Update tracking
|
|
331
|
+
this.lastRenderContent = this.buffer;
|
|
332
|
+
this.lastRenderCursor = this.cursor;
|
|
333
|
+
}
|
|
198
334
|
/**
|
|
199
335
|
* Set the input mode
|
|
200
336
|
*
|
|
201
|
-
* Streaming
|
|
202
|
-
*
|
|
337
|
+
* Streaming mode: NO scroll region, NO bottom-pinned input.
|
|
338
|
+
* Content flows naturally after the initial launch layout.
|
|
339
|
+
* After streaming ends, input area renders inline at current position.
|
|
203
340
|
*/
|
|
204
341
|
setMode(mode) {
|
|
205
342
|
const prevMode = this.mode;
|
|
206
343
|
this.mode = mode;
|
|
207
344
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
208
|
-
//
|
|
209
|
-
this.
|
|
210
|
-
|
|
345
|
+
// Track streaming start time for elapsed display
|
|
346
|
+
this.streamingStartTime = Date.now();
|
|
347
|
+
// Ensure unified UI is initialized (if not already done on launch)
|
|
348
|
+
if (!this.unifiedUIInitialized) {
|
|
349
|
+
this.initializeUnifiedUI();
|
|
350
|
+
}
|
|
351
|
+
// NO scroll region - let content flow naturally
|
|
352
|
+
// NO bottom-pinned input area
|
|
353
|
+
// Don't clear anything - content flows from current cursor position
|
|
354
|
+
// The cursor should already be positioned after the user's prompt
|
|
211
355
|
this.renderDirty = true;
|
|
212
|
-
this.render();
|
|
213
356
|
}
|
|
214
357
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
358
|
+
// Stop streaming render timer (if any)
|
|
359
|
+
if (this.streamingRenderTimer) {
|
|
360
|
+
clearInterval(this.streamingRenderTimer);
|
|
361
|
+
this.streamingRenderTimer = null;
|
|
362
|
+
}
|
|
363
|
+
// Reset streaming time
|
|
364
|
+
this.streamingStartTime = null;
|
|
365
|
+
// Reset flow mode tracking
|
|
366
|
+
this.flowModeRenderedLines = 0;
|
|
367
|
+
// Add spacing after streamed content
|
|
368
|
+
this.write('\n\n');
|
|
369
|
+
// Render input area inline at current position (below streamed content)
|
|
370
|
+
writeLock.withLock(() => {
|
|
371
|
+
this.renderInlineInputAreaAtCursor();
|
|
372
|
+
}, 'terminalInput.streamingEnd');
|
|
219
373
|
}
|
|
220
374
|
}
|
|
221
375
|
/**
|
|
222
|
-
*
|
|
376
|
+
* Enable or disable flow mode.
|
|
377
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
378
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
379
|
+
*/
|
|
380
|
+
setFlowMode(enabled) {
|
|
381
|
+
if (this.flowMode === enabled)
|
|
382
|
+
return;
|
|
383
|
+
this.flowMode = enabled;
|
|
384
|
+
this.renderDirty = true;
|
|
385
|
+
this.scheduleRender();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Check if flow mode is enabled.
|
|
389
|
+
*/
|
|
390
|
+
isFlowMode() {
|
|
391
|
+
return this.flowMode;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Set the row where content ends (for idle mode positioning).
|
|
395
|
+
* Input area will render starting from this row + 1.
|
|
396
|
+
*/
|
|
397
|
+
setContentEndRow(row) {
|
|
398
|
+
this.contentEndRow = Math.max(0, row);
|
|
399
|
+
this.renderDirty = true;
|
|
400
|
+
this.scheduleRender();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Set available slash commands for auto-complete suggestions.
|
|
404
|
+
*/
|
|
405
|
+
setCommands(commands) {
|
|
406
|
+
this.commandSuggestions = commands;
|
|
407
|
+
this.updateSuggestions();
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Update filtered suggestions based on current input.
|
|
411
|
+
*/
|
|
412
|
+
updateSuggestions() {
|
|
413
|
+
const input = this.buffer.trim();
|
|
414
|
+
// Only show suggestions when input starts with "/"
|
|
415
|
+
if (!input.startsWith('/')) {
|
|
416
|
+
this.showSuggestions = false;
|
|
417
|
+
this.filteredSuggestions = [];
|
|
418
|
+
this.selectedSuggestionIndex = 0;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const query = input.toLowerCase();
|
|
422
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
423
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
424
|
+
// Show suggestions if we have matches
|
|
425
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
426
|
+
// Keep selection in bounds
|
|
427
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
428
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Select next suggestion (arrow down / tab).
|
|
433
|
+
*/
|
|
434
|
+
selectNextSuggestion() {
|
|
435
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
436
|
+
return;
|
|
437
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
438
|
+
this.renderDirty = true;
|
|
439
|
+
this.scheduleRender();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
443
|
+
*/
|
|
444
|
+
selectPrevSuggestion() {
|
|
445
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
446
|
+
return;
|
|
447
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
448
|
+
? this.filteredSuggestions.length - 1
|
|
449
|
+
: this.selectedSuggestionIndex - 1;
|
|
450
|
+
this.renderDirty = true;
|
|
451
|
+
this.scheduleRender();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Accept current suggestion and insert into buffer.
|
|
455
|
+
*/
|
|
456
|
+
acceptSuggestion() {
|
|
457
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
458
|
+
return false;
|
|
459
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
460
|
+
if (!selected)
|
|
461
|
+
return false;
|
|
462
|
+
// Replace buffer with selected command
|
|
463
|
+
this.buffer = selected.command + ' ';
|
|
464
|
+
this.cursor = this.buffer.length;
|
|
465
|
+
this.showSuggestions = false;
|
|
466
|
+
this.renderDirty = true;
|
|
467
|
+
this.scheduleRender();
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Check if suggestions are visible.
|
|
472
|
+
*/
|
|
473
|
+
areSuggestionsVisible() {
|
|
474
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Update token count for metrics display
|
|
478
|
+
*/
|
|
479
|
+
setTokensUsed(tokens) {
|
|
480
|
+
this.tokensUsed = tokens;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Toggle thinking/reasoning mode
|
|
484
|
+
*/
|
|
485
|
+
toggleThinking() {
|
|
486
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
487
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
488
|
+
this.scheduleRender();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get thinking enabled state
|
|
492
|
+
*/
|
|
493
|
+
isThinkingEnabled() {
|
|
494
|
+
return this.thinkingEnabled;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Keep the top N rows pinned (used for the launch banner tracking).
|
|
498
|
+
* Note: No longer uses scroll regions - inline rendering only.
|
|
223
499
|
*/
|
|
224
500
|
setPinnedHeaderLines(count) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
501
|
+
this.pinnedTopRows = count;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
505
|
+
* restore the default bottom-aligned layout.
|
|
506
|
+
*/
|
|
507
|
+
setInlineAnchor(row) {
|
|
508
|
+
if (row === null || row === undefined) {
|
|
509
|
+
this.inlineAnchorRow = null;
|
|
510
|
+
this.inlineLayout = false;
|
|
511
|
+
this.renderDirty = true;
|
|
512
|
+
this.render();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const { rows } = this.getSize();
|
|
516
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
517
|
+
this.inlineAnchorRow = clamped;
|
|
518
|
+
this.inlineLayout = true;
|
|
519
|
+
this.renderDirty = true;
|
|
520
|
+
this.render();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
524
|
+
* output by re-evaluating the anchor before each render.
|
|
525
|
+
*/
|
|
526
|
+
setInlineAnchorProvider(provider) {
|
|
527
|
+
this.anchorProvider = provider;
|
|
528
|
+
if (!provider) {
|
|
529
|
+
this.inlineLayout = false;
|
|
530
|
+
this.inlineAnchorRow = null;
|
|
531
|
+
this.renderDirty = true;
|
|
532
|
+
this.render();
|
|
533
|
+
return;
|
|
231
534
|
}
|
|
535
|
+
this.inlineLayout = true;
|
|
536
|
+
this.renderDirty = true;
|
|
537
|
+
this.render();
|
|
232
538
|
}
|
|
233
539
|
/**
|
|
234
540
|
* Get current mode
|
|
@@ -261,14 +567,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
261
567
|
}
|
|
262
568
|
/**
|
|
263
569
|
* Clear the buffer
|
|
570
|
+
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
264
571
|
*/
|
|
265
|
-
clear() {
|
|
572
|
+
clear(skipRender = false) {
|
|
266
573
|
this.buffer = '';
|
|
267
574
|
this.cursor = 0;
|
|
268
575
|
this.historyIndex = -1;
|
|
269
576
|
this.tempInput = '';
|
|
270
577
|
this.pastePlaceholders = [];
|
|
271
|
-
|
|
578
|
+
if (!skipRender) {
|
|
579
|
+
this.scheduleRender();
|
|
580
|
+
}
|
|
272
581
|
}
|
|
273
582
|
/**
|
|
274
583
|
* Get queued inputs
|
|
@@ -339,37 +648,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
339
648
|
this.streamingLabel = next;
|
|
340
649
|
this.scheduleRender();
|
|
341
650
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
344
|
-
*/
|
|
345
|
-
setMetaStatus(meta) {
|
|
346
|
-
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
347
|
-
? Math.floor(meta.elapsedSeconds)
|
|
348
|
-
: null;
|
|
349
|
-
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
350
|
-
? Math.floor(meta.tokensUsed)
|
|
351
|
-
: null;
|
|
352
|
-
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
353
|
-
? Math.floor(meta.tokenLimit)
|
|
354
|
-
: null;
|
|
355
|
-
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
356
|
-
? Math.floor(meta.thinkingMs)
|
|
357
|
-
: null;
|
|
358
|
-
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
359
|
-
if (this.metaElapsedSeconds === nextElapsed &&
|
|
360
|
-
this.metaTokensUsed === nextTokens &&
|
|
361
|
-
this.metaTokenLimit === nextLimit &&
|
|
362
|
-
this.metaThinkingMs === nextThinking &&
|
|
363
|
-
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
this.metaElapsedSeconds = nextElapsed;
|
|
367
|
-
this.metaTokensUsed = nextTokens;
|
|
368
|
-
this.metaTokenLimit = nextLimit;
|
|
369
|
-
this.metaThinkingMs = nextThinking;
|
|
370
|
-
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
371
|
-
this.scheduleRender();
|
|
372
|
-
}
|
|
373
651
|
/**
|
|
374
652
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
375
653
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -379,22 +657,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
379
657
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
380
658
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
381
659
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
382
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
383
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
384
660
|
if (this.verificationEnabled === nextVerification &&
|
|
385
661
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
386
662
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
387
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
388
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
389
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
663
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
390
664
|
return;
|
|
391
665
|
}
|
|
392
666
|
this.verificationEnabled = nextVerification;
|
|
393
667
|
this.autoContinueEnabled = nextAutoContinue;
|
|
394
668
|
this.verificationHotkey = nextVerifyHotkey;
|
|
395
669
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
396
|
-
this.
|
|
397
|
-
|
|
670
|
+
this.scheduleRender();
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
674
|
+
* This is displayed persistently above the input area.
|
|
675
|
+
*/
|
|
676
|
+
setModelInfo(info) {
|
|
677
|
+
if (this.modelInfo === info)
|
|
678
|
+
return;
|
|
679
|
+
this.modelInfo = info;
|
|
398
680
|
this.scheduleRender();
|
|
399
681
|
}
|
|
400
682
|
/**
|
|
@@ -407,161 +689,39 @@ export class TerminalInput extends EventEmitter {
|
|
|
407
689
|
this.scheduleRender();
|
|
408
690
|
}
|
|
409
691
|
/**
|
|
410
|
-
*
|
|
411
|
-
*/
|
|
412
|
-
setModelContext(options) {
|
|
413
|
-
const nextModel = options.model?.trim() || null;
|
|
414
|
-
const nextProvider = options.provider?.trim() || null;
|
|
415
|
-
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
this.modelLabel = nextModel;
|
|
419
|
-
this.providerLabel = nextProvider;
|
|
420
|
-
this.scheduleRender();
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Render the input area - Claude Code style with mode controls
|
|
692
|
+
* Render the input area
|
|
424
693
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
694
|
+
* - Idle mode: Renders inline input area
|
|
695
|
+
* - Streaming mode: NO render (content flows naturally, no UI updates)
|
|
696
|
+
* - Ready mode: Renders inline input area
|
|
428
697
|
*/
|
|
429
698
|
render() {
|
|
430
699
|
if (!this.canRender())
|
|
431
700
|
return;
|
|
432
701
|
if (this.isRendering)
|
|
433
702
|
return;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
438
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
439
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
440
|
-
if (waitMs > 0) {
|
|
441
|
-
this.renderDirty = true;
|
|
442
|
-
this.scheduleStreamingRender(waitMs);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
703
|
+
// During streaming, do NOT render - let content flow naturally
|
|
704
|
+
if (this.mode === 'streaming') {
|
|
705
|
+
return;
|
|
445
706
|
}
|
|
446
707
|
const shouldSkip = !this.renderDirty &&
|
|
447
708
|
this.buffer === this.lastRenderContent &&
|
|
448
709
|
this.cursor === this.lastRenderCursor;
|
|
449
710
|
this.renderDirty = false;
|
|
450
|
-
// Skip if nothing changed
|
|
711
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
451
712
|
if (shouldSkip) {
|
|
452
713
|
return;
|
|
453
714
|
}
|
|
454
|
-
// If write lock is held, defer render
|
|
715
|
+
// If write lock is held, defer render
|
|
455
716
|
if (writeLock.isLocked()) {
|
|
456
717
|
writeLock.safeWrite(() => this.render());
|
|
457
718
|
return;
|
|
458
719
|
}
|
|
459
|
-
const performRender = () => {
|
|
460
|
-
if (!this.scrollRegionActive) {
|
|
461
|
-
this.enableScrollRegion();
|
|
462
|
-
}
|
|
463
|
-
const { rows, cols } = this.getSize();
|
|
464
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
465
|
-
// Wrap buffer into display lines
|
|
466
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
467
|
-
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
468
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
469
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
470
|
-
const metaLines = this.buildMetaLines(cols - 2);
|
|
471
|
-
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
472
|
-
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
473
|
-
// Calculate display window (keep cursor visible)
|
|
474
|
-
let startLine = 0;
|
|
475
|
-
if (lines.length > displayLines) {
|
|
476
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
477
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
478
|
-
}
|
|
479
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
480
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
481
|
-
// Hide cursor during render to prevent flicker
|
|
482
|
-
this.write(ESC.HIDE);
|
|
483
|
-
this.write(ESC.RESET);
|
|
484
|
-
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
485
|
-
let currentRow = startRow;
|
|
486
|
-
// Clear the reserved block to avoid stale meta/status lines
|
|
487
|
-
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
488
|
-
// Meta/status header (elapsed, tokens/context)
|
|
489
|
-
for (const metaLine of metaLines) {
|
|
490
|
-
this.write(ESC.TO(currentRow, 1));
|
|
491
|
-
this.write(ESC.CLEAR_LINE);
|
|
492
|
-
this.write(metaLine);
|
|
493
|
-
currentRow += 1;
|
|
494
|
-
}
|
|
495
|
-
// Separator line
|
|
496
|
-
this.write(ESC.TO(currentRow, 1));
|
|
497
|
-
this.write(ESC.CLEAR_LINE);
|
|
498
|
-
const divider = renderDivider(cols - 2);
|
|
499
|
-
this.write(divider);
|
|
500
|
-
currentRow += 1;
|
|
501
|
-
// Render input lines
|
|
502
|
-
let finalRow = currentRow;
|
|
503
|
-
let finalCol = 3;
|
|
504
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
505
|
-
const rowNum = currentRow + i;
|
|
506
|
-
this.write(ESC.TO(rowNum, 1));
|
|
507
|
-
this.write(ESC.CLEAR_LINE);
|
|
508
|
-
const line = visibleLines[i] ?? '';
|
|
509
|
-
const absoluteLineIdx = startLine + i;
|
|
510
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
511
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
512
|
-
// Background
|
|
513
|
-
this.write(ESC.BG_DARK);
|
|
514
|
-
// Prompt prefix
|
|
515
|
-
this.write(ESC.DIM);
|
|
516
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
517
|
-
this.write(ESC.RESET);
|
|
518
|
-
this.write(ESC.BG_DARK);
|
|
519
|
-
if (isCursorLine) {
|
|
520
|
-
// Render with block cursor
|
|
521
|
-
const col = Math.min(cursorCol, line.length);
|
|
522
|
-
const before = line.slice(0, col);
|
|
523
|
-
const at = col < line.length ? line[col] : ' ';
|
|
524
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
525
|
-
this.write(before);
|
|
526
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
527
|
-
this.write(at);
|
|
528
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
529
|
-
this.write(after);
|
|
530
|
-
finalRow = rowNum;
|
|
531
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
this.write(line);
|
|
535
|
-
}
|
|
536
|
-
// Pad to edge for clean look
|
|
537
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
538
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
539
|
-
if (padding > 0)
|
|
540
|
-
this.write(' '.repeat(padding));
|
|
541
|
-
this.write(ESC.RESET);
|
|
542
|
-
}
|
|
543
|
-
// Mode controls line (Claude Code style)
|
|
544
|
-
const controlRow = currentRow + visibleLines.length;
|
|
545
|
-
this.write(ESC.TO(controlRow, 1));
|
|
546
|
-
this.write(ESC.CLEAR_LINE);
|
|
547
|
-
this.write(this.buildModeControls(cols));
|
|
548
|
-
// Position cursor in the input box for user editing
|
|
549
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
550
|
-
this.write(ESC.SHOW);
|
|
551
|
-
// Update state
|
|
552
|
-
this.lastRenderContent = this.buffer;
|
|
553
|
-
this.lastRenderCursor = this.cursor;
|
|
554
|
-
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
555
|
-
if (this.streamingRenderTimer) {
|
|
556
|
-
clearTimeout(this.streamingRenderTimer);
|
|
557
|
-
this.streamingRenderTimer = null;
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
// Use write lock during render to prevent interleaved output
|
|
561
|
-
writeLock.lock('terminalInput.render');
|
|
562
720
|
this.isRendering = true;
|
|
721
|
+
writeLock.lock('terminalInput.render');
|
|
563
722
|
try {
|
|
564
|
-
|
|
723
|
+
// Render inline input area at current position
|
|
724
|
+
this.renderInlineInputAreaAtCursor();
|
|
565
725
|
}
|
|
566
726
|
finally {
|
|
567
727
|
writeLock.unlock();
|
|
@@ -569,228 +729,99 @@ export class TerminalInput extends EventEmitter {
|
|
|
569
729
|
}
|
|
570
730
|
}
|
|
571
731
|
/**
|
|
572
|
-
* Build
|
|
573
|
-
*
|
|
732
|
+
* Build status bar showing streaming/ready status and key info.
|
|
733
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
574
734
|
*/
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
//
|
|
579
|
-
if (this.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (streamingActive) {
|
|
587
|
-
const parts = [];
|
|
588
|
-
// Essential streaming info
|
|
589
|
-
if (this.metaThinkingMs !== null) {
|
|
590
|
-
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
591
|
-
}
|
|
592
|
-
if (this.metaElapsedSeconds !== null) {
|
|
593
|
-
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
594
|
-
}
|
|
595
|
-
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
596
|
-
if (parts.length) {
|
|
597
|
-
lines.push(renderStatusLine(parts, width));
|
|
735
|
+
buildStatusBar(cols) {
|
|
736
|
+
const maxWidth = cols - 2;
|
|
737
|
+
const parts = [];
|
|
738
|
+
// Streaming status with elapsed time (left side)
|
|
739
|
+
if (this.mode === 'streaming') {
|
|
740
|
+
let statusText = '● Streaming';
|
|
741
|
+
if (this.streamingStartTime) {
|
|
742
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
743
|
+
const mins = Math.floor(elapsed / 60);
|
|
744
|
+
const secs = elapsed % 60;
|
|
745
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
598
746
|
}
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
// Non-streaming: show full status info (model line already added above)
|
|
602
|
-
if (this.metaThinkingMs !== null) {
|
|
603
|
-
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
604
|
-
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
605
|
-
}
|
|
606
|
-
const statusParts = [];
|
|
607
|
-
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
608
|
-
if (statusLabel) {
|
|
609
|
-
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
610
|
-
}
|
|
611
|
-
if (this.metaElapsedSeconds !== null) {
|
|
612
|
-
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
747
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
613
748
|
}
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
749
|
+
// Queue indicator during streaming
|
|
750
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
751
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
617
752
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (this.metaTokensUsed !== null) {
|
|
623
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
624
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
625
|
-
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
753
|
+
// Paste indicator
|
|
754
|
+
if (this.pastePlaceholders.length > 0) {
|
|
755
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
756
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
626
757
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
758
|
+
// Override/warning status
|
|
759
|
+
if (this.overrideStatusMessage) {
|
|
760
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
631
761
|
}
|
|
632
|
-
|
|
633
|
-
|
|
762
|
+
// If idle with empty buffer, show quick shortcuts
|
|
763
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
764
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
634
765
|
}
|
|
635
|
-
|
|
636
|
-
|
|
766
|
+
// Multi-line indicator
|
|
767
|
+
if (this.buffer.includes('\n')) {
|
|
768
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
637
769
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
642
|
-
*/
|
|
643
|
-
clearReservedArea(startRow, reservedLines, cols) {
|
|
644
|
-
const width = Math.max(1, cols);
|
|
645
|
-
for (let i = 0; i < reservedLines; i++) {
|
|
646
|
-
const row = startRow + i;
|
|
647
|
-
this.write(ESC.TO(row, 1));
|
|
648
|
-
this.write(' '.repeat(width));
|
|
770
|
+
if (parts.length === 0) {
|
|
771
|
+
return ''; // Empty status bar when idle
|
|
649
772
|
}
|
|
773
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
774
|
+
return joined.slice(0, maxWidth);
|
|
650
775
|
}
|
|
651
776
|
/**
|
|
652
|
-
* Build
|
|
653
|
-
*
|
|
777
|
+
* Build mode controls line showing toggles and context info.
|
|
778
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
779
|
+
*
|
|
780
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
654
781
|
*/
|
|
655
782
|
buildModeControls(cols) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (this.
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
if (this.statusMessage) {
|
|
666
|
-
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
667
|
-
}
|
|
668
|
-
const editHotkey = this.formatHotkey('shift+tab');
|
|
669
|
-
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
670
|
-
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
671
|
-
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
672
|
-
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
673
|
-
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
674
|
-
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
675
|
-
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
676
|
-
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
677
|
-
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
678
|
-
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
679
|
-
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
783
|
+
const maxWidth = cols - 2;
|
|
784
|
+
// Use schema-defined colors for consistency
|
|
785
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
786
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
787
|
+
const toggles = [];
|
|
788
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
789
|
+
if (this.editMode === 'display-edits') {
|
|
790
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
680
791
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (this.
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
715
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
716
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
717
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
718
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
719
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
720
|
-
}
|
|
721
|
-
formatHotkey(hotkey) {
|
|
722
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
723
|
-
if (!normalized)
|
|
724
|
-
return hotkey;
|
|
725
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
726
|
-
const map = {
|
|
727
|
-
shift: '⇧',
|
|
728
|
-
sh: '⇧',
|
|
729
|
-
alt: '⌥',
|
|
730
|
-
option: '⌥',
|
|
731
|
-
opt: '⌥',
|
|
732
|
-
ctrl: '⌃',
|
|
733
|
-
control: '⌃',
|
|
734
|
-
cmd: '⌘',
|
|
735
|
-
meta: '⌘',
|
|
736
|
-
};
|
|
737
|
-
const formatted = parts
|
|
738
|
-
.map((part) => {
|
|
739
|
-
const symbol = map[part];
|
|
740
|
-
if (symbol)
|
|
741
|
-
return symbol;
|
|
742
|
-
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
743
|
-
})
|
|
744
|
-
.join('');
|
|
745
|
-
return formatted || hotkey;
|
|
746
|
-
}
|
|
747
|
-
computeContextRemaining() {
|
|
748
|
-
if (this.contextUsage === null) {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
752
|
-
}
|
|
753
|
-
computeTokensRemaining() {
|
|
754
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
758
|
-
return this.formatTokenCount(remaining);
|
|
759
|
-
}
|
|
760
|
-
formatElapsedLabel(seconds) {
|
|
761
|
-
if (seconds < 60) {
|
|
762
|
-
return `${seconds}s`;
|
|
763
|
-
}
|
|
764
|
-
const mins = Math.floor(seconds / 60);
|
|
765
|
-
const secs = seconds % 60;
|
|
766
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
767
|
-
}
|
|
768
|
-
formatTokenCount(value) {
|
|
769
|
-
if (!Number.isFinite(value)) {
|
|
770
|
-
return `${value}`;
|
|
771
|
-
}
|
|
772
|
-
if (value >= 1_000_000) {
|
|
773
|
-
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
774
|
-
}
|
|
775
|
-
if (value >= 1_000) {
|
|
776
|
-
return `${(value / 1_000).toFixed(1)}k`;
|
|
777
|
-
}
|
|
778
|
-
return `${Math.round(value)}`;
|
|
779
|
-
}
|
|
780
|
-
visibleLength(value) {
|
|
781
|
-
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
782
|
-
return value.replace(ansiPattern, '').length;
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Debug-only snapshot used by tests to assert rendered strings without
|
|
786
|
-
* needing a TTY. Not used by production code.
|
|
787
|
-
*/
|
|
788
|
-
getDebugUiSnapshot(width) {
|
|
789
|
-
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
790
|
-
return {
|
|
791
|
-
meta: this.buildMetaLines(cols - 2),
|
|
792
|
-
controls: this.buildModeControls(cols),
|
|
793
|
-
};
|
|
792
|
+
else {
|
|
793
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
794
|
+
}
|
|
795
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
796
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
797
|
+
// Verification (green when on) - per schema.verificationMode
|
|
798
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
799
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
800
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
801
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
802
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
803
|
+
let rightPart = '';
|
|
804
|
+
if (this.contextUsage !== null) {
|
|
805
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
806
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
807
|
+
if (rem < 10)
|
|
808
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
809
|
+
else if (rem < 25)
|
|
810
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
811
|
+
else
|
|
812
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
813
|
+
}
|
|
814
|
+
// Calculate visible lengths (strip ANSI)
|
|
815
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
816
|
+
const leftLen = strip(leftPart).length;
|
|
817
|
+
const rightLen = strip(rightPart).length;
|
|
818
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
819
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
820
|
+
}
|
|
821
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
822
|
+
return `${leftPart} ${rightPart}`;
|
|
823
|
+
}
|
|
824
|
+
return leftPart;
|
|
794
825
|
}
|
|
795
826
|
/**
|
|
796
827
|
* Force a re-render
|
|
@@ -813,19 +844,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
813
844
|
handleResize() {
|
|
814
845
|
this.lastRenderContent = '';
|
|
815
846
|
this.lastRenderCursor = -1;
|
|
816
|
-
this.resetStreamingRenderThrottle();
|
|
817
847
|
// Re-clamp pinned header rows to the new terminal height
|
|
818
848
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
819
|
-
if (this.scrollRegionActive) {
|
|
820
|
-
this.disableScrollRegion();
|
|
821
|
-
this.enableScrollRegion();
|
|
822
|
-
}
|
|
823
849
|
this.scheduleRender();
|
|
824
850
|
}
|
|
825
851
|
/**
|
|
826
852
|
* Register with display's output interceptor to position cursor correctly.
|
|
827
853
|
* When scroll region is active, output needs to go to the scroll region,
|
|
828
854
|
* not the protected bottom area where the input is rendered.
|
|
855
|
+
*
|
|
856
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
857
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
829
858
|
*/
|
|
830
859
|
registerOutputInterceptor(display) {
|
|
831
860
|
if (this.outputInterceptorCleanup) {
|
|
@@ -833,66 +862,25 @@ export class TerminalInput extends EventEmitter {
|
|
|
833
862
|
}
|
|
834
863
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
835
864
|
beforeWrite: () => {
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
if (this.scrollRegionActive) {
|
|
839
|
-
const { rows } = this.getSize();
|
|
840
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
841
|
-
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
842
|
-
this.write(ESC.SAVE);
|
|
843
|
-
this.write(ESC.TO(targetRow, 1));
|
|
844
|
-
}
|
|
865
|
+
// Scroll region handles content containment automatically
|
|
866
|
+
// No per-write cursor manipulation needed
|
|
845
867
|
},
|
|
846
|
-
afterWrite: (
|
|
847
|
-
//
|
|
848
|
-
if (this.scrollRegionActive) {
|
|
849
|
-
const { rows } = this.getSize();
|
|
850
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
851
|
-
// Count newlines in content to advance by correct amount
|
|
852
|
-
const lineCount = content ? (content.match(/\n/g) || []).length + 1 : 1;
|
|
853
|
-
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
854
|
-
this.write(ESC.RESTORE);
|
|
855
|
-
}
|
|
868
|
+
afterWrite: () => {
|
|
869
|
+
// No cursor manipulation needed
|
|
856
870
|
},
|
|
857
871
|
});
|
|
858
872
|
}
|
|
859
|
-
/**
|
|
860
|
-
* Write content directly into the scroll region (for banner, user prompts, etc.).
|
|
861
|
-
* Content starts at top and flows down, then scrolls when bottom is reached.
|
|
862
|
-
*/
|
|
863
|
-
writeToScrollRegion(content) {
|
|
864
|
-
if (!content)
|
|
865
|
-
return;
|
|
866
|
-
// Ensure scroll region is active
|
|
867
|
-
if (!this.scrollRegionActive) {
|
|
868
|
-
this.enableScrollRegion();
|
|
869
|
-
}
|
|
870
|
-
const { rows } = this.getSize();
|
|
871
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
872
|
-
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
873
|
-
// Write at current content position
|
|
874
|
-
this.write(ESC.SAVE);
|
|
875
|
-
this.write(ESC.TO(targetRow, 1));
|
|
876
|
-
this.write(content);
|
|
877
|
-
this.write(ESC.RESTORE);
|
|
878
|
-
// Advance contentRow by number of lines written
|
|
879
|
-
const lineCount = (content.match(/\n/g) || []).length + 1;
|
|
880
|
-
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Reset content position to start of scroll region.
|
|
884
|
-
* Does NOT clear the terminal - content starts from current position.
|
|
885
|
-
*/
|
|
886
|
-
resetContentPosition() {
|
|
887
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
888
|
-
this.contentRow = scrollTop;
|
|
889
|
-
}
|
|
890
873
|
/**
|
|
891
874
|
* Dispose and clean up
|
|
892
875
|
*/
|
|
893
876
|
dispose() {
|
|
894
877
|
if (this.disposed)
|
|
895
878
|
return;
|
|
879
|
+
// Clean up streaming render timer
|
|
880
|
+
if (this.streamingRenderTimer) {
|
|
881
|
+
clearInterval(this.streamingRenderTimer);
|
|
882
|
+
this.streamingRenderTimer = null;
|
|
883
|
+
}
|
|
896
884
|
// Clean up output interceptor
|
|
897
885
|
if (this.outputInterceptorCleanup) {
|
|
898
886
|
this.outputInterceptorCleanup();
|
|
@@ -900,8 +888,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
900
888
|
}
|
|
901
889
|
this.disposed = true;
|
|
902
890
|
this.enabled = false;
|
|
903
|
-
|
|
904
|
-
this.
|
|
891
|
+
// Reset scroll region if it was set
|
|
892
|
+
this.write(ESC.RESET_SCROLL);
|
|
905
893
|
this.disableBracketedPaste();
|
|
906
894
|
this.buffer = '';
|
|
907
895
|
this.queue = [];
|
|
@@ -1006,7 +994,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1006
994
|
this.toggleEditMode();
|
|
1007
995
|
return true;
|
|
1008
996
|
}
|
|
1009
|
-
|
|
997
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
998
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
999
|
+
this.togglePasteExpansion();
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
this.toggleThinking();
|
|
1003
|
+
}
|
|
1004
|
+
return true;
|
|
1005
|
+
case 'escape':
|
|
1006
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1007
|
+
if (this.mode === 'streaming') {
|
|
1008
|
+
this.emit('interrupt');
|
|
1009
|
+
}
|
|
1010
|
+
else if (this.buffer.length > 0) {
|
|
1011
|
+
this.clear();
|
|
1012
|
+
}
|
|
1010
1013
|
return true;
|
|
1011
1014
|
}
|
|
1012
1015
|
return false;
|
|
@@ -1024,6 +1027,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1024
1027
|
this.insertPlainText(chunk, insertPos);
|
|
1025
1028
|
this.cursor = insertPos + chunk.length;
|
|
1026
1029
|
this.emit('change', this.buffer);
|
|
1030
|
+
this.updateSuggestions();
|
|
1027
1031
|
this.scheduleRender();
|
|
1028
1032
|
}
|
|
1029
1033
|
insertNewline() {
|
|
@@ -1048,6 +1052,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1048
1052
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1049
1053
|
}
|
|
1050
1054
|
this.emit('change', this.buffer);
|
|
1055
|
+
this.updateSuggestions();
|
|
1051
1056
|
this.scheduleRender();
|
|
1052
1057
|
}
|
|
1053
1058
|
deleteForward() {
|
|
@@ -1275,12 +1280,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1275
1280
|
timestamp: Date.now(),
|
|
1276
1281
|
});
|
|
1277
1282
|
this.emit('queue', text);
|
|
1278
|
-
this.clear(); // Clear immediately for queued input
|
|
1283
|
+
this.clear(); // Clear immediately for queued input, re-render to update queue display
|
|
1279
1284
|
}
|
|
1280
1285
|
else {
|
|
1281
|
-
// In idle mode, clear the input
|
|
1282
|
-
// The
|
|
1283
|
-
|
|
1286
|
+
// In idle mode, clear the input WITHOUT rendering.
|
|
1287
|
+
// The caller will display the user message and start streaming.
|
|
1288
|
+
// We'll render the input area again after streaming ends.
|
|
1289
|
+
this.clear(true); // Skip render - streaming will handle display
|
|
1284
1290
|
this.emit('submit', text);
|
|
1285
1291
|
}
|
|
1286
1292
|
}
|
|
@@ -1297,9 +1303,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1297
1303
|
if (available <= 0)
|
|
1298
1304
|
return;
|
|
1299
1305
|
const chunk = clean.slice(0, available);
|
|
1300
|
-
|
|
1301
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1302
|
-
if (isMultiline && !isShortMultiline) {
|
|
1306
|
+
if (isMultilinePaste(chunk)) {
|
|
1303
1307
|
this.insertPastePlaceholder(chunk);
|
|
1304
1308
|
}
|
|
1305
1309
|
else {
|
|
@@ -1312,41 +1316,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1312
1316
|
this.scheduleRender();
|
|
1313
1317
|
}
|
|
1314
1318
|
// ===========================================================================
|
|
1315
|
-
// SCROLL REGION
|
|
1316
|
-
// ===========================================================================
|
|
1317
|
-
enableScrollRegion() {
|
|
1318
|
-
if (this.scrollRegionActive || !this.isTTY())
|
|
1319
|
-
return;
|
|
1320
|
-
this.applyScrollRegion();
|
|
1321
|
-
this.scrollRegionActive = true;
|
|
1322
|
-
this.forceRender();
|
|
1323
|
-
}
|
|
1324
|
-
disableScrollRegion() {
|
|
1325
|
-
if (!this.scrollRegionActive)
|
|
1326
|
-
return;
|
|
1327
|
-
this.write(ESC.RESET_SCROLL);
|
|
1328
|
-
this.scrollRegionActive = false;
|
|
1329
|
-
}
|
|
1330
|
-
applyScrollRegion() {
|
|
1331
|
-
const { rows } = this.getSize();
|
|
1332
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
1333
|
-
const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
|
|
1334
|
-
this.write(ESC.SET_SCROLL(scrollTop, scrollBottom));
|
|
1335
|
-
}
|
|
1336
|
-
updateReservedLines(contentLines) {
|
|
1337
|
-
const { rows } = this.getSize();
|
|
1338
|
-
const baseLines = 2; // separator + control bar
|
|
1339
|
-
const needed = baseLines + contentLines;
|
|
1340
|
-
const maxAllowed = Math.max(baseLines, rows - 1 - this.pinnedTopRows);
|
|
1341
|
-
const newReserved = Math.min(Math.max(baseLines, needed), maxAllowed);
|
|
1342
|
-
if (newReserved !== this.reservedLines) {
|
|
1343
|
-
this.reservedLines = newReserved;
|
|
1344
|
-
if (this.scrollRegionActive) {
|
|
1345
|
-
this.applyScrollRegion();
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
// ===========================================================================
|
|
1350
1319
|
// BUFFER WRAPPING
|
|
1351
1320
|
// ===========================================================================
|
|
1352
1321
|
wrapBuffer(maxWidth) {
|
|
@@ -1470,19 +1439,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1470
1439
|
this.shiftPlaceholders(position, text.length);
|
|
1471
1440
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1472
1441
|
}
|
|
1473
|
-
shouldInlineMultiline(content) {
|
|
1474
|
-
const lines = content.split('\n').length;
|
|
1475
|
-
const maxInlineLines = 4;
|
|
1476
|
-
const maxInlineChars = 240;
|
|
1477
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1478
|
-
}
|
|
1479
1442
|
findPlaceholderAt(position) {
|
|
1480
1443
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1481
1444
|
}
|
|
1482
|
-
buildPlaceholder(
|
|
1445
|
+
buildPlaceholder(summary) {
|
|
1483
1446
|
const id = ++this.pasteCounter;
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1447
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1448
|
+
// Show first line preview (truncated)
|
|
1449
|
+
const preview = summary.preview.length > 30
|
|
1450
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1451
|
+
: summary.preview;
|
|
1452
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1486
1453
|
return { id, placeholder };
|
|
1487
1454
|
}
|
|
1488
1455
|
insertPastePlaceholder(content) {
|
|
@@ -1490,21 +1457,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1490
1457
|
if (available <= 0)
|
|
1491
1458
|
return;
|
|
1492
1459
|
const cleanContent = content.slice(0, available);
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1460
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1461
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1462
|
+
if (summary.lineCount < 5) {
|
|
1463
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1464
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1465
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1466
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1495
1470
|
const insertPos = this.cursor;
|
|
1496
1471
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1497
1472
|
this.pastePlaceholders.push({
|
|
1498
1473
|
id,
|
|
1499
1474
|
content: cleanContent,
|
|
1500
|
-
lineCount,
|
|
1475
|
+
lineCount: summary.lineCount,
|
|
1501
1476
|
placeholder,
|
|
1502
1477
|
start: insertPos,
|
|
1503
1478
|
end: insertPos + placeholder.length,
|
|
1479
|
+
summary,
|
|
1480
|
+
expanded: false,
|
|
1504
1481
|
});
|
|
1505
1482
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1506
1483
|
this.cursor = insertPos + placeholder.length;
|
|
1507
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1487
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1488
|
+
*/
|
|
1489
|
+
togglePasteExpansion() {
|
|
1490
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1491
|
+
if (!placeholder)
|
|
1492
|
+
return false;
|
|
1493
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1494
|
+
// Update the placeholder text in buffer
|
|
1495
|
+
const newPlaceholder = placeholder.expanded
|
|
1496
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1497
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1498
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1499
|
+
// Update buffer
|
|
1500
|
+
this.buffer =
|
|
1501
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1502
|
+
newPlaceholder +
|
|
1503
|
+
this.buffer.slice(placeholder.end);
|
|
1504
|
+
// Update placeholder tracking
|
|
1505
|
+
placeholder.placeholder = newPlaceholder;
|
|
1506
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1507
|
+
// Shift other placeholders
|
|
1508
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1509
|
+
this.scheduleRender();
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
buildExpandedPlaceholder(ph) {
|
|
1513
|
+
const lines = ph.content.split('\n');
|
|
1514
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1515
|
+
const lastLines = lines.length > 5
|
|
1516
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1517
|
+
: '';
|
|
1518
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1519
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1520
|
+
}
|
|
1508
1521
|
deletePlaceholder(placeholder) {
|
|
1509
1522
|
const length = placeholder.end - placeholder.start;
|
|
1510
1523
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1512,11 +1525,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1512
1525
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1513
1526
|
this.cursor = placeholder.start;
|
|
1514
1527
|
}
|
|
1515
|
-
updateContextUsage(value
|
|
1516
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1517
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1518
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1519
|
-
}
|
|
1528
|
+
updateContextUsage(value) {
|
|
1520
1529
|
if (value === null || !Number.isFinite(value)) {
|
|
1521
1530
|
this.contextUsage = null;
|
|
1522
1531
|
}
|
|
@@ -1543,22 +1552,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1543
1552
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1544
1553
|
this.setEditMode(next);
|
|
1545
1554
|
}
|
|
1546
|
-
scheduleStreamingRender(delayMs) {
|
|
1547
|
-
if (this.streamingRenderTimer)
|
|
1548
|
-
return;
|
|
1549
|
-
const wait = Math.max(16, delayMs);
|
|
1550
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1551
|
-
this.streamingRenderTimer = null;
|
|
1552
|
-
this.render();
|
|
1553
|
-
}, wait);
|
|
1554
|
-
}
|
|
1555
|
-
resetStreamingRenderThrottle() {
|
|
1556
|
-
if (this.streamingRenderTimer) {
|
|
1557
|
-
clearTimeout(this.streamingRenderTimer);
|
|
1558
|
-
this.streamingRenderTimer = null;
|
|
1559
|
-
}
|
|
1560
|
-
this.lastStreamingRender = 0;
|
|
1561
|
-
}
|
|
1562
1555
|
scheduleRender() {
|
|
1563
1556
|
if (!this.canRender())
|
|
1564
1557
|
return;
|