erosolar-cli 1.7.270 → 1.7.271
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 -11
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +157 -195
- 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 +168 -76
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +831 -477
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +25 -28
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +36 -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 +0 -23
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +33 -137
- 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,37 +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 next content row in scroll region (banner flows up as content pushes from below)
|
|
90
|
-
nextContentRow = 1;
|
|
91
|
-
thinkingModeLabel = null;
|
|
92
94
|
editMode = 'display-edits';
|
|
93
95
|
verificationEnabled = true;
|
|
94
96
|
autoContinueEnabled = false;
|
|
95
97
|
verificationHotkey = 'alt+v';
|
|
96
98
|
autoContinueHotkey = 'alt+c';
|
|
97
|
-
thinkingHotkey = '/thinking';
|
|
98
|
-
modelLabel = null;
|
|
99
|
-
providerLabel = null;
|
|
100
99
|
// Output interceptor cleanup
|
|
101
100
|
outputInterceptorCleanup;
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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)
|
|
105
107
|
streamingRenderTimer = null;
|
|
106
108
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
109
|
super();
|
|
108
110
|
this.out = writeStream;
|
|
111
|
+
// Use schema defaults for configuration consistency
|
|
109
112
|
this.config = {
|
|
110
|
-
maxLines: config.maxLines ??
|
|
111
|
-
maxLength: config.maxLength ??
|
|
113
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
114
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
112
115
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
113
|
-
promptChar: config.promptChar ??
|
|
114
|
-
continuationChar: config.continuationChar ??
|
|
116
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
117
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
115
118
|
};
|
|
116
119
|
}
|
|
117
120
|
// ===========================================================================
|
|
@@ -190,6 +193,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
190
193
|
if (handled)
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
196
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
197
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
198
|
+
this.emit('showHelp');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
193
201
|
// Insert printable characters
|
|
194
202
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
203
|
this.insertText(str);
|
|
@@ -198,38 +206,492 @@ export class TerminalInput extends EventEmitter {
|
|
|
198
206
|
/**
|
|
199
207
|
* Set the input mode
|
|
200
208
|
*
|
|
201
|
-
* Streaming
|
|
202
|
-
*
|
|
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).
|
|
203
212
|
*/
|
|
204
213
|
setMode(mode) {
|
|
205
214
|
const prevMode = this.mode;
|
|
206
215
|
this.mode = mode;
|
|
207
216
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
208
|
-
//
|
|
209
|
-
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
|
|
210
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);
|
|
211
233
|
this.renderDirty = true;
|
|
212
|
-
this.render();
|
|
213
234
|
}
|
|
214
235
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
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
|
+
// Keep scroll region active for consistent bottom-pinned UI
|
|
244
|
+
// (scroll region reserves bottom for input area in all modes)
|
|
245
|
+
// Reset flow mode tracking
|
|
246
|
+
this.flowModeRenderedLines = 0;
|
|
247
|
+
// Render using unified bottom input area (same layout as streaming)
|
|
248
|
+
writeLock.withLock(() => {
|
|
249
|
+
this.renderBottomInputArea();
|
|
250
|
+
}, 'terminalInput.streamingEnd');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Update streaming status label (called by timer)
|
|
255
|
+
*/
|
|
256
|
+
updateStreamingStatus() {
|
|
257
|
+
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
258
|
+
return;
|
|
259
|
+
// Calculate elapsed time
|
|
260
|
+
const elapsed = Date.now() - this.streamingStartTime;
|
|
261
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
262
|
+
const minutes = Math.floor(seconds / 60);
|
|
263
|
+
const secs = seconds % 60;
|
|
264
|
+
// Format elapsed time
|
|
265
|
+
let elapsedStr;
|
|
266
|
+
if (minutes > 0) {
|
|
267
|
+
elapsedStr = `${minutes}m ${secs}s`;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
elapsedStr = `${secs}s`;
|
|
271
|
+
}
|
|
272
|
+
// Update streaming label
|
|
273
|
+
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Render input area - unified for streaming and normal modes.
|
|
277
|
+
*
|
|
278
|
+
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
279
|
+
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
280
|
+
*/
|
|
281
|
+
renderPinnedInputArea() {
|
|
282
|
+
const { rows, cols } = this.getSize();
|
|
283
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
284
|
+
const divider = renderDivider(cols - 2);
|
|
285
|
+
const isStreaming = this.mode === 'streaming';
|
|
286
|
+
// Wrap buffer into display lines (multi-line support)
|
|
287
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
288
|
+
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
289
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
290
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
291
|
+
// Calculate display window (keep cursor visible)
|
|
292
|
+
let startLine = 0;
|
|
293
|
+
if (lines.length > displayLines) {
|
|
294
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
295
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
296
|
+
}
|
|
297
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
298
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
299
|
+
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
300
|
+
const hasModelInfo = !!this.modelInfo;
|
|
301
|
+
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
302
|
+
// Save cursor position during streaming (so content flow resumes correctly)
|
|
303
|
+
if (isStreaming) {
|
|
304
|
+
this.write(ESC.SAVE);
|
|
305
|
+
}
|
|
306
|
+
this.write(ESC.HIDE);
|
|
307
|
+
this.write(ESC.RESET);
|
|
308
|
+
// Calculate start row based on mode:
|
|
309
|
+
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
310
|
+
// - Normal: right after content (contentEndRow + 1)
|
|
311
|
+
let currentRow;
|
|
312
|
+
if (isStreaming) {
|
|
313
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// In normal mode, render right after content
|
|
317
|
+
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
318
|
+
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
319
|
+
currentRow = Math.max(1, contentRow + 1);
|
|
320
|
+
}
|
|
321
|
+
let finalRow = currentRow;
|
|
322
|
+
let finalCol = 3;
|
|
323
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
324
|
+
this.write(ESC.TO(currentRow, 1));
|
|
325
|
+
this.write(ESC.CLEAR_TO_END);
|
|
326
|
+
// Status bar
|
|
327
|
+
this.write(ESC.TO(currentRow, 1));
|
|
328
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
329
|
+
currentRow++;
|
|
330
|
+
// Model info line (if set) - displayed below status, above input
|
|
331
|
+
if (hasModelInfo) {
|
|
332
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
333
|
+
this.write(ESC.TO(currentRow, 1));
|
|
334
|
+
// Build model info with context usage
|
|
335
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
336
|
+
if (this.contextUsage !== null) {
|
|
337
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
338
|
+
if (rem < 10)
|
|
339
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
340
|
+
else if (rem < 25)
|
|
341
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
342
|
+
else
|
|
343
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
344
|
+
}
|
|
345
|
+
this.write(modelLine);
|
|
346
|
+
currentRow++;
|
|
347
|
+
}
|
|
348
|
+
// Top divider
|
|
349
|
+
this.write(ESC.TO(currentRow, 1));
|
|
350
|
+
this.write(divider);
|
|
351
|
+
currentRow++;
|
|
352
|
+
// Input lines with background styling
|
|
353
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
354
|
+
this.write(ESC.TO(currentRow, 1));
|
|
355
|
+
const line = visibleLines[i] ?? '';
|
|
356
|
+
const absoluteLineIdx = startLine + i;
|
|
357
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
358
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
359
|
+
// Background
|
|
360
|
+
this.write(ESC.BG_DARK);
|
|
361
|
+
// Prompt prefix
|
|
362
|
+
this.write(ESC.DIM);
|
|
363
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
364
|
+
this.write(ESC.RESET);
|
|
365
|
+
this.write(ESC.BG_DARK);
|
|
366
|
+
if (isCursorLine) {
|
|
367
|
+
const col = Math.min(cursorCol, line.length);
|
|
368
|
+
const before = line.slice(0, col);
|
|
369
|
+
const at = col < line.length ? line[col] : ' ';
|
|
370
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
371
|
+
this.write(before);
|
|
372
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
373
|
+
this.write(at);
|
|
374
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
375
|
+
this.write(after);
|
|
376
|
+
finalRow = currentRow;
|
|
377
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
this.write(line);
|
|
381
|
+
}
|
|
382
|
+
// Pad to edge
|
|
383
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
384
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
385
|
+
if (padding > 0)
|
|
386
|
+
this.write(' '.repeat(padding));
|
|
387
|
+
this.write(ESC.RESET);
|
|
388
|
+
currentRow++;
|
|
389
|
+
}
|
|
390
|
+
// Bottom divider
|
|
391
|
+
this.write(ESC.TO(currentRow, 1));
|
|
392
|
+
this.write(divider);
|
|
393
|
+
currentRow++;
|
|
394
|
+
// Mode controls line
|
|
395
|
+
this.write(ESC.TO(currentRow, 1));
|
|
396
|
+
this.write(this.buildModeControls(cols));
|
|
397
|
+
// Restore cursor position during streaming, or show cursor in normal mode
|
|
398
|
+
if (isStreaming) {
|
|
399
|
+
this.write(ESC.RESTORE);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// Position cursor in input area
|
|
403
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
404
|
+
this.write(ESC.SHOW);
|
|
405
|
+
}
|
|
406
|
+
// Update reserved lines for scroll region calculations
|
|
407
|
+
this.updateReservedLines(totalHeight);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Render input area during streaming (alias for unified method)
|
|
411
|
+
*/
|
|
412
|
+
renderStreamingInputArea() {
|
|
413
|
+
this.renderPinnedInputArea();
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Render bottom input area - UNIFIED for all modes.
|
|
417
|
+
* Uses cursor save/restore to update bottom without affecting content flow.
|
|
418
|
+
*
|
|
419
|
+
* Layout (same for idle/streaming/ready):
|
|
420
|
+
* - Status bar (streaming timer or "Type a message")
|
|
421
|
+
* - Model info line (provider · model · ctx)
|
|
422
|
+
* - Divider
|
|
423
|
+
* - Input area
|
|
424
|
+
* - Divider
|
|
425
|
+
* - Mode controls
|
|
426
|
+
*/
|
|
427
|
+
renderBottomInputArea() {
|
|
428
|
+
const { rows, cols } = this.getSize();
|
|
429
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
430
|
+
const divider = renderDivider(cols - 2);
|
|
431
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
432
|
+
const isStreaming = this.mode === 'streaming';
|
|
433
|
+
// Wrap buffer into display lines
|
|
434
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
435
|
+
// Allow multi-line in non-streaming, single line during streaming
|
|
436
|
+
const maxDisplayLines = isStreaming ? 1 : 3;
|
|
437
|
+
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
438
|
+
const visibleLines = lines.slice(0, displayLines);
|
|
439
|
+
// Calculate total height for bottom area
|
|
440
|
+
const hasModelInfo = !!this.modelInfo;
|
|
441
|
+
const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
|
|
442
|
+
// Ensure scroll region is always enabled (unified behavior)
|
|
443
|
+
if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
|
|
444
|
+
this.reservedLines = totalHeight;
|
|
217
445
|
this.enableScrollRegion();
|
|
218
|
-
this.forceRender();
|
|
219
446
|
}
|
|
447
|
+
const startRow = Math.max(1, rows - totalHeight + 1);
|
|
448
|
+
// Save cursor, hide it
|
|
449
|
+
this.write(ESC.SAVE);
|
|
450
|
+
this.write(ESC.HIDE);
|
|
451
|
+
let currentRow = startRow;
|
|
452
|
+
// Clear the bottom reserved area
|
|
453
|
+
for (let r = startRow; r <= rows; r++) {
|
|
454
|
+
this.write(ESC.TO(r, 1));
|
|
455
|
+
this.write(ESC.CLEAR_LINE);
|
|
456
|
+
}
|
|
457
|
+
// Status bar - UNIFIED: same format for all modes
|
|
458
|
+
this.write(ESC.TO(currentRow, 1));
|
|
459
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
460
|
+
currentRow++;
|
|
461
|
+
// Model info line (if set)
|
|
462
|
+
if (hasModelInfo) {
|
|
463
|
+
this.write(ESC.TO(currentRow, 1));
|
|
464
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
465
|
+
if (this.contextUsage !== null) {
|
|
466
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
467
|
+
if (rem < 10)
|
|
468
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
469
|
+
else if (rem < 25)
|
|
470
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
471
|
+
else
|
|
472
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
473
|
+
}
|
|
474
|
+
this.write(modelLine);
|
|
475
|
+
currentRow++;
|
|
476
|
+
}
|
|
477
|
+
// Top divider
|
|
478
|
+
this.write(ESC.TO(currentRow, 1));
|
|
479
|
+
this.write(divider);
|
|
480
|
+
currentRow++;
|
|
481
|
+
// Input lines with background styling
|
|
482
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
483
|
+
this.write(ESC.TO(currentRow, 1));
|
|
484
|
+
const line = visibleLines[i] ?? '';
|
|
485
|
+
const isFirstLine = i === 0;
|
|
486
|
+
this.write(ESC.BG_DARK);
|
|
487
|
+
this.write(ESC.DIM);
|
|
488
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
489
|
+
this.write(ESC.RESET);
|
|
490
|
+
this.write(ESC.BG_DARK);
|
|
491
|
+
this.write(line);
|
|
492
|
+
// Pad to edge
|
|
493
|
+
const lineLen = this.config.promptChar.length + line.length;
|
|
494
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
495
|
+
if (padding > 0)
|
|
496
|
+
this.write(' '.repeat(padding));
|
|
497
|
+
this.write(ESC.RESET);
|
|
498
|
+
currentRow++;
|
|
499
|
+
}
|
|
500
|
+
// Bottom divider
|
|
501
|
+
this.write(ESC.TO(currentRow, 1));
|
|
502
|
+
this.write(divider);
|
|
503
|
+
currentRow++;
|
|
504
|
+
// Mode controls
|
|
505
|
+
this.write(ESC.TO(currentRow, 1));
|
|
506
|
+
this.write(this.buildModeControls(cols));
|
|
507
|
+
// Cursor positioning depends on mode:
|
|
508
|
+
// - Streaming: restore to content area (where streaming output continues)
|
|
509
|
+
// - Normal: position in input area for typing
|
|
510
|
+
if (isStreaming) {
|
|
511
|
+
this.write(ESC.RESTORE);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Position cursor in input area
|
|
515
|
+
// Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
|
|
516
|
+
const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
|
|
517
|
+
const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
|
|
518
|
+
const targetCol = this.config.promptChar.length + cursorCol + 1;
|
|
519
|
+
this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
|
|
520
|
+
}
|
|
521
|
+
this.write(ESC.SHOW);
|
|
522
|
+
// Track last render state
|
|
523
|
+
this.lastRenderContent = this.buffer;
|
|
524
|
+
this.lastRenderCursor = this.cursor;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Enable or disable flow mode.
|
|
528
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
529
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
530
|
+
*/
|
|
531
|
+
setFlowMode(enabled) {
|
|
532
|
+
if (this.flowMode === enabled)
|
|
533
|
+
return;
|
|
534
|
+
this.flowMode = enabled;
|
|
535
|
+
this.renderDirty = true;
|
|
536
|
+
this.scheduleRender();
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Check if flow mode is enabled.
|
|
540
|
+
*/
|
|
541
|
+
isFlowMode() {
|
|
542
|
+
return this.flowMode;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Set the row where content ends (for idle mode positioning).
|
|
546
|
+
* Input area will render starting from this row + 1.
|
|
547
|
+
*/
|
|
548
|
+
setContentEndRow(row) {
|
|
549
|
+
this.contentEndRow = Math.max(0, row);
|
|
550
|
+
this.renderDirty = true;
|
|
551
|
+
this.scheduleRender();
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Set available slash commands for auto-complete suggestions.
|
|
555
|
+
*/
|
|
556
|
+
setCommands(commands) {
|
|
557
|
+
this.commandSuggestions = commands;
|
|
558
|
+
this.updateSuggestions();
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Update filtered suggestions based on current input.
|
|
562
|
+
*/
|
|
563
|
+
updateSuggestions() {
|
|
564
|
+
const input = this.buffer.trim();
|
|
565
|
+
// Only show suggestions when input starts with "/"
|
|
566
|
+
if (!input.startsWith('/')) {
|
|
567
|
+
this.showSuggestions = false;
|
|
568
|
+
this.filteredSuggestions = [];
|
|
569
|
+
this.selectedSuggestionIndex = 0;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const query = input.toLowerCase();
|
|
573
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
574
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
575
|
+
// Show suggestions if we have matches
|
|
576
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
577
|
+
// Keep selection in bounds
|
|
578
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
579
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Select next suggestion (arrow down / tab).
|
|
584
|
+
*/
|
|
585
|
+
selectNextSuggestion() {
|
|
586
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
587
|
+
return;
|
|
588
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
589
|
+
this.renderDirty = true;
|
|
590
|
+
this.scheduleRender();
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
594
|
+
*/
|
|
595
|
+
selectPrevSuggestion() {
|
|
596
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
597
|
+
return;
|
|
598
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
599
|
+
? this.filteredSuggestions.length - 1
|
|
600
|
+
: this.selectedSuggestionIndex - 1;
|
|
601
|
+
this.renderDirty = true;
|
|
602
|
+
this.scheduleRender();
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Accept current suggestion and insert into buffer.
|
|
606
|
+
*/
|
|
607
|
+
acceptSuggestion() {
|
|
608
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
609
|
+
return false;
|
|
610
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
611
|
+
if (!selected)
|
|
612
|
+
return false;
|
|
613
|
+
// Replace buffer with selected command
|
|
614
|
+
this.buffer = selected.command + ' ';
|
|
615
|
+
this.cursor = this.buffer.length;
|
|
616
|
+
this.showSuggestions = false;
|
|
617
|
+
this.renderDirty = true;
|
|
618
|
+
this.scheduleRender();
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Check if suggestions are visible.
|
|
623
|
+
*/
|
|
624
|
+
areSuggestionsVisible() {
|
|
625
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Update token count for metrics display
|
|
629
|
+
*/
|
|
630
|
+
setTokensUsed(tokens) {
|
|
631
|
+
this.tokensUsed = tokens;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Toggle thinking/reasoning mode
|
|
635
|
+
*/
|
|
636
|
+
toggleThinking() {
|
|
637
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
638
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
639
|
+
this.scheduleRender();
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Get thinking enabled state
|
|
643
|
+
*/
|
|
644
|
+
isThinkingEnabled() {
|
|
645
|
+
return this.thinkingEnabled;
|
|
220
646
|
}
|
|
221
647
|
/**
|
|
222
648
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
223
649
|
*/
|
|
224
650
|
setPinnedHeaderLines(count) {
|
|
225
|
-
//
|
|
226
|
-
if (this.pinnedTopRows !==
|
|
227
|
-
this.pinnedTopRows =
|
|
651
|
+
// Set pinned header rows (banner area that scroll region excludes)
|
|
652
|
+
if (this.pinnedTopRows !== count) {
|
|
653
|
+
this.pinnedTopRows = count;
|
|
228
654
|
if (this.scrollRegionActive) {
|
|
229
655
|
this.applyScrollRegion();
|
|
230
656
|
}
|
|
231
657
|
}
|
|
232
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
661
|
+
* restore the default bottom-aligned layout.
|
|
662
|
+
*/
|
|
663
|
+
setInlineAnchor(row) {
|
|
664
|
+
if (row === null || row === undefined) {
|
|
665
|
+
this.inlineAnchorRow = null;
|
|
666
|
+
this.inlineLayout = false;
|
|
667
|
+
this.renderDirty = true;
|
|
668
|
+
this.render();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const { rows } = this.getSize();
|
|
672
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
673
|
+
this.inlineAnchorRow = clamped;
|
|
674
|
+
this.inlineLayout = true;
|
|
675
|
+
this.renderDirty = true;
|
|
676
|
+
this.render();
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
680
|
+
* output by re-evaluating the anchor before each render.
|
|
681
|
+
*/
|
|
682
|
+
setInlineAnchorProvider(provider) {
|
|
683
|
+
this.anchorProvider = provider;
|
|
684
|
+
if (!provider) {
|
|
685
|
+
this.inlineLayout = false;
|
|
686
|
+
this.inlineAnchorRow = null;
|
|
687
|
+
this.renderDirty = true;
|
|
688
|
+
this.render();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
this.inlineLayout = true;
|
|
692
|
+
this.renderDirty = true;
|
|
693
|
+
this.render();
|
|
694
|
+
}
|
|
233
695
|
/**
|
|
234
696
|
* Get current mode
|
|
235
697
|
*/
|
|
@@ -339,37 +801,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
339
801
|
this.streamingLabel = next;
|
|
340
802
|
this.scheduleRender();
|
|
341
803
|
}
|
|
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
804
|
/**
|
|
374
805
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
375
806
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -379,22 +810,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
379
810
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
380
811
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
381
812
|
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
813
|
if (this.verificationEnabled === nextVerification &&
|
|
385
814
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
386
815
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
387
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
388
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
389
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
816
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
390
817
|
return;
|
|
391
818
|
}
|
|
392
819
|
this.verificationEnabled = nextVerification;
|
|
393
820
|
this.autoContinueEnabled = nextAutoContinue;
|
|
394
821
|
this.verificationHotkey = nextVerifyHotkey;
|
|
395
822
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
396
|
-
this.
|
|
397
|
-
|
|
823
|
+
this.scheduleRender();
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
827
|
+
* This is displayed persistently above the input area.
|
|
828
|
+
*/
|
|
829
|
+
setModelInfo(info) {
|
|
830
|
+
if (this.modelInfo === info)
|
|
831
|
+
return;
|
|
832
|
+
this.modelInfo = info;
|
|
398
833
|
this.scheduleRender();
|
|
399
834
|
}
|
|
400
835
|
/**
|
|
@@ -407,390 +842,298 @@ export class TerminalInput extends EventEmitter {
|
|
|
407
842
|
this.scheduleRender();
|
|
408
843
|
}
|
|
409
844
|
/**
|
|
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
|
|
845
|
+
* Render the input area - UNIFIED for all modes
|
|
424
846
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
847
|
+
* Uses the same bottom-pinned layout with scroll regions for:
|
|
848
|
+
* - Idle mode: Shows "Type a message" hint
|
|
849
|
+
* - Streaming mode: Shows "● Streaming Xs" timer
|
|
850
|
+
* - Ready mode: Shows status info
|
|
428
851
|
*/
|
|
429
852
|
render() {
|
|
430
853
|
if (!this.canRender())
|
|
431
854
|
return;
|
|
432
855
|
if (this.isRendering)
|
|
433
856
|
return;
|
|
434
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
435
|
-
// During streaming we still render the pinned input/status region, but throttle
|
|
436
|
-
// to avoid fighting with the streamed content flow.
|
|
437
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
438
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
439
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
440
|
-
if (waitMs > 0) {
|
|
441
|
-
this.renderDirty = true;
|
|
442
|
-
this.scheduleStreamingRender(waitMs);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
857
|
const shouldSkip = !this.renderDirty &&
|
|
447
858
|
this.buffer === this.lastRenderContent &&
|
|
448
859
|
this.cursor === this.lastRenderCursor;
|
|
449
860
|
this.renderDirty = false;
|
|
450
|
-
// Skip if nothing changed
|
|
861
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
451
862
|
if (shouldSkip) {
|
|
452
863
|
return;
|
|
453
864
|
}
|
|
454
|
-
// If write lock is held, defer render
|
|
865
|
+
// If write lock is held, defer render
|
|
455
866
|
if (writeLock.isLocked()) {
|
|
456
867
|
writeLock.safeWrite(() => this.render());
|
|
457
868
|
return;
|
|
458
869
|
}
|
|
459
|
-
|
|
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
|
-
|
|
870
|
+
this.isRendering = true;
|
|
871
|
+
writeLock.lock('terminalInput.render');
|
|
872
|
+
try {
|
|
873
|
+
// UNIFIED: Use the same bottom input area for all modes
|
|
874
|
+
this.renderBottomInputArea();
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
writeLock.unlock();
|
|
878
|
+
this.isRendering = false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
883
|
+
*
|
|
884
|
+
* Flow mode attempted inline rendering but caused duplicate renders
|
|
885
|
+
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
886
|
+
*/
|
|
887
|
+
renderFlowMode() {
|
|
888
|
+
// Use stable bottom-pinned approach
|
|
889
|
+
this.renderBottomPinned();
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
893
|
+
*
|
|
894
|
+
* Works for both normal and streaming modes:
|
|
895
|
+
* - During streaming: saves/restores cursor position
|
|
896
|
+
* - Status bar shows streaming info or "Type a message"
|
|
897
|
+
*
|
|
898
|
+
* Layout when suggestions visible:
|
|
899
|
+
* - Top divider
|
|
900
|
+
* - Input line(s)
|
|
901
|
+
* - Bottom divider
|
|
902
|
+
* - Suggestions (command list)
|
|
903
|
+
*
|
|
904
|
+
* Layout when suggestions hidden:
|
|
905
|
+
* - Status bar (Ready/Streaming)
|
|
906
|
+
* - Top divider
|
|
907
|
+
* - Input line(s)
|
|
908
|
+
* - Bottom divider
|
|
909
|
+
* - Mode controls
|
|
910
|
+
*/
|
|
911
|
+
renderBottomPinned() {
|
|
912
|
+
const { rows, cols } = this.getSize();
|
|
913
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
914
|
+
const isStreaming = this.mode === 'streaming';
|
|
915
|
+
// Use unified pinned input area (works for both streaming and normal)
|
|
916
|
+
// Only use complex rendering when suggestions are visible
|
|
917
|
+
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
918
|
+
if (!hasSuggestions) {
|
|
919
|
+
this.renderPinnedInputArea();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// Wrap buffer into display lines
|
|
923
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
924
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
925
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
926
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
927
|
+
// Calculate display window (keep cursor visible)
|
|
928
|
+
let startLine = 0;
|
|
929
|
+
if (lines.length > displayLines) {
|
|
930
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
931
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
932
|
+
}
|
|
933
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
934
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
935
|
+
// Calculate suggestion display (not during streaming)
|
|
936
|
+
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
937
|
+
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
938
|
+
: [];
|
|
939
|
+
const suggestionLines = suggestionsToShow.length;
|
|
940
|
+
this.write(ESC.HIDE);
|
|
941
|
+
this.write(ESC.RESET);
|
|
942
|
+
const divider = renderDivider(cols - 2);
|
|
943
|
+
// Calculate positions from absolute bottom
|
|
944
|
+
let currentRow;
|
|
945
|
+
if (suggestionLines > 0) {
|
|
946
|
+
// With suggestions: input area + dividers + suggestions
|
|
947
|
+
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
948
|
+
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
949
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
950
|
+
this.updateReservedLines(totalHeight);
|
|
951
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
952
|
+
this.write(ESC.TO(currentRow, 1));
|
|
953
|
+
this.write(ESC.CLEAR_TO_END);
|
|
954
|
+
// Top divider
|
|
496
955
|
this.write(ESC.TO(currentRow, 1));
|
|
497
|
-
this.write(ESC.CLEAR_LINE);
|
|
498
|
-
const divider = renderDivider(cols - 2);
|
|
499
956
|
this.write(divider);
|
|
500
|
-
currentRow
|
|
501
|
-
//
|
|
957
|
+
currentRow++;
|
|
958
|
+
// Input lines
|
|
502
959
|
let finalRow = currentRow;
|
|
503
960
|
let finalCol = 3;
|
|
504
961
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
505
|
-
|
|
506
|
-
this.write(ESC.TO(rowNum, 1));
|
|
507
|
-
this.write(ESC.CLEAR_LINE);
|
|
962
|
+
this.write(ESC.TO(currentRow, 1));
|
|
508
963
|
const line = visibleLines[i] ?? '';
|
|
509
964
|
const absoluteLineIdx = startLine + i;
|
|
510
965
|
const isFirstLine = absoluteLineIdx === 0;
|
|
511
966
|
const isCursorLine = i === adjustedCursorLine;
|
|
512
|
-
// Background
|
|
513
|
-
this.write(ESC.BG_DARK);
|
|
514
|
-
// Prompt prefix
|
|
515
|
-
this.write(ESC.DIM);
|
|
516
967
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
517
|
-
this.write(ESC.RESET);
|
|
518
|
-
this.write(ESC.BG_DARK);
|
|
519
968
|
if (isCursorLine) {
|
|
520
|
-
// Render with block cursor
|
|
521
969
|
const col = Math.min(cursorCol, line.length);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.write(
|
|
526
|
-
this.write(
|
|
527
|
-
|
|
528
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
529
|
-
this.write(after);
|
|
530
|
-
finalRow = rowNum;
|
|
970
|
+
this.write(line.slice(0, col));
|
|
971
|
+
this.write(ESC.REVERSE);
|
|
972
|
+
this.write(col < line.length ? line[col] : ' ');
|
|
973
|
+
this.write(ESC.RESET);
|
|
974
|
+
this.write(line.slice(col + 1));
|
|
975
|
+
finalRow = currentRow;
|
|
531
976
|
finalCol = this.config.promptChar.length + col + 1;
|
|
532
977
|
}
|
|
533
978
|
else {
|
|
534
979
|
this.write(line);
|
|
535
980
|
}
|
|
536
|
-
|
|
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);
|
|
981
|
+
currentRow++;
|
|
542
982
|
}
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
this.write(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
983
|
+
// Bottom divider
|
|
984
|
+
this.write(ESC.TO(currentRow, 1));
|
|
985
|
+
this.write(divider);
|
|
986
|
+
currentRow++;
|
|
987
|
+
// Suggestions (Claude Code style)
|
|
988
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
989
|
+
this.write(ESC.TO(currentRow, 1));
|
|
990
|
+
const suggestion = suggestionsToShow[i];
|
|
991
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
992
|
+
// Indent and highlight selected
|
|
993
|
+
this.write(' ');
|
|
994
|
+
if (isSelected) {
|
|
995
|
+
this.write(ESC.REVERSE);
|
|
996
|
+
this.write(ESC.BOLD);
|
|
997
|
+
}
|
|
998
|
+
this.write(suggestion.command);
|
|
999
|
+
if (isSelected) {
|
|
1000
|
+
this.write(ESC.RESET);
|
|
1001
|
+
}
|
|
1002
|
+
// Description (dimmed)
|
|
1003
|
+
const descSpace = cols - suggestion.command.length - 8;
|
|
1004
|
+
if (descSpace > 10 && suggestion.description) {
|
|
1005
|
+
const desc = suggestion.description.slice(0, descSpace);
|
|
1006
|
+
this.write(ESC.RESET);
|
|
1007
|
+
this.write(ESC.DIM);
|
|
1008
|
+
this.write(' ');
|
|
1009
|
+
this.write(desc);
|
|
1010
|
+
this.write(ESC.RESET);
|
|
1011
|
+
}
|
|
1012
|
+
currentRow++;
|
|
558
1013
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
writeLock.lock('terminalInput.render');
|
|
562
|
-
this.isRendering = true;
|
|
563
|
-
try {
|
|
564
|
-
performRender();
|
|
565
|
-
}
|
|
566
|
-
finally {
|
|
567
|
-
writeLock.unlock();
|
|
568
|
-
this.isRendering = false;
|
|
1014
|
+
// Position cursor in input area
|
|
1015
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
569
1016
|
}
|
|
1017
|
+
this.write(ESC.SHOW);
|
|
1018
|
+
// Update state
|
|
1019
|
+
this.lastRenderContent = this.buffer;
|
|
1020
|
+
this.lastRenderCursor = this.cursor;
|
|
570
1021
|
}
|
|
571
1022
|
/**
|
|
572
|
-
* Build
|
|
573
|
-
* During streaming, shows model line pinned above streaming info.
|
|
1023
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
574
1024
|
*/
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (this.
|
|
580
|
-
const
|
|
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));
|
|
598
|
-
}
|
|
599
|
-
return lines;
|
|
600
|
-
}
|
|
601
|
-
// Non-streaming: show full status info (model line already added above)
|
|
602
|
-
if (this.metaThinkingMs !== null) {
|
|
603
|
-
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
604
|
-
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
605
|
-
}
|
|
606
|
-
const statusParts = [];
|
|
607
|
-
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
608
|
-
if (statusLabel) {
|
|
609
|
-
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
610
|
-
}
|
|
611
|
-
if (this.metaElapsedSeconds !== null) {
|
|
612
|
-
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
613
|
-
}
|
|
614
|
-
const tokensRemaining = this.computeTokensRemaining();
|
|
615
|
-
if (tokensRemaining !== null) {
|
|
616
|
-
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
617
|
-
}
|
|
618
|
-
if (statusParts.length) {
|
|
619
|
-
lines.push(renderStatusLine(statusParts, width));
|
|
620
|
-
}
|
|
621
|
-
const usageParts = [];
|
|
622
|
-
if (this.metaTokensUsed !== null) {
|
|
623
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
624
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
625
|
-
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
626
|
-
}
|
|
627
|
-
if (this.contextUsage !== null) {
|
|
628
|
-
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
629
|
-
const left = Math.max(0, 100 - this.contextUsage);
|
|
630
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
631
|
-
}
|
|
1025
|
+
buildStreamingStatusBar(cols) {
|
|
1026
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
1027
|
+
// Streaming status with elapsed time
|
|
1028
|
+
let elapsed = '0s';
|
|
1029
|
+
if (this.streamingStartTime) {
|
|
1030
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1031
|
+
const mins = Math.floor(secs / 60);
|
|
1032
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
1033
|
+
}
|
|
1034
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
1035
|
+
// Queue indicator
|
|
632
1036
|
if (this.queue.length > 0) {
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
if (usageParts.length) {
|
|
636
|
-
lines.push(renderStatusLine(usageParts, width));
|
|
1037
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
637
1038
|
}
|
|
638
|
-
|
|
1039
|
+
// Hint for typing
|
|
1040
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
1041
|
+
return status;
|
|
639
1042
|
}
|
|
640
1043
|
/**
|
|
641
|
-
*
|
|
1044
|
+
* Build status bar showing streaming/ready status and key info.
|
|
1045
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
642
1046
|
*/
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const width = Math.max(8, cols - 2);
|
|
657
|
-
const leftParts = [];
|
|
658
|
-
const rightParts = [];
|
|
659
|
-
if (this.streamingLabel) {
|
|
660
|
-
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
1047
|
+
buildStatusBar(cols) {
|
|
1048
|
+
const maxWidth = cols - 2;
|
|
1049
|
+
const parts = [];
|
|
1050
|
+
// Streaming status with elapsed time (left side)
|
|
1051
|
+
if (this.mode === 'streaming') {
|
|
1052
|
+
let statusText = '● Streaming';
|
|
1053
|
+
if (this.streamingStartTime) {
|
|
1054
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1055
|
+
const mins = Math.floor(elapsed / 60);
|
|
1056
|
+
const secs = elapsed % 60;
|
|
1057
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
1058
|
+
}
|
|
1059
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
661
1060
|
}
|
|
662
|
-
|
|
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' });
|
|
680
|
-
}
|
|
681
|
-
if (this.buffer.includes('\n')) {
|
|
682
|
-
const lineCount = this.buffer.split('\n').length;
|
|
683
|
-
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
1061
|
+
// Queue indicator during streaming
|
|
1062
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
1063
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
684
1064
|
}
|
|
1065
|
+
// Paste indicator
|
|
685
1066
|
if (this.pastePlaceholders.length > 0) {
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
689
|
-
tone: 'info',
|
|
690
|
-
});
|
|
1067
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
1068
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
691
1069
|
}
|
|
692
|
-
|
|
693
|
-
if (this.
|
|
694
|
-
|
|
695
|
-
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
696
|
-
}
|
|
697
|
-
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
698
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
699
|
-
if (this.modelLabel && !streamingActive) {
|
|
700
|
-
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
701
|
-
rightParts.push({ text: modelText, tone: 'muted' });
|
|
702
|
-
}
|
|
703
|
-
if (contextRemaining !== null) {
|
|
704
|
-
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
705
|
-
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
706
|
-
? 'Context auto-compact imminent'
|
|
707
|
-
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
708
|
-
rightParts.push({ text: label, tone });
|
|
709
|
-
}
|
|
710
|
-
if (!rightParts.length || width < 60) {
|
|
711
|
-
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
712
|
-
return renderStatusLine(merged, width);
|
|
713
|
-
}
|
|
714
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
715
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
716
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
717
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
718
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
719
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
720
|
-
}
|
|
721
|
-
formatHotkey(hotkey) {
|
|
722
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
723
|
-
if (!normalized)
|
|
724
|
-
return hotkey;
|
|
725
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
726
|
-
const map = {
|
|
727
|
-
shift: '⇧',
|
|
728
|
-
sh: '⇧',
|
|
729
|
-
alt: '⌥',
|
|
730
|
-
option: '⌥',
|
|
731
|
-
opt: '⌥',
|
|
732
|
-
ctrl: '⌃',
|
|
733
|
-
control: '⌃',
|
|
734
|
-
cmd: '⌘',
|
|
735
|
-
meta: '⌘',
|
|
736
|
-
};
|
|
737
|
-
const formatted = parts
|
|
738
|
-
.map((part) => {
|
|
739
|
-
const symbol = map[part];
|
|
740
|
-
if (symbol)
|
|
741
|
-
return symbol;
|
|
742
|
-
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
743
|
-
})
|
|
744
|
-
.join('');
|
|
745
|
-
return formatted || hotkey;
|
|
746
|
-
}
|
|
747
|
-
computeContextRemaining() {
|
|
748
|
-
if (this.contextUsage === null) {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
752
|
-
}
|
|
753
|
-
computeTokensRemaining() {
|
|
754
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
755
|
-
return null;
|
|
756
|
-
}
|
|
757
|
-
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
758
|
-
return this.formatTokenCount(remaining);
|
|
759
|
-
}
|
|
760
|
-
formatElapsedLabel(seconds) {
|
|
761
|
-
if (seconds < 60) {
|
|
762
|
-
return `${seconds}s`;
|
|
763
|
-
}
|
|
764
|
-
const mins = Math.floor(seconds / 60);
|
|
765
|
-
const secs = seconds % 60;
|
|
766
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
767
|
-
}
|
|
768
|
-
formatTokenCount(value) {
|
|
769
|
-
if (!Number.isFinite(value)) {
|
|
770
|
-
return `${value}`;
|
|
1070
|
+
// Override/warning status
|
|
1071
|
+
if (this.overrideStatusMessage) {
|
|
1072
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
771
1073
|
}
|
|
772
|
-
|
|
773
|
-
|
|
1074
|
+
// If idle with empty buffer, show quick shortcuts
|
|
1075
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
1076
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
774
1077
|
}
|
|
775
|
-
|
|
776
|
-
|
|
1078
|
+
// Multi-line indicator
|
|
1079
|
+
if (this.buffer.includes('\n')) {
|
|
1080
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
777
1081
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
return
|
|
1082
|
+
if (parts.length === 0) {
|
|
1083
|
+
return ''; // Empty status bar when idle
|
|
1084
|
+
}
|
|
1085
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
1086
|
+
return joined.slice(0, maxWidth);
|
|
783
1087
|
}
|
|
784
1088
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
1089
|
+
* Build mode controls line showing toggles and context info.
|
|
1090
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
1091
|
+
*
|
|
1092
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
787
1093
|
*/
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1094
|
+
buildModeControls(cols) {
|
|
1095
|
+
const maxWidth = cols - 2;
|
|
1096
|
+
// Use schema-defined colors for consistency
|
|
1097
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
1098
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
1099
|
+
const toggles = [];
|
|
1100
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
1101
|
+
if (this.editMode === 'display-edits') {
|
|
1102
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
1106
|
+
}
|
|
1107
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
1108
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
1109
|
+
// Verification (green when on) - per schema.verificationMode
|
|
1110
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
1111
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
1112
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
1113
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
1114
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
1115
|
+
let rightPart = '';
|
|
1116
|
+
if (this.contextUsage !== null) {
|
|
1117
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
1118
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
1119
|
+
if (rem < 10)
|
|
1120
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1121
|
+
else if (rem < 25)
|
|
1122
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1123
|
+
else
|
|
1124
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1125
|
+
}
|
|
1126
|
+
// Calculate visible lengths (strip ANSI)
|
|
1127
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1128
|
+
const leftLen = strip(leftPart).length;
|
|
1129
|
+
const rightLen = strip(rightPart).length;
|
|
1130
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
1131
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1132
|
+
}
|
|
1133
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1134
|
+
return `${leftPart} ${rightPart}`;
|
|
1135
|
+
}
|
|
1136
|
+
return leftPart;
|
|
794
1137
|
}
|
|
795
1138
|
/**
|
|
796
1139
|
* Force a re-render
|
|
@@ -813,19 +1156,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
813
1156
|
handleResize() {
|
|
814
1157
|
this.lastRenderContent = '';
|
|
815
1158
|
this.lastRenderCursor = -1;
|
|
816
|
-
this.resetStreamingRenderThrottle();
|
|
817
1159
|
// Re-clamp pinned header rows to the new terminal height
|
|
818
1160
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
819
|
-
if (this.scrollRegionActive) {
|
|
820
|
-
this.disableScrollRegion();
|
|
821
|
-
this.enableScrollRegion();
|
|
822
|
-
}
|
|
823
1161
|
this.scheduleRender();
|
|
824
1162
|
}
|
|
825
1163
|
/**
|
|
826
1164
|
* Register with display's output interceptor to position cursor correctly.
|
|
827
1165
|
* When scroll region is active, output needs to go to the scroll region,
|
|
828
1166
|
* not the protected bottom area where the input is rendered.
|
|
1167
|
+
*
|
|
1168
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
1169
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
829
1170
|
*/
|
|
830
1171
|
registerOutputInterceptor(display) {
|
|
831
1172
|
if (this.outputInterceptorCleanup) {
|
|
@@ -833,49 +1174,25 @@ export class TerminalInput extends EventEmitter {
|
|
|
833
1174
|
}
|
|
834
1175
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
835
1176
|
beforeWrite: () => {
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
if (this.scrollRegionActive) {
|
|
839
|
-
const { rows } = this.getSize();
|
|
840
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
841
|
-
// Use tracked position, clamped to scroll region bounds
|
|
842
|
-
const targetRow = Math.min(this.nextContentRow, scrollBottom);
|
|
843
|
-
this.write(ESC.SAVE);
|
|
844
|
-
this.write(ESC.TO(targetRow, 1));
|
|
845
|
-
}
|
|
1177
|
+
// Scroll region handles content containment automatically
|
|
1178
|
+
// No per-write cursor manipulation needed
|
|
846
1179
|
},
|
|
847
1180
|
afterWrite: () => {
|
|
848
|
-
//
|
|
849
|
-
if (this.scrollRegionActive) {
|
|
850
|
-
const { rows } = this.getSize();
|
|
851
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
852
|
-
// Advance row for next content (clamp at scrollBottom, terminal handles scrolling)
|
|
853
|
-
this.nextContentRow = Math.min(this.nextContentRow + 1, scrollBottom);
|
|
854
|
-
this.write(ESC.RESTORE);
|
|
855
|
-
}
|
|
1181
|
+
// No cursor manipulation needed
|
|
856
1182
|
},
|
|
857
1183
|
});
|
|
858
1184
|
}
|
|
859
|
-
/**
|
|
860
|
-
* Advance content cursor by specified lines (call after writing known number of lines).
|
|
861
|
-
*/
|
|
862
|
-
advanceContentRow(lines = 1) {
|
|
863
|
-
const { rows } = this.getSize();
|
|
864
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
865
|
-
this.nextContentRow = Math.min(this.nextContentRow + lines, scrollBottom);
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Reset content cursor to start of scroll region.
|
|
869
|
-
*/
|
|
870
|
-
resetContentRow() {
|
|
871
|
-
this.nextContentRow = Math.max(1, this.pinnedTopRows + 1);
|
|
872
|
-
}
|
|
873
1185
|
/**
|
|
874
1186
|
* Dispose and clean up
|
|
875
1187
|
*/
|
|
876
1188
|
dispose() {
|
|
877
1189
|
if (this.disposed)
|
|
878
1190
|
return;
|
|
1191
|
+
// Clean up streaming render timer
|
|
1192
|
+
if (this.streamingRenderTimer) {
|
|
1193
|
+
clearInterval(this.streamingRenderTimer);
|
|
1194
|
+
this.streamingRenderTimer = null;
|
|
1195
|
+
}
|
|
879
1196
|
// Clean up output interceptor
|
|
880
1197
|
if (this.outputInterceptorCleanup) {
|
|
881
1198
|
this.outputInterceptorCleanup();
|
|
@@ -883,7 +1200,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
883
1200
|
}
|
|
884
1201
|
this.disposed = true;
|
|
885
1202
|
this.enabled = false;
|
|
886
|
-
this.resetStreamingRenderThrottle();
|
|
887
1203
|
this.disableScrollRegion();
|
|
888
1204
|
this.disableBracketedPaste();
|
|
889
1205
|
this.buffer = '';
|
|
@@ -989,7 +1305,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
989
1305
|
this.toggleEditMode();
|
|
990
1306
|
return true;
|
|
991
1307
|
}
|
|
992
|
-
|
|
1308
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1309
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1310
|
+
this.togglePasteExpansion();
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
this.toggleThinking();
|
|
1314
|
+
}
|
|
1315
|
+
return true;
|
|
1316
|
+
case 'escape':
|
|
1317
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1318
|
+
if (this.mode === 'streaming') {
|
|
1319
|
+
this.emit('interrupt');
|
|
1320
|
+
}
|
|
1321
|
+
else if (this.buffer.length > 0) {
|
|
1322
|
+
this.clear();
|
|
1323
|
+
}
|
|
993
1324
|
return true;
|
|
994
1325
|
}
|
|
995
1326
|
return false;
|
|
@@ -1007,6 +1338,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1007
1338
|
this.insertPlainText(chunk, insertPos);
|
|
1008
1339
|
this.cursor = insertPos + chunk.length;
|
|
1009
1340
|
this.emit('change', this.buffer);
|
|
1341
|
+
this.updateSuggestions();
|
|
1010
1342
|
this.scheduleRender();
|
|
1011
1343
|
}
|
|
1012
1344
|
insertNewline() {
|
|
@@ -1031,6 +1363,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1031
1363
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1032
1364
|
}
|
|
1033
1365
|
this.emit('change', this.buffer);
|
|
1366
|
+
this.updateSuggestions();
|
|
1034
1367
|
this.scheduleRender();
|
|
1035
1368
|
}
|
|
1036
1369
|
deleteForward() {
|
|
@@ -1280,9 +1613,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1280
1613
|
if (available <= 0)
|
|
1281
1614
|
return;
|
|
1282
1615
|
const chunk = clean.slice(0, available);
|
|
1283
|
-
|
|
1284
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1285
|
-
if (isMultiline && !isShortMultiline) {
|
|
1616
|
+
if (isMultilinePaste(chunk)) {
|
|
1286
1617
|
this.insertPastePlaceholder(chunk);
|
|
1287
1618
|
}
|
|
1288
1619
|
else {
|
|
@@ -1302,7 +1633,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1302
1633
|
return;
|
|
1303
1634
|
this.applyScrollRegion();
|
|
1304
1635
|
this.scrollRegionActive = true;
|
|
1305
|
-
this.forceRender();
|
|
1306
1636
|
}
|
|
1307
1637
|
disableScrollRegion() {
|
|
1308
1638
|
if (!this.scrollRegionActive)
|
|
@@ -1453,19 +1783,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1453
1783
|
this.shiftPlaceholders(position, text.length);
|
|
1454
1784
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1455
1785
|
}
|
|
1456
|
-
shouldInlineMultiline(content) {
|
|
1457
|
-
const lines = content.split('\n').length;
|
|
1458
|
-
const maxInlineLines = 4;
|
|
1459
|
-
const maxInlineChars = 240;
|
|
1460
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1461
|
-
}
|
|
1462
1786
|
findPlaceholderAt(position) {
|
|
1463
1787
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1464
1788
|
}
|
|
1465
|
-
buildPlaceholder(
|
|
1789
|
+
buildPlaceholder(summary) {
|
|
1466
1790
|
const id = ++this.pasteCounter;
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1791
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1792
|
+
// Show first line preview (truncated)
|
|
1793
|
+
const preview = summary.preview.length > 30
|
|
1794
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1795
|
+
: summary.preview;
|
|
1796
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1469
1797
|
return { id, placeholder };
|
|
1470
1798
|
}
|
|
1471
1799
|
insertPastePlaceholder(content) {
|
|
@@ -1473,21 +1801,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1473
1801
|
if (available <= 0)
|
|
1474
1802
|
return;
|
|
1475
1803
|
const cleanContent = content.slice(0, available);
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1804
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1805
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1806
|
+
if (summary.lineCount < 5) {
|
|
1807
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1808
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1809
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1810
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1478
1814
|
const insertPos = this.cursor;
|
|
1479
1815
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1480
1816
|
this.pastePlaceholders.push({
|
|
1481
1817
|
id,
|
|
1482
1818
|
content: cleanContent,
|
|
1483
|
-
lineCount,
|
|
1819
|
+
lineCount: summary.lineCount,
|
|
1484
1820
|
placeholder,
|
|
1485
1821
|
start: insertPos,
|
|
1486
1822
|
end: insertPos + placeholder.length,
|
|
1823
|
+
summary,
|
|
1824
|
+
expanded: false,
|
|
1487
1825
|
});
|
|
1488
1826
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1489
1827
|
this.cursor = insertPos + placeholder.length;
|
|
1490
1828
|
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1831
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1832
|
+
*/
|
|
1833
|
+
togglePasteExpansion() {
|
|
1834
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1835
|
+
if (!placeholder)
|
|
1836
|
+
return false;
|
|
1837
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1838
|
+
// Update the placeholder text in buffer
|
|
1839
|
+
const newPlaceholder = placeholder.expanded
|
|
1840
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1841
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1842
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1843
|
+
// Update buffer
|
|
1844
|
+
this.buffer =
|
|
1845
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1846
|
+
newPlaceholder +
|
|
1847
|
+
this.buffer.slice(placeholder.end);
|
|
1848
|
+
// Update placeholder tracking
|
|
1849
|
+
placeholder.placeholder = newPlaceholder;
|
|
1850
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1851
|
+
// Shift other placeholders
|
|
1852
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1853
|
+
this.scheduleRender();
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
buildExpandedPlaceholder(ph) {
|
|
1857
|
+
const lines = ph.content.split('\n');
|
|
1858
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1859
|
+
const lastLines = lines.length > 5
|
|
1860
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1861
|
+
: '';
|
|
1862
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1863
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1864
|
+
}
|
|
1491
1865
|
deletePlaceholder(placeholder) {
|
|
1492
1866
|
const length = placeholder.end - placeholder.start;
|
|
1493
1867
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1495,11 +1869,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1495
1869
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1496
1870
|
this.cursor = placeholder.start;
|
|
1497
1871
|
}
|
|
1498
|
-
updateContextUsage(value
|
|
1499
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1500
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1501
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1502
|
-
}
|
|
1872
|
+
updateContextUsage(value) {
|
|
1503
1873
|
if (value === null || !Number.isFinite(value)) {
|
|
1504
1874
|
this.contextUsage = null;
|
|
1505
1875
|
}
|
|
@@ -1526,22 +1896,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1526
1896
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1527
1897
|
this.setEditMode(next);
|
|
1528
1898
|
}
|
|
1529
|
-
scheduleStreamingRender(delayMs) {
|
|
1530
|
-
if (this.streamingRenderTimer)
|
|
1531
|
-
return;
|
|
1532
|
-
const wait = Math.max(16, delayMs);
|
|
1533
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1534
|
-
this.streamingRenderTimer = null;
|
|
1535
|
-
this.render();
|
|
1536
|
-
}, wait);
|
|
1537
|
-
}
|
|
1538
|
-
resetStreamingRenderThrottle() {
|
|
1539
|
-
if (this.streamingRenderTimer) {
|
|
1540
|
-
clearTimeout(this.streamingRenderTimer);
|
|
1541
|
-
this.streamingRenderTimer = null;
|
|
1542
|
-
}
|
|
1543
|
-
this.lastStreamingRender = 0;
|
|
1544
|
-
}
|
|
1545
1899
|
scheduleRender() {
|
|
1546
1900
|
if (!this.canRender())
|
|
1547
1901
|
return;
|