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