erosolar-cli 1.7.286 → 1.7.290
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -24
- package/dist/alpha-zero/agentWrapper.d.ts +84 -0
- package/dist/alpha-zero/agentWrapper.d.ts.map +1 -0
- package/dist/alpha-zero/agentWrapper.js +171 -0
- package/dist/alpha-zero/agentWrapper.js.map +1 -0
- package/dist/alpha-zero/codeEvaluator.d.ts +25 -0
- package/dist/alpha-zero/codeEvaluator.d.ts.map +1 -0
- package/dist/alpha-zero/codeEvaluator.js +273 -0
- package/dist/alpha-zero/codeEvaluator.js.map +1 -0
- package/dist/alpha-zero/competitiveRunner.d.ts +66 -0
- package/dist/alpha-zero/competitiveRunner.d.ts.map +1 -0
- package/dist/alpha-zero/competitiveRunner.js +224 -0
- package/dist/alpha-zero/competitiveRunner.js.map +1 -0
- package/dist/alpha-zero/index.d.ts +67 -0
- package/dist/alpha-zero/index.d.ts.map +1 -0
- package/dist/alpha-zero/index.js +99 -0
- package/dist/alpha-zero/index.js.map +1 -0
- package/dist/alpha-zero/introspection.d.ts +128 -0
- package/dist/alpha-zero/introspection.d.ts.map +1 -0
- package/dist/alpha-zero/introspection.js +300 -0
- package/dist/alpha-zero/introspection.js.map +1 -0
- package/dist/alpha-zero/metricsTracker.d.ts +71 -0
- package/dist/alpha-zero/metricsTracker.d.ts.map +1 -0
- package/dist/{core → alpha-zero}/metricsTracker.js +5 -2
- package/dist/alpha-zero/metricsTracker.js.map +1 -0
- package/dist/alpha-zero/security/core.d.ts +125 -0
- package/dist/alpha-zero/security/core.d.ts.map +1 -0
- package/dist/alpha-zero/security/core.js +271 -0
- package/dist/alpha-zero/security/core.js.map +1 -0
- package/dist/alpha-zero/security/google.d.ts +125 -0
- package/dist/alpha-zero/security/google.d.ts.map +1 -0
- package/dist/alpha-zero/security/google.js +311 -0
- package/dist/alpha-zero/security/google.js.map +1 -0
- package/dist/alpha-zero/security/googleLoader.d.ts +17 -0
- package/dist/alpha-zero/security/googleLoader.d.ts.map +1 -0
- package/dist/alpha-zero/security/googleLoader.js +41 -0
- package/dist/alpha-zero/security/googleLoader.js.map +1 -0
- package/dist/alpha-zero/security/index.d.ts +29 -0
- package/dist/alpha-zero/security/index.d.ts.map +1 -0
- package/dist/alpha-zero/security/index.js +32 -0
- package/dist/alpha-zero/security/index.js.map +1 -0
- package/dist/alpha-zero/security/simulation.d.ts +124 -0
- package/dist/alpha-zero/security/simulation.d.ts.map +1 -0
- package/dist/alpha-zero/security/simulation.js +277 -0
- package/dist/alpha-zero/security/simulation.js.map +1 -0
- package/dist/alpha-zero/selfModification.d.ts +109 -0
- package/dist/alpha-zero/selfModification.d.ts.map +1 -0
- package/dist/alpha-zero/selfModification.js +233 -0
- package/dist/alpha-zero/selfModification.js.map +1 -0
- package/dist/alpha-zero/types.d.ts +170 -0
- package/dist/alpha-zero/types.d.ts.map +1 -0
- package/dist/alpha-zero/types.js +31 -0
- package/dist/alpha-zero/types.js.map +1 -0
- package/dist/bin/erosolar.js +0 -1
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +31 -56
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/capabilities/securityTestingCapability.d.ts +13 -0
- package/dist/capabilities/securityTestingCapability.d.ts.map +1 -0
- package/dist/capabilities/securityTestingCapability.js +25 -0
- package/dist/capabilities/securityTestingCapability.js.map +1 -0
- package/dist/contracts/agent-schemas.json +15 -0
- package/dist/contracts/tools.schema.json +9 -0
- package/dist/core/agent.d.ts +2 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js.map +1 -1
- package/dist/core/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/customCommands.d.ts +0 -1
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +0 -3
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/core/hooksSystem.d.ts +65 -0
- package/dist/core/hooksSystem.d.ts.map +1 -0
- package/dist/core/hooksSystem.js +273 -0
- package/dist/core/hooksSystem.js.map +1 -0
- package/dist/core/memorySystem.d.ts +48 -0
- package/dist/core/memorySystem.d.ts.map +1 -0
- package/dist/core/memorySystem.js +271 -0
- package/dist/core/memorySystem.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +14 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +1 -22
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +5 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +3 -14
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/unified/errors.d.ts +189 -0
- package/dist/core/unified/errors.d.ts.map +1 -0
- package/dist/core/unified/errors.js +497 -0
- package/dist/core/unified/errors.js.map +1 -0
- package/dist/core/unified/index.d.ts +19 -0
- package/dist/core/unified/index.d.ts.map +1 -0
- package/dist/core/unified/index.js +68 -0
- package/dist/core/unified/index.js.map +1 -0
- package/dist/core/unified/schema.d.ts +101 -0
- package/dist/core/unified/schema.d.ts.map +1 -0
- package/dist/core/unified/schema.js +350 -0
- package/dist/core/unified/schema.js.map +1 -0
- package/dist/core/unified/toolRuntime.d.ts +179 -0
- package/dist/core/unified/toolRuntime.d.ts.map +1 -0
- package/dist/core/unified/toolRuntime.js +517 -0
- package/dist/core/unified/toolRuntime.js.map +1 -0
- package/dist/core/unified/tools.d.ts +127 -0
- package/dist/core/unified/tools.d.ts.map +1 -0
- package/dist/core/unified/tools.js +1333 -0
- package/dist/core/unified/tools.js.map +1 -0
- package/dist/core/unified/types.d.ts +352 -0
- package/dist/core/unified/types.d.ts.map +1 -0
- package/dist/core/unified/types.js +12 -0
- package/dist/core/unified/types.js.map +1 -0
- package/dist/core/unified/version.d.ts +209 -0
- package/dist/core/unified/version.d.ts.map +1 -0
- package/dist/core/unified/version.js +454 -0
- package/dist/core/unified/version.js.map +1 -0
- package/dist/core/validationRunner.d.ts +3 -1
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -21
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +3 -0
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +1 -0
- package/dist/plugins/tools/security/securityPlugin.js +12 -0
- package/dist/plugins/tools/security/securityPlugin.js.map +1 -0
- package/dist/runtime/agentSession.d.ts +2 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/security/active-stack-security.d.ts +112 -0
- package/dist/security/active-stack-security.d.ts.map +1 -0
- package/dist/security/active-stack-security.js +296 -0
- package/dist/security/active-stack-security.js.map +1 -0
- package/dist/security/advanced-persistence-research.d.ts +92 -0
- package/dist/security/advanced-persistence-research.d.ts.map +1 -0
- package/dist/security/advanced-persistence-research.js +195 -0
- package/dist/security/advanced-persistence-research.js.map +1 -0
- package/dist/security/advanced-targeting.d.ts +119 -0
- package/dist/security/advanced-targeting.d.ts.map +1 -0
- package/dist/security/advanced-targeting.js +233 -0
- package/dist/security/advanced-targeting.js.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +104 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.js +315 -0
- package/dist/security/assessment/vulnerabilityAssessment.js.map +1 -0
- package/dist/security/authorization/securityAuthorization.d.ts +88 -0
- package/dist/security/authorization/securityAuthorization.d.ts.map +1 -0
- package/dist/security/authorization/securityAuthorization.js +172 -0
- package/dist/security/authorization/securityAuthorization.js.map +1 -0
- package/dist/security/comprehensive-targeting.d.ts +85 -0
- package/dist/security/comprehensive-targeting.d.ts.map +1 -0
- package/dist/security/comprehensive-targeting.js +438 -0
- package/dist/security/comprehensive-targeting.js.map +1 -0
- package/dist/security/global-security-integration.d.ts +91 -0
- package/dist/security/global-security-integration.d.ts.map +1 -0
- package/dist/security/global-security-integration.js +218 -0
- package/dist/security/global-security-integration.js.map +1 -0
- package/dist/security/index.d.ts +38 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +47 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/persistence-analyzer.d.ts +56 -0
- package/dist/security/persistence-analyzer.d.ts.map +1 -0
- package/dist/security/persistence-analyzer.js +187 -0
- package/dist/security/persistence-analyzer.js.map +1 -0
- package/dist/security/persistence-cli.d.ts +36 -0
- package/dist/security/persistence-cli.d.ts.map +1 -0
- package/dist/security/persistence-cli.js +160 -0
- package/dist/security/persistence-cli.js.map +1 -0
- package/dist/security/persistence-research.d.ts +92 -0
- package/dist/security/persistence-research.d.ts.map +1 -0
- package/dist/security/persistence-research.js +364 -0
- package/dist/security/persistence-research.js.map +1 -0
- package/dist/security/research/persistenceResearch.d.ts +97 -0
- package/dist/security/research/persistenceResearch.d.ts.map +1 -0
- package/dist/security/research/persistenceResearch.js +282 -0
- package/dist/security/research/persistenceResearch.js.map +1 -0
- package/dist/security/security-integration.d.ts +74 -0
- package/dist/security/security-integration.d.ts.map +1 -0
- package/dist/security/security-integration.js +137 -0
- package/dist/security/security-integration.js.map +1 -0
- package/dist/security/security-testing-framework.d.ts +112 -0
- package/dist/security/security-testing-framework.d.ts.map +1 -0
- package/dist/security/security-testing-framework.js +364 -0
- package/dist/security/security-testing-framework.js.map +1 -0
- package/dist/security/simulation/attackSimulation.d.ts +93 -0
- package/dist/security/simulation/attackSimulation.d.ts.map +1 -0
- package/dist/security/simulation/attackSimulation.js +341 -0
- package/dist/security/simulation/attackSimulation.js.map +1 -0
- package/dist/security/strategic-operations.d.ts +100 -0
- package/dist/security/strategic-operations.d.ts.map +1 -0
- package/dist/security/strategic-operations.js +276 -0
- package/dist/security/strategic-operations.js.map +1 -0
- package/dist/security/tool-security-wrapper.d.ts +58 -0
- package/dist/security/tool-security-wrapper.d.ts.map +1 -0
- package/dist/security/tool-security-wrapper.js +156 -0
- package/dist/security/tool-security-wrapper.js.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +7 -22
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +159 -229
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/metricsTracker.d.ts +60 -0
- package/dist/shell/metricsTracker.d.ts.map +1 -0
- package/dist/shell/metricsTracker.js +119 -0
- package/dist/shell/metricsTracker.js.map +1 -0
- package/dist/shell/shellApp.d.ts +0 -2
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +9 -40
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +157 -87
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +655 -558
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +35 -28
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +50 -26
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/taskRunner.d.ts +1 -7
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +47 -180
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/securityTools.d.ts +22 -0
- package/dist/tools/securityTools.d.ts.map +1 -0
- package/dist/tools/securityTools.js +448 -0
- package/dist/tools/securityTools.js.map +1 -0
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +12 -13
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +45 -24
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +259 -140
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/terminalUISchema.d.ts +195 -0
- package/dist/ui/terminalUISchema.d.ts.map +1 -0
- package/dist/ui/terminalUISchema.js +113 -0
- package/dist/ui/terminalUISchema.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +158 -0
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +348 -0
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +0 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +25 -15
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- package/scripts/deploy-security-capabilities.js +178 -0
- package/dist/core/hooks.d.ts +0 -113
- package/dist/core/hooks.d.ts.map +0 -1
- package/dist/core/hooks.js +0 -267
- package/dist/core/hooks.js.map +0 -1
- package/dist/core/metricsTracker.d.ts +0 -122
- package/dist/core/metricsTracker.d.ts.map +0 -1
- package/dist/core/metricsTracker.js.map +0 -1
- package/dist/core/securityAssessment.d.ts +0 -91
- package/dist/core/securityAssessment.d.ts.map +0 -1
- package/dist/core/securityAssessment.js +0 -580
- package/dist/core/securityAssessment.js.map +0 -1
- package/dist/core/verification.d.ts +0 -137
- package/dist/core/verification.d.ts.map +0 -1
- package/dist/core/verification.js +0 -323
- package/dist/core/verification.js.map +0 -1
- package/dist/subagents/agentConfig.d.ts +0 -27
- package/dist/subagents/agentConfig.d.ts.map +0 -1
- package/dist/subagents/agentConfig.js +0 -89
- package/dist/subagents/agentConfig.js.map +0 -1
- package/dist/subagents/agentRegistry.d.ts +0 -33
- package/dist/subagents/agentRegistry.d.ts.map +0 -1
- package/dist/subagents/agentRegistry.js +0 -162
- package/dist/subagents/agentRegistry.js.map +0 -1
- package/dist/utils/frontmatter.d.ts +0 -10
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js +0 -78
- package/dist/utils/frontmatter.js.map +0 -1
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
-
* - One bottom-pinned chat box for the entire session (no inline anchors)
|
|
7
6
|
* - Native bracketed paste support (no heuristics)
|
|
8
7
|
* - Clean cursor model with render-time wrapping
|
|
9
8
|
* - State machine for different input modes
|
|
10
9
|
* - No readline dependency for display
|
|
11
10
|
*/
|
|
12
11
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
14
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
15
|
-
import {
|
|
16
|
-
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
17
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
14
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
18
15
|
// ANSI escape codes
|
|
19
16
|
const ESC = {
|
|
20
17
|
// Cursor control
|
|
@@ -24,6 +21,9 @@ const ESC = {
|
|
|
24
21
|
SHOW: '\x1b[?25h',
|
|
25
22
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
26
23
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
+
// Screen control
|
|
25
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
+
HOME: '\x1b[H',
|
|
27
27
|
// Line control
|
|
28
28
|
CLEAR_LINE: '\x1b[2K',
|
|
29
29
|
CLEAR_TO_END: '\x1b[0J',
|
|
@@ -69,49 +69,56 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
69
|
statusMessage = null;
|
|
70
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
71
|
streamingLabel = null; // Streaming progress indicator
|
|
72
|
-
|
|
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
|
-
reservedLines = 2;
|
|
72
|
+
reservedLines = 6; // Lines reserved for input area at bottom
|
|
78
73
|
scrollRegionActive = false;
|
|
79
74
|
lastRenderContent = '';
|
|
80
75
|
lastRenderCursor = -1;
|
|
81
76
|
renderDirty = false;
|
|
82
77
|
isRendering = false;
|
|
83
78
|
pinnedTopRows = 0;
|
|
79
|
+
inlineAnchorRow = null;
|
|
80
|
+
inlineLayout = false;
|
|
81
|
+
anchorProvider = null;
|
|
82
|
+
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
83
|
+
flowMode = true;
|
|
84
|
+
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
85
|
+
contentEndRow = 0; // Row where content ends (for idle mode positioning)
|
|
86
|
+
// Command suggestions (Claude Code style auto-complete)
|
|
87
|
+
commandSuggestions = [];
|
|
88
|
+
filteredSuggestions = [];
|
|
89
|
+
selectedSuggestionIndex = 0;
|
|
90
|
+
showSuggestions = false;
|
|
91
|
+
maxVisibleSuggestions = 10;
|
|
84
92
|
// Lifecycle
|
|
85
93
|
disposed = false;
|
|
86
94
|
enabled = true;
|
|
87
95
|
contextUsage = null;
|
|
88
|
-
contextAutoCompactThreshold = 90;
|
|
89
|
-
// Track current content row in scroll region (starts at top, moves down)
|
|
90
|
-
contentRow = 1;
|
|
91
|
-
thinkingModeLabel = null;
|
|
92
96
|
editMode = 'display-edits';
|
|
93
97
|
verificationEnabled = true;
|
|
94
98
|
autoContinueEnabled = false;
|
|
95
99
|
verificationHotkey = 'alt+v';
|
|
96
100
|
autoContinueHotkey = 'alt+c';
|
|
97
|
-
thinkingHotkey = '/thinking';
|
|
98
|
-
modelLabel = null;
|
|
99
|
-
providerLabel = null;
|
|
100
101
|
// Output interceptor cleanup
|
|
101
102
|
outputInterceptorCleanup;
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Metrics tracking for status bar
|
|
104
|
+
streamingStartTime = null;
|
|
105
|
+
tokensUsed = 0;
|
|
106
|
+
thinkingEnabled = true;
|
|
107
|
+
modelInfo = null; // Provider · Model info
|
|
108
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
105
109
|
streamingRenderTimer = null;
|
|
110
|
+
// Unified UI initialization flag
|
|
111
|
+
unifiedUIInitialized = false;
|
|
106
112
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
113
|
super();
|
|
108
114
|
this.out = writeStream;
|
|
115
|
+
// Use schema defaults for configuration consistency
|
|
109
116
|
this.config = {
|
|
110
|
-
maxLines: config.maxLines ??
|
|
111
|
-
maxLength: config.maxLength ??
|
|
117
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
118
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
112
119
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
113
|
-
promptChar: config.promptChar ??
|
|
114
|
-
continuationChar: config.continuationChar ??
|
|
120
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
121
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
124
|
// ===========================================================================
|
|
@@ -190,45 +197,453 @@ export class TerminalInput extends EventEmitter {
|
|
|
190
197
|
if (handled)
|
|
191
198
|
return;
|
|
192
199
|
}
|
|
200
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
201
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
202
|
+
this.emit('showHelp');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
193
205
|
// Insert printable characters
|
|
194
206
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
207
|
this.insertText(str);
|
|
196
208
|
}
|
|
197
209
|
}
|
|
210
|
+
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
211
|
+
bannerContent = null;
|
|
212
|
+
/**
|
|
213
|
+
* Set banner content to be written when unified UI initializes.
|
|
214
|
+
*/
|
|
215
|
+
setBannerContent(content) {
|
|
216
|
+
this.bannerContent = content;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Initialize the unified UI system.
|
|
220
|
+
*
|
|
221
|
+
* Creates a floating bottom chat box:
|
|
222
|
+
* 1. Clear screen
|
|
223
|
+
* 2. Render input area at row 1 (floating at "bottom" of empty content)
|
|
224
|
+
* 3. Track input area position
|
|
225
|
+
* 4. Banner will be streamed, pushing input area down
|
|
226
|
+
*/
|
|
227
|
+
initializeUnifiedUI() {
|
|
228
|
+
if (this.unifiedUIInitialized) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Reserve lines for input area
|
|
232
|
+
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
233
|
+
// Hide cursor during setup
|
|
234
|
+
this.write(ESC.HIDE);
|
|
235
|
+
// Clear screen
|
|
236
|
+
this.write(ESC.HOME);
|
|
237
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
238
|
+
// Position at top
|
|
239
|
+
this.write(ESC.TO(1, 1));
|
|
240
|
+
// Stream banner first (if set)
|
|
241
|
+
if (this.bannerContent) {
|
|
242
|
+
process.stdout.write(this.bannerContent + '\n\n');
|
|
243
|
+
}
|
|
244
|
+
// Mark unified UI as initialized
|
|
245
|
+
this.unifiedUIInitialized = true;
|
|
246
|
+
// Render input area at current cursor position (floating after content)
|
|
247
|
+
this.inputAreaStartRow = this.getCurrentRow();
|
|
248
|
+
this.renderFloatingInputArea();
|
|
249
|
+
// Show cursor
|
|
250
|
+
this.write(ESC.SHOW);
|
|
251
|
+
}
|
|
252
|
+
// Track where the floating input area starts
|
|
253
|
+
inputAreaStartRow = 1;
|
|
254
|
+
/**
|
|
255
|
+
* Get approximate current cursor row (based on content)
|
|
256
|
+
*/
|
|
257
|
+
getCurrentRow() {
|
|
258
|
+
// We can't easily query cursor position, so track it
|
|
259
|
+
// For now, estimate based on banner content
|
|
260
|
+
if (this.bannerContent) {
|
|
261
|
+
return this.bannerContent.split('\n').length + 2;
|
|
262
|
+
}
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Render floating input area at tracked position.
|
|
267
|
+
* Updates in place using absolute positioning.
|
|
268
|
+
*/
|
|
269
|
+
renderFloatingInputArea() {
|
|
270
|
+
const { cols } = this.getSize();
|
|
271
|
+
const divider = '─'.repeat(cols - 1);
|
|
272
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
273
|
+
const startRow = this.inputAreaStartRow;
|
|
274
|
+
// Hide cursor during update
|
|
275
|
+
this.write(ESC.HIDE);
|
|
276
|
+
let currentRow = startRow;
|
|
277
|
+
// Clear input area lines (using absolute positioning)
|
|
278
|
+
for (let i = 0; i < this.reservedLines; i++) {
|
|
279
|
+
this.write(ESC.TO(startRow + i, 1));
|
|
280
|
+
this.write(ESC.CLEAR_LINE);
|
|
281
|
+
}
|
|
282
|
+
// Status bar
|
|
283
|
+
this.write(ESC.TO(currentRow, 1));
|
|
284
|
+
this.write(this.buildStatusBar(cols));
|
|
285
|
+
currentRow++;
|
|
286
|
+
// Model info line (if set)
|
|
287
|
+
if (this.modelInfo) {
|
|
288
|
+
this.write(ESC.TO(currentRow, 1));
|
|
289
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
290
|
+
if (this.contextUsage !== null) {
|
|
291
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
292
|
+
if (rem < 10)
|
|
293
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
294
|
+
else if (rem < 25)
|
|
295
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
296
|
+
else
|
|
297
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
298
|
+
}
|
|
299
|
+
this.write(modelLine);
|
|
300
|
+
currentRow++;
|
|
301
|
+
}
|
|
302
|
+
// Top divider
|
|
303
|
+
this.write(ESC.TO(currentRow, 1));
|
|
304
|
+
this.write(divider);
|
|
305
|
+
currentRow++;
|
|
306
|
+
// Input line with prompt and buffer content
|
|
307
|
+
const { lines } = this.wrapBuffer(cols - 4);
|
|
308
|
+
const displayLine = lines[0] ?? '';
|
|
309
|
+
this.write(ESC.TO(currentRow, 1));
|
|
310
|
+
this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
311
|
+
this.write(ESC.BG_DARK + displayLine);
|
|
312
|
+
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
313
|
+
if (padding > 0)
|
|
314
|
+
this.write(' '.repeat(padding));
|
|
315
|
+
this.write(ESC.RESET);
|
|
316
|
+
// Position cursor in input field
|
|
317
|
+
const cursorCol = this.config.promptChar.length + Math.min(this.cursor, displayLine.length) + 1;
|
|
318
|
+
this.write(ESC.TO(currentRow, cursorCol));
|
|
319
|
+
currentRow++;
|
|
320
|
+
// Bottom divider
|
|
321
|
+
this.write(ESC.TO(currentRow, 1));
|
|
322
|
+
this.write(divider);
|
|
323
|
+
currentRow++;
|
|
324
|
+
// Mode controls
|
|
325
|
+
this.write(ESC.TO(currentRow, 1));
|
|
326
|
+
this.write(this.buildModeControls(cols));
|
|
327
|
+
// Position cursor back in input line
|
|
328
|
+
const inputRow = startRow + (this.modelInfo ? 3 : 2);
|
|
329
|
+
const inputCol = this.config.promptChar.length + Math.min(this.cursor, displayLine.length) + 1;
|
|
330
|
+
this.write(ESC.TO(inputRow, inputCol));
|
|
331
|
+
this.write(ESC.SHOW);
|
|
332
|
+
// Update tracking
|
|
333
|
+
this.lastRenderContent = this.buffer;
|
|
334
|
+
this.lastRenderCursor = this.cursor;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Render input area at a specific row (inline, not pinned to bottom).
|
|
338
|
+
* Used on launch for compact layout.
|
|
339
|
+
*/
|
|
340
|
+
renderInlineInputArea(startRow) {
|
|
341
|
+
const { cols } = this.getSize();
|
|
342
|
+
const divider = '─'.repeat(cols - 1);
|
|
343
|
+
// Move to start row
|
|
344
|
+
this.write(ESC.TO(startRow, 1));
|
|
345
|
+
// Status bar
|
|
346
|
+
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
347
|
+
// Model info line (if set)
|
|
348
|
+
if (this.modelInfo) {
|
|
349
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
350
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
351
|
+
if (this.contextUsage !== null) {
|
|
352
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
353
|
+
if (rem < 10)
|
|
354
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
355
|
+
else if (rem < 25)
|
|
356
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
357
|
+
else
|
|
358
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
359
|
+
}
|
|
360
|
+
process.stdout.write(modelLine + '\n');
|
|
361
|
+
}
|
|
362
|
+
// Top divider
|
|
363
|
+
process.stdout.write(divider + '\n');
|
|
364
|
+
// Input line with prompt
|
|
365
|
+
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
366
|
+
process.stdout.write(ESC.BG_DARK + ' '.repeat(Math.max(0, cols - this.config.promptChar.length - 1)) + ESC.RESET + '\n');
|
|
367
|
+
// Bottom divider
|
|
368
|
+
process.stdout.write(divider + '\n');
|
|
369
|
+
// Mode controls
|
|
370
|
+
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
371
|
+
// Position cursor in input area
|
|
372
|
+
this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Render input area at current cursor position (inline, not pinned).
|
|
376
|
+
* Used after streaming ends - renders input below the streamed content.
|
|
377
|
+
*/
|
|
378
|
+
renderInlineInputAreaAtCursor() {
|
|
379
|
+
const { cols } = this.getSize();
|
|
380
|
+
const divider = '─'.repeat(cols - 1);
|
|
381
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
382
|
+
// Status bar - shows "Type a message" hint
|
|
383
|
+
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
384
|
+
// Model info line (if set)
|
|
385
|
+
if (this.modelInfo) {
|
|
386
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
387
|
+
if (this.contextUsage !== null) {
|
|
388
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
389
|
+
if (rem < 10)
|
|
390
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
391
|
+
else if (rem < 25)
|
|
392
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
393
|
+
else
|
|
394
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
395
|
+
}
|
|
396
|
+
process.stdout.write(modelLine + '\n');
|
|
397
|
+
}
|
|
398
|
+
// Top divider
|
|
399
|
+
process.stdout.write(divider + '\n');
|
|
400
|
+
// Input line with prompt and any buffer content
|
|
401
|
+
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
402
|
+
const displayLine = lines[0] ?? '';
|
|
403
|
+
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
404
|
+
process.stdout.write(ESC.BG_DARK + displayLine);
|
|
405
|
+
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
406
|
+
if (padding > 0)
|
|
407
|
+
process.stdout.write(' '.repeat(padding));
|
|
408
|
+
process.stdout.write(ESC.RESET + '\n');
|
|
409
|
+
// Bottom divider
|
|
410
|
+
process.stdout.write(divider + '\n');
|
|
411
|
+
// Mode controls
|
|
412
|
+
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
413
|
+
// Show cursor
|
|
414
|
+
this.write(ESC.SHOW);
|
|
415
|
+
// Update tracking
|
|
416
|
+
this.lastRenderContent = this.buffer;
|
|
417
|
+
this.lastRenderCursor = this.cursor;
|
|
418
|
+
}
|
|
198
419
|
/**
|
|
199
420
|
* Set the input mode
|
|
200
421
|
*
|
|
201
|
-
* Streaming
|
|
202
|
-
*
|
|
422
|
+
* Streaming mode: Input area stays at tracked position.
|
|
423
|
+
* Content streams above the input area.
|
|
424
|
+
* After streaming ends, update input area position.
|
|
203
425
|
*/
|
|
204
426
|
setMode(mode) {
|
|
205
427
|
const prevMode = this.mode;
|
|
206
428
|
this.mode = mode;
|
|
207
429
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
208
|
-
//
|
|
209
|
-
this.
|
|
210
|
-
|
|
430
|
+
// Track streaming start time for elapsed display
|
|
431
|
+
this.streamingStartTime = Date.now();
|
|
432
|
+
// Ensure unified UI is initialized (if not already done on launch)
|
|
433
|
+
if (!this.unifiedUIInitialized) {
|
|
434
|
+
this.initializeUnifiedUI();
|
|
435
|
+
}
|
|
436
|
+
// Clear the input area visual before streaming starts
|
|
437
|
+
// Content will stream where the input area was
|
|
438
|
+
this.clearInputAreaVisual();
|
|
211
439
|
this.renderDirty = true;
|
|
212
|
-
this.render();
|
|
213
440
|
}
|
|
214
441
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
442
|
+
// Stop streaming render timer (if any)
|
|
443
|
+
if (this.streamingRenderTimer) {
|
|
444
|
+
clearInterval(this.streamingRenderTimer);
|
|
445
|
+
this.streamingRenderTimer = null;
|
|
446
|
+
}
|
|
447
|
+
// Reset streaming time
|
|
448
|
+
this.streamingStartTime = null;
|
|
449
|
+
// Reset flow mode tracking
|
|
450
|
+
this.flowModeRenderedLines = 0;
|
|
451
|
+
// Add spacing after streamed content
|
|
452
|
+
this.write('\n\n');
|
|
453
|
+
// Update input area position to after the new content
|
|
454
|
+
// Estimate new row based on terminal state
|
|
455
|
+
this.inputAreaStartRow = this.estimateCurrentRow();
|
|
456
|
+
// Render input area at new position
|
|
457
|
+
writeLock.withLock(() => {
|
|
458
|
+
this.renderFloatingInputArea();
|
|
459
|
+
}, 'terminalInput.streamingEnd');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Clear the input area visual (before streaming replaces it with content)
|
|
464
|
+
*/
|
|
465
|
+
clearInputAreaVisual() {
|
|
466
|
+
const startRow = this.inputAreaStartRow;
|
|
467
|
+
// Clear the lines where input area was
|
|
468
|
+
for (let i = 0; i < this.reservedLines; i++) {
|
|
469
|
+
this.write(ESC.TO(startRow + i, 1));
|
|
470
|
+
this.write(ESC.CLEAR_LINE);
|
|
219
471
|
}
|
|
472
|
+
// Position cursor at start for streaming content
|
|
473
|
+
this.write(ESC.TO(startRow, 1));
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Estimate current cursor row after streaming
|
|
477
|
+
*/
|
|
478
|
+
estimateCurrentRow() {
|
|
479
|
+
// This is approximate - we track based on inputAreaStartRow
|
|
480
|
+
// After streaming, content has replaced the input area and possibly more
|
|
481
|
+
// For now, assume content added some lines
|
|
482
|
+
return this.inputAreaStartRow + 5; // Rough estimate
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Enable or disable flow mode.
|
|
486
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
487
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
488
|
+
*/
|
|
489
|
+
setFlowMode(enabled) {
|
|
490
|
+
if (this.flowMode === enabled)
|
|
491
|
+
return;
|
|
492
|
+
this.flowMode = enabled;
|
|
493
|
+
this.renderDirty = true;
|
|
494
|
+
this.scheduleRender();
|
|
220
495
|
}
|
|
221
496
|
/**
|
|
222
|
-
*
|
|
497
|
+
* Check if flow mode is enabled.
|
|
498
|
+
*/
|
|
499
|
+
isFlowMode() {
|
|
500
|
+
return this.flowMode;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Set the row where content ends (for idle mode positioning).
|
|
504
|
+
* Input area will render starting from this row + 1.
|
|
505
|
+
*/
|
|
506
|
+
setContentEndRow(row) {
|
|
507
|
+
this.contentEndRow = Math.max(0, row);
|
|
508
|
+
this.renderDirty = true;
|
|
509
|
+
this.scheduleRender();
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Set available slash commands for auto-complete suggestions.
|
|
513
|
+
*/
|
|
514
|
+
setCommands(commands) {
|
|
515
|
+
this.commandSuggestions = commands;
|
|
516
|
+
this.updateSuggestions();
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Update filtered suggestions based on current input.
|
|
520
|
+
*/
|
|
521
|
+
updateSuggestions() {
|
|
522
|
+
const input = this.buffer.trim();
|
|
523
|
+
// Only show suggestions when input starts with "/"
|
|
524
|
+
if (!input.startsWith('/')) {
|
|
525
|
+
this.showSuggestions = false;
|
|
526
|
+
this.filteredSuggestions = [];
|
|
527
|
+
this.selectedSuggestionIndex = 0;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const query = input.toLowerCase();
|
|
531
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
532
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
533
|
+
// Show suggestions if we have matches
|
|
534
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
535
|
+
// Keep selection in bounds
|
|
536
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
537
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Select next suggestion (arrow down / tab).
|
|
542
|
+
*/
|
|
543
|
+
selectNextSuggestion() {
|
|
544
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
545
|
+
return;
|
|
546
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
547
|
+
this.renderDirty = true;
|
|
548
|
+
this.scheduleRender();
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
552
|
+
*/
|
|
553
|
+
selectPrevSuggestion() {
|
|
554
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
555
|
+
return;
|
|
556
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
557
|
+
? this.filteredSuggestions.length - 1
|
|
558
|
+
: this.selectedSuggestionIndex - 1;
|
|
559
|
+
this.renderDirty = true;
|
|
560
|
+
this.scheduleRender();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Accept current suggestion and insert into buffer.
|
|
564
|
+
*/
|
|
565
|
+
acceptSuggestion() {
|
|
566
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
567
|
+
return false;
|
|
568
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
569
|
+
if (!selected)
|
|
570
|
+
return false;
|
|
571
|
+
// Replace buffer with selected command
|
|
572
|
+
this.buffer = selected.command + ' ';
|
|
573
|
+
this.cursor = this.buffer.length;
|
|
574
|
+
this.showSuggestions = false;
|
|
575
|
+
this.renderDirty = true;
|
|
576
|
+
this.scheduleRender();
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Check if suggestions are visible.
|
|
581
|
+
*/
|
|
582
|
+
areSuggestionsVisible() {
|
|
583
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Update token count for metrics display
|
|
587
|
+
*/
|
|
588
|
+
setTokensUsed(tokens) {
|
|
589
|
+
this.tokensUsed = tokens;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Toggle thinking/reasoning mode
|
|
593
|
+
*/
|
|
594
|
+
toggleThinking() {
|
|
595
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
596
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
597
|
+
this.scheduleRender();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Get thinking enabled state
|
|
601
|
+
*/
|
|
602
|
+
isThinkingEnabled() {
|
|
603
|
+
return this.thinkingEnabled;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Keep the top N rows pinned (used for the launch banner tracking).
|
|
607
|
+
* Note: No longer uses scroll regions - inline rendering only.
|
|
223
608
|
*/
|
|
224
609
|
setPinnedHeaderLines(count) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
610
|
+
this.pinnedTopRows = count;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
614
|
+
* restore the default bottom-aligned layout.
|
|
615
|
+
*/
|
|
616
|
+
setInlineAnchor(row) {
|
|
617
|
+
if (row === null || row === undefined) {
|
|
618
|
+
this.inlineAnchorRow = null;
|
|
619
|
+
this.inlineLayout = false;
|
|
620
|
+
this.renderDirty = true;
|
|
621
|
+
this.render();
|
|
622
|
+
return;
|
|
231
623
|
}
|
|
624
|
+
const { rows } = this.getSize();
|
|
625
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
626
|
+
this.inlineAnchorRow = clamped;
|
|
627
|
+
this.inlineLayout = true;
|
|
628
|
+
this.renderDirty = true;
|
|
629
|
+
this.render();
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
633
|
+
* output by re-evaluating the anchor before each render.
|
|
634
|
+
*/
|
|
635
|
+
setInlineAnchorProvider(provider) {
|
|
636
|
+
this.anchorProvider = provider;
|
|
637
|
+
if (!provider) {
|
|
638
|
+
this.inlineLayout = false;
|
|
639
|
+
this.inlineAnchorRow = null;
|
|
640
|
+
this.renderDirty = true;
|
|
641
|
+
this.render();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
this.inlineLayout = true;
|
|
645
|
+
this.renderDirty = true;
|
|
646
|
+
this.render();
|
|
232
647
|
}
|
|
233
648
|
/**
|
|
234
649
|
* Get current mode
|
|
@@ -261,14 +676,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
261
676
|
}
|
|
262
677
|
/**
|
|
263
678
|
* Clear the buffer
|
|
679
|
+
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
264
680
|
*/
|
|
265
|
-
clear() {
|
|
681
|
+
clear(skipRender = false) {
|
|
266
682
|
this.buffer = '';
|
|
267
683
|
this.cursor = 0;
|
|
268
684
|
this.historyIndex = -1;
|
|
269
685
|
this.tempInput = '';
|
|
270
686
|
this.pastePlaceholders = [];
|
|
271
|
-
|
|
687
|
+
if (!skipRender) {
|
|
688
|
+
this.scheduleRender();
|
|
689
|
+
}
|
|
272
690
|
}
|
|
273
691
|
/**
|
|
274
692
|
* Get queued inputs
|
|
@@ -339,37 +757,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
339
757
|
this.streamingLabel = next;
|
|
340
758
|
this.scheduleRender();
|
|
341
759
|
}
|
|
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
760
|
/**
|
|
374
761
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
375
762
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -379,22 +766,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
379
766
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
380
767
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
381
768
|
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
769
|
if (this.verificationEnabled === nextVerification &&
|
|
385
770
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
386
771
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
387
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
388
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
389
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
772
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
390
773
|
return;
|
|
391
774
|
}
|
|
392
775
|
this.verificationEnabled = nextVerification;
|
|
393
776
|
this.autoContinueEnabled = nextAutoContinue;
|
|
394
777
|
this.verificationHotkey = nextVerifyHotkey;
|
|
395
778
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
396
|
-
this.
|
|
397
|
-
|
|
779
|
+
this.scheduleRender();
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
783
|
+
* This is displayed persistently above the input area.
|
|
784
|
+
*/
|
|
785
|
+
setModelInfo(info) {
|
|
786
|
+
if (this.modelInfo === info)
|
|
787
|
+
return;
|
|
788
|
+
this.modelInfo = info;
|
|
398
789
|
this.scheduleRender();
|
|
399
790
|
}
|
|
400
791
|
/**
|
|
@@ -407,161 +798,34 @@ export class TerminalInput extends EventEmitter {
|
|
|
407
798
|
this.scheduleRender();
|
|
408
799
|
}
|
|
409
800
|
/**
|
|
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
|
|
801
|
+
* Render the input area
|
|
424
802
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* naturally above while elapsed time and status stay fresh.
|
|
803
|
+
* Uses floating input area that updates in place at tracked position.
|
|
804
|
+
* During streaming, still renders to keep UI responsive.
|
|
428
805
|
*/
|
|
429
806
|
render() {
|
|
430
807
|
if (!this.canRender())
|
|
431
808
|
return;
|
|
432
809
|
if (this.isRendering)
|
|
433
810
|
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
811
|
const shouldSkip = !this.renderDirty &&
|
|
447
812
|
this.buffer === this.lastRenderContent &&
|
|
448
813
|
this.cursor === this.lastRenderCursor;
|
|
449
814
|
this.renderDirty = false;
|
|
450
|
-
// Skip if nothing changed
|
|
815
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
451
816
|
if (shouldSkip) {
|
|
452
817
|
return;
|
|
453
818
|
}
|
|
454
|
-
// If write lock is held, defer render
|
|
819
|
+
// If write lock is held, defer render
|
|
455
820
|
if (writeLock.isLocked()) {
|
|
456
821
|
writeLock.safeWrite(() => this.render());
|
|
457
822
|
return;
|
|
458
823
|
}
|
|
459
|
-
const performRender = () => {
|
|
460
|
-
if (!this.scrollRegionActive) {
|
|
461
|
-
this.enableScrollRegion();
|
|
462
|
-
}
|
|
463
|
-
const { rows, cols } = this.getSize();
|
|
464
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
465
|
-
// Wrap buffer into display lines
|
|
466
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
467
|
-
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
468
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
469
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
470
|
-
const metaLines = this.buildMetaLines(cols - 2);
|
|
471
|
-
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
472
|
-
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
473
|
-
// Calculate display window (keep cursor visible)
|
|
474
|
-
let startLine = 0;
|
|
475
|
-
if (lines.length > displayLines) {
|
|
476
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
477
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
478
|
-
}
|
|
479
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
480
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
481
|
-
// Hide cursor during render to prevent flicker
|
|
482
|
-
this.write(ESC.HIDE);
|
|
483
|
-
this.write(ESC.RESET);
|
|
484
|
-
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
485
|
-
let currentRow = startRow;
|
|
486
|
-
// Clear the reserved block to avoid stale meta/status lines
|
|
487
|
-
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
488
|
-
// Meta/status header (elapsed, tokens/context)
|
|
489
|
-
for (const metaLine of metaLines) {
|
|
490
|
-
this.write(ESC.TO(currentRow, 1));
|
|
491
|
-
this.write(ESC.CLEAR_LINE);
|
|
492
|
-
this.write(metaLine);
|
|
493
|
-
currentRow += 1;
|
|
494
|
-
}
|
|
495
|
-
// Separator line
|
|
496
|
-
this.write(ESC.TO(currentRow, 1));
|
|
497
|
-
this.write(ESC.CLEAR_LINE);
|
|
498
|
-
const divider = renderDivider(cols - 2);
|
|
499
|
-
this.write(divider);
|
|
500
|
-
currentRow += 1;
|
|
501
|
-
// Render input lines
|
|
502
|
-
let finalRow = currentRow;
|
|
503
|
-
let finalCol = 3;
|
|
504
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
505
|
-
const rowNum = currentRow + i;
|
|
506
|
-
this.write(ESC.TO(rowNum, 1));
|
|
507
|
-
this.write(ESC.CLEAR_LINE);
|
|
508
|
-
const line = visibleLines[i] ?? '';
|
|
509
|
-
const absoluteLineIdx = startLine + i;
|
|
510
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
511
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
512
|
-
// Background
|
|
513
|
-
this.write(ESC.BG_DARK);
|
|
514
|
-
// Prompt prefix
|
|
515
|
-
this.write(ESC.DIM);
|
|
516
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
517
|
-
this.write(ESC.RESET);
|
|
518
|
-
this.write(ESC.BG_DARK);
|
|
519
|
-
if (isCursorLine) {
|
|
520
|
-
// Render with block cursor
|
|
521
|
-
const col = Math.min(cursorCol, line.length);
|
|
522
|
-
const before = line.slice(0, col);
|
|
523
|
-
const at = col < line.length ? line[col] : ' ';
|
|
524
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
525
|
-
this.write(before);
|
|
526
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
527
|
-
this.write(at);
|
|
528
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
529
|
-
this.write(after);
|
|
530
|
-
finalRow = rowNum;
|
|
531
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
this.write(line);
|
|
535
|
-
}
|
|
536
|
-
// Pad to edge for clean look
|
|
537
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
538
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
539
|
-
if (padding > 0)
|
|
540
|
-
this.write(' '.repeat(padding));
|
|
541
|
-
this.write(ESC.RESET);
|
|
542
|
-
}
|
|
543
|
-
// Mode controls line (Claude Code style)
|
|
544
|
-
const controlRow = currentRow + visibleLines.length;
|
|
545
|
-
this.write(ESC.TO(controlRow, 1));
|
|
546
|
-
this.write(ESC.CLEAR_LINE);
|
|
547
|
-
this.write(this.buildModeControls(cols));
|
|
548
|
-
// Position cursor in the input box for user editing
|
|
549
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
550
|
-
this.write(ESC.SHOW);
|
|
551
|
-
// Update state
|
|
552
|
-
this.lastRenderContent = this.buffer;
|
|
553
|
-
this.lastRenderCursor = this.cursor;
|
|
554
|
-
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
555
|
-
if (this.streamingRenderTimer) {
|
|
556
|
-
clearTimeout(this.streamingRenderTimer);
|
|
557
|
-
this.streamingRenderTimer = null;
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
// Use write lock during render to prevent interleaved output
|
|
561
|
-
writeLock.lock('terminalInput.render');
|
|
562
824
|
this.isRendering = true;
|
|
825
|
+
writeLock.lock('terminalInput.render');
|
|
563
826
|
try {
|
|
564
|
-
|
|
827
|
+
// Render floating input area at tracked position
|
|
828
|
+
this.renderFloatingInputArea();
|
|
565
829
|
}
|
|
566
830
|
finally {
|
|
567
831
|
writeLock.unlock();
|
|
@@ -569,228 +833,99 @@ export class TerminalInput extends EventEmitter {
|
|
|
569
833
|
}
|
|
570
834
|
}
|
|
571
835
|
/**
|
|
572
|
-
* Build
|
|
573
|
-
*
|
|
836
|
+
* Build status bar showing streaming/ready status and key info.
|
|
837
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
574
838
|
*/
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
//
|
|
579
|
-
if (this.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (streamingActive) {
|
|
587
|
-
const parts = [];
|
|
588
|
-
// Essential streaming info
|
|
589
|
-
if (this.metaThinkingMs !== null) {
|
|
590
|
-
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
591
|
-
}
|
|
592
|
-
if (this.metaElapsedSeconds !== null) {
|
|
593
|
-
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
594
|
-
}
|
|
595
|
-
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
596
|
-
if (parts.length) {
|
|
597
|
-
lines.push(renderStatusLine(parts, width));
|
|
839
|
+
buildStatusBar(cols) {
|
|
840
|
+
const maxWidth = cols - 2;
|
|
841
|
+
const parts = [];
|
|
842
|
+
// Streaming status with elapsed time (left side)
|
|
843
|
+
if (this.mode === 'streaming') {
|
|
844
|
+
let statusText = '● Streaming';
|
|
845
|
+
if (this.streamingStartTime) {
|
|
846
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
847
|
+
const mins = Math.floor(elapsed / 60);
|
|
848
|
+
const secs = elapsed % 60;
|
|
849
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
598
850
|
}
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
// Non-streaming: show full status info (model line already added above)
|
|
602
|
-
if (this.metaThinkingMs !== null) {
|
|
603
|
-
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
604
|
-
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
605
|
-
}
|
|
606
|
-
const statusParts = [];
|
|
607
|
-
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
608
|
-
if (statusLabel) {
|
|
609
|
-
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
851
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
610
852
|
}
|
|
611
|
-
|
|
612
|
-
|
|
853
|
+
// Queue indicator during streaming
|
|
854
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
855
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
613
856
|
}
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
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' });
|
|
857
|
+
// Paste indicator
|
|
858
|
+
if (this.pastePlaceholders.length > 0) {
|
|
859
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
860
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
626
861
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
862
|
+
// Override/warning status
|
|
863
|
+
if (this.overrideStatusMessage) {
|
|
864
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
631
865
|
}
|
|
632
|
-
|
|
633
|
-
|
|
866
|
+
// If idle with empty buffer, show quick shortcuts
|
|
867
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
868
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
634
869
|
}
|
|
635
|
-
|
|
636
|
-
|
|
870
|
+
// Multi-line indicator
|
|
871
|
+
if (this.buffer.includes('\n')) {
|
|
872
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
637
873
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
642
|
-
*/
|
|
643
|
-
clearReservedArea(startRow, reservedLines, cols) {
|
|
644
|
-
const width = Math.max(1, cols);
|
|
645
|
-
for (let i = 0; i < reservedLines; i++) {
|
|
646
|
-
const row = startRow + i;
|
|
647
|
-
this.write(ESC.TO(row, 1));
|
|
648
|
-
this.write(' '.repeat(width));
|
|
874
|
+
if (parts.length === 0) {
|
|
875
|
+
return ''; // Empty status bar when idle
|
|
649
876
|
}
|
|
877
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
878
|
+
return joined.slice(0, maxWidth);
|
|
650
879
|
}
|
|
651
880
|
/**
|
|
652
|
-
* Build
|
|
653
|
-
*
|
|
881
|
+
* Build mode controls line showing toggles and context info.
|
|
882
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
883
|
+
*
|
|
884
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
654
885
|
*/
|
|
655
886
|
buildModeControls(cols) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (this.
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
if (this.statusMessage) {
|
|
666
|
-
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
667
|
-
}
|
|
668
|
-
const editHotkey = this.formatHotkey('shift+tab');
|
|
669
|
-
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
670
|
-
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
671
|
-
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
672
|
-
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
673
|
-
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
674
|
-
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
675
|
-
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
676
|
-
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
677
|
-
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
678
|
-
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
679
|
-
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
680
|
-
}
|
|
681
|
-
if (this.buffer.includes('\n')) {
|
|
682
|
-
const lineCount = this.buffer.split('\n').length;
|
|
683
|
-
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
684
|
-
}
|
|
685
|
-
if (this.pastePlaceholders.length > 0) {
|
|
686
|
-
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
687
|
-
leftParts.push({
|
|
688
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
689
|
-
tone: 'info',
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
const contextRemaining = this.computeContextRemaining();
|
|
693
|
-
if (this.thinkingModeLabel) {
|
|
694
|
-
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
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`;
|
|
887
|
+
const maxWidth = cols - 2;
|
|
888
|
+
// Use schema-defined colors for consistency
|
|
889
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
890
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
891
|
+
const toggles = [];
|
|
892
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
893
|
+
if (this.editMode === 'display-edits') {
|
|
894
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
763
895
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
896
|
+
else {
|
|
897
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
898
|
+
}
|
|
899
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
900
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
901
|
+
// Verification (green when on) - per schema.verificationMode
|
|
902
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
903
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
904
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
905
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
906
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
907
|
+
let rightPart = '';
|
|
908
|
+
if (this.contextUsage !== null) {
|
|
909
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
910
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
911
|
+
if (rem < 10)
|
|
912
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
913
|
+
else if (rem < 25)
|
|
914
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
915
|
+
else
|
|
916
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
917
|
+
}
|
|
918
|
+
// Calculate visible lengths (strip ANSI)
|
|
919
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
920
|
+
const leftLen = strip(leftPart).length;
|
|
921
|
+
const rightLen = strip(rightPart).length;
|
|
922
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
923
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
924
|
+
}
|
|
925
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
926
|
+
return `${leftPart} ${rightPart}`;
|
|
927
|
+
}
|
|
928
|
+
return leftPart;
|
|
794
929
|
}
|
|
795
930
|
/**
|
|
796
931
|
* Force a re-render
|
|
@@ -813,19 +948,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
813
948
|
handleResize() {
|
|
814
949
|
this.lastRenderContent = '';
|
|
815
950
|
this.lastRenderCursor = -1;
|
|
816
|
-
this.resetStreamingRenderThrottle();
|
|
817
951
|
// Re-clamp pinned header rows to the new terminal height
|
|
818
952
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
819
|
-
if (this.scrollRegionActive) {
|
|
820
|
-
this.disableScrollRegion();
|
|
821
|
-
this.enableScrollRegion();
|
|
822
|
-
}
|
|
823
953
|
this.scheduleRender();
|
|
824
954
|
}
|
|
825
955
|
/**
|
|
826
956
|
* Register with display's output interceptor to position cursor correctly.
|
|
827
957
|
* When scroll region is active, output needs to go to the scroll region,
|
|
828
958
|
* not the protected bottom area where the input is rendered.
|
|
959
|
+
*
|
|
960
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
961
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
829
962
|
*/
|
|
830
963
|
registerOutputInterceptor(display) {
|
|
831
964
|
if (this.outputInterceptorCleanup) {
|
|
@@ -833,66 +966,25 @@ export class TerminalInput extends EventEmitter {
|
|
|
833
966
|
}
|
|
834
967
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
835
968
|
beforeWrite: () => {
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
if (this.scrollRegionActive) {
|
|
839
|
-
const { rows } = this.getSize();
|
|
840
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
841
|
-
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
842
|
-
this.write(ESC.SAVE);
|
|
843
|
-
this.write(ESC.TO(targetRow, 1));
|
|
844
|
-
}
|
|
969
|
+
// Scroll region handles content containment automatically
|
|
970
|
+
// No per-write cursor manipulation needed
|
|
845
971
|
},
|
|
846
|
-
afterWrite: (
|
|
847
|
-
//
|
|
848
|
-
if (this.scrollRegionActive) {
|
|
849
|
-
const { rows } = this.getSize();
|
|
850
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
851
|
-
// Count newlines in content to advance by correct amount
|
|
852
|
-
const lineCount = content ? (content.match(/\n/g) || []).length + 1 : 1;
|
|
853
|
-
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
854
|
-
this.write(ESC.RESTORE);
|
|
855
|
-
}
|
|
972
|
+
afterWrite: () => {
|
|
973
|
+
// No cursor manipulation needed
|
|
856
974
|
},
|
|
857
975
|
});
|
|
858
976
|
}
|
|
859
|
-
/**
|
|
860
|
-
* Write content directly into the scroll region (for banner, user prompts, etc.).
|
|
861
|
-
* Content starts at top and flows down, then scrolls when bottom is reached.
|
|
862
|
-
*/
|
|
863
|
-
writeToScrollRegion(content) {
|
|
864
|
-
if (!content)
|
|
865
|
-
return;
|
|
866
|
-
// Ensure scroll region is active
|
|
867
|
-
if (!this.scrollRegionActive) {
|
|
868
|
-
this.enableScrollRegion();
|
|
869
|
-
}
|
|
870
|
-
const { rows } = this.getSize();
|
|
871
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
872
|
-
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
873
|
-
// Write at current content position
|
|
874
|
-
this.write(ESC.SAVE);
|
|
875
|
-
this.write(ESC.TO(targetRow, 1));
|
|
876
|
-
this.write(content);
|
|
877
|
-
this.write(ESC.RESTORE);
|
|
878
|
-
// Advance contentRow by number of lines written
|
|
879
|
-
const lineCount = (content.match(/\n/g) || []).length + 1;
|
|
880
|
-
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Reset content position to start of scroll region.
|
|
884
|
-
* Does NOT clear the terminal - content starts from current position.
|
|
885
|
-
*/
|
|
886
|
-
resetContentPosition() {
|
|
887
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
888
|
-
this.contentRow = scrollTop;
|
|
889
|
-
}
|
|
890
977
|
/**
|
|
891
978
|
* Dispose and clean up
|
|
892
979
|
*/
|
|
893
980
|
dispose() {
|
|
894
981
|
if (this.disposed)
|
|
895
982
|
return;
|
|
983
|
+
// Clean up streaming render timer
|
|
984
|
+
if (this.streamingRenderTimer) {
|
|
985
|
+
clearInterval(this.streamingRenderTimer);
|
|
986
|
+
this.streamingRenderTimer = null;
|
|
987
|
+
}
|
|
896
988
|
// Clean up output interceptor
|
|
897
989
|
if (this.outputInterceptorCleanup) {
|
|
898
990
|
this.outputInterceptorCleanup();
|
|
@@ -900,8 +992,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
900
992
|
}
|
|
901
993
|
this.disposed = true;
|
|
902
994
|
this.enabled = false;
|
|
903
|
-
|
|
904
|
-
this.
|
|
995
|
+
// Reset scroll region if it was set
|
|
996
|
+
this.write(ESC.RESET_SCROLL);
|
|
905
997
|
this.disableBracketedPaste();
|
|
906
998
|
this.buffer = '';
|
|
907
999
|
this.queue = [];
|
|
@@ -1006,7 +1098,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1006
1098
|
this.toggleEditMode();
|
|
1007
1099
|
return true;
|
|
1008
1100
|
}
|
|
1009
|
-
|
|
1101
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1102
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1103
|
+
this.togglePasteExpansion();
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
this.toggleThinking();
|
|
1107
|
+
}
|
|
1108
|
+
return true;
|
|
1109
|
+
case 'escape':
|
|
1110
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1111
|
+
if (this.mode === 'streaming') {
|
|
1112
|
+
this.emit('interrupt');
|
|
1113
|
+
}
|
|
1114
|
+
else if (this.buffer.length > 0) {
|
|
1115
|
+
this.clear();
|
|
1116
|
+
}
|
|
1010
1117
|
return true;
|
|
1011
1118
|
}
|
|
1012
1119
|
return false;
|
|
@@ -1024,6 +1131,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1024
1131
|
this.insertPlainText(chunk, insertPos);
|
|
1025
1132
|
this.cursor = insertPos + chunk.length;
|
|
1026
1133
|
this.emit('change', this.buffer);
|
|
1134
|
+
this.updateSuggestions();
|
|
1027
1135
|
this.scheduleRender();
|
|
1028
1136
|
}
|
|
1029
1137
|
insertNewline() {
|
|
@@ -1048,6 +1156,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1048
1156
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1049
1157
|
}
|
|
1050
1158
|
this.emit('change', this.buffer);
|
|
1159
|
+
this.updateSuggestions();
|
|
1051
1160
|
this.scheduleRender();
|
|
1052
1161
|
}
|
|
1053
1162
|
deleteForward() {
|
|
@@ -1275,12 +1384,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1275
1384
|
timestamp: Date.now(),
|
|
1276
1385
|
});
|
|
1277
1386
|
this.emit('queue', text);
|
|
1278
|
-
this.clear(); // Clear immediately for queued input
|
|
1387
|
+
this.clear(); // Clear immediately for queued input, re-render to update queue display
|
|
1279
1388
|
}
|
|
1280
1389
|
else {
|
|
1281
|
-
// In idle mode, clear the input
|
|
1282
|
-
// The
|
|
1283
|
-
|
|
1390
|
+
// In idle mode, clear the input WITHOUT rendering.
|
|
1391
|
+
// The caller will display the user message and start streaming.
|
|
1392
|
+
// We'll render the input area again after streaming ends.
|
|
1393
|
+
this.clear(true); // Skip render - streaming will handle display
|
|
1284
1394
|
this.emit('submit', text);
|
|
1285
1395
|
}
|
|
1286
1396
|
}
|
|
@@ -1297,9 +1407,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1297
1407
|
if (available <= 0)
|
|
1298
1408
|
return;
|
|
1299
1409
|
const chunk = clean.slice(0, available);
|
|
1300
|
-
|
|
1301
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1302
|
-
if (isMultiline && !isShortMultiline) {
|
|
1410
|
+
if (isMultilinePaste(chunk)) {
|
|
1303
1411
|
this.insertPastePlaceholder(chunk);
|
|
1304
1412
|
}
|
|
1305
1413
|
else {
|
|
@@ -1312,41 +1420,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1312
1420
|
this.scheduleRender();
|
|
1313
1421
|
}
|
|
1314
1422
|
// ===========================================================================
|
|
1315
|
-
// SCROLL REGION
|
|
1316
|
-
// ===========================================================================
|
|
1317
|
-
enableScrollRegion() {
|
|
1318
|
-
if (this.scrollRegionActive || !this.isTTY())
|
|
1319
|
-
return;
|
|
1320
|
-
this.applyScrollRegion();
|
|
1321
|
-
this.scrollRegionActive = true;
|
|
1322
|
-
this.forceRender();
|
|
1323
|
-
}
|
|
1324
|
-
disableScrollRegion() {
|
|
1325
|
-
if (!this.scrollRegionActive)
|
|
1326
|
-
return;
|
|
1327
|
-
this.write(ESC.RESET_SCROLL);
|
|
1328
|
-
this.scrollRegionActive = false;
|
|
1329
|
-
}
|
|
1330
|
-
applyScrollRegion() {
|
|
1331
|
-
const { rows } = this.getSize();
|
|
1332
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
1333
|
-
const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
|
|
1334
|
-
this.write(ESC.SET_SCROLL(scrollTop, scrollBottom));
|
|
1335
|
-
}
|
|
1336
|
-
updateReservedLines(contentLines) {
|
|
1337
|
-
const { rows } = this.getSize();
|
|
1338
|
-
const baseLines = 2; // separator + control bar
|
|
1339
|
-
const needed = baseLines + contentLines;
|
|
1340
|
-
const maxAllowed = Math.max(baseLines, rows - 1 - this.pinnedTopRows);
|
|
1341
|
-
const newReserved = Math.min(Math.max(baseLines, needed), maxAllowed);
|
|
1342
|
-
if (newReserved !== this.reservedLines) {
|
|
1343
|
-
this.reservedLines = newReserved;
|
|
1344
|
-
if (this.scrollRegionActive) {
|
|
1345
|
-
this.applyScrollRegion();
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
// ===========================================================================
|
|
1350
1423
|
// BUFFER WRAPPING
|
|
1351
1424
|
// ===========================================================================
|
|
1352
1425
|
wrapBuffer(maxWidth) {
|
|
@@ -1470,19 +1543,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1470
1543
|
this.shiftPlaceholders(position, text.length);
|
|
1471
1544
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1472
1545
|
}
|
|
1473
|
-
shouldInlineMultiline(content) {
|
|
1474
|
-
const lines = content.split('\n').length;
|
|
1475
|
-
const maxInlineLines = 4;
|
|
1476
|
-
const maxInlineChars = 240;
|
|
1477
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1478
|
-
}
|
|
1479
1546
|
findPlaceholderAt(position) {
|
|
1480
1547
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1481
1548
|
}
|
|
1482
|
-
buildPlaceholder(
|
|
1549
|
+
buildPlaceholder(summary) {
|
|
1483
1550
|
const id = ++this.pasteCounter;
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1551
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1552
|
+
// Show first line preview (truncated)
|
|
1553
|
+
const preview = summary.preview.length > 30
|
|
1554
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1555
|
+
: summary.preview;
|
|
1556
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1486
1557
|
return { id, placeholder };
|
|
1487
1558
|
}
|
|
1488
1559
|
insertPastePlaceholder(content) {
|
|
@@ -1490,21 +1561,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1490
1561
|
if (available <= 0)
|
|
1491
1562
|
return;
|
|
1492
1563
|
const cleanContent = content.slice(0, available);
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1564
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1565
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1566
|
+
if (summary.lineCount < 5) {
|
|
1567
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1568
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1569
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1570
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1495
1574
|
const insertPos = this.cursor;
|
|
1496
1575
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1497
1576
|
this.pastePlaceholders.push({
|
|
1498
1577
|
id,
|
|
1499
1578
|
content: cleanContent,
|
|
1500
|
-
lineCount,
|
|
1579
|
+
lineCount: summary.lineCount,
|
|
1501
1580
|
placeholder,
|
|
1502
1581
|
start: insertPos,
|
|
1503
1582
|
end: insertPos + placeholder.length,
|
|
1583
|
+
summary,
|
|
1584
|
+
expanded: false,
|
|
1504
1585
|
});
|
|
1505
1586
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1506
1587
|
this.cursor = insertPos + placeholder.length;
|
|
1507
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1591
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1592
|
+
*/
|
|
1593
|
+
togglePasteExpansion() {
|
|
1594
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1595
|
+
if (!placeholder)
|
|
1596
|
+
return false;
|
|
1597
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1598
|
+
// Update the placeholder text in buffer
|
|
1599
|
+
const newPlaceholder = placeholder.expanded
|
|
1600
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1601
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1602
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1603
|
+
// Update buffer
|
|
1604
|
+
this.buffer =
|
|
1605
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1606
|
+
newPlaceholder +
|
|
1607
|
+
this.buffer.slice(placeholder.end);
|
|
1608
|
+
// Update placeholder tracking
|
|
1609
|
+
placeholder.placeholder = newPlaceholder;
|
|
1610
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1611
|
+
// Shift other placeholders
|
|
1612
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1613
|
+
this.scheduleRender();
|
|
1614
|
+
return true;
|
|
1615
|
+
}
|
|
1616
|
+
buildExpandedPlaceholder(ph) {
|
|
1617
|
+
const lines = ph.content.split('\n');
|
|
1618
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1619
|
+
const lastLines = lines.length > 5
|
|
1620
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1621
|
+
: '';
|
|
1622
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1623
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1624
|
+
}
|
|
1508
1625
|
deletePlaceholder(placeholder) {
|
|
1509
1626
|
const length = placeholder.end - placeholder.start;
|
|
1510
1627
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1512,11 +1629,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1512
1629
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1513
1630
|
this.cursor = placeholder.start;
|
|
1514
1631
|
}
|
|
1515
|
-
updateContextUsage(value
|
|
1516
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1517
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1518
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1519
|
-
}
|
|
1632
|
+
updateContextUsage(value) {
|
|
1520
1633
|
if (value === null || !Number.isFinite(value)) {
|
|
1521
1634
|
this.contextUsage = null;
|
|
1522
1635
|
}
|
|
@@ -1543,22 +1656,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1543
1656
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1544
1657
|
this.setEditMode(next);
|
|
1545
1658
|
}
|
|
1546
|
-
scheduleStreamingRender(delayMs) {
|
|
1547
|
-
if (this.streamingRenderTimer)
|
|
1548
|
-
return;
|
|
1549
|
-
const wait = Math.max(16, delayMs);
|
|
1550
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1551
|
-
this.streamingRenderTimer = null;
|
|
1552
|
-
this.render();
|
|
1553
|
-
}, wait);
|
|
1554
|
-
}
|
|
1555
|
-
resetStreamingRenderThrottle() {
|
|
1556
|
-
if (this.streamingRenderTimer) {
|
|
1557
|
-
clearTimeout(this.streamingRenderTimer);
|
|
1558
|
-
this.streamingRenderTimer = null;
|
|
1559
|
-
}
|
|
1560
|
-
this.lastStreamingRender = 0;
|
|
1561
|
-
}
|
|
1562
1659
|
scheduleRender() {
|
|
1563
1660
|
if (!this.canRender())
|
|
1564
1661
|
return;
|