erosolar-cli 1.7.267 → 1.7.269
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/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 -11
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +157 -190
- 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 +1 -36
- 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 +158 -77
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +803 -488
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +25 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +36 -14
- 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 +0 -19
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +33 -131
- 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,16 @@
|
|
|
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 { renderDivider
|
|
16
|
-
import {
|
|
17
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
14
|
+
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
18
16
|
// ANSI escape codes
|
|
19
17
|
const ESC = {
|
|
20
18
|
// Cursor control
|
|
@@ -69,11 +67,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
67
|
statusMessage = null;
|
|
70
68
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
69
|
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
70
|
reservedLines = 2;
|
|
78
71
|
scrollRegionActive = false;
|
|
79
72
|
lastRenderContent = '';
|
|
@@ -81,38 +74,47 @@ export class TerminalInput extends EventEmitter {
|
|
|
81
74
|
renderDirty = false;
|
|
82
75
|
isRendering = false;
|
|
83
76
|
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;
|
|
84
90
|
// Lifecycle
|
|
85
91
|
disposed = false;
|
|
86
92
|
enabled = true;
|
|
87
93
|
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;
|
|
93
94
|
editMode = 'display-edits';
|
|
94
95
|
verificationEnabled = true;
|
|
95
96
|
autoContinueEnabled = false;
|
|
96
97
|
verificationHotkey = 'alt+v';
|
|
97
98
|
autoContinueHotkey = 'alt+c';
|
|
98
|
-
thinkingHotkey = '/thinking';
|
|
99
|
-
modelLabel = null;
|
|
100
|
-
providerLabel = null;
|
|
101
99
|
// Output interceptor cleanup
|
|
102
100
|
outputInterceptorCleanup;
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
// Metrics tracking for status bar
|
|
102
|
+
streamingStartTime = null;
|
|
103
|
+
tokensUsed = 0;
|
|
104
|
+
thinkingEnabled = true;
|
|
105
|
+
modelInfo = null; // Provider · Model info
|
|
106
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
106
107
|
streamingRenderTimer = null;
|
|
107
108
|
constructor(writeStream = process.stdout, config = {}) {
|
|
108
109
|
super();
|
|
109
110
|
this.out = writeStream;
|
|
111
|
+
// Use schema defaults for configuration consistency
|
|
110
112
|
this.config = {
|
|
111
|
-
maxLines: config.maxLines ??
|
|
112
|
-
maxLength: config.maxLength ??
|
|
113
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
114
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
113
115
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
114
|
-
promptChar: config.promptChar ??
|
|
115
|
-
continuationChar: config.continuationChar ??
|
|
116
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
117
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
// ===========================================================================
|
|
@@ -191,6 +193,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
191
193
|
if (handled)
|
|
192
194
|
return;
|
|
193
195
|
}
|
|
196
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
197
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
198
|
+
this.emit('showHelp');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
194
201
|
// Insert printable characters
|
|
195
202
|
if (str && !key?.ctrl && !key?.meta) {
|
|
196
203
|
this.insertText(str);
|
|
@@ -199,38 +206,462 @@ export class TerminalInput extends EventEmitter {
|
|
|
199
206
|
/**
|
|
200
207
|
* Set the input mode
|
|
201
208
|
*
|
|
202
|
-
* Streaming
|
|
203
|
-
*
|
|
209
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
210
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
211
|
+
* the cursor is (below the streamed content).
|
|
204
212
|
*/
|
|
205
213
|
setMode(mode) {
|
|
206
214
|
const prevMode = this.mode;
|
|
207
215
|
this.mode = mode;
|
|
208
216
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
209
|
-
//
|
|
210
|
-
this.
|
|
217
|
+
// Track streaming start time for elapsed display
|
|
218
|
+
this.streamingStartTime = Date.now();
|
|
219
|
+
// Set up scroll region to reserve bottom for persistent input area
|
|
220
|
+
this.pinnedTopRows = 0;
|
|
221
|
+
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
222
|
+
// Enable scroll region: content scrolls above, bottom is reserved
|
|
211
223
|
this.enableScrollRegion();
|
|
224
|
+
// Initial render of bottom input area
|
|
225
|
+
this.renderBottomInputArea();
|
|
226
|
+
// Start timer to update bottom input area (updates elapsed time)
|
|
227
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
228
|
+
if (this.mode === 'streaming') {
|
|
229
|
+
this.updateStreamingStatus();
|
|
230
|
+
this.renderBottomInputArea();
|
|
231
|
+
}
|
|
232
|
+
}, 1000);
|
|
212
233
|
this.renderDirty = true;
|
|
213
|
-
this.render();
|
|
214
234
|
}
|
|
215
235
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
216
|
-
//
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
// Stop streaming render timer
|
|
237
|
+
if (this.streamingRenderTimer) {
|
|
238
|
+
clearInterval(this.streamingRenderTimer);
|
|
239
|
+
this.streamingRenderTimer = null;
|
|
240
|
+
}
|
|
241
|
+
// Reset streaming time
|
|
242
|
+
this.streamingStartTime = null;
|
|
243
|
+
this.pinnedTopRows = 0;
|
|
244
|
+
// Ensure no scroll region is active
|
|
245
|
+
this.disableScrollRegion();
|
|
246
|
+
// Reset flow mode tracking
|
|
247
|
+
this.flowModeRenderedLines = 0;
|
|
248
|
+
// Render input area using unified method (same as streaming, but normal mode)
|
|
249
|
+
writeLock.withLock(() => {
|
|
250
|
+
this.renderPinnedInputArea();
|
|
251
|
+
}, 'terminalInput.streamingEnd');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Update streaming status label (called by timer)
|
|
256
|
+
*/
|
|
257
|
+
updateStreamingStatus() {
|
|
258
|
+
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
259
|
+
return;
|
|
260
|
+
// Calculate elapsed time
|
|
261
|
+
const elapsed = Date.now() - this.streamingStartTime;
|
|
262
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
263
|
+
const minutes = Math.floor(seconds / 60);
|
|
264
|
+
const secs = seconds % 60;
|
|
265
|
+
// Format elapsed time
|
|
266
|
+
let elapsedStr;
|
|
267
|
+
if (minutes > 0) {
|
|
268
|
+
elapsedStr = `${minutes}m ${secs}s`;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
elapsedStr = `${secs}s`;
|
|
272
|
+
}
|
|
273
|
+
// Update streaming label
|
|
274
|
+
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Render input area - unified for streaming and normal modes.
|
|
278
|
+
*
|
|
279
|
+
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
280
|
+
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
281
|
+
*/
|
|
282
|
+
renderPinnedInputArea() {
|
|
283
|
+
const { rows, cols } = this.getSize();
|
|
284
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
285
|
+
const divider = renderDivider(cols - 2);
|
|
286
|
+
const isStreaming = this.mode === 'streaming';
|
|
287
|
+
// Wrap buffer into display lines (multi-line support)
|
|
288
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
289
|
+
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
290
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
291
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
292
|
+
// Calculate display window (keep cursor visible)
|
|
293
|
+
let startLine = 0;
|
|
294
|
+
if (lines.length > displayLines) {
|
|
295
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
296
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
297
|
+
}
|
|
298
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
299
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
300
|
+
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
301
|
+
const hasModelInfo = !!this.modelInfo;
|
|
302
|
+
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
303
|
+
// Save cursor position during streaming (so content flow resumes correctly)
|
|
304
|
+
if (isStreaming) {
|
|
305
|
+
this.write(ESC.SAVE);
|
|
306
|
+
}
|
|
307
|
+
this.write(ESC.HIDE);
|
|
308
|
+
this.write(ESC.RESET);
|
|
309
|
+
// Calculate start row based on mode:
|
|
310
|
+
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
311
|
+
// - Normal: right after content (contentEndRow + 1)
|
|
312
|
+
let currentRow;
|
|
313
|
+
if (isStreaming) {
|
|
314
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// In normal mode, render right after content
|
|
318
|
+
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
319
|
+
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
320
|
+
currentRow = Math.max(1, contentRow + 1);
|
|
321
|
+
}
|
|
322
|
+
let finalRow = currentRow;
|
|
323
|
+
let finalCol = 3;
|
|
324
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
325
|
+
this.write(ESC.TO(currentRow, 1));
|
|
326
|
+
this.write(ESC.CLEAR_TO_END);
|
|
327
|
+
// Status bar
|
|
328
|
+
this.write(ESC.TO(currentRow, 1));
|
|
329
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
330
|
+
currentRow++;
|
|
331
|
+
// Model info line (if set) - displayed below status, above input
|
|
332
|
+
if (hasModelInfo) {
|
|
333
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
334
|
+
this.write(ESC.TO(currentRow, 1));
|
|
335
|
+
// Build model info with context usage
|
|
336
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
337
|
+
if (this.contextUsage !== null) {
|
|
338
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
339
|
+
if (rem < 10)
|
|
340
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
341
|
+
else if (rem < 25)
|
|
342
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
343
|
+
else
|
|
344
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
345
|
+
}
|
|
346
|
+
this.write(modelLine);
|
|
347
|
+
currentRow++;
|
|
348
|
+
}
|
|
349
|
+
// Top divider
|
|
350
|
+
this.write(ESC.TO(currentRow, 1));
|
|
351
|
+
this.write(divider);
|
|
352
|
+
currentRow++;
|
|
353
|
+
// Input lines with background styling
|
|
354
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
355
|
+
this.write(ESC.TO(currentRow, 1));
|
|
356
|
+
const line = visibleLines[i] ?? '';
|
|
357
|
+
const absoluteLineIdx = startLine + i;
|
|
358
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
359
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
360
|
+
// Background
|
|
361
|
+
this.write(ESC.BG_DARK);
|
|
362
|
+
// Prompt prefix
|
|
363
|
+
this.write(ESC.DIM);
|
|
364
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
365
|
+
this.write(ESC.RESET);
|
|
366
|
+
this.write(ESC.BG_DARK);
|
|
367
|
+
if (isCursorLine) {
|
|
368
|
+
const col = Math.min(cursorCol, line.length);
|
|
369
|
+
const before = line.slice(0, col);
|
|
370
|
+
const at = col < line.length ? line[col] : ' ';
|
|
371
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
372
|
+
this.write(before);
|
|
373
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
374
|
+
this.write(at);
|
|
375
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
376
|
+
this.write(after);
|
|
377
|
+
finalRow = currentRow;
|
|
378
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
this.write(line);
|
|
382
|
+
}
|
|
383
|
+
// Pad to edge
|
|
384
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
385
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
386
|
+
if (padding > 0)
|
|
387
|
+
this.write(' '.repeat(padding));
|
|
388
|
+
this.write(ESC.RESET);
|
|
389
|
+
currentRow++;
|
|
390
|
+
}
|
|
391
|
+
// Bottom divider
|
|
392
|
+
this.write(ESC.TO(currentRow, 1));
|
|
393
|
+
this.write(divider);
|
|
394
|
+
currentRow++;
|
|
395
|
+
// Mode controls line
|
|
396
|
+
this.write(ESC.TO(currentRow, 1));
|
|
397
|
+
this.write(this.buildModeControls(cols));
|
|
398
|
+
// Restore cursor position during streaming, or show cursor in normal mode
|
|
399
|
+
if (isStreaming) {
|
|
400
|
+
this.write(ESC.RESTORE);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
// Position cursor in input area
|
|
404
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
405
|
+
this.write(ESC.SHOW);
|
|
406
|
+
}
|
|
407
|
+
// Update reserved lines for scroll region calculations
|
|
408
|
+
this.updateReservedLines(totalHeight);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Render input area during streaming (alias for unified method)
|
|
412
|
+
*/
|
|
413
|
+
renderStreamingInputArea() {
|
|
414
|
+
this.renderPinnedInputArea();
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Render bottom input area in the reserved scroll region space.
|
|
418
|
+
* Uses cursor save/restore to update bottom without affecting content flow.
|
|
419
|
+
*/
|
|
420
|
+
renderBottomInputArea() {
|
|
421
|
+
const { rows, cols } = this.getSize();
|
|
422
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
423
|
+
const divider = renderDivider(cols - 2);
|
|
424
|
+
const { dim: DIM, reset: R, green: GREEN } = UI_COLORS;
|
|
425
|
+
// Wrap buffer into display lines
|
|
426
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
427
|
+
const displayLines = Math.min(lines.length, 1); // Just show first line during streaming
|
|
428
|
+
const visibleLines = lines.slice(0, displayLines);
|
|
429
|
+
// Calculate total height for bottom area
|
|
430
|
+
const hasModelInfo = !!this.modelInfo;
|
|
431
|
+
const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
|
|
432
|
+
const startRow = Math.max(1, rows - totalHeight + 1);
|
|
433
|
+
// Save cursor, hide it
|
|
434
|
+
this.write(ESC.SAVE);
|
|
435
|
+
this.write(ESC.HIDE);
|
|
436
|
+
let currentRow = startRow;
|
|
437
|
+
// Clear the bottom reserved area
|
|
438
|
+
for (let r = startRow; r <= rows; r++) {
|
|
439
|
+
this.write(ESC.TO(r, 1));
|
|
440
|
+
this.write(ESC.CLEAR_LINE);
|
|
441
|
+
}
|
|
442
|
+
// Status bar (streaming timer)
|
|
443
|
+
this.write(ESC.TO(currentRow, 1));
|
|
444
|
+
this.write(this.buildStreamingStatusBar(cols));
|
|
445
|
+
currentRow++;
|
|
446
|
+
// Model info line (if set)
|
|
447
|
+
if (hasModelInfo) {
|
|
448
|
+
this.write(ESC.TO(currentRow, 1));
|
|
449
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
450
|
+
if (this.contextUsage !== null) {
|
|
451
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
452
|
+
if (rem < 10)
|
|
453
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
454
|
+
else if (rem < 25)
|
|
455
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
456
|
+
else
|
|
457
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
458
|
+
}
|
|
459
|
+
this.write(modelLine);
|
|
460
|
+
currentRow++;
|
|
461
|
+
}
|
|
462
|
+
// Top divider
|
|
463
|
+
this.write(ESC.TO(currentRow, 1));
|
|
464
|
+
this.write(divider);
|
|
465
|
+
currentRow++;
|
|
466
|
+
// Input lines with background styling
|
|
467
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
468
|
+
this.write(ESC.TO(currentRow, 1));
|
|
469
|
+
const line = visibleLines[i] ?? '';
|
|
470
|
+
const isFirstLine = i === 0;
|
|
471
|
+
this.write(ESC.BG_DARK);
|
|
472
|
+
this.write(ESC.DIM);
|
|
473
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
474
|
+
this.write(ESC.RESET);
|
|
475
|
+
this.write(ESC.BG_DARK);
|
|
476
|
+
this.write(line);
|
|
477
|
+
// Pad to edge
|
|
478
|
+
const lineLen = this.config.promptChar.length + line.length;
|
|
479
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
480
|
+
if (padding > 0)
|
|
481
|
+
this.write(' '.repeat(padding));
|
|
482
|
+
this.write(ESC.RESET);
|
|
483
|
+
currentRow++;
|
|
484
|
+
}
|
|
485
|
+
// Bottom divider
|
|
486
|
+
this.write(ESC.TO(currentRow, 1));
|
|
487
|
+
this.write(divider);
|
|
488
|
+
currentRow++;
|
|
489
|
+
// Mode controls
|
|
490
|
+
this.write(ESC.TO(currentRow, 1));
|
|
491
|
+
this.write(this.buildModeControls(cols));
|
|
492
|
+
// Restore cursor position (back to content area)
|
|
493
|
+
this.write(ESC.RESTORE);
|
|
494
|
+
this.write(ESC.SHOW);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Enable or disable flow mode.
|
|
498
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
499
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
500
|
+
*/
|
|
501
|
+
setFlowMode(enabled) {
|
|
502
|
+
if (this.flowMode === enabled)
|
|
503
|
+
return;
|
|
504
|
+
this.flowMode = enabled;
|
|
505
|
+
this.renderDirty = true;
|
|
506
|
+
this.scheduleRender();
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Check if flow mode is enabled.
|
|
510
|
+
*/
|
|
511
|
+
isFlowMode() {
|
|
512
|
+
return this.flowMode;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Set the row where content ends (for idle mode positioning).
|
|
516
|
+
* Input area will render starting from this row + 1.
|
|
517
|
+
*/
|
|
518
|
+
setContentEndRow(row) {
|
|
519
|
+
this.contentEndRow = Math.max(0, row);
|
|
520
|
+
this.renderDirty = true;
|
|
521
|
+
this.scheduleRender();
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Set available slash commands for auto-complete suggestions.
|
|
525
|
+
*/
|
|
526
|
+
setCommands(commands) {
|
|
527
|
+
this.commandSuggestions = commands;
|
|
528
|
+
this.updateSuggestions();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Update filtered suggestions based on current input.
|
|
532
|
+
*/
|
|
533
|
+
updateSuggestions() {
|
|
534
|
+
const input = this.buffer.trim();
|
|
535
|
+
// Only show suggestions when input starts with "/"
|
|
536
|
+
if (!input.startsWith('/')) {
|
|
537
|
+
this.showSuggestions = false;
|
|
538
|
+
this.filteredSuggestions = [];
|
|
539
|
+
this.selectedSuggestionIndex = 0;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const query = input.toLowerCase();
|
|
543
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
544
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
545
|
+
// Show suggestions if we have matches
|
|
546
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
547
|
+
// Keep selection in bounds
|
|
548
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
549
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
220
550
|
}
|
|
221
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Select next suggestion (arrow down / tab).
|
|
554
|
+
*/
|
|
555
|
+
selectNextSuggestion() {
|
|
556
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
557
|
+
return;
|
|
558
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
559
|
+
this.renderDirty = true;
|
|
560
|
+
this.scheduleRender();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
564
|
+
*/
|
|
565
|
+
selectPrevSuggestion() {
|
|
566
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
567
|
+
return;
|
|
568
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
569
|
+
? this.filteredSuggestions.length - 1
|
|
570
|
+
: this.selectedSuggestionIndex - 1;
|
|
571
|
+
this.renderDirty = true;
|
|
572
|
+
this.scheduleRender();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Accept current suggestion and insert into buffer.
|
|
576
|
+
*/
|
|
577
|
+
acceptSuggestion() {
|
|
578
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
579
|
+
return false;
|
|
580
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
581
|
+
if (!selected)
|
|
582
|
+
return false;
|
|
583
|
+
// Replace buffer with selected command
|
|
584
|
+
this.buffer = selected.command + ' ';
|
|
585
|
+
this.cursor = this.buffer.length;
|
|
586
|
+
this.showSuggestions = false;
|
|
587
|
+
this.renderDirty = true;
|
|
588
|
+
this.scheduleRender();
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Check if suggestions are visible.
|
|
593
|
+
*/
|
|
594
|
+
areSuggestionsVisible() {
|
|
595
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Update token count for metrics display
|
|
599
|
+
*/
|
|
600
|
+
setTokensUsed(tokens) {
|
|
601
|
+
this.tokensUsed = tokens;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Toggle thinking/reasoning mode
|
|
605
|
+
*/
|
|
606
|
+
toggleThinking() {
|
|
607
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
608
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
609
|
+
this.scheduleRender();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get thinking enabled state
|
|
613
|
+
*/
|
|
614
|
+
isThinkingEnabled() {
|
|
615
|
+
return this.thinkingEnabled;
|
|
616
|
+
}
|
|
222
617
|
/**
|
|
223
618
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
224
619
|
*/
|
|
225
620
|
setPinnedHeaderLines(count) {
|
|
226
|
-
//
|
|
227
|
-
if (this.pinnedTopRows !==
|
|
228
|
-
this.pinnedTopRows =
|
|
621
|
+
// Set pinned header rows (banner area that scroll region excludes)
|
|
622
|
+
if (this.pinnedTopRows !== count) {
|
|
623
|
+
this.pinnedTopRows = count;
|
|
229
624
|
if (this.scrollRegionActive) {
|
|
230
625
|
this.applyScrollRegion();
|
|
231
626
|
}
|
|
232
627
|
}
|
|
233
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
631
|
+
* restore the default bottom-aligned layout.
|
|
632
|
+
*/
|
|
633
|
+
setInlineAnchor(row) {
|
|
634
|
+
if (row === null || row === undefined) {
|
|
635
|
+
this.inlineAnchorRow = null;
|
|
636
|
+
this.inlineLayout = false;
|
|
637
|
+
this.renderDirty = true;
|
|
638
|
+
this.render();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const { rows } = this.getSize();
|
|
642
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
643
|
+
this.inlineAnchorRow = clamped;
|
|
644
|
+
this.inlineLayout = true;
|
|
645
|
+
this.renderDirty = true;
|
|
646
|
+
this.render();
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
650
|
+
* output by re-evaluating the anchor before each render.
|
|
651
|
+
*/
|
|
652
|
+
setInlineAnchorProvider(provider) {
|
|
653
|
+
this.anchorProvider = provider;
|
|
654
|
+
if (!provider) {
|
|
655
|
+
this.inlineLayout = false;
|
|
656
|
+
this.inlineAnchorRow = null;
|
|
657
|
+
this.renderDirty = true;
|
|
658
|
+
this.render();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.inlineLayout = true;
|
|
662
|
+
this.renderDirty = true;
|
|
663
|
+
this.render();
|
|
664
|
+
}
|
|
234
665
|
/**
|
|
235
666
|
* Get current mode
|
|
236
667
|
*/
|
|
@@ -340,37 +771,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
340
771
|
this.streamingLabel = next;
|
|
341
772
|
this.scheduleRender();
|
|
342
773
|
}
|
|
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
|
-
}
|
|
374
774
|
/**
|
|
375
775
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
376
776
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -380,22 +780,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
380
780
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
381
781
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
382
782
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
383
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
384
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
385
783
|
if (this.verificationEnabled === nextVerification &&
|
|
386
784
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
387
785
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
388
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
389
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
390
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
786
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
391
787
|
return;
|
|
392
788
|
}
|
|
393
789
|
this.verificationEnabled = nextVerification;
|
|
394
790
|
this.autoContinueEnabled = nextAutoContinue;
|
|
395
791
|
this.verificationHotkey = nextVerifyHotkey;
|
|
396
792
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
397
|
-
this.
|
|
398
|
-
|
|
793
|
+
this.scheduleRender();
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
797
|
+
* This is displayed persistently above the input area.
|
|
798
|
+
*/
|
|
799
|
+
setModelInfo(info) {
|
|
800
|
+
if (this.modelInfo === info)
|
|
801
|
+
return;
|
|
802
|
+
this.modelInfo = info;
|
|
399
803
|
this.scheduleRender();
|
|
400
804
|
}
|
|
401
805
|
/**
|
|
@@ -407,400 +811,302 @@ export class TerminalInput extends EventEmitter {
|
|
|
407
811
|
this.streamingLabel = null;
|
|
408
812
|
this.scheduleRender();
|
|
409
813
|
}
|
|
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
|
-
}
|
|
423
814
|
/**
|
|
424
815
|
* Render the input area - Claude Code style with mode controls
|
|
425
816
|
*
|
|
426
|
-
* During streaming
|
|
427
|
-
*
|
|
428
|
-
* naturally above while elapsed time and status stay fresh.
|
|
817
|
+
* During streaming: Uses renderBottomInputArea() with scroll regions
|
|
818
|
+
* After streaming: Renders the full input area inline
|
|
429
819
|
*/
|
|
430
820
|
render() {
|
|
431
821
|
if (!this.canRender())
|
|
432
822
|
return;
|
|
433
823
|
if (this.isRendering)
|
|
434
824
|
return;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
}
|
|
825
|
+
// During streaming, use the bottom input area renderer (with scroll regions)
|
|
826
|
+
if (this.mode === 'streaming') {
|
|
827
|
+
this.renderBottomInputArea();
|
|
828
|
+
return;
|
|
446
829
|
}
|
|
447
830
|
const shouldSkip = !this.renderDirty &&
|
|
448
831
|
this.buffer === this.lastRenderContent &&
|
|
449
832
|
this.cursor === this.lastRenderCursor;
|
|
450
833
|
this.renderDirty = false;
|
|
451
|
-
// Skip if nothing changed
|
|
834
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
452
835
|
if (shouldSkip) {
|
|
453
836
|
return;
|
|
454
837
|
}
|
|
455
|
-
// If write lock is held, defer render
|
|
838
|
+
// If write lock is held, defer render
|
|
456
839
|
if (writeLock.isLocked()) {
|
|
457
840
|
writeLock.safeWrite(() => this.render());
|
|
458
841
|
return;
|
|
459
842
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
843
|
+
this.isRendering = true;
|
|
844
|
+
writeLock.lock('terminalInput.render');
|
|
845
|
+
try {
|
|
846
|
+
// Render input area at bottom (outside scroll region)
|
|
847
|
+
this.renderBottomPinned();
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
writeLock.unlock();
|
|
851
|
+
this.isRendering = false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
856
|
+
*
|
|
857
|
+
* Flow mode attempted inline rendering but caused duplicate renders
|
|
858
|
+
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
859
|
+
*/
|
|
860
|
+
renderFlowMode() {
|
|
861
|
+
// Use stable bottom-pinned approach
|
|
862
|
+
this.renderBottomPinned();
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
866
|
+
*
|
|
867
|
+
* Works for both normal and streaming modes:
|
|
868
|
+
* - During streaming: saves/restores cursor position
|
|
869
|
+
* - Status bar shows streaming info or "Type a message"
|
|
870
|
+
*
|
|
871
|
+
* Layout when suggestions visible:
|
|
872
|
+
* - Top divider
|
|
873
|
+
* - Input line(s)
|
|
874
|
+
* - Bottom divider
|
|
875
|
+
* - Suggestions (command list)
|
|
876
|
+
*
|
|
877
|
+
* Layout when suggestions hidden:
|
|
878
|
+
* - Status bar (Ready/Streaming)
|
|
879
|
+
* - Top divider
|
|
880
|
+
* - Input line(s)
|
|
881
|
+
* - Bottom divider
|
|
882
|
+
* - Mode controls
|
|
883
|
+
*/
|
|
884
|
+
renderBottomPinned() {
|
|
885
|
+
const { rows, cols } = this.getSize();
|
|
886
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
887
|
+
const isStreaming = this.mode === 'streaming';
|
|
888
|
+
// Use unified pinned input area (works for both streaming and normal)
|
|
889
|
+
// Only use complex rendering when suggestions are visible
|
|
890
|
+
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
891
|
+
if (!hasSuggestions) {
|
|
892
|
+
this.renderPinnedInputArea();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
// Wrap buffer into display lines
|
|
896
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
897
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
898
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
899
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
900
|
+
// Calculate display window (keep cursor visible)
|
|
901
|
+
let startLine = 0;
|
|
902
|
+
if (lines.length > displayLines) {
|
|
903
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
904
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
905
|
+
}
|
|
906
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
907
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
908
|
+
// Calculate suggestion display (not during streaming)
|
|
909
|
+
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
910
|
+
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
911
|
+
: [];
|
|
912
|
+
const suggestionLines = suggestionsToShow.length;
|
|
913
|
+
this.write(ESC.HIDE);
|
|
914
|
+
this.write(ESC.RESET);
|
|
915
|
+
const divider = renderDivider(cols - 2);
|
|
916
|
+
// Calculate positions from absolute bottom
|
|
917
|
+
let currentRow;
|
|
918
|
+
if (suggestionLines > 0) {
|
|
919
|
+
// With suggestions: input area + dividers + suggestions
|
|
920
|
+
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
921
|
+
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
922
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
923
|
+
this.updateReservedLines(totalHeight);
|
|
924
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
925
|
+
this.write(ESC.TO(currentRow, 1));
|
|
926
|
+
this.write(ESC.CLEAR_TO_END);
|
|
927
|
+
// Top divider
|
|
497
928
|
this.write(ESC.TO(currentRow, 1));
|
|
498
|
-
this.write(ESC.CLEAR_LINE);
|
|
499
|
-
const divider = renderDivider(cols - 2);
|
|
500
929
|
this.write(divider);
|
|
501
|
-
currentRow
|
|
502
|
-
//
|
|
930
|
+
currentRow++;
|
|
931
|
+
// Input lines
|
|
503
932
|
let finalRow = currentRow;
|
|
504
933
|
let finalCol = 3;
|
|
505
934
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
506
|
-
|
|
507
|
-
this.write(ESC.TO(rowNum, 1));
|
|
508
|
-
this.write(ESC.CLEAR_LINE);
|
|
935
|
+
this.write(ESC.TO(currentRow, 1));
|
|
509
936
|
const line = visibleLines[i] ?? '';
|
|
510
937
|
const absoluteLineIdx = startLine + i;
|
|
511
938
|
const isFirstLine = absoluteLineIdx === 0;
|
|
512
939
|
const isCursorLine = i === adjustedCursorLine;
|
|
513
|
-
// Background
|
|
514
|
-
this.write(ESC.BG_DARK);
|
|
515
|
-
// Prompt prefix
|
|
516
|
-
this.write(ESC.DIM);
|
|
517
940
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
518
|
-
this.write(ESC.RESET);
|
|
519
|
-
this.write(ESC.BG_DARK);
|
|
520
941
|
if (isCursorLine) {
|
|
521
|
-
// Render with block cursor
|
|
522
942
|
const col = Math.min(cursorCol, line.length);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
this.write(
|
|
527
|
-
this.write(
|
|
528
|
-
|
|
529
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
530
|
-
this.write(after);
|
|
531
|
-
finalRow = rowNum;
|
|
943
|
+
this.write(line.slice(0, col));
|
|
944
|
+
this.write(ESC.REVERSE);
|
|
945
|
+
this.write(col < line.length ? line[col] : ' ');
|
|
946
|
+
this.write(ESC.RESET);
|
|
947
|
+
this.write(line.slice(col + 1));
|
|
948
|
+
finalRow = currentRow;
|
|
532
949
|
finalCol = this.config.promptChar.length + col + 1;
|
|
533
950
|
}
|
|
534
951
|
else {
|
|
535
952
|
this.write(line);
|
|
536
953
|
}
|
|
537
|
-
|
|
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);
|
|
543
|
-
}
|
|
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);
|
|
954
|
+
currentRow++;
|
|
555
955
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
956
|
+
// Bottom divider
|
|
957
|
+
this.write(ESC.TO(currentRow, 1));
|
|
958
|
+
this.write(divider);
|
|
959
|
+
currentRow++;
|
|
960
|
+
// Suggestions (Claude Code style)
|
|
961
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
962
|
+
this.write(ESC.TO(currentRow, 1));
|
|
963
|
+
const suggestion = suggestionsToShow[i];
|
|
964
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
965
|
+
// Indent and highlight selected
|
|
966
|
+
this.write(' ');
|
|
967
|
+
if (isSelected) {
|
|
968
|
+
this.write(ESC.REVERSE);
|
|
969
|
+
this.write(ESC.BOLD);
|
|
970
|
+
}
|
|
971
|
+
this.write(suggestion.command);
|
|
972
|
+
if (isSelected) {
|
|
973
|
+
this.write(ESC.RESET);
|
|
974
|
+
}
|
|
975
|
+
// Description (dimmed)
|
|
976
|
+
const descSpace = cols - suggestion.command.length - 8;
|
|
977
|
+
if (descSpace > 10 && suggestion.description) {
|
|
978
|
+
const desc = suggestion.description.slice(0, descSpace);
|
|
979
|
+
this.write(ESC.RESET);
|
|
980
|
+
this.write(ESC.DIM);
|
|
981
|
+
this.write(' ');
|
|
982
|
+
this.write(desc);
|
|
983
|
+
this.write(ESC.RESET);
|
|
984
|
+
}
|
|
985
|
+
currentRow++;
|
|
568
986
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
writeLock.lock('terminalInput.render');
|
|
572
|
-
this.isRendering = true;
|
|
573
|
-
try {
|
|
574
|
-
performRender();
|
|
575
|
-
}
|
|
576
|
-
finally {
|
|
577
|
-
writeLock.unlock();
|
|
578
|
-
this.isRendering = false;
|
|
987
|
+
// Position cursor in input area
|
|
988
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
579
989
|
}
|
|
990
|
+
this.write(ESC.SHOW);
|
|
991
|
+
// Update state
|
|
992
|
+
this.lastRenderContent = this.buffer;
|
|
993
|
+
this.lastRenderCursor = this.cursor;
|
|
580
994
|
}
|
|
581
995
|
/**
|
|
582
|
-
* Build
|
|
583
|
-
* During streaming, shows model line pinned above streaming info.
|
|
996
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
584
997
|
*/
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (this.
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
//
|
|
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
|
-
}
|
|
998
|
+
buildStreamingStatusBar(cols) {
|
|
999
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
1000
|
+
// Streaming status with elapsed time
|
|
1001
|
+
let elapsed = '0s';
|
|
1002
|
+
if (this.streamingStartTime) {
|
|
1003
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1004
|
+
const mins = Math.floor(secs / 60);
|
|
1005
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
1006
|
+
}
|
|
1007
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
1008
|
+
// Queue indicator
|
|
642
1009
|
if (this.queue.length > 0) {
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
if (usageParts.length) {
|
|
646
|
-
lines.push(renderStatusLine(usageParts, width));
|
|
1010
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
647
1011
|
}
|
|
648
|
-
|
|
1012
|
+
// Hint for typing
|
|
1013
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
1014
|
+
return status;
|
|
649
1015
|
}
|
|
650
1016
|
/**
|
|
651
|
-
*
|
|
1017
|
+
* Build status bar showing streaming/ready status and key info.
|
|
1018
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
652
1019
|
*/
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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' });
|
|
671
|
-
}
|
|
672
|
-
if (this.overrideStatusMessage) {
|
|
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' });
|
|
1020
|
+
buildStatusBar(cols) {
|
|
1021
|
+
const maxWidth = cols - 2;
|
|
1022
|
+
const parts = [];
|
|
1023
|
+
// Streaming status with elapsed time (left side)
|
|
1024
|
+
if (this.mode === 'streaming') {
|
|
1025
|
+
let statusText = '● Streaming';
|
|
1026
|
+
if (this.streamingStartTime) {
|
|
1027
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1028
|
+
const mins = Math.floor(elapsed / 60);
|
|
1029
|
+
const secs = elapsed % 60;
|
|
1030
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
1031
|
+
}
|
|
1032
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
690
1033
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1034
|
+
// Queue indicator during streaming
|
|
1035
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
1036
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
694
1037
|
}
|
|
1038
|
+
// Paste indicator
|
|
695
1039
|
if (this.pastePlaceholders.length > 0) {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
699
|
-
tone: 'info',
|
|
700
|
-
});
|
|
1040
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
1041
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
701
1042
|
}
|
|
702
|
-
|
|
703
|
-
if (this.
|
|
704
|
-
|
|
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}`;
|
|
1043
|
+
// Override/warning status
|
|
1044
|
+
if (this.overrideStatusMessage) {
|
|
1045
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
781
1046
|
}
|
|
782
|
-
|
|
783
|
-
|
|
1047
|
+
// If idle with empty buffer, show quick shortcuts
|
|
1048
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
1049
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
784
1050
|
}
|
|
785
|
-
|
|
786
|
-
|
|
1051
|
+
// Multi-line indicator
|
|
1052
|
+
if (this.buffer.includes('\n')) {
|
|
1053
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
787
1054
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
return
|
|
1055
|
+
if (parts.length === 0) {
|
|
1056
|
+
return ''; // Empty status bar when idle
|
|
1057
|
+
}
|
|
1058
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
1059
|
+
return joined.slice(0, maxWidth);
|
|
793
1060
|
}
|
|
794
1061
|
/**
|
|
795
|
-
*
|
|
796
|
-
*
|
|
1062
|
+
* Build mode controls line showing toggles and context info.
|
|
1063
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
1064
|
+
*
|
|
1065
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
797
1066
|
*/
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1067
|
+
buildModeControls(cols) {
|
|
1068
|
+
const maxWidth = cols - 2;
|
|
1069
|
+
// Use schema-defined colors for consistency
|
|
1070
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
1071
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
1072
|
+
const toggles = [];
|
|
1073
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
1074
|
+
if (this.editMode === 'display-edits') {
|
|
1075
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
1079
|
+
}
|
|
1080
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
1081
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
1082
|
+
// Verification (green when on) - per schema.verificationMode
|
|
1083
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
1084
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
1085
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
1086
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
1087
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
1088
|
+
let rightPart = '';
|
|
1089
|
+
if (this.contextUsage !== null) {
|
|
1090
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
1091
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
1092
|
+
if (rem < 10)
|
|
1093
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1094
|
+
else if (rem < 25)
|
|
1095
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1096
|
+
else
|
|
1097
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1098
|
+
}
|
|
1099
|
+
// Calculate visible lengths (strip ANSI)
|
|
1100
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1101
|
+
const leftLen = strip(leftPart).length;
|
|
1102
|
+
const rightLen = strip(rightPart).length;
|
|
1103
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
1104
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1105
|
+
}
|
|
1106
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1107
|
+
return `${leftPart} ${rightPart}`;
|
|
1108
|
+
}
|
|
1109
|
+
return leftPart;
|
|
804
1110
|
}
|
|
805
1111
|
/**
|
|
806
1112
|
* Force a re-render
|
|
@@ -823,19 +1129,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
823
1129
|
handleResize() {
|
|
824
1130
|
this.lastRenderContent = '';
|
|
825
1131
|
this.lastRenderCursor = -1;
|
|
826
|
-
this.resetStreamingRenderThrottle();
|
|
827
1132
|
// Re-clamp pinned header rows to the new terminal height
|
|
828
1133
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
829
|
-
if (this.scrollRegionActive) {
|
|
830
|
-
this.disableScrollRegion();
|
|
831
|
-
this.enableScrollRegion();
|
|
832
|
-
}
|
|
833
1134
|
this.scheduleRender();
|
|
834
1135
|
}
|
|
835
1136
|
/**
|
|
836
1137
|
* Register with display's output interceptor to position cursor correctly.
|
|
837
1138
|
* When scroll region is active, output needs to go to the scroll region,
|
|
838
1139
|
* not the protected bottom area where the input is rendered.
|
|
1140
|
+
*
|
|
1141
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
1142
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
839
1143
|
*/
|
|
840
1144
|
registerOutputInterceptor(display) {
|
|
841
1145
|
if (this.outputInterceptorCleanup) {
|
|
@@ -843,51 +1147,25 @@ export class TerminalInput extends EventEmitter {
|
|
|
843
1147
|
}
|
|
844
1148
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
845
1149
|
beforeWrite: () => {
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
if (this.scrollRegionActive) {
|
|
849
|
-
this.write(ESC.TO(this.contentCursorRow, this.contentCursorCol));
|
|
850
|
-
}
|
|
1150
|
+
// Scroll region handles content containment automatically
|
|
1151
|
+
// No per-write cursor manipulation needed
|
|
851
1152
|
},
|
|
852
1153
|
afterWrite: () => {
|
|
853
|
-
//
|
|
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
|
-
}
|
|
1154
|
+
// No cursor manipulation needed
|
|
866
1155
|
},
|
|
867
1156
|
});
|
|
868
1157
|
}
|
|
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
|
-
}
|
|
885
1158
|
/**
|
|
886
1159
|
* Dispose and clean up
|
|
887
1160
|
*/
|
|
888
1161
|
dispose() {
|
|
889
1162
|
if (this.disposed)
|
|
890
1163
|
return;
|
|
1164
|
+
// Clean up streaming render timer
|
|
1165
|
+
if (this.streamingRenderTimer) {
|
|
1166
|
+
clearInterval(this.streamingRenderTimer);
|
|
1167
|
+
this.streamingRenderTimer = null;
|
|
1168
|
+
}
|
|
891
1169
|
// Clean up output interceptor
|
|
892
1170
|
if (this.outputInterceptorCleanup) {
|
|
893
1171
|
this.outputInterceptorCleanup();
|
|
@@ -895,7 +1173,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
895
1173
|
}
|
|
896
1174
|
this.disposed = true;
|
|
897
1175
|
this.enabled = false;
|
|
898
|
-
this.resetStreamingRenderThrottle();
|
|
899
1176
|
this.disableScrollRegion();
|
|
900
1177
|
this.disableBracketedPaste();
|
|
901
1178
|
this.buffer = '';
|
|
@@ -1001,7 +1278,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1001
1278
|
this.toggleEditMode();
|
|
1002
1279
|
return true;
|
|
1003
1280
|
}
|
|
1004
|
-
|
|
1281
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1282
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1283
|
+
this.togglePasteExpansion();
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
this.toggleThinking();
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
case 'escape':
|
|
1290
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1291
|
+
if (this.mode === 'streaming') {
|
|
1292
|
+
this.emit('interrupt');
|
|
1293
|
+
}
|
|
1294
|
+
else if (this.buffer.length > 0) {
|
|
1295
|
+
this.clear();
|
|
1296
|
+
}
|
|
1005
1297
|
return true;
|
|
1006
1298
|
}
|
|
1007
1299
|
return false;
|
|
@@ -1019,6 +1311,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1019
1311
|
this.insertPlainText(chunk, insertPos);
|
|
1020
1312
|
this.cursor = insertPos + chunk.length;
|
|
1021
1313
|
this.emit('change', this.buffer);
|
|
1314
|
+
this.updateSuggestions();
|
|
1022
1315
|
this.scheduleRender();
|
|
1023
1316
|
}
|
|
1024
1317
|
insertNewline() {
|
|
@@ -1043,6 +1336,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1043
1336
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1044
1337
|
}
|
|
1045
1338
|
this.emit('change', this.buffer);
|
|
1339
|
+
this.updateSuggestions();
|
|
1046
1340
|
this.scheduleRender();
|
|
1047
1341
|
}
|
|
1048
1342
|
deleteForward() {
|
|
@@ -1292,9 +1586,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1292
1586
|
if (available <= 0)
|
|
1293
1587
|
return;
|
|
1294
1588
|
const chunk = clean.slice(0, available);
|
|
1295
|
-
|
|
1296
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1297
|
-
if (isMultiline && !isShortMultiline) {
|
|
1589
|
+
if (isMultilinePaste(chunk)) {
|
|
1298
1590
|
this.insertPastePlaceholder(chunk);
|
|
1299
1591
|
}
|
|
1300
1592
|
else {
|
|
@@ -1314,7 +1606,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1314
1606
|
return;
|
|
1315
1607
|
this.applyScrollRegion();
|
|
1316
1608
|
this.scrollRegionActive = true;
|
|
1317
|
-
this.forceRender();
|
|
1318
1609
|
}
|
|
1319
1610
|
disableScrollRegion() {
|
|
1320
1611
|
if (!this.scrollRegionActive)
|
|
@@ -1465,19 +1756,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1465
1756
|
this.shiftPlaceholders(position, text.length);
|
|
1466
1757
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1467
1758
|
}
|
|
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
|
-
}
|
|
1474
1759
|
findPlaceholderAt(position) {
|
|
1475
1760
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1476
1761
|
}
|
|
1477
|
-
buildPlaceholder(
|
|
1762
|
+
buildPlaceholder(summary) {
|
|
1478
1763
|
const id = ++this.pasteCounter;
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1764
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1765
|
+
// Show first line preview (truncated)
|
|
1766
|
+
const preview = summary.preview.length > 30
|
|
1767
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1768
|
+
: summary.preview;
|
|
1769
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1481
1770
|
return { id, placeholder };
|
|
1482
1771
|
}
|
|
1483
1772
|
insertPastePlaceholder(content) {
|
|
@@ -1485,21 +1774,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1485
1774
|
if (available <= 0)
|
|
1486
1775
|
return;
|
|
1487
1776
|
const cleanContent = content.slice(0, available);
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1777
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1778
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1779
|
+
if (summary.lineCount < 5) {
|
|
1780
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1781
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1782
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1783
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1490
1787
|
const insertPos = this.cursor;
|
|
1491
1788
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1492
1789
|
this.pastePlaceholders.push({
|
|
1493
1790
|
id,
|
|
1494
1791
|
content: cleanContent,
|
|
1495
|
-
lineCount,
|
|
1792
|
+
lineCount: summary.lineCount,
|
|
1496
1793
|
placeholder,
|
|
1497
1794
|
start: insertPos,
|
|
1498
1795
|
end: insertPos + placeholder.length,
|
|
1796
|
+
summary,
|
|
1797
|
+
expanded: false,
|
|
1499
1798
|
});
|
|
1500
1799
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1501
1800
|
this.cursor = insertPos + placeholder.length;
|
|
1502
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1804
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1805
|
+
*/
|
|
1806
|
+
togglePasteExpansion() {
|
|
1807
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1808
|
+
if (!placeholder)
|
|
1809
|
+
return false;
|
|
1810
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1811
|
+
// Update the placeholder text in buffer
|
|
1812
|
+
const newPlaceholder = placeholder.expanded
|
|
1813
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1814
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1815
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1816
|
+
// Update buffer
|
|
1817
|
+
this.buffer =
|
|
1818
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1819
|
+
newPlaceholder +
|
|
1820
|
+
this.buffer.slice(placeholder.end);
|
|
1821
|
+
// Update placeholder tracking
|
|
1822
|
+
placeholder.placeholder = newPlaceholder;
|
|
1823
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1824
|
+
// Shift other placeholders
|
|
1825
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1826
|
+
this.scheduleRender();
|
|
1827
|
+
return true;
|
|
1828
|
+
}
|
|
1829
|
+
buildExpandedPlaceholder(ph) {
|
|
1830
|
+
const lines = ph.content.split('\n');
|
|
1831
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1832
|
+
const lastLines = lines.length > 5
|
|
1833
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1834
|
+
: '';
|
|
1835
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1836
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1837
|
+
}
|
|
1503
1838
|
deletePlaceholder(placeholder) {
|
|
1504
1839
|
const length = placeholder.end - placeholder.start;
|
|
1505
1840
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1507,11 +1842,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1507
1842
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1508
1843
|
this.cursor = placeholder.start;
|
|
1509
1844
|
}
|
|
1510
|
-
updateContextUsage(value
|
|
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
|
-
}
|
|
1845
|
+
updateContextUsage(value) {
|
|
1515
1846
|
if (value === null || !Number.isFinite(value)) {
|
|
1516
1847
|
this.contextUsage = null;
|
|
1517
1848
|
}
|
|
@@ -1538,22 +1869,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1538
1869
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1539
1870
|
this.setEditMode(next);
|
|
1540
1871
|
}
|
|
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
|
-
}
|
|
1557
1872
|
scheduleRender() {
|
|
1558
1873
|
if (!this.canRender())
|
|
1559
1874
|
return;
|