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