erosolar-cli 1.7.274 → 1.7.275
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 -10
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +160 -198
- 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 +176 -74
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +879 -490
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +33 -28
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +46 -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 +8 -23
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +55 -141
- 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
|
|
@@ -24,6 +22,9 @@ const ESC = {
|
|
|
24
22
|
SHOW: '\x1b[?25h',
|
|
25
23
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
26
24
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
25
|
+
// Screen control
|
|
26
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
27
|
+
HOME: '\x1b[H',
|
|
27
28
|
// Line control
|
|
28
29
|
CLEAR_LINE: '\x1b[2K',
|
|
29
30
|
CLEAR_TO_END: '\x1b[0J',
|
|
@@ -69,11 +70,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
70
|
statusMessage = null;
|
|
70
71
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
72
|
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
73
|
reservedLines = 2;
|
|
78
74
|
scrollRegionActive = false;
|
|
79
75
|
lastRenderContent = '';
|
|
@@ -81,35 +77,51 @@ export class TerminalInput extends EventEmitter {
|
|
|
81
77
|
renderDirty = false;
|
|
82
78
|
isRendering = false;
|
|
83
79
|
pinnedTopRows = 0;
|
|
80
|
+
inlineAnchorRow = null;
|
|
81
|
+
inlineLayout = false;
|
|
82
|
+
anchorProvider = null;
|
|
83
|
+
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
84
|
+
flowMode = true;
|
|
85
|
+
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
86
|
+
contentEndRow = 0; // Row where content ends (for idle mode positioning)
|
|
87
|
+
// Command suggestions (Claude Code style auto-complete)
|
|
88
|
+
commandSuggestions = [];
|
|
89
|
+
filteredSuggestions = [];
|
|
90
|
+
selectedSuggestionIndex = 0;
|
|
91
|
+
showSuggestions = false;
|
|
92
|
+
maxVisibleSuggestions = 10;
|
|
84
93
|
// Lifecycle
|
|
85
94
|
disposed = false;
|
|
86
95
|
enabled = true;
|
|
87
96
|
contextUsage = null;
|
|
88
|
-
contextAutoCompactThreshold = 90;
|
|
89
|
-
thinkingModeLabel = null;
|
|
90
97
|
editMode = 'display-edits';
|
|
91
98
|
verificationEnabled = true;
|
|
92
99
|
autoContinueEnabled = false;
|
|
93
100
|
verificationHotkey = 'alt+v';
|
|
94
101
|
autoContinueHotkey = 'alt+c';
|
|
95
|
-
thinkingHotkey = '/thinking';
|
|
96
|
-
modelLabel = null;
|
|
97
|
-
providerLabel = null;
|
|
98
102
|
// Output interceptor cleanup
|
|
99
103
|
outputInterceptorCleanup;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
// Metrics tracking for status bar
|
|
105
|
+
streamingStartTime = null;
|
|
106
|
+
tokensUsed = 0;
|
|
107
|
+
thinkingEnabled = true;
|
|
108
|
+
modelInfo = null; // Provider · Model info
|
|
109
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
103
110
|
streamingRenderTimer = null;
|
|
111
|
+
// Banner renderer callback - called when entering streaming mode to re-render banner
|
|
112
|
+
// inside the scroll region (unified UI system)
|
|
113
|
+
bannerRenderer = null;
|
|
114
|
+
unifiedUIInitialized = false;
|
|
104
115
|
constructor(writeStream = process.stdout, config = {}) {
|
|
105
116
|
super();
|
|
106
117
|
this.out = writeStream;
|
|
118
|
+
// Use schema defaults for configuration consistency
|
|
107
119
|
this.config = {
|
|
108
|
-
maxLines: config.maxLines ??
|
|
109
|
-
maxLength: config.maxLength ??
|
|
120
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
121
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
110
122
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
111
|
-
promptChar: config.promptChar ??
|
|
112
|
-
continuationChar: config.continuationChar ??
|
|
123
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
124
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
113
125
|
};
|
|
114
126
|
}
|
|
115
127
|
// ===========================================================================
|
|
@@ -188,6 +200,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
188
200
|
if (handled)
|
|
189
201
|
return;
|
|
190
202
|
}
|
|
203
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
204
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
205
|
+
this.emit('showHelp');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
191
208
|
// Insert printable characters
|
|
192
209
|
if (str && !key?.ctrl && !key?.meta) {
|
|
193
210
|
this.insertText(str);
|
|
@@ -196,38 +213,525 @@ export class TerminalInput extends EventEmitter {
|
|
|
196
213
|
/**
|
|
197
214
|
* Set the input mode
|
|
198
215
|
*
|
|
199
|
-
* Streaming
|
|
200
|
-
*
|
|
216
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
217
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
218
|
+
* the cursor is (below the streamed content).
|
|
201
219
|
*/
|
|
202
220
|
setMode(mode) {
|
|
203
221
|
const prevMode = this.mode;
|
|
204
222
|
this.mode = mode;
|
|
205
223
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
206
|
-
//
|
|
207
|
-
this.
|
|
208
|
-
this.
|
|
224
|
+
// Track streaming start time for elapsed display
|
|
225
|
+
this.streamingStartTime = Date.now();
|
|
226
|
+
const { rows } = this.getSize();
|
|
227
|
+
// Set up scroll region to reserve bottom for persistent input area
|
|
228
|
+
this.pinnedTopRows = 0;
|
|
229
|
+
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
230
|
+
// UNIFIED UI INITIALIZATION: On first streaming transition, clear screen
|
|
231
|
+
// and re-render banner inside the scroll region for a consistent layout
|
|
232
|
+
if (!this.unifiedUIInitialized && this.bannerRenderer) {
|
|
233
|
+
// Hide cursor during screen redraw
|
|
234
|
+
this.write(ESC.HIDE);
|
|
235
|
+
// Clear screen and move to home position
|
|
236
|
+
this.write(ESC.HOME);
|
|
237
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
238
|
+
// Set up scroll region FIRST (reserve bottom for input area)
|
|
239
|
+
this.enableScrollRegion();
|
|
240
|
+
// Move to top of content area (row 1 inside scroll region)
|
|
241
|
+
this.write(ESC.TO(1, 1));
|
|
242
|
+
// Re-render banner inside the scroll region
|
|
243
|
+
const bannerLines = this.bannerRenderer();
|
|
244
|
+
// Position cursor just after banner for content flow
|
|
245
|
+
const contentStartRow = Math.max(1, bannerLines + 1);
|
|
246
|
+
this.write(ESC.TO(contentStartRow, 1));
|
|
247
|
+
// Mark unified UI as initialized
|
|
248
|
+
this.unifiedUIInitialized = true;
|
|
249
|
+
// Initial render of bottom input area
|
|
250
|
+
this.renderBottomInputArea();
|
|
251
|
+
// Show cursor
|
|
252
|
+
this.write(ESC.SHOW);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Normal streaming transition (not first time)
|
|
256
|
+
// CRITICAL: Position cursor in content area BEFORE enabling scroll region
|
|
257
|
+
// Content area is rows 1 to (rows - reservedLines)
|
|
258
|
+
// Move cursor to just after the banner (where content should appear)
|
|
259
|
+
const contentBottomRow = Math.max(1, rows - this.reservedLines);
|
|
260
|
+
this.write(ESC.TO(contentBottomRow, 1));
|
|
261
|
+
// Enable scroll region: content scrolls above, bottom is reserved
|
|
262
|
+
this.enableScrollRegion();
|
|
263
|
+
// Initial render of bottom input area (will save cursor at content area position)
|
|
264
|
+
this.renderBottomInputArea();
|
|
265
|
+
}
|
|
266
|
+
// Start timer to update bottom input area (updates elapsed time)
|
|
267
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
268
|
+
if (this.mode === 'streaming') {
|
|
269
|
+
this.updateStreamingStatus();
|
|
270
|
+
this.renderBottomInputArea();
|
|
271
|
+
}
|
|
272
|
+
}, 1000);
|
|
209
273
|
this.renderDirty = true;
|
|
210
|
-
this.render();
|
|
211
274
|
}
|
|
212
275
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
213
|
-
//
|
|
214
|
-
this.
|
|
276
|
+
// Stop streaming render timer
|
|
277
|
+
if (this.streamingRenderTimer) {
|
|
278
|
+
clearInterval(this.streamingRenderTimer);
|
|
279
|
+
this.streamingRenderTimer = null;
|
|
280
|
+
}
|
|
281
|
+
// Reset streaming time
|
|
282
|
+
this.streamingStartTime = null;
|
|
283
|
+
// Keep scroll region active for consistent bottom-pinned UI
|
|
284
|
+
// (scroll region reserves bottom for input area in all modes)
|
|
285
|
+
// Reset flow mode tracking
|
|
286
|
+
this.flowModeRenderedLines = 0;
|
|
287
|
+
// Render using unified bottom input area (same layout as streaming)
|
|
288
|
+
writeLock.withLock(() => {
|
|
289
|
+
this.renderBottomInputArea();
|
|
290
|
+
}, 'terminalInput.streamingEnd');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Update streaming status label (called by timer)
|
|
295
|
+
*/
|
|
296
|
+
updateStreamingStatus() {
|
|
297
|
+
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
298
|
+
return;
|
|
299
|
+
// Calculate elapsed time
|
|
300
|
+
const elapsed = Date.now() - this.streamingStartTime;
|
|
301
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
302
|
+
const minutes = Math.floor(seconds / 60);
|
|
303
|
+
const secs = seconds % 60;
|
|
304
|
+
// Format elapsed time
|
|
305
|
+
let elapsedStr;
|
|
306
|
+
if (minutes > 0) {
|
|
307
|
+
elapsedStr = `${minutes}m ${secs}s`;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
elapsedStr = `${secs}s`;
|
|
311
|
+
}
|
|
312
|
+
// Update streaming label
|
|
313
|
+
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Render input area - unified for streaming and normal modes.
|
|
317
|
+
*
|
|
318
|
+
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
319
|
+
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
320
|
+
*/
|
|
321
|
+
renderPinnedInputArea() {
|
|
322
|
+
const { rows, cols } = this.getSize();
|
|
323
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
324
|
+
const divider = renderDivider(cols - 2);
|
|
325
|
+
const isStreaming = this.mode === 'streaming';
|
|
326
|
+
// Wrap buffer into display lines (multi-line support)
|
|
327
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
328
|
+
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
329
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
330
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
331
|
+
// Calculate display window (keep cursor visible)
|
|
332
|
+
let startLine = 0;
|
|
333
|
+
if (lines.length > displayLines) {
|
|
334
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
335
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
336
|
+
}
|
|
337
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
338
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
339
|
+
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
340
|
+
const hasModelInfo = !!this.modelInfo;
|
|
341
|
+
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
342
|
+
// Save cursor position during streaming (so content flow resumes correctly)
|
|
343
|
+
if (isStreaming) {
|
|
344
|
+
this.write(ESC.SAVE);
|
|
345
|
+
}
|
|
346
|
+
this.write(ESC.HIDE);
|
|
347
|
+
this.write(ESC.RESET);
|
|
348
|
+
// Calculate start row based on mode:
|
|
349
|
+
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
350
|
+
// - Normal: right after content (contentEndRow + 1)
|
|
351
|
+
let currentRow;
|
|
352
|
+
if (isStreaming) {
|
|
353
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// In normal mode, render right after content
|
|
357
|
+
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
358
|
+
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
359
|
+
currentRow = Math.max(1, contentRow + 1);
|
|
360
|
+
}
|
|
361
|
+
let finalRow = currentRow;
|
|
362
|
+
let finalCol = 3;
|
|
363
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
364
|
+
this.write(ESC.TO(currentRow, 1));
|
|
365
|
+
this.write(ESC.CLEAR_TO_END);
|
|
366
|
+
// Status bar
|
|
367
|
+
this.write(ESC.TO(currentRow, 1));
|
|
368
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
369
|
+
currentRow++;
|
|
370
|
+
// Model info line (if set) - displayed below status, above input
|
|
371
|
+
if (hasModelInfo) {
|
|
372
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
373
|
+
this.write(ESC.TO(currentRow, 1));
|
|
374
|
+
// Build model info with context usage
|
|
375
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
376
|
+
if (this.contextUsage !== null) {
|
|
377
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
378
|
+
if (rem < 10)
|
|
379
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
380
|
+
else if (rem < 25)
|
|
381
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
382
|
+
else
|
|
383
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
384
|
+
}
|
|
385
|
+
this.write(modelLine);
|
|
386
|
+
currentRow++;
|
|
387
|
+
}
|
|
388
|
+
// Top divider
|
|
389
|
+
this.write(ESC.TO(currentRow, 1));
|
|
390
|
+
this.write(divider);
|
|
391
|
+
currentRow++;
|
|
392
|
+
// Input lines with background styling
|
|
393
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
394
|
+
this.write(ESC.TO(currentRow, 1));
|
|
395
|
+
const line = visibleLines[i] ?? '';
|
|
396
|
+
const absoluteLineIdx = startLine + i;
|
|
397
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
398
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
399
|
+
// Background
|
|
400
|
+
this.write(ESC.BG_DARK);
|
|
401
|
+
// Prompt prefix
|
|
402
|
+
this.write(ESC.DIM);
|
|
403
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
404
|
+
this.write(ESC.RESET);
|
|
405
|
+
this.write(ESC.BG_DARK);
|
|
406
|
+
if (isCursorLine) {
|
|
407
|
+
const col = Math.min(cursorCol, line.length);
|
|
408
|
+
const before = line.slice(0, col);
|
|
409
|
+
const at = col < line.length ? line[col] : ' ';
|
|
410
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
411
|
+
this.write(before);
|
|
412
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
413
|
+
this.write(at);
|
|
414
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
415
|
+
this.write(after);
|
|
416
|
+
finalRow = currentRow;
|
|
417
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.write(line);
|
|
421
|
+
}
|
|
422
|
+
// Pad to edge
|
|
423
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
424
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
425
|
+
if (padding > 0)
|
|
426
|
+
this.write(' '.repeat(padding));
|
|
427
|
+
this.write(ESC.RESET);
|
|
428
|
+
currentRow++;
|
|
429
|
+
}
|
|
430
|
+
// Bottom divider
|
|
431
|
+
this.write(ESC.TO(currentRow, 1));
|
|
432
|
+
this.write(divider);
|
|
433
|
+
currentRow++;
|
|
434
|
+
// Mode controls line
|
|
435
|
+
this.write(ESC.TO(currentRow, 1));
|
|
436
|
+
this.write(this.buildModeControls(cols));
|
|
437
|
+
// Restore cursor position during streaming, or show cursor in normal mode
|
|
438
|
+
if (isStreaming) {
|
|
439
|
+
this.write(ESC.RESTORE);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Position cursor in input area
|
|
443
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
444
|
+
this.write(ESC.SHOW);
|
|
445
|
+
}
|
|
446
|
+
// Update reserved lines for scroll region calculations
|
|
447
|
+
this.updateReservedLines(totalHeight);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Render input area during streaming (alias for unified method)
|
|
451
|
+
*/
|
|
452
|
+
renderStreamingInputArea() {
|
|
453
|
+
this.renderPinnedInputArea();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Render bottom input area - UNIFIED for all modes.
|
|
457
|
+
* Uses cursor save/restore to update bottom without affecting content flow.
|
|
458
|
+
*
|
|
459
|
+
* Layout (same for idle/streaming/ready):
|
|
460
|
+
* - Status bar (streaming timer or "Type a message")
|
|
461
|
+
* - Model info line (provider · model · ctx)
|
|
462
|
+
* - Divider
|
|
463
|
+
* - Input area
|
|
464
|
+
* - Divider
|
|
465
|
+
* - Mode controls
|
|
466
|
+
*/
|
|
467
|
+
renderBottomInputArea() {
|
|
468
|
+
const { rows, cols } = this.getSize();
|
|
469
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
470
|
+
const divider = renderDivider(cols - 2);
|
|
471
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
472
|
+
const isStreaming = this.mode === 'streaming';
|
|
473
|
+
// Wrap buffer into display lines
|
|
474
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
475
|
+
// Allow multi-line in non-streaming, single line during streaming
|
|
476
|
+
const maxDisplayLines = isStreaming ? 1 : 3;
|
|
477
|
+
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
478
|
+
const visibleLines = lines.slice(0, displayLines);
|
|
479
|
+
// Calculate total height for bottom area
|
|
480
|
+
const hasModelInfo = !!this.modelInfo;
|
|
481
|
+
const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
|
|
482
|
+
// Ensure scroll region is always enabled (unified behavior)
|
|
483
|
+
if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
|
|
484
|
+
this.reservedLines = totalHeight;
|
|
215
485
|
this.enableScrollRegion();
|
|
216
|
-
this.forceRender();
|
|
217
486
|
}
|
|
487
|
+
const startRow = Math.max(1, rows - totalHeight + 1);
|
|
488
|
+
// Save cursor, hide it
|
|
489
|
+
this.write(ESC.SAVE);
|
|
490
|
+
this.write(ESC.HIDE);
|
|
491
|
+
let currentRow = startRow;
|
|
492
|
+
// Clear the bottom reserved area
|
|
493
|
+
for (let r = startRow; r <= rows; r++) {
|
|
494
|
+
this.write(ESC.TO(r, 1));
|
|
495
|
+
this.write(ESC.CLEAR_LINE);
|
|
496
|
+
}
|
|
497
|
+
// Status bar - UNIFIED: same format for all modes
|
|
498
|
+
this.write(ESC.TO(currentRow, 1));
|
|
499
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
500
|
+
currentRow++;
|
|
501
|
+
// Model info line (if set)
|
|
502
|
+
if (hasModelInfo) {
|
|
503
|
+
this.write(ESC.TO(currentRow, 1));
|
|
504
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
505
|
+
if (this.contextUsage !== null) {
|
|
506
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
507
|
+
if (rem < 10)
|
|
508
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
509
|
+
else if (rem < 25)
|
|
510
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
511
|
+
else
|
|
512
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
513
|
+
}
|
|
514
|
+
this.write(modelLine);
|
|
515
|
+
currentRow++;
|
|
516
|
+
}
|
|
517
|
+
// Top divider
|
|
518
|
+
this.write(ESC.TO(currentRow, 1));
|
|
519
|
+
this.write(divider);
|
|
520
|
+
currentRow++;
|
|
521
|
+
// Input lines with background styling
|
|
522
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
523
|
+
this.write(ESC.TO(currentRow, 1));
|
|
524
|
+
const line = visibleLines[i] ?? '';
|
|
525
|
+
const isFirstLine = i === 0;
|
|
526
|
+
this.write(ESC.BG_DARK);
|
|
527
|
+
this.write(ESC.DIM);
|
|
528
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
529
|
+
this.write(ESC.RESET);
|
|
530
|
+
this.write(ESC.BG_DARK);
|
|
531
|
+
this.write(line);
|
|
532
|
+
// Pad to edge
|
|
533
|
+
const lineLen = this.config.promptChar.length + line.length;
|
|
534
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
535
|
+
if (padding > 0)
|
|
536
|
+
this.write(' '.repeat(padding));
|
|
537
|
+
this.write(ESC.RESET);
|
|
538
|
+
currentRow++;
|
|
539
|
+
}
|
|
540
|
+
// Bottom divider
|
|
541
|
+
this.write(ESC.TO(currentRow, 1));
|
|
542
|
+
this.write(divider);
|
|
543
|
+
currentRow++;
|
|
544
|
+
// Mode controls
|
|
545
|
+
this.write(ESC.TO(currentRow, 1));
|
|
546
|
+
this.write(this.buildModeControls(cols));
|
|
547
|
+
// Cursor positioning depends on mode:
|
|
548
|
+
// - Streaming: restore to content area (where streaming output continues)
|
|
549
|
+
// - Normal: position in input area for typing
|
|
550
|
+
if (isStreaming) {
|
|
551
|
+
this.write(ESC.RESTORE);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// Position cursor in input area
|
|
555
|
+
// Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
|
|
556
|
+
const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
|
|
557
|
+
const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
|
|
558
|
+
const targetCol = this.config.promptChar.length + cursorCol + 1;
|
|
559
|
+
this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
|
|
560
|
+
}
|
|
561
|
+
this.write(ESC.SHOW);
|
|
562
|
+
// Track last render state
|
|
563
|
+
this.lastRenderContent = this.buffer;
|
|
564
|
+
this.lastRenderCursor = this.cursor;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Enable or disable flow mode.
|
|
568
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
569
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
570
|
+
*/
|
|
571
|
+
setFlowMode(enabled) {
|
|
572
|
+
if (this.flowMode === enabled)
|
|
573
|
+
return;
|
|
574
|
+
this.flowMode = enabled;
|
|
575
|
+
this.renderDirty = true;
|
|
576
|
+
this.scheduleRender();
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Check if flow mode is enabled.
|
|
580
|
+
*/
|
|
581
|
+
isFlowMode() {
|
|
582
|
+
return this.flowMode;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Set the row where content ends (for idle mode positioning).
|
|
586
|
+
* Input area will render starting from this row + 1.
|
|
587
|
+
*/
|
|
588
|
+
setContentEndRow(row) {
|
|
589
|
+
this.contentEndRow = Math.max(0, row);
|
|
590
|
+
this.renderDirty = true;
|
|
591
|
+
this.scheduleRender();
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Set available slash commands for auto-complete suggestions.
|
|
595
|
+
*/
|
|
596
|
+
setCommands(commands) {
|
|
597
|
+
this.commandSuggestions = commands;
|
|
598
|
+
this.updateSuggestions();
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Update filtered suggestions based on current input.
|
|
602
|
+
*/
|
|
603
|
+
updateSuggestions() {
|
|
604
|
+
const input = this.buffer.trim();
|
|
605
|
+
// Only show suggestions when input starts with "/"
|
|
606
|
+
if (!input.startsWith('/')) {
|
|
607
|
+
this.showSuggestions = false;
|
|
608
|
+
this.filteredSuggestions = [];
|
|
609
|
+
this.selectedSuggestionIndex = 0;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const query = input.toLowerCase();
|
|
613
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
614
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
615
|
+
// Show suggestions if we have matches
|
|
616
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
617
|
+
// Keep selection in bounds
|
|
618
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
619
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Select next suggestion (arrow down / tab).
|
|
624
|
+
*/
|
|
625
|
+
selectNextSuggestion() {
|
|
626
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
627
|
+
return;
|
|
628
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
629
|
+
this.renderDirty = true;
|
|
630
|
+
this.scheduleRender();
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
634
|
+
*/
|
|
635
|
+
selectPrevSuggestion() {
|
|
636
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
637
|
+
return;
|
|
638
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
639
|
+
? this.filteredSuggestions.length - 1
|
|
640
|
+
: this.selectedSuggestionIndex - 1;
|
|
641
|
+
this.renderDirty = true;
|
|
642
|
+
this.scheduleRender();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Accept current suggestion and insert into buffer.
|
|
646
|
+
*/
|
|
647
|
+
acceptSuggestion() {
|
|
648
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
649
|
+
return false;
|
|
650
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
651
|
+
if (!selected)
|
|
652
|
+
return false;
|
|
653
|
+
// Replace buffer with selected command
|
|
654
|
+
this.buffer = selected.command + ' ';
|
|
655
|
+
this.cursor = this.buffer.length;
|
|
656
|
+
this.showSuggestions = false;
|
|
657
|
+
this.renderDirty = true;
|
|
658
|
+
this.scheduleRender();
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Check if suggestions are visible.
|
|
663
|
+
*/
|
|
664
|
+
areSuggestionsVisible() {
|
|
665
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Update token count for metrics display
|
|
669
|
+
*/
|
|
670
|
+
setTokensUsed(tokens) {
|
|
671
|
+
this.tokensUsed = tokens;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Toggle thinking/reasoning mode
|
|
675
|
+
*/
|
|
676
|
+
toggleThinking() {
|
|
677
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
678
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
679
|
+
this.scheduleRender();
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get thinking enabled state
|
|
683
|
+
*/
|
|
684
|
+
isThinkingEnabled() {
|
|
685
|
+
return this.thinkingEnabled;
|
|
218
686
|
}
|
|
219
687
|
/**
|
|
220
688
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
221
689
|
*/
|
|
222
690
|
setPinnedHeaderLines(count) {
|
|
223
|
-
//
|
|
224
|
-
if (this.pinnedTopRows !==
|
|
225
|
-
this.pinnedTopRows =
|
|
691
|
+
// Set pinned header rows (banner area that scroll region excludes)
|
|
692
|
+
if (this.pinnedTopRows !== count) {
|
|
693
|
+
this.pinnedTopRows = count;
|
|
226
694
|
if (this.scrollRegionActive) {
|
|
227
695
|
this.applyScrollRegion();
|
|
228
696
|
}
|
|
229
697
|
}
|
|
230
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
701
|
+
* restore the default bottom-aligned layout.
|
|
702
|
+
*/
|
|
703
|
+
setInlineAnchor(row) {
|
|
704
|
+
if (row === null || row === undefined) {
|
|
705
|
+
this.inlineAnchorRow = null;
|
|
706
|
+
this.inlineLayout = false;
|
|
707
|
+
this.renderDirty = true;
|
|
708
|
+
this.render();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const { rows } = this.getSize();
|
|
712
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
713
|
+
this.inlineAnchorRow = clamped;
|
|
714
|
+
this.inlineLayout = true;
|
|
715
|
+
this.renderDirty = true;
|
|
716
|
+
this.render();
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
720
|
+
* output by re-evaluating the anchor before each render.
|
|
721
|
+
*/
|
|
722
|
+
setInlineAnchorProvider(provider) {
|
|
723
|
+
this.anchorProvider = provider;
|
|
724
|
+
if (!provider) {
|
|
725
|
+
this.inlineLayout = false;
|
|
726
|
+
this.inlineAnchorRow = null;
|
|
727
|
+
this.renderDirty = true;
|
|
728
|
+
this.render();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
this.inlineLayout = true;
|
|
732
|
+
this.renderDirty = true;
|
|
733
|
+
this.render();
|
|
734
|
+
}
|
|
231
735
|
/**
|
|
232
736
|
* Get current mode
|
|
233
737
|
*/
|
|
@@ -337,37 +841,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
337
841
|
this.streamingLabel = next;
|
|
338
842
|
this.scheduleRender();
|
|
339
843
|
}
|
|
340
|
-
/**
|
|
341
|
-
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
342
|
-
*/
|
|
343
|
-
setMetaStatus(meta) {
|
|
344
|
-
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
345
|
-
? Math.floor(meta.elapsedSeconds)
|
|
346
|
-
: null;
|
|
347
|
-
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
348
|
-
? Math.floor(meta.tokensUsed)
|
|
349
|
-
: null;
|
|
350
|
-
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
351
|
-
? Math.floor(meta.tokenLimit)
|
|
352
|
-
: null;
|
|
353
|
-
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
354
|
-
? Math.floor(meta.thinkingMs)
|
|
355
|
-
: null;
|
|
356
|
-
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
357
|
-
if (this.metaElapsedSeconds === nextElapsed &&
|
|
358
|
-
this.metaTokensUsed === nextTokens &&
|
|
359
|
-
this.metaTokenLimit === nextLimit &&
|
|
360
|
-
this.metaThinkingMs === nextThinking &&
|
|
361
|
-
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
this.metaElapsedSeconds = nextElapsed;
|
|
365
|
-
this.metaTokensUsed = nextTokens;
|
|
366
|
-
this.metaTokenLimit = nextLimit;
|
|
367
|
-
this.metaThinkingMs = nextThinking;
|
|
368
|
-
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
369
|
-
this.scheduleRender();
|
|
370
|
-
}
|
|
371
844
|
/**
|
|
372
845
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
373
846
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -377,22 +850,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
377
850
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
378
851
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
379
852
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
380
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
381
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
382
853
|
if (this.verificationEnabled === nextVerification &&
|
|
383
854
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
384
855
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
385
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
386
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
387
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
856
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
388
857
|
return;
|
|
389
858
|
}
|
|
390
859
|
this.verificationEnabled = nextVerification;
|
|
391
860
|
this.autoContinueEnabled = nextAutoContinue;
|
|
392
861
|
this.verificationHotkey = nextVerifyHotkey;
|
|
393
862
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
394
|
-
this.
|
|
395
|
-
|
|
863
|
+
this.scheduleRender();
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
867
|
+
* This is displayed persistently above the input area.
|
|
868
|
+
*/
|
|
869
|
+
setModelInfo(info) {
|
|
870
|
+
if (this.modelInfo === info)
|
|
871
|
+
return;
|
|
872
|
+
this.modelInfo = info;
|
|
396
873
|
this.scheduleRender();
|
|
397
874
|
}
|
|
398
875
|
/**
|
|
@@ -405,390 +882,298 @@ export class TerminalInput extends EventEmitter {
|
|
|
405
882
|
this.scheduleRender();
|
|
406
883
|
}
|
|
407
884
|
/**
|
|
408
|
-
*
|
|
409
|
-
*/
|
|
410
|
-
setModelContext(options) {
|
|
411
|
-
const nextModel = options.model?.trim() || null;
|
|
412
|
-
const nextProvider = options.provider?.trim() || null;
|
|
413
|
-
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
this.modelLabel = nextModel;
|
|
417
|
-
this.providerLabel = nextProvider;
|
|
418
|
-
this.scheduleRender();
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Render the input area - Claude Code style with mode controls
|
|
885
|
+
* Render the input area - UNIFIED for all modes
|
|
422
886
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
887
|
+
* Uses the same bottom-pinned layout with scroll regions for:
|
|
888
|
+
* - Idle mode: Shows "Type a message" hint
|
|
889
|
+
* - Streaming mode: Shows "● Streaming Xs" timer
|
|
890
|
+
* - Ready mode: Shows status info
|
|
426
891
|
*/
|
|
427
892
|
render() {
|
|
428
893
|
if (!this.canRender())
|
|
429
894
|
return;
|
|
430
895
|
if (this.isRendering)
|
|
431
896
|
return;
|
|
432
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
433
|
-
// During streaming we still render the pinned input/status region, but throttle
|
|
434
|
-
// to avoid fighting with the streamed content flow.
|
|
435
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
436
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
437
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
438
|
-
if (waitMs > 0) {
|
|
439
|
-
this.renderDirty = true;
|
|
440
|
-
this.scheduleStreamingRender(waitMs);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
897
|
const shouldSkip = !this.renderDirty &&
|
|
445
898
|
this.buffer === this.lastRenderContent &&
|
|
446
899
|
this.cursor === this.lastRenderCursor;
|
|
447
900
|
this.renderDirty = false;
|
|
448
|
-
// Skip if nothing changed
|
|
901
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
449
902
|
if (shouldSkip) {
|
|
450
903
|
return;
|
|
451
904
|
}
|
|
452
|
-
// If write lock is held, defer render
|
|
905
|
+
// If write lock is held, defer render
|
|
453
906
|
if (writeLock.isLocked()) {
|
|
454
907
|
writeLock.safeWrite(() => this.render());
|
|
455
908
|
return;
|
|
456
909
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
910
|
+
this.isRendering = true;
|
|
911
|
+
writeLock.lock('terminalInput.render');
|
|
912
|
+
try {
|
|
913
|
+
// UNIFIED: Use the same bottom input area for all modes
|
|
914
|
+
this.renderBottomInputArea();
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
writeLock.unlock();
|
|
918
|
+
this.isRendering = false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
923
|
+
*
|
|
924
|
+
* Flow mode attempted inline rendering but caused duplicate renders
|
|
925
|
+
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
926
|
+
*/
|
|
927
|
+
renderFlowMode() {
|
|
928
|
+
// Use stable bottom-pinned approach
|
|
929
|
+
this.renderBottomPinned();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
933
|
+
*
|
|
934
|
+
* Works for both normal and streaming modes:
|
|
935
|
+
* - During streaming: saves/restores cursor position
|
|
936
|
+
* - Status bar shows streaming info or "Type a message"
|
|
937
|
+
*
|
|
938
|
+
* Layout when suggestions visible:
|
|
939
|
+
* - Top divider
|
|
940
|
+
* - Input line(s)
|
|
941
|
+
* - Bottom divider
|
|
942
|
+
* - Suggestions (command list)
|
|
943
|
+
*
|
|
944
|
+
* Layout when suggestions hidden:
|
|
945
|
+
* - Status bar (Ready/Streaming)
|
|
946
|
+
* - Top divider
|
|
947
|
+
* - Input line(s)
|
|
948
|
+
* - Bottom divider
|
|
949
|
+
* - Mode controls
|
|
950
|
+
*/
|
|
951
|
+
renderBottomPinned() {
|
|
952
|
+
const { rows, cols } = this.getSize();
|
|
953
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
954
|
+
const isStreaming = this.mode === 'streaming';
|
|
955
|
+
// Use unified pinned input area (works for both streaming and normal)
|
|
956
|
+
// Only use complex rendering when suggestions are visible
|
|
957
|
+
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
958
|
+
if (!hasSuggestions) {
|
|
959
|
+
this.renderPinnedInputArea();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
// Wrap buffer into display lines
|
|
963
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
964
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
965
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
966
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
967
|
+
// Calculate display window (keep cursor visible)
|
|
968
|
+
let startLine = 0;
|
|
969
|
+
if (lines.length > displayLines) {
|
|
970
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
971
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
972
|
+
}
|
|
973
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
974
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
975
|
+
// Calculate suggestion display (not during streaming)
|
|
976
|
+
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
977
|
+
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
978
|
+
: [];
|
|
979
|
+
const suggestionLines = suggestionsToShow.length;
|
|
980
|
+
this.write(ESC.HIDE);
|
|
981
|
+
this.write(ESC.RESET);
|
|
982
|
+
const divider = renderDivider(cols - 2);
|
|
983
|
+
// Calculate positions from absolute bottom
|
|
984
|
+
let currentRow;
|
|
985
|
+
if (suggestionLines > 0) {
|
|
986
|
+
// With suggestions: input area + dividers + suggestions
|
|
987
|
+
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
988
|
+
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
989
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
990
|
+
this.updateReservedLines(totalHeight);
|
|
991
|
+
// Clear from current position to end of screen to remove any "ghost" content
|
|
992
|
+
this.write(ESC.TO(currentRow, 1));
|
|
993
|
+
this.write(ESC.CLEAR_TO_END);
|
|
994
|
+
// Top divider
|
|
494
995
|
this.write(ESC.TO(currentRow, 1));
|
|
495
|
-
this.write(ESC.CLEAR_LINE);
|
|
496
|
-
const divider = renderDivider(cols - 2);
|
|
497
996
|
this.write(divider);
|
|
498
|
-
currentRow
|
|
499
|
-
//
|
|
997
|
+
currentRow++;
|
|
998
|
+
// Input lines
|
|
500
999
|
let finalRow = currentRow;
|
|
501
1000
|
let finalCol = 3;
|
|
502
1001
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
503
|
-
|
|
504
|
-
this.write(ESC.TO(rowNum, 1));
|
|
505
|
-
this.write(ESC.CLEAR_LINE);
|
|
1002
|
+
this.write(ESC.TO(currentRow, 1));
|
|
506
1003
|
const line = visibleLines[i] ?? '';
|
|
507
1004
|
const absoluteLineIdx = startLine + i;
|
|
508
1005
|
const isFirstLine = absoluteLineIdx === 0;
|
|
509
1006
|
const isCursorLine = i === adjustedCursorLine;
|
|
510
|
-
// Background
|
|
511
|
-
this.write(ESC.BG_DARK);
|
|
512
|
-
// Prompt prefix
|
|
513
|
-
this.write(ESC.DIM);
|
|
514
1007
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
515
|
-
this.write(ESC.RESET);
|
|
516
|
-
this.write(ESC.BG_DARK);
|
|
517
1008
|
if (isCursorLine) {
|
|
518
|
-
// Render with block cursor
|
|
519
1009
|
const col = Math.min(cursorCol, line.length);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.write(
|
|
524
|
-
this.write(
|
|
525
|
-
|
|
526
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
527
|
-
this.write(after);
|
|
528
|
-
finalRow = rowNum;
|
|
1010
|
+
this.write(line.slice(0, col));
|
|
1011
|
+
this.write(ESC.REVERSE);
|
|
1012
|
+
this.write(col < line.length ? line[col] : ' ');
|
|
1013
|
+
this.write(ESC.RESET);
|
|
1014
|
+
this.write(line.slice(col + 1));
|
|
1015
|
+
finalRow = currentRow;
|
|
529
1016
|
finalCol = this.config.promptChar.length + col + 1;
|
|
530
1017
|
}
|
|
531
1018
|
else {
|
|
532
1019
|
this.write(line);
|
|
533
1020
|
}
|
|
534
|
-
|
|
535
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
536
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
537
|
-
if (padding > 0)
|
|
538
|
-
this.write(' '.repeat(padding));
|
|
539
|
-
this.write(ESC.RESET);
|
|
1021
|
+
currentRow++;
|
|
540
1022
|
}
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
this.write(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1023
|
+
// Bottom divider
|
|
1024
|
+
this.write(ESC.TO(currentRow, 1));
|
|
1025
|
+
this.write(divider);
|
|
1026
|
+
currentRow++;
|
|
1027
|
+
// Suggestions (Claude Code style)
|
|
1028
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
1029
|
+
this.write(ESC.TO(currentRow, 1));
|
|
1030
|
+
const suggestion = suggestionsToShow[i];
|
|
1031
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
1032
|
+
// Indent and highlight selected
|
|
1033
|
+
this.write(' ');
|
|
1034
|
+
if (isSelected) {
|
|
1035
|
+
this.write(ESC.REVERSE);
|
|
1036
|
+
this.write(ESC.BOLD);
|
|
1037
|
+
}
|
|
1038
|
+
this.write(suggestion.command);
|
|
1039
|
+
if (isSelected) {
|
|
1040
|
+
this.write(ESC.RESET);
|
|
1041
|
+
}
|
|
1042
|
+
// Description (dimmed)
|
|
1043
|
+
const descSpace = cols - suggestion.command.length - 8;
|
|
1044
|
+
if (descSpace > 10 && suggestion.description) {
|
|
1045
|
+
const desc = suggestion.description.slice(0, descSpace);
|
|
1046
|
+
this.write(ESC.RESET);
|
|
1047
|
+
this.write(ESC.DIM);
|
|
1048
|
+
this.write(' ');
|
|
1049
|
+
this.write(desc);
|
|
1050
|
+
this.write(ESC.RESET);
|
|
1051
|
+
}
|
|
1052
|
+
currentRow++;
|
|
556
1053
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
writeLock.lock('terminalInput.render');
|
|
560
|
-
this.isRendering = true;
|
|
561
|
-
try {
|
|
562
|
-
performRender();
|
|
563
|
-
}
|
|
564
|
-
finally {
|
|
565
|
-
writeLock.unlock();
|
|
566
|
-
this.isRendering = false;
|
|
1054
|
+
// Position cursor in input area
|
|
1055
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
567
1056
|
}
|
|
1057
|
+
this.write(ESC.SHOW);
|
|
1058
|
+
// Update state
|
|
1059
|
+
this.lastRenderContent = this.buffer;
|
|
1060
|
+
this.lastRenderCursor = this.cursor;
|
|
568
1061
|
}
|
|
569
1062
|
/**
|
|
570
|
-
* Build
|
|
571
|
-
* During streaming, shows model line pinned above streaming info.
|
|
1063
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
572
1064
|
*/
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (this.
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
//
|
|
584
|
-
if (streamingActive) {
|
|
585
|
-
const parts = [];
|
|
586
|
-
// Essential streaming info
|
|
587
|
-
if (this.metaThinkingMs !== null) {
|
|
588
|
-
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
589
|
-
}
|
|
590
|
-
if (this.metaElapsedSeconds !== null) {
|
|
591
|
-
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
592
|
-
}
|
|
593
|
-
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
594
|
-
if (parts.length) {
|
|
595
|
-
lines.push(renderStatusLine(parts, width));
|
|
596
|
-
}
|
|
597
|
-
return lines;
|
|
598
|
-
}
|
|
599
|
-
// Non-streaming: show full status info (model line already added above)
|
|
600
|
-
if (this.metaThinkingMs !== null) {
|
|
601
|
-
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
602
|
-
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
603
|
-
}
|
|
604
|
-
const statusParts = [];
|
|
605
|
-
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
606
|
-
if (statusLabel) {
|
|
607
|
-
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
608
|
-
}
|
|
609
|
-
if (this.metaElapsedSeconds !== null) {
|
|
610
|
-
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
611
|
-
}
|
|
612
|
-
const tokensRemaining = this.computeTokensRemaining();
|
|
613
|
-
if (tokensRemaining !== null) {
|
|
614
|
-
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
615
|
-
}
|
|
616
|
-
if (statusParts.length) {
|
|
617
|
-
lines.push(renderStatusLine(statusParts, width));
|
|
618
|
-
}
|
|
619
|
-
const usageParts = [];
|
|
620
|
-
if (this.metaTokensUsed !== null) {
|
|
621
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
622
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
623
|
-
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
624
|
-
}
|
|
625
|
-
if (this.contextUsage !== null) {
|
|
626
|
-
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
627
|
-
const left = Math.max(0, 100 - this.contextUsage);
|
|
628
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
629
|
-
}
|
|
1065
|
+
buildStreamingStatusBar(cols) {
|
|
1066
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
1067
|
+
// Streaming status with elapsed time
|
|
1068
|
+
let elapsed = '0s';
|
|
1069
|
+
if (this.streamingStartTime) {
|
|
1070
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1071
|
+
const mins = Math.floor(secs / 60);
|
|
1072
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
1073
|
+
}
|
|
1074
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
1075
|
+
// Queue indicator
|
|
630
1076
|
if (this.queue.length > 0) {
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
if (usageParts.length) {
|
|
634
|
-
lines.push(renderStatusLine(usageParts, width));
|
|
635
|
-
}
|
|
636
|
-
return lines;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
640
|
-
*/
|
|
641
|
-
clearReservedArea(startRow, reservedLines, cols) {
|
|
642
|
-
const width = Math.max(1, cols);
|
|
643
|
-
for (let i = 0; i < reservedLines; i++) {
|
|
644
|
-
const row = startRow + i;
|
|
645
|
-
this.write(ESC.TO(row, 1));
|
|
646
|
-
this.write(' '.repeat(width));
|
|
1077
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
647
1078
|
}
|
|
1079
|
+
// Hint for typing
|
|
1080
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
1081
|
+
return status;
|
|
648
1082
|
}
|
|
649
1083
|
/**
|
|
650
|
-
* Build
|
|
651
|
-
*
|
|
1084
|
+
* Build status bar showing streaming/ready status and key info.
|
|
1085
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
652
1086
|
*/
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
if (this.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const editHotkey = this.formatHotkey('shift+tab');
|
|
667
|
-
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
668
|
-
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
669
|
-
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
670
|
-
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
671
|
-
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
672
|
-
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
673
|
-
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
674
|
-
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
675
|
-
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
676
|
-
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
677
|
-
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
1087
|
+
buildStatusBar(cols) {
|
|
1088
|
+
const maxWidth = cols - 2;
|
|
1089
|
+
const parts = [];
|
|
1090
|
+
// Streaming status with elapsed time (left side)
|
|
1091
|
+
if (this.mode === 'streaming') {
|
|
1092
|
+
let statusText = '● Streaming';
|
|
1093
|
+
if (this.streamingStartTime) {
|
|
1094
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1095
|
+
const mins = Math.floor(elapsed / 60);
|
|
1096
|
+
const secs = elapsed % 60;
|
|
1097
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
1098
|
+
}
|
|
1099
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
678
1100
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1101
|
+
// Queue indicator during streaming
|
|
1102
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
1103
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
682
1104
|
}
|
|
1105
|
+
// Paste indicator
|
|
683
1106
|
if (this.pastePlaceholders.length > 0) {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
687
|
-
tone: 'info',
|
|
688
|
-
});
|
|
1107
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
1108
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
689
1109
|
}
|
|
690
|
-
|
|
691
|
-
if (this.
|
|
692
|
-
|
|
693
|
-
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
694
|
-
}
|
|
695
|
-
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
696
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
697
|
-
if (this.modelLabel && !streamingActive) {
|
|
698
|
-
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
699
|
-
rightParts.push({ text: modelText, tone: 'muted' });
|
|
700
|
-
}
|
|
701
|
-
if (contextRemaining !== null) {
|
|
702
|
-
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
703
|
-
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
704
|
-
? 'Context auto-compact imminent'
|
|
705
|
-
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
706
|
-
rightParts.push({ text: label, tone });
|
|
707
|
-
}
|
|
708
|
-
if (!rightParts.length || width < 60) {
|
|
709
|
-
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
710
|
-
return renderStatusLine(merged, width);
|
|
711
|
-
}
|
|
712
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
713
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
714
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
715
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
716
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
717
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
718
|
-
}
|
|
719
|
-
formatHotkey(hotkey) {
|
|
720
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
721
|
-
if (!normalized)
|
|
722
|
-
return hotkey;
|
|
723
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
724
|
-
const map = {
|
|
725
|
-
shift: '⇧',
|
|
726
|
-
sh: '⇧',
|
|
727
|
-
alt: '⌥',
|
|
728
|
-
option: '⌥',
|
|
729
|
-
opt: '⌥',
|
|
730
|
-
ctrl: '⌃',
|
|
731
|
-
control: '⌃',
|
|
732
|
-
cmd: '⌘',
|
|
733
|
-
meta: '⌘',
|
|
734
|
-
};
|
|
735
|
-
const formatted = parts
|
|
736
|
-
.map((part) => {
|
|
737
|
-
const symbol = map[part];
|
|
738
|
-
if (symbol)
|
|
739
|
-
return symbol;
|
|
740
|
-
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
741
|
-
})
|
|
742
|
-
.join('');
|
|
743
|
-
return formatted || hotkey;
|
|
744
|
-
}
|
|
745
|
-
computeContextRemaining() {
|
|
746
|
-
if (this.contextUsage === null) {
|
|
747
|
-
return null;
|
|
748
|
-
}
|
|
749
|
-
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
750
|
-
}
|
|
751
|
-
computeTokensRemaining() {
|
|
752
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
753
|
-
return null;
|
|
754
|
-
}
|
|
755
|
-
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
756
|
-
return this.formatTokenCount(remaining);
|
|
757
|
-
}
|
|
758
|
-
formatElapsedLabel(seconds) {
|
|
759
|
-
if (seconds < 60) {
|
|
760
|
-
return `${seconds}s`;
|
|
761
|
-
}
|
|
762
|
-
const mins = Math.floor(seconds / 60);
|
|
763
|
-
const secs = seconds % 60;
|
|
764
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
765
|
-
}
|
|
766
|
-
formatTokenCount(value) {
|
|
767
|
-
if (!Number.isFinite(value)) {
|
|
768
|
-
return `${value}`;
|
|
1110
|
+
// Override/warning status
|
|
1111
|
+
if (this.overrideStatusMessage) {
|
|
1112
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
769
1113
|
}
|
|
770
|
-
|
|
771
|
-
|
|
1114
|
+
// If idle with empty buffer, show quick shortcuts
|
|
1115
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
1116
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
772
1117
|
}
|
|
773
|
-
|
|
774
|
-
|
|
1118
|
+
// Multi-line indicator
|
|
1119
|
+
if (this.buffer.includes('\n')) {
|
|
1120
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
775
1121
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
return
|
|
1122
|
+
if (parts.length === 0) {
|
|
1123
|
+
return ''; // Empty status bar when idle
|
|
1124
|
+
}
|
|
1125
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
1126
|
+
return joined.slice(0, maxWidth);
|
|
781
1127
|
}
|
|
782
1128
|
/**
|
|
783
|
-
*
|
|
784
|
-
*
|
|
1129
|
+
* Build mode controls line showing toggles and context info.
|
|
1130
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
1131
|
+
*
|
|
1132
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
785
1133
|
*/
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1134
|
+
buildModeControls(cols) {
|
|
1135
|
+
const maxWidth = cols - 2;
|
|
1136
|
+
// Use schema-defined colors for consistency
|
|
1137
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
1138
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
1139
|
+
const toggles = [];
|
|
1140
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
1141
|
+
if (this.editMode === 'display-edits') {
|
|
1142
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
1146
|
+
}
|
|
1147
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
1148
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
1149
|
+
// Verification (green when on) - per schema.verificationMode
|
|
1150
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
1151
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
1152
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
1153
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
1154
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
1155
|
+
let rightPart = '';
|
|
1156
|
+
if (this.contextUsage !== null) {
|
|
1157
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
1158
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
1159
|
+
if (rem < 10)
|
|
1160
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1161
|
+
else if (rem < 25)
|
|
1162
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1163
|
+
else
|
|
1164
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1165
|
+
}
|
|
1166
|
+
// Calculate visible lengths (strip ANSI)
|
|
1167
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1168
|
+
const leftLen = strip(leftPart).length;
|
|
1169
|
+
const rightLen = strip(rightPart).length;
|
|
1170
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
1171
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1172
|
+
}
|
|
1173
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1174
|
+
return `${leftPart} ${rightPart}`;
|
|
1175
|
+
}
|
|
1176
|
+
return leftPart;
|
|
792
1177
|
}
|
|
793
1178
|
/**
|
|
794
1179
|
* Force a re-render
|
|
@@ -811,19 +1196,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
811
1196
|
handleResize() {
|
|
812
1197
|
this.lastRenderContent = '';
|
|
813
1198
|
this.lastRenderCursor = -1;
|
|
814
|
-
this.resetStreamingRenderThrottle();
|
|
815
1199
|
// Re-clamp pinned header rows to the new terminal height
|
|
816
1200
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
817
|
-
if (this.scrollRegionActive) {
|
|
818
|
-
this.disableScrollRegion();
|
|
819
|
-
this.enableScrollRegion();
|
|
820
|
-
}
|
|
821
1201
|
this.scheduleRender();
|
|
822
1202
|
}
|
|
823
1203
|
/**
|
|
824
1204
|
* Register with display's output interceptor to position cursor correctly.
|
|
825
1205
|
* When scroll region is active, output needs to go to the scroll region,
|
|
826
1206
|
* not the protected bottom area where the input is rendered.
|
|
1207
|
+
*
|
|
1208
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
1209
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
827
1210
|
*/
|
|
828
1211
|
registerOutputInterceptor(display) {
|
|
829
1212
|
if (this.outputInterceptorCleanup) {
|
|
@@ -831,59 +1214,23 @@ export class TerminalInput extends EventEmitter {
|
|
|
831
1214
|
}
|
|
832
1215
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
833
1216
|
beforeWrite: () => {
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
if (this.scrollRegionActive) {
|
|
837
|
-
const { rows } = this.getSize();
|
|
838
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
839
|
-
this.write(ESC.SAVE);
|
|
840
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
841
|
-
}
|
|
1217
|
+
// Scroll region handles content containment automatically
|
|
1218
|
+
// No per-write cursor manipulation needed
|
|
842
1219
|
},
|
|
843
1220
|
afterWrite: () => {
|
|
844
|
-
//
|
|
845
|
-
if (this.scrollRegionActive) {
|
|
846
|
-
this.write(ESC.RESTORE);
|
|
847
|
-
}
|
|
1221
|
+
// No cursor manipulation needed
|
|
848
1222
|
},
|
|
849
1223
|
});
|
|
850
1224
|
}
|
|
851
1225
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
return;
|
|
858
|
-
// Ensure scroll region is active
|
|
859
|
-
if (!this.scrollRegionActive) {
|
|
860
|
-
this.enableScrollRegion();
|
|
861
|
-
}
|
|
862
|
-
const { rows } = this.getSize();
|
|
863
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
864
|
-
// Save cursor, write at scroll bottom, restore
|
|
865
|
-
this.write(ESC.SAVE);
|
|
866
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
867
|
-
this.write(content);
|
|
868
|
-
this.write(ESC.RESTORE);
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Clear the scroll region and prepare for fresh content.
|
|
1226
|
+
* Set the banner renderer callback for unified UI initialization.
|
|
1227
|
+
* This callback is called when entering streaming mode for the first time,
|
|
1228
|
+
* to re-render the banner inside the scroll region.
|
|
1229
|
+
*
|
|
1230
|
+
* @param renderer Function that renders the banner and returns the number of lines written
|
|
872
1231
|
*/
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
this.enableScrollRegion();
|
|
876
|
-
}
|
|
877
|
-
const { rows, cols } = this.getSize();
|
|
878
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
879
|
-
const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
|
|
880
|
-
// Clear each line in the scroll region
|
|
881
|
-
this.write(ESC.SAVE);
|
|
882
|
-
for (let row = scrollTop; row <= scrollBottom; row++) {
|
|
883
|
-
this.write(ESC.TO(row, 1));
|
|
884
|
-
this.write(' '.repeat(cols));
|
|
885
|
-
}
|
|
886
|
-
this.write(ESC.RESTORE);
|
|
1232
|
+
setBannerRenderer(renderer) {
|
|
1233
|
+
this.bannerRenderer = renderer;
|
|
887
1234
|
}
|
|
888
1235
|
/**
|
|
889
1236
|
* Dispose and clean up
|
|
@@ -891,6 +1238,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
891
1238
|
dispose() {
|
|
892
1239
|
if (this.disposed)
|
|
893
1240
|
return;
|
|
1241
|
+
// Clean up streaming render timer
|
|
1242
|
+
if (this.streamingRenderTimer) {
|
|
1243
|
+
clearInterval(this.streamingRenderTimer);
|
|
1244
|
+
this.streamingRenderTimer = null;
|
|
1245
|
+
}
|
|
894
1246
|
// Clean up output interceptor
|
|
895
1247
|
if (this.outputInterceptorCleanup) {
|
|
896
1248
|
this.outputInterceptorCleanup();
|
|
@@ -898,7 +1250,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
898
1250
|
}
|
|
899
1251
|
this.disposed = true;
|
|
900
1252
|
this.enabled = false;
|
|
901
|
-
this.resetStreamingRenderThrottle();
|
|
902
1253
|
this.disableScrollRegion();
|
|
903
1254
|
this.disableBracketedPaste();
|
|
904
1255
|
this.buffer = '';
|
|
@@ -1004,7 +1355,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1004
1355
|
this.toggleEditMode();
|
|
1005
1356
|
return true;
|
|
1006
1357
|
}
|
|
1007
|
-
|
|
1358
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1359
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1360
|
+
this.togglePasteExpansion();
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
this.toggleThinking();
|
|
1364
|
+
}
|
|
1365
|
+
return true;
|
|
1366
|
+
case 'escape':
|
|
1367
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1368
|
+
if (this.mode === 'streaming') {
|
|
1369
|
+
this.emit('interrupt');
|
|
1370
|
+
}
|
|
1371
|
+
else if (this.buffer.length > 0) {
|
|
1372
|
+
this.clear();
|
|
1373
|
+
}
|
|
1008
1374
|
return true;
|
|
1009
1375
|
}
|
|
1010
1376
|
return false;
|
|
@@ -1022,6 +1388,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1022
1388
|
this.insertPlainText(chunk, insertPos);
|
|
1023
1389
|
this.cursor = insertPos + chunk.length;
|
|
1024
1390
|
this.emit('change', this.buffer);
|
|
1391
|
+
this.updateSuggestions();
|
|
1025
1392
|
this.scheduleRender();
|
|
1026
1393
|
}
|
|
1027
1394
|
insertNewline() {
|
|
@@ -1046,6 +1413,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1046
1413
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1047
1414
|
}
|
|
1048
1415
|
this.emit('change', this.buffer);
|
|
1416
|
+
this.updateSuggestions();
|
|
1049
1417
|
this.scheduleRender();
|
|
1050
1418
|
}
|
|
1051
1419
|
deleteForward() {
|
|
@@ -1295,9 +1663,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1295
1663
|
if (available <= 0)
|
|
1296
1664
|
return;
|
|
1297
1665
|
const chunk = clean.slice(0, available);
|
|
1298
|
-
|
|
1299
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1300
|
-
if (isMultiline && !isShortMultiline) {
|
|
1666
|
+
if (isMultilinePaste(chunk)) {
|
|
1301
1667
|
this.insertPastePlaceholder(chunk);
|
|
1302
1668
|
}
|
|
1303
1669
|
else {
|
|
@@ -1317,7 +1683,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1317
1683
|
return;
|
|
1318
1684
|
this.applyScrollRegion();
|
|
1319
1685
|
this.scrollRegionActive = true;
|
|
1320
|
-
this.forceRender();
|
|
1321
1686
|
}
|
|
1322
1687
|
disableScrollRegion() {
|
|
1323
1688
|
if (!this.scrollRegionActive)
|
|
@@ -1468,19 +1833,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1468
1833
|
this.shiftPlaceholders(position, text.length);
|
|
1469
1834
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1470
1835
|
}
|
|
1471
|
-
shouldInlineMultiline(content) {
|
|
1472
|
-
const lines = content.split('\n').length;
|
|
1473
|
-
const maxInlineLines = 4;
|
|
1474
|
-
const maxInlineChars = 240;
|
|
1475
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1476
|
-
}
|
|
1477
1836
|
findPlaceholderAt(position) {
|
|
1478
1837
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1479
1838
|
}
|
|
1480
|
-
buildPlaceholder(
|
|
1839
|
+
buildPlaceholder(summary) {
|
|
1481
1840
|
const id = ++this.pasteCounter;
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1841
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1842
|
+
// Show first line preview (truncated)
|
|
1843
|
+
const preview = summary.preview.length > 30
|
|
1844
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1845
|
+
: summary.preview;
|
|
1846
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1484
1847
|
return { id, placeholder };
|
|
1485
1848
|
}
|
|
1486
1849
|
insertPastePlaceholder(content) {
|
|
@@ -1488,21 +1851,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1488
1851
|
if (available <= 0)
|
|
1489
1852
|
return;
|
|
1490
1853
|
const cleanContent = content.slice(0, available);
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1854
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1855
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1856
|
+
if (summary.lineCount < 5) {
|
|
1857
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1858
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1859
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1860
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1493
1864
|
const insertPos = this.cursor;
|
|
1494
1865
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1495
1866
|
this.pastePlaceholders.push({
|
|
1496
1867
|
id,
|
|
1497
1868
|
content: cleanContent,
|
|
1498
|
-
lineCount,
|
|
1869
|
+
lineCount: summary.lineCount,
|
|
1499
1870
|
placeholder,
|
|
1500
1871
|
start: insertPos,
|
|
1501
1872
|
end: insertPos + placeholder.length,
|
|
1873
|
+
summary,
|
|
1874
|
+
expanded: false,
|
|
1502
1875
|
});
|
|
1503
1876
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1504
1877
|
this.cursor = insertPos + placeholder.length;
|
|
1505
1878
|
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1881
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1882
|
+
*/
|
|
1883
|
+
togglePasteExpansion() {
|
|
1884
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1885
|
+
if (!placeholder)
|
|
1886
|
+
return false;
|
|
1887
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1888
|
+
// Update the placeholder text in buffer
|
|
1889
|
+
const newPlaceholder = placeholder.expanded
|
|
1890
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1891
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1892
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1893
|
+
// Update buffer
|
|
1894
|
+
this.buffer =
|
|
1895
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1896
|
+
newPlaceholder +
|
|
1897
|
+
this.buffer.slice(placeholder.end);
|
|
1898
|
+
// Update placeholder tracking
|
|
1899
|
+
placeholder.placeholder = newPlaceholder;
|
|
1900
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1901
|
+
// Shift other placeholders
|
|
1902
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1903
|
+
this.scheduleRender();
|
|
1904
|
+
return true;
|
|
1905
|
+
}
|
|
1906
|
+
buildExpandedPlaceholder(ph) {
|
|
1907
|
+
const lines = ph.content.split('\n');
|
|
1908
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1909
|
+
const lastLines = lines.length > 5
|
|
1910
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1911
|
+
: '';
|
|
1912
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1913
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1914
|
+
}
|
|
1506
1915
|
deletePlaceholder(placeholder) {
|
|
1507
1916
|
const length = placeholder.end - placeholder.start;
|
|
1508
1917
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1510,11 +1919,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1510
1919
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1511
1920
|
this.cursor = placeholder.start;
|
|
1512
1921
|
}
|
|
1513
|
-
updateContextUsage(value
|
|
1514
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1515
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1516
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1517
|
-
}
|
|
1922
|
+
updateContextUsage(value) {
|
|
1518
1923
|
if (value === null || !Number.isFinite(value)) {
|
|
1519
1924
|
this.contextUsage = null;
|
|
1520
1925
|
}
|
|
@@ -1541,22 +1946,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1541
1946
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1542
1947
|
this.setEditMode(next);
|
|
1543
1948
|
}
|
|
1544
|
-
scheduleStreamingRender(delayMs) {
|
|
1545
|
-
if (this.streamingRenderTimer)
|
|
1546
|
-
return;
|
|
1547
|
-
const wait = Math.max(16, delayMs);
|
|
1548
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1549
|
-
this.streamingRenderTimer = null;
|
|
1550
|
-
this.render();
|
|
1551
|
-
}, wait);
|
|
1552
|
-
}
|
|
1553
|
-
resetStreamingRenderThrottle() {
|
|
1554
|
-
if (this.streamingRenderTimer) {
|
|
1555
|
-
clearTimeout(this.streamingRenderTimer);
|
|
1556
|
-
this.streamingRenderTimer = null;
|
|
1557
|
-
}
|
|
1558
|
-
this.lastStreamingRender = 0;
|
|
1559
|
-
}
|
|
1560
1949
|
scheduleRender() {
|
|
1561
1950
|
if (!this.canRender())
|
|
1562
1951
|
return;
|