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