erosolar-cli 1.7.321 → 1.7.322
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 +24 -148
- package/dist/bin/erosolar.js +5 -21
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +56 -31
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/contracts/agent-schemas.json +0 -15
- package/dist/contracts/tools.schema.json +0 -9
- 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/customCommands.d.ts +1 -0
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +3 -0
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/hooks.d.ts +113 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +267 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/metricsTracker.d.ts +122 -0
- package/dist/core/metricsTracker.d.ts.map +1 -0
- package/dist/{alpha-zero → core}/metricsTracker.js +2 -5
- package/dist/core/metricsTracker.js.map +1 -0
- package/dist/core/securityAssessment.d.ts +91 -0
- package/dist/core/securityAssessment.d.ts.map +1 -0
- package/dist/core/securityAssessment.js +580 -0
- package/dist/core/securityAssessment.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +0 -14
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +22 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +0 -5
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +14 -3
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/validationRunner.d.ts +1 -3
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/core/verification.d.ts +137 -0
- package/dist/core/verification.d.ts.map +1 -0
- package/dist/core/verification.js +323 -0
- package/dist/core/verification.js.map +1 -0
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +21 -0
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +9 -18
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +4 -10
- 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 +0 -2
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- 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/shell/interactiveShell.d.ts +16 -7
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +233 -164
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts +2 -0
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +40 -9
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +1 -4
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +136 -115
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +560 -511
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +71 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +87 -30
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/agentConfig.d.ts +27 -0
- package/dist/subagents/agentConfig.d.ts.map +1 -0
- package/dist/subagents/agentConfig.js +89 -0
- package/dist/subagents/agentConfig.js.map +1 -0
- package/dist/subagents/agentRegistry.d.ts +33 -0
- package/dist/subagents/agentRegistry.d.ts.map +1 -0
- package/dist/subagents/agentRegistry.js +162 -0
- package/dist/subagents/agentRegistry.js.map +1 -0
- package/dist/subagents/taskRunner.d.ts +7 -1
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +180 -47
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +13 -12
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +24 -45
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +140 -259
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +6 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +0 -158
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +0 -348
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +1 -0
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +15 -25
- package/dist/ui/unified/layout.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +10 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +78 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/package.json +4 -4
- package/dist/alpha-zero/agentWrapper.d.ts +0 -84
- package/dist/alpha-zero/agentWrapper.d.ts.map +0 -1
- package/dist/alpha-zero/agentWrapper.js +0 -171
- package/dist/alpha-zero/agentWrapper.js.map +0 -1
- package/dist/alpha-zero/codeEvaluator.d.ts +0 -25
- package/dist/alpha-zero/codeEvaluator.d.ts.map +0 -1
- package/dist/alpha-zero/codeEvaluator.js +0 -273
- package/dist/alpha-zero/codeEvaluator.js.map +0 -1
- package/dist/alpha-zero/competitiveRunner.d.ts +0 -66
- package/dist/alpha-zero/competitiveRunner.d.ts.map +0 -1
- package/dist/alpha-zero/competitiveRunner.js +0 -224
- package/dist/alpha-zero/competitiveRunner.js.map +0 -1
- package/dist/alpha-zero/index.d.ts +0 -67
- package/dist/alpha-zero/index.d.ts.map +0 -1
- package/dist/alpha-zero/index.js +0 -99
- package/dist/alpha-zero/index.js.map +0 -1
- package/dist/alpha-zero/introspection.d.ts +0 -128
- package/dist/alpha-zero/introspection.d.ts.map +0 -1
- package/dist/alpha-zero/introspection.js +0 -300
- package/dist/alpha-zero/introspection.js.map +0 -1
- package/dist/alpha-zero/metricsTracker.d.ts +0 -71
- package/dist/alpha-zero/metricsTracker.d.ts.map +0 -1
- package/dist/alpha-zero/metricsTracker.js.map +0 -1
- package/dist/alpha-zero/security/core.d.ts +0 -125
- package/dist/alpha-zero/security/core.d.ts.map +0 -1
- package/dist/alpha-zero/security/core.js +0 -271
- package/dist/alpha-zero/security/core.js.map +0 -1
- package/dist/alpha-zero/security/google.d.ts +0 -125
- package/dist/alpha-zero/security/google.d.ts.map +0 -1
- package/dist/alpha-zero/security/google.js +0 -311
- package/dist/alpha-zero/security/google.js.map +0 -1
- package/dist/alpha-zero/security/googleLoader.d.ts +0 -17
- package/dist/alpha-zero/security/googleLoader.d.ts.map +0 -1
- package/dist/alpha-zero/security/googleLoader.js +0 -41
- package/dist/alpha-zero/security/googleLoader.js.map +0 -1
- package/dist/alpha-zero/security/index.d.ts +0 -29
- package/dist/alpha-zero/security/index.d.ts.map +0 -1
- package/dist/alpha-zero/security/index.js +0 -32
- package/dist/alpha-zero/security/index.js.map +0 -1
- package/dist/alpha-zero/security/simulation.d.ts +0 -124
- package/dist/alpha-zero/security/simulation.d.ts.map +0 -1
- package/dist/alpha-zero/security/simulation.js +0 -277
- package/dist/alpha-zero/security/simulation.js.map +0 -1
- package/dist/alpha-zero/selfModification.d.ts +0 -109
- package/dist/alpha-zero/selfModification.d.ts.map +0 -1
- package/dist/alpha-zero/selfModification.js +0 -233
- package/dist/alpha-zero/selfModification.js.map +0 -1
- package/dist/alpha-zero/types.d.ts +0 -170
- package/dist/alpha-zero/types.d.ts.map +0 -1
- package/dist/alpha-zero/types.js +0 -31
- package/dist/alpha-zero/types.js.map +0 -1
- package/dist/capabilities/securityTestingCapability.d.ts +0 -13
- package/dist/capabilities/securityTestingCapability.d.ts.map +0 -1
- package/dist/capabilities/securityTestingCapability.js +0 -25
- package/dist/capabilities/securityTestingCapability.js.map +0 -1
- package/dist/core/aiFlowOptimizer.d.ts +0 -26
- package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
- package/dist/core/aiFlowOptimizer.js +0 -31
- package/dist/core/aiFlowOptimizer.js.map +0 -1
- package/dist/core/aiOptimizationEngine.d.ts +0 -158
- package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
- package/dist/core/aiOptimizationEngine.js +0 -428
- package/dist/core/aiOptimizationEngine.js.map +0 -1
- package/dist/core/aiOptimizationIntegration.d.ts +0 -93
- package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
- package/dist/core/aiOptimizationIntegration.js +0 -250
- package/dist/core/aiOptimizationIntegration.js.map +0 -1
- package/dist/core/enhancedErrorRecovery.d.ts +0 -100
- package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
- package/dist/core/enhancedErrorRecovery.js +0 -345
- package/dist/core/enhancedErrorRecovery.js.map +0 -1
- package/dist/core/hooksSystem.d.ts +0 -65
- package/dist/core/hooksSystem.d.ts.map +0 -1
- package/dist/core/hooksSystem.js +0 -273
- package/dist/core/hooksSystem.js.map +0 -1
- package/dist/core/memorySystem.d.ts +0 -48
- package/dist/core/memorySystem.d.ts.map +0 -1
- package/dist/core/memorySystem.js +0 -271
- package/dist/core/memorySystem.js.map +0 -1
- package/dist/core/unified/errors.d.ts +0 -189
- package/dist/core/unified/errors.d.ts.map +0 -1
- package/dist/core/unified/errors.js +0 -497
- package/dist/core/unified/errors.js.map +0 -1
- package/dist/core/unified/index.d.ts +0 -19
- package/dist/core/unified/index.d.ts.map +0 -1
- package/dist/core/unified/index.js +0 -68
- package/dist/core/unified/index.js.map +0 -1
- package/dist/core/unified/schema.d.ts +0 -101
- package/dist/core/unified/schema.d.ts.map +0 -1
- package/dist/core/unified/schema.js +0 -350
- package/dist/core/unified/schema.js.map +0 -1
- package/dist/core/unified/toolRuntime.d.ts +0 -179
- package/dist/core/unified/toolRuntime.d.ts.map +0 -1
- package/dist/core/unified/toolRuntime.js +0 -517
- package/dist/core/unified/toolRuntime.js.map +0 -1
- package/dist/core/unified/tools.d.ts +0 -127
- package/dist/core/unified/tools.d.ts.map +0 -1
- package/dist/core/unified/tools.js +0 -1333
- package/dist/core/unified/tools.js.map +0 -1
- package/dist/core/unified/types.d.ts +0 -352
- package/dist/core/unified/types.d.ts.map +0 -1
- package/dist/core/unified/types.js +0 -12
- package/dist/core/unified/types.js.map +0 -1
- package/dist/core/unified/version.d.ts +0 -209
- package/dist/core/unified/version.d.ts.map +0 -1
- package/dist/core/unified/version.js +0 -454
- package/dist/core/unified/version.js.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +0 -3
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.js +0 -12
- package/dist/plugins/tools/security/securityPlugin.js.map +0 -1
- package/dist/security/active-stack-security.d.ts +0 -112
- package/dist/security/active-stack-security.d.ts.map +0 -1
- package/dist/security/active-stack-security.js +0 -296
- package/dist/security/active-stack-security.js.map +0 -1
- package/dist/security/advanced-persistence-research.d.ts +0 -92
- package/dist/security/advanced-persistence-research.d.ts.map +0 -1
- package/dist/security/advanced-persistence-research.js +0 -195
- package/dist/security/advanced-persistence-research.js.map +0 -1
- package/dist/security/advanced-targeting.d.ts +0 -119
- package/dist/security/advanced-targeting.d.ts.map +0 -1
- package/dist/security/advanced-targeting.js +0 -233
- package/dist/security/advanced-targeting.js.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +0 -104
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.js +0 -315
- package/dist/security/assessment/vulnerabilityAssessment.js.map +0 -1
- package/dist/security/authorization/securityAuthorization.d.ts +0 -88
- package/dist/security/authorization/securityAuthorization.d.ts.map +0 -1
- package/dist/security/authorization/securityAuthorization.js +0 -172
- package/dist/security/authorization/securityAuthorization.js.map +0 -1
- package/dist/security/comprehensive-targeting.d.ts +0 -85
- package/dist/security/comprehensive-targeting.d.ts.map +0 -1
- package/dist/security/comprehensive-targeting.js +0 -438
- package/dist/security/comprehensive-targeting.js.map +0 -1
- package/dist/security/global-security-integration.d.ts +0 -91
- package/dist/security/global-security-integration.d.ts.map +0 -1
- package/dist/security/global-security-integration.js +0 -218
- package/dist/security/global-security-integration.js.map +0 -1
- package/dist/security/index.d.ts +0 -38
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/index.js +0 -47
- package/dist/security/index.js.map +0 -1
- package/dist/security/persistence-analyzer.d.ts +0 -56
- package/dist/security/persistence-analyzer.d.ts.map +0 -1
- package/dist/security/persistence-analyzer.js +0 -187
- package/dist/security/persistence-analyzer.js.map +0 -1
- package/dist/security/persistence-cli.d.ts +0 -36
- package/dist/security/persistence-cli.d.ts.map +0 -1
- package/dist/security/persistence-cli.js +0 -160
- package/dist/security/persistence-cli.js.map +0 -1
- package/dist/security/persistence-research.d.ts +0 -92
- package/dist/security/persistence-research.d.ts.map +0 -1
- package/dist/security/persistence-research.js +0 -364
- package/dist/security/persistence-research.js.map +0 -1
- package/dist/security/research/persistenceResearch.d.ts +0 -97
- package/dist/security/research/persistenceResearch.d.ts.map +0 -1
- package/dist/security/research/persistenceResearch.js +0 -282
- package/dist/security/research/persistenceResearch.js.map +0 -1
- package/dist/security/security-integration.d.ts +0 -74
- package/dist/security/security-integration.d.ts.map +0 -1
- package/dist/security/security-integration.js +0 -137
- package/dist/security/security-integration.js.map +0 -1
- package/dist/security/security-testing-framework.d.ts +0 -112
- package/dist/security/security-testing-framework.d.ts.map +0 -1
- package/dist/security/security-testing-framework.js +0 -364
- package/dist/security/security-testing-framework.js.map +0 -1
- package/dist/security/simulation/attackSimulation.d.ts +0 -93
- package/dist/security/simulation/attackSimulation.d.ts.map +0 -1
- package/dist/security/simulation/attackSimulation.js +0 -341
- package/dist/security/simulation/attackSimulation.js.map +0 -1
- package/dist/security/strategic-operations.d.ts +0 -100
- package/dist/security/strategic-operations.d.ts.map +0 -1
- package/dist/security/strategic-operations.js +0 -276
- package/dist/security/strategic-operations.js.map +0 -1
- package/dist/security/tool-security-wrapper.d.ts +0 -58
- package/dist/security/tool-security-wrapper.d.ts.map +0 -1
- package/dist/security/tool-security-wrapper.js +0 -156
- package/dist/security/tool-security-wrapper.js.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.js +0 -322
- package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
- package/dist/shell/inputQueueManager.d.ts +0 -144
- package/dist/shell/inputQueueManager.d.ts.map +0 -1
- package/dist/shell/inputQueueManager.js +0 -290
- package/dist/shell/inputQueueManager.js.map +0 -1
- package/dist/shell/metricsTracker.d.ts +0 -60
- package/dist/shell/metricsTracker.d.ts.map +0 -1
- package/dist/shell/metricsTracker.js +0 -119
- package/dist/shell/metricsTracker.js.map +0 -1
- package/dist/shell/streamingOutputManager.d.ts +0 -115
- package/dist/shell/streamingOutputManager.d.ts.map +0 -1
- package/dist/shell/streamingOutputManager.js +0 -225
- package/dist/shell/streamingOutputManager.js.map +0 -1
- package/dist/tools/securityTools.d.ts +0 -22
- package/dist/tools/securityTools.d.ts.map +0 -1
- package/dist/tools/securityTools.js +0 -448
- package/dist/tools/securityTools.js.map +0 -1
- package/dist/ui/persistentPrompt.d.ts +0 -50
- package/dist/ui/persistentPrompt.d.ts.map +0 -1
- package/dist/ui/persistentPrompt.js +0 -92
- package/dist/ui/persistentPrompt.js.map +0 -1
- package/dist/ui/terminalUISchema.d.ts +0 -195
- package/dist/ui/terminalUISchema.d.ts.map +0 -1
- package/dist/ui/terminalUISchema.js +0 -113
- package/dist/ui/terminalUISchema.js.map +0 -1
- package/scripts/deploy-security-capabilities.js +0 -178
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - Hybrid floating/scroll approach:
|
|
7
|
+
* - Initially: chat box floats below content
|
|
8
|
+
* - When terminal fills: scroll region activates, chat box pins to bottom
|
|
6
9
|
* - Native bracketed paste support (no heuristics)
|
|
7
10
|
* - Clean cursor model with render-time wrapping
|
|
8
11
|
* - State machine for different input modes
|
|
9
12
|
* - No readline dependency for display
|
|
10
13
|
*/
|
|
11
14
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
15
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
16
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import {
|
|
17
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
18
|
+
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
19
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
15
20
|
// ANSI escape codes
|
|
16
21
|
const ESC = {
|
|
17
22
|
// Cursor control
|
|
@@ -21,12 +26,15 @@ const ESC = {
|
|
|
21
26
|
SHOW: '\x1b[?25h',
|
|
22
27
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
23
28
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
-
// Screen control
|
|
25
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
-
HOME: '\x1b[H',
|
|
27
29
|
// Line control
|
|
28
30
|
CLEAR_LINE: '\x1b[2K',
|
|
29
31
|
CLEAR_TO_END: '\x1b[0J',
|
|
32
|
+
// Screen control
|
|
33
|
+
HOME: '\x1b[H',
|
|
34
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
35
|
+
// Scroll region
|
|
36
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
37
|
+
RESET_SCROLL: '\x1b[r',
|
|
30
38
|
// Style
|
|
31
39
|
RESET: '\x1b[0m',
|
|
32
40
|
DIM: '\x1b[2m',
|
|
@@ -66,47 +74,46 @@ export class TerminalInput extends EventEmitter {
|
|
|
66
74
|
statusMessage = null;
|
|
67
75
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
68
76
|
streamingLabel = null; // Streaming progress indicator
|
|
77
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
78
|
+
metaTokensUsed = null; // Optional token usage
|
|
79
|
+
metaTokenLimit = null; // Optional token window
|
|
80
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
81
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
69
82
|
lastRenderContent = '';
|
|
70
83
|
lastRenderCursor = -1;
|
|
71
84
|
renderDirty = false;
|
|
72
85
|
isRendering = false;
|
|
73
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
74
|
-
inputAreaStartRow = 0; // Track absolute row position of input area
|
|
75
|
-
contentEndRow = 0; // Row where content ends (chat box renders below this)
|
|
76
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
77
|
-
commandSuggestions = [];
|
|
78
|
-
filteredSuggestions = [];
|
|
79
|
-
selectedSuggestionIndex = 0;
|
|
80
|
-
showSuggestions = false;
|
|
81
86
|
// Lifecycle
|
|
82
87
|
disposed = false;
|
|
83
88
|
enabled = true;
|
|
84
89
|
contextUsage = null;
|
|
90
|
+
contextAutoCompactThreshold = 90;
|
|
91
|
+
// Track current content row (starts at top, moves down)
|
|
92
|
+
contentRow = 1;
|
|
93
|
+
// Track if scroll region is currently active
|
|
94
|
+
scrollRegionActive = false;
|
|
95
|
+
thinkingModeLabel = null;
|
|
85
96
|
editMode = 'display-edits';
|
|
86
97
|
verificationEnabled = true;
|
|
87
98
|
autoContinueEnabled = false;
|
|
88
99
|
verificationHotkey = 'alt+v';
|
|
89
100
|
autoContinueHotkey = 'alt+c';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
101
|
+
thinkingHotkey = '/thinking';
|
|
102
|
+
modelLabel = null;
|
|
103
|
+
providerLabel = null;
|
|
104
|
+
// Streaming render throttle
|
|
105
|
+
lastStreamingRender = 0;
|
|
106
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
97
107
|
streamingRenderTimer = null;
|
|
98
|
-
// Unified UI initialization flag
|
|
99
|
-
unifiedUIInitialized = false;
|
|
100
108
|
constructor(writeStream = process.stdout, config = {}) {
|
|
101
109
|
super();
|
|
102
110
|
this.out = writeStream;
|
|
103
|
-
// Use schema defaults for configuration consistency
|
|
104
111
|
this.config = {
|
|
105
|
-
maxLines: config.maxLines ??
|
|
106
|
-
maxLength: config.maxLength ??
|
|
112
|
+
maxLines: config.maxLines ?? 1000,
|
|
113
|
+
maxLength: config.maxLength ?? 10000,
|
|
107
114
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
108
|
-
promptChar: config.promptChar ??
|
|
109
|
-
continuationChar: config.continuationChar ??
|
|
115
|
+
promptChar: config.promptChar ?? '> ',
|
|
116
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
// ===========================================================================
|
|
@@ -185,293 +192,36 @@ export class TerminalInput extends EventEmitter {
|
|
|
185
192
|
if (handled)
|
|
186
193
|
return;
|
|
187
194
|
}
|
|
188
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
189
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
190
|
-
this.emit('showHelp');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
195
|
// Insert printable characters
|
|
194
196
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
197
|
this.insertText(str);
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
199
|
-
bannerContent = null;
|
|
200
|
-
/**
|
|
201
|
-
* Set banner content to be written when unified UI initializes.
|
|
202
|
-
*/
|
|
203
|
-
setBannerContent(content) {
|
|
204
|
-
this.bannerContent = content;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Initialize the unified UI system with BOTTOM PINNED chat box.
|
|
208
|
-
*
|
|
209
|
-
* Layout:
|
|
210
|
-
* 1. Clear screen
|
|
211
|
-
* 2. Write banner at top
|
|
212
|
-
* 3. Position cursor after banner and save it
|
|
213
|
-
* 4. Render chat box at bottom (sets up scroll region)
|
|
214
|
-
*/
|
|
215
|
-
initializeUnifiedUI() {
|
|
216
|
-
if (this.unifiedUIInitialized) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Hide cursor during setup
|
|
220
|
-
this.write(ESC.HIDE);
|
|
221
|
-
// Clear screen and go home
|
|
222
|
-
this.write(ESC.HOME);
|
|
223
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
224
|
-
// Write banner at top
|
|
225
|
-
let bannerLines = 0;
|
|
226
|
-
if (this.bannerContent) {
|
|
227
|
-
const lines = this.bannerContent.split('\n');
|
|
228
|
-
bannerLines = lines.length + 2; // +2 for trailing \n\n
|
|
229
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
230
|
-
}
|
|
231
|
-
// Position cursor after banner (in scroll region)
|
|
232
|
-
const cursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
|
|
233
|
-
this.write(ESC.TO(cursorRow, 1));
|
|
234
|
-
// Save this cursor position - it will be restored after chat box renders
|
|
235
|
-
this.write('\x1b7');
|
|
236
|
-
// Mark initialized
|
|
237
|
-
this.unifiedUIInitialized = true;
|
|
238
|
-
// Render chat box at bottom (sets scroll region, restores cursor)
|
|
239
|
-
this.renderFloatingInputArea();
|
|
240
|
-
this.write(ESC.SHOW);
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Clear the input area at its tracked position.
|
|
244
|
-
* Returns true if something was cleared.
|
|
245
|
-
*/
|
|
246
|
-
clearInputArea() {
|
|
247
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
248
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
249
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
250
|
-
this.write(ESC.CLEAR_LINE);
|
|
251
|
-
}
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Reset input area tracking state.
|
|
258
|
-
*/
|
|
259
|
-
resetInputAreaTracking() {
|
|
260
|
-
this.inputAreaStartRow = 0;
|
|
261
|
-
this.flowModeRenderedLines = 0;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Render chat box - BOTTOM PINNED with scroll region (SSE).
|
|
265
|
-
* Chat box is always at terminal bottom, content scrolls above it.
|
|
266
|
-
*/
|
|
267
|
-
renderFloatingInputArea() {
|
|
268
|
-
const { rows, cols } = this.getSize();
|
|
269
|
-
const divider = '─'.repeat(cols);
|
|
270
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
271
|
-
// Chat box is 4 lines: divider + input + divider + controls
|
|
272
|
-
const chatBoxHeight = 4;
|
|
273
|
-
const chatBoxStartRow = rows - chatBoxHeight + 1;
|
|
274
|
-
// Save cursor position (content cursor in scroll region)
|
|
275
|
-
this.write('\x1b7'); // Save cursor
|
|
276
|
-
// Set scroll region to protect chat box area
|
|
277
|
-
// Content scrolls in rows 1 to (chatBoxStartRow - 1)
|
|
278
|
-
this.write(`\x1b[1;${chatBoxStartRow - 1}r`);
|
|
279
|
-
// Hide cursor during render
|
|
280
|
-
this.write(ESC.HIDE);
|
|
281
|
-
// Track position
|
|
282
|
-
this.inputAreaStartRow = chatBoxStartRow;
|
|
283
|
-
let currentRow = chatBoxStartRow;
|
|
284
|
-
// Helper to write a line at absolute position (clears then writes)
|
|
285
|
-
const writeLine = (content) => {
|
|
286
|
-
this.write(ESC.TO(currentRow, 1));
|
|
287
|
-
this.write(ESC.CLEAR_LINE);
|
|
288
|
-
this.write(content);
|
|
289
|
-
currentRow++;
|
|
290
|
-
};
|
|
291
|
-
// Top divider
|
|
292
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
293
|
-
// Input line with > prompt
|
|
294
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 3);
|
|
295
|
-
const displayLine = lines[0] ?? '';
|
|
296
|
-
const inputRow = currentRow;
|
|
297
|
-
writeLine(`${DIM}>${R} ${displayLine}`);
|
|
298
|
-
// Bottom divider
|
|
299
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
300
|
-
// Mode controls line - Claude Code style
|
|
301
|
-
this.write(ESC.TO(currentRow, 1));
|
|
302
|
-
this.write(ESC.CLEAR_LINE);
|
|
303
|
-
this.write(this.buildClaudeStyleControls(cols));
|
|
304
|
-
// Track lines rendered
|
|
305
|
-
this.flowModeRenderedLines = chatBoxHeight;
|
|
306
|
-
// Restore cursor position (back to content area in scroll region)
|
|
307
|
-
this.write('\x1b8'); // Restore cursor
|
|
308
|
-
// Show cursor
|
|
309
|
-
this.write(ESC.SHOW);
|
|
310
|
-
// Update tracking
|
|
311
|
-
this.lastRenderContent = this.buffer;
|
|
312
|
-
this.lastRenderCursor = this.cursor;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Build Claude Code style controls line.
|
|
316
|
-
* Shows: edit mode indicator (shift+tab to cycle)
|
|
317
|
-
*/
|
|
318
|
-
buildClaudeStyleControls(cols) {
|
|
319
|
-
const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
|
|
320
|
-
// Edit mode indicator
|
|
321
|
-
let editModeText;
|
|
322
|
-
if (this.editMode === 'display-edits') {
|
|
323
|
-
editModeText = `${GREEN}⏵⏵${R} accept edits on`;
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
|
|
327
|
-
}
|
|
328
|
-
// Build controls line
|
|
329
|
-
const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
|
|
330
|
-
// Add thinking mode if enabled
|
|
331
|
-
if (this.thinkingEnabled) {
|
|
332
|
-
parts.push(`${CYAN}💭${R}`);
|
|
333
|
-
}
|
|
334
|
-
// Add context usage if available
|
|
335
|
-
if (this.contextUsage !== null) {
|
|
336
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
337
|
-
if (rem < 10) {
|
|
338
|
-
parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
|
|
339
|
-
}
|
|
340
|
-
else if (rem < 25) {
|
|
341
|
-
parts.push(`${YELLOW}ctx ${rem}%${R}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return parts.join(` ${DIM}·${R} `);
|
|
345
|
-
}
|
|
346
200
|
/**
|
|
347
201
|
* Set the input mode
|
|
348
202
|
*
|
|
349
|
-
*
|
|
350
|
-
* Scroll region protects chat box, content scrolls above it.
|
|
203
|
+
* Content flows naturally - no scroll region pinning.
|
|
351
204
|
*/
|
|
352
205
|
setMode(mode) {
|
|
353
206
|
const prevMode = this.mode;
|
|
354
207
|
this.mode = mode;
|
|
355
208
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
356
|
-
|
|
357
|
-
this.streamingStartTime = Date.now();
|
|
358
|
-
// Ensure unified UI is initialized
|
|
359
|
-
if (!this.unifiedUIInitialized) {
|
|
360
|
-
this.initializeUnifiedUI();
|
|
361
|
-
}
|
|
362
|
-
// Re-render to ensure scroll region is set correctly
|
|
209
|
+
this.resetStreamingRenderThrottle();
|
|
363
210
|
this.renderDirty = true;
|
|
364
|
-
this.
|
|
211
|
+
this.render();
|
|
365
212
|
}
|
|
366
213
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.streamingRenderTimer = null;
|
|
371
|
-
}
|
|
372
|
-
// Reset streaming time
|
|
373
|
-
this.streamingStartTime = null;
|
|
374
|
-
// Re-render chat box
|
|
375
|
-
this.renderDirty = true;
|
|
376
|
-
this.scheduleRender();
|
|
214
|
+
// Streaming ended - render the input area
|
|
215
|
+
this.resetStreamingRenderThrottle();
|
|
216
|
+
this.forceRender();
|
|
377
217
|
}
|
|
378
218
|
}
|
|
379
219
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*/
|
|
383
|
-
setContentEndRow(row) {
|
|
384
|
-
this.contentEndRow = Math.max(0, row);
|
|
385
|
-
this.renderDirty = true;
|
|
386
|
-
this.scheduleRender();
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Set available slash commands for auto-complete suggestions.
|
|
390
|
-
*/
|
|
391
|
-
setCommands(commands) {
|
|
392
|
-
this.commandSuggestions = commands;
|
|
393
|
-
this.updateSuggestions();
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Update filtered suggestions based on current input.
|
|
220
|
+
* Legacy method - no longer used (content flows naturally).
|
|
221
|
+
* @deprecated Use setContentRow instead
|
|
397
222
|
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// Only show suggestions when input starts with "/"
|
|
401
|
-
if (!input.startsWith('/')) {
|
|
402
|
-
this.showSuggestions = false;
|
|
403
|
-
this.filteredSuggestions = [];
|
|
404
|
-
this.selectedSuggestionIndex = 0;
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const query = input.toLowerCase();
|
|
408
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
409
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
410
|
-
// Show suggestions if we have matches
|
|
411
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
412
|
-
// Keep selection in bounds
|
|
413
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
414
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Select next suggestion (arrow down / tab).
|
|
419
|
-
*/
|
|
420
|
-
selectNextSuggestion() {
|
|
421
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
422
|
-
return;
|
|
423
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
424
|
-
this.renderDirty = true;
|
|
425
|
-
this.scheduleRender();
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
429
|
-
*/
|
|
430
|
-
selectPrevSuggestion() {
|
|
431
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
432
|
-
return;
|
|
433
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
434
|
-
? this.filteredSuggestions.length - 1
|
|
435
|
-
: this.selectedSuggestionIndex - 1;
|
|
436
|
-
this.renderDirty = true;
|
|
437
|
-
this.scheduleRender();
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Accept current suggestion and insert into buffer.
|
|
441
|
-
*/
|
|
442
|
-
acceptSuggestion() {
|
|
443
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
444
|
-
return false;
|
|
445
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
446
|
-
if (!selected)
|
|
447
|
-
return false;
|
|
448
|
-
// Replace buffer with selected command
|
|
449
|
-
this.buffer = selected.command + ' ';
|
|
450
|
-
this.cursor = this.buffer.length;
|
|
451
|
-
this.showSuggestions = false;
|
|
452
|
-
this.renderDirty = true;
|
|
453
|
-
this.scheduleRender();
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Check if suggestions are visible.
|
|
458
|
-
*/
|
|
459
|
-
areSuggestionsVisible() {
|
|
460
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Toggle thinking/reasoning mode
|
|
464
|
-
*/
|
|
465
|
-
toggleThinking() {
|
|
466
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
467
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
468
|
-
this.scheduleRender();
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Get thinking enabled state
|
|
472
|
-
*/
|
|
473
|
-
isThinkingEnabled() {
|
|
474
|
-
return this.thinkingEnabled;
|
|
223
|
+
setPinnedHeaderLines(_count) {
|
|
224
|
+
// No-op: scroll region pinning removed
|
|
475
225
|
}
|
|
476
226
|
/**
|
|
477
227
|
* Get current mode
|
|
@@ -504,17 +254,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
504
254
|
}
|
|
505
255
|
/**
|
|
506
256
|
* Clear the buffer
|
|
507
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
508
257
|
*/
|
|
509
|
-
clear(
|
|
258
|
+
clear() {
|
|
510
259
|
this.buffer = '';
|
|
511
260
|
this.cursor = 0;
|
|
512
261
|
this.historyIndex = -1;
|
|
513
262
|
this.tempInput = '';
|
|
514
263
|
this.pastePlaceholders = [];
|
|
515
|
-
|
|
516
|
-
this.scheduleRender();
|
|
517
|
-
}
|
|
264
|
+
this.scheduleRender();
|
|
518
265
|
}
|
|
519
266
|
/**
|
|
520
267
|
* Get queued inputs
|
|
@@ -585,6 +332,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
585
332
|
this.streamingLabel = next;
|
|
586
333
|
this.scheduleRender();
|
|
587
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
337
|
+
*/
|
|
338
|
+
setMetaStatus(meta) {
|
|
339
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
340
|
+
? Math.floor(meta.elapsedSeconds)
|
|
341
|
+
: null;
|
|
342
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
343
|
+
? Math.floor(meta.tokensUsed)
|
|
344
|
+
: null;
|
|
345
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
346
|
+
? Math.floor(meta.tokenLimit)
|
|
347
|
+
: null;
|
|
348
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
349
|
+
? Math.floor(meta.thinkingMs)
|
|
350
|
+
: null;
|
|
351
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
352
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
353
|
+
this.metaTokensUsed === nextTokens &&
|
|
354
|
+
this.metaTokenLimit === nextLimit &&
|
|
355
|
+
this.metaThinkingMs === nextThinking &&
|
|
356
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
360
|
+
this.metaTokensUsed = nextTokens;
|
|
361
|
+
this.metaTokenLimit = nextLimit;
|
|
362
|
+
this.metaThinkingMs = nextThinking;
|
|
363
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
364
|
+
this.scheduleRender();
|
|
365
|
+
}
|
|
588
366
|
/**
|
|
589
367
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
590
368
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -594,26 +372,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
594
372
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
595
373
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
596
374
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
375
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
376
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
597
377
|
if (this.verificationEnabled === nextVerification &&
|
|
598
378
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
599
379
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
600
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
380
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
381
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
382
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
601
383
|
return;
|
|
602
384
|
}
|
|
603
385
|
this.verificationEnabled = nextVerification;
|
|
604
386
|
this.autoContinueEnabled = nextAutoContinue;
|
|
605
387
|
this.verificationHotkey = nextVerifyHotkey;
|
|
606
388
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
607
|
-
this.
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
611
|
-
* This is displayed persistently above the input area.
|
|
612
|
-
*/
|
|
613
|
-
setModelInfo(info) {
|
|
614
|
-
if (this.modelInfo === info)
|
|
615
|
-
return;
|
|
616
|
-
this.modelInfo = info;
|
|
389
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
390
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
617
391
|
this.scheduleRender();
|
|
618
392
|
}
|
|
619
393
|
/**
|
|
@@ -626,33 +400,158 @@ export class TerminalInput extends EventEmitter {
|
|
|
626
400
|
this.scheduleRender();
|
|
627
401
|
}
|
|
628
402
|
/**
|
|
629
|
-
*
|
|
630
|
-
|
|
631
|
-
|
|
403
|
+
* Surface model/provider context in the controls bar.
|
|
404
|
+
*/
|
|
405
|
+
setModelContext(options) {
|
|
406
|
+
const nextModel = options.model?.trim() || null;
|
|
407
|
+
const nextProvider = options.provider?.trim() || null;
|
|
408
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.modelLabel = nextModel;
|
|
412
|
+
this.providerLabel = nextProvider;
|
|
413
|
+
this.scheduleRender();
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Render the floating input area at contentRow.
|
|
417
|
+
*
|
|
418
|
+
* The chat box "floats" - it renders right below the last streamed content.
|
|
419
|
+
* As content is added, contentRow advances, and the chat box moves down.
|
|
420
|
+
* No scroll regions - pure floating behavior.
|
|
632
421
|
*/
|
|
633
422
|
render() {
|
|
634
423
|
if (!this.canRender())
|
|
635
424
|
return;
|
|
636
425
|
if (this.isRendering)
|
|
637
426
|
return;
|
|
427
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
428
|
+
// During streaming, throttle re-renders
|
|
429
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
430
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
431
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
432
|
+
if (waitMs > 0) {
|
|
433
|
+
this.renderDirty = true;
|
|
434
|
+
this.scheduleStreamingRender(waitMs);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
638
438
|
const shouldSkip = !this.renderDirty &&
|
|
639
439
|
this.buffer === this.lastRenderContent &&
|
|
640
440
|
this.cursor === this.lastRenderCursor;
|
|
641
441
|
this.renderDirty = false;
|
|
642
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
643
442
|
if (shouldSkip) {
|
|
644
443
|
return;
|
|
645
444
|
}
|
|
646
|
-
// If write lock is held, defer render
|
|
647
445
|
if (writeLock.isLocked()) {
|
|
648
446
|
writeLock.safeWrite(() => this.render());
|
|
649
447
|
return;
|
|
650
448
|
}
|
|
449
|
+
this.renderFloatingInputArea();
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Core floating input area renderer.
|
|
453
|
+
* Chat box always floats at contentRow (below streamed content).
|
|
454
|
+
* This creates "persistent bottom floating" behavior.
|
|
455
|
+
*/
|
|
456
|
+
renderFloatingInputArea() {
|
|
457
|
+
const { rows, cols } = this.getSize();
|
|
458
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
459
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
460
|
+
// Wrap buffer into display lines
|
|
461
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
462
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
463
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
464
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
465
|
+
// Calculate display window (keep cursor visible)
|
|
466
|
+
let startLine = 0;
|
|
467
|
+
if (lines.length > displayLines) {
|
|
468
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
469
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
470
|
+
}
|
|
471
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
472
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
473
|
+
// Chat box height (must match getChatBoxHeight calculation)
|
|
474
|
+
const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
|
|
475
|
+
// Pin chat box at bottom of terminal
|
|
476
|
+
// This ensures streaming content stays in the scroll region above
|
|
477
|
+
const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
|
|
478
|
+
writeLock.lock('terminalInput.renderFloating');
|
|
651
479
|
this.isRendering = true;
|
|
652
|
-
writeLock.lock('terminalInput.render');
|
|
653
480
|
try {
|
|
654
|
-
//
|
|
655
|
-
this.
|
|
481
|
+
// Hide cursor during render
|
|
482
|
+
this.write(ESC.HIDE);
|
|
483
|
+
this.write(ESC.RESET);
|
|
484
|
+
// Clear the chat box area
|
|
485
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
486
|
+
const row = chatBoxStartRow + i;
|
|
487
|
+
if (row <= rows) {
|
|
488
|
+
this.write(ESC.TO(row, 1));
|
|
489
|
+
this.write(ESC.CLEAR_LINE);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let currentRow = chatBoxStartRow;
|
|
493
|
+
// Meta/status header
|
|
494
|
+
for (const metaLine of metaLines) {
|
|
495
|
+
this.write(ESC.TO(currentRow, 1));
|
|
496
|
+
this.write(metaLine);
|
|
497
|
+
currentRow += 1;
|
|
498
|
+
}
|
|
499
|
+
// Separator line
|
|
500
|
+
this.write(ESC.TO(currentRow, 1));
|
|
501
|
+
this.write(renderDivider(cols - 2));
|
|
502
|
+
currentRow += 1;
|
|
503
|
+
// Render input lines
|
|
504
|
+
let finalRow = currentRow;
|
|
505
|
+
let finalCol = 3;
|
|
506
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
507
|
+
const rowNum = currentRow + i;
|
|
508
|
+
this.write(ESC.TO(rowNum, 1));
|
|
509
|
+
const line = visibleLines[i] ?? '';
|
|
510
|
+
const isFirstLine = (startLine + i) === 0;
|
|
511
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
512
|
+
this.write(ESC.BG_DARK);
|
|
513
|
+
this.write(ESC.DIM);
|
|
514
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
515
|
+
this.write(ESC.RESET);
|
|
516
|
+
this.write(ESC.BG_DARK);
|
|
517
|
+
if (isCursorLine) {
|
|
518
|
+
const col = Math.min(cursorCol, line.length);
|
|
519
|
+
const before = line.slice(0, col);
|
|
520
|
+
const at = col < line.length ? line[col] : ' ';
|
|
521
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
522
|
+
this.write(before);
|
|
523
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
524
|
+
this.write(at);
|
|
525
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
526
|
+
this.write(after);
|
|
527
|
+
finalRow = rowNum;
|
|
528
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
this.write(line);
|
|
532
|
+
}
|
|
533
|
+
// Pad to edge
|
|
534
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
535
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
536
|
+
if (padding > 0)
|
|
537
|
+
this.write(' '.repeat(padding));
|
|
538
|
+
this.write(ESC.RESET);
|
|
539
|
+
}
|
|
540
|
+
// Mode controls line
|
|
541
|
+
const controlRow = currentRow + visibleLines.length;
|
|
542
|
+
this.write(ESC.TO(controlRow, 1));
|
|
543
|
+
this.write(this.buildModeControls(cols));
|
|
544
|
+
// Position cursor in input box
|
|
545
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
546
|
+
this.write(ESC.SHOW);
|
|
547
|
+
// Update state
|
|
548
|
+
this.lastRenderContent = this.buffer;
|
|
549
|
+
this.lastRenderCursor = this.cursor;
|
|
550
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
551
|
+
if (this.streamingRenderTimer) {
|
|
552
|
+
clearTimeout(this.streamingRenderTimer);
|
|
553
|
+
this.streamingRenderTimer = null;
|
|
554
|
+
}
|
|
656
555
|
}
|
|
657
556
|
finally {
|
|
658
557
|
writeLock.unlock();
|
|
@@ -660,99 +559,181 @@ export class TerminalInput extends EventEmitter {
|
|
|
660
559
|
}
|
|
661
560
|
}
|
|
662
561
|
/**
|
|
663
|
-
* Build
|
|
664
|
-
*
|
|
562
|
+
* Build compact meta line above the divider.
|
|
563
|
+
* Shows model/provider and key metrics in a single line.
|
|
564
|
+
* Status message is shown in mode controls to avoid duplication.
|
|
665
565
|
*/
|
|
666
|
-
|
|
667
|
-
const maxWidth = cols - 2;
|
|
566
|
+
buildMetaLines(width) {
|
|
668
567
|
const parts = [];
|
|
669
|
-
//
|
|
670
|
-
if (this.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const secs = elapsed % 60;
|
|
676
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
677
|
-
}
|
|
678
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
568
|
+
// Model/provider info
|
|
569
|
+
if (this.modelLabel) {
|
|
570
|
+
const modelText = this.providerLabel
|
|
571
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
572
|
+
: this.modelLabel;
|
|
573
|
+
parts.push({ text: modelText, tone: 'info' });
|
|
679
574
|
}
|
|
680
|
-
//
|
|
681
|
-
if (this.
|
|
682
|
-
parts.push(
|
|
575
|
+
// Elapsed time
|
|
576
|
+
if (this.metaElapsedSeconds !== null) {
|
|
577
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
683
578
|
}
|
|
684
|
-
//
|
|
685
|
-
if (this.
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// Override/warning status
|
|
690
|
-
if (this.overrideStatusMessage) {
|
|
691
|
-
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
579
|
+
// Token usage (compact)
|
|
580
|
+
if (this.metaTokensUsed !== null) {
|
|
581
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
582
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
583
|
+
parts.push({ text: `${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
692
584
|
}
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
585
|
+
// Context remaining (only show if concerning)
|
|
586
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
587
|
+
if (tokensRemaining !== null) {
|
|
588
|
+
parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
|
|
696
589
|
}
|
|
697
|
-
//
|
|
698
|
-
if (this.
|
|
699
|
-
parts.push(
|
|
590
|
+
// Thinking indicator
|
|
591
|
+
if (this.metaThinkingMs !== null) {
|
|
592
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
700
593
|
}
|
|
701
|
-
if (parts.length
|
|
702
|
-
return
|
|
594
|
+
if (!parts.length) {
|
|
595
|
+
return [];
|
|
703
596
|
}
|
|
704
|
-
|
|
705
|
-
return joined.slice(0, maxWidth);
|
|
597
|
+
return [renderStatusLine(parts, width)];
|
|
706
598
|
}
|
|
707
599
|
/**
|
|
708
|
-
* Build mode controls line
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
600
|
+
* Build Claude Code style mode controls line.
|
|
601
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
712
602
|
*/
|
|
713
603
|
buildModeControls(cols) {
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
720
|
-
if (this.editMode === 'display-edits') {
|
|
721
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
604
|
+
const width = Math.max(8, cols - 2);
|
|
605
|
+
const leftParts = [];
|
|
606
|
+
const rightParts = [];
|
|
607
|
+
if (this.streamingLabel) {
|
|
608
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
722
609
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
610
|
+
if (this.overrideStatusMessage) {
|
|
611
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
612
|
+
}
|
|
613
|
+
if (this.statusMessage) {
|
|
614
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
615
|
+
}
|
|
616
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
617
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
618
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
619
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
620
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
621
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
622
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
623
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
624
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
625
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
626
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
627
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
628
|
+
}
|
|
629
|
+
if (this.buffer.includes('\n')) {
|
|
630
|
+
const lineCount = this.buffer.split('\n').length;
|
|
631
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
632
|
+
}
|
|
633
|
+
if (this.pastePlaceholders.length > 0) {
|
|
634
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
635
|
+
leftParts.push({
|
|
636
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
637
|
+
tone: 'info',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const contextRemaining = this.computeContextRemaining();
|
|
641
|
+
if (this.thinkingModeLabel) {
|
|
642
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
643
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
644
|
+
}
|
|
645
|
+
// Model info is now in meta lines only - no duplication here
|
|
646
|
+
if (contextRemaining !== null) {
|
|
647
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
648
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
649
|
+
? 'Context auto-compact imminent'
|
|
650
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
651
|
+
rightParts.push({ text: label, tone });
|
|
652
|
+
}
|
|
653
|
+
if (!rightParts.length || width < 60) {
|
|
654
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
655
|
+
return renderStatusLine(merged, width);
|
|
656
|
+
}
|
|
657
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
658
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
659
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
660
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
661
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
662
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
663
|
+
}
|
|
664
|
+
formatHotkey(hotkey) {
|
|
665
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
666
|
+
if (!normalized)
|
|
667
|
+
return hotkey;
|
|
668
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
669
|
+
const map = {
|
|
670
|
+
shift: '⇧',
|
|
671
|
+
sh: '⇧',
|
|
672
|
+
alt: '⌥',
|
|
673
|
+
option: '⌥',
|
|
674
|
+
opt: '⌥',
|
|
675
|
+
ctrl: '⌃',
|
|
676
|
+
control: '⌃',
|
|
677
|
+
cmd: '⌘',
|
|
678
|
+
meta: '⌘',
|
|
679
|
+
};
|
|
680
|
+
const formatted = parts
|
|
681
|
+
.map((part) => {
|
|
682
|
+
const symbol = map[part];
|
|
683
|
+
if (symbol)
|
|
684
|
+
return symbol;
|
|
685
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
686
|
+
})
|
|
687
|
+
.join('');
|
|
688
|
+
return formatted || hotkey;
|
|
689
|
+
}
|
|
690
|
+
computeContextRemaining() {
|
|
691
|
+
if (this.contextUsage === null) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
695
|
+
}
|
|
696
|
+
computeTokensRemaining() {
|
|
697
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
701
|
+
return this.formatTokenCount(remaining);
|
|
702
|
+
}
|
|
703
|
+
formatElapsedLabel(seconds) {
|
|
704
|
+
if (seconds < 60) {
|
|
705
|
+
return `${seconds}s`;
|
|
706
|
+
}
|
|
707
|
+
const mins = Math.floor(seconds / 60);
|
|
708
|
+
const secs = seconds % 60;
|
|
709
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
710
|
+
}
|
|
711
|
+
formatTokenCount(value) {
|
|
712
|
+
if (!Number.isFinite(value)) {
|
|
713
|
+
return `${value}`;
|
|
714
|
+
}
|
|
715
|
+
if (value >= 1_000_000) {
|
|
716
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
717
|
+
}
|
|
718
|
+
if (value >= 1_000) {
|
|
719
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
720
|
+
}
|
|
721
|
+
return `${Math.round(value)}`;
|
|
722
|
+
}
|
|
723
|
+
visibleLength(value) {
|
|
724
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
725
|
+
return value.replace(ansiPattern, '').length;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
729
|
+
* needing a TTY. Not used by production code.
|
|
730
|
+
*/
|
|
731
|
+
getDebugUiSnapshot(width) {
|
|
732
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
733
|
+
return {
|
|
734
|
+
meta: this.buildMetaLines(cols - 2),
|
|
735
|
+
controls: this.buildModeControls(cols),
|
|
736
|
+
};
|
|
756
737
|
}
|
|
757
738
|
/**
|
|
758
739
|
* Force a re-render
|
|
@@ -775,30 +756,148 @@ export class TerminalInput extends EventEmitter {
|
|
|
775
756
|
handleResize() {
|
|
776
757
|
this.lastRenderContent = '';
|
|
777
758
|
this.lastRenderCursor = -1;
|
|
759
|
+
this.resetStreamingRenderThrottle();
|
|
778
760
|
this.scheduleRender();
|
|
779
761
|
}
|
|
780
762
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
763
|
+
* Enter streaming mode with scroll region.
|
|
764
|
+
* Call this ONCE at the start of streaming.
|
|
765
|
+
* Renders chat box at bottom and sets up scroll region above it.
|
|
783
766
|
*/
|
|
784
|
-
|
|
785
|
-
if (this.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
767
|
+
enterStreamingScrollRegion() {
|
|
768
|
+
if (this.scrollRegionActive)
|
|
769
|
+
return;
|
|
770
|
+
writeLock.lock('enterScrollRegion');
|
|
771
|
+
try {
|
|
772
|
+
const { rows } = this.getSize();
|
|
773
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
774
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
775
|
+
// Render chat box at the bottom first
|
|
776
|
+
this.forceRender();
|
|
777
|
+
// Set scroll region to content area only (excludes chat box)
|
|
778
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
779
|
+
// Position cursor at content row
|
|
780
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
781
|
+
this.scrollRegionActive = true;
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
writeLock.unlock();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Exit streaming scroll region.
|
|
789
|
+
* Call this ONCE at the end of streaming.
|
|
790
|
+
*/
|
|
791
|
+
exitStreamingScrollRegion() {
|
|
792
|
+
if (!this.scrollRegionActive)
|
|
793
|
+
return;
|
|
794
|
+
writeLock.lock('exitScrollRegion');
|
|
795
|
+
try {
|
|
796
|
+
// Reset scroll region to full terminal
|
|
797
|
+
this.write(ESC.RESET_SCROLL);
|
|
798
|
+
this.scrollRegionActive = false;
|
|
799
|
+
}
|
|
800
|
+
finally {
|
|
801
|
+
writeLock.unlock();
|
|
802
|
+
}
|
|
803
|
+
// Final render of chat box
|
|
804
|
+
this.forceRender();
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Stream content - simple write when scroll region is active.
|
|
808
|
+
* Content flows naturally within the scroll region.
|
|
809
|
+
*/
|
|
810
|
+
streamContent(content) {
|
|
811
|
+
if (!content)
|
|
812
|
+
return;
|
|
813
|
+
writeLock.lock('streamContent');
|
|
814
|
+
try {
|
|
815
|
+
// Just write content - if scroll region is active, terminal handles scrolling
|
|
816
|
+
// If not active, set up scroll region first
|
|
817
|
+
if (!this.scrollRegionActive) {
|
|
818
|
+
const { rows } = this.getSize();
|
|
819
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
820
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
821
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
822
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
823
|
+
this.scrollRegionActive = true;
|
|
824
|
+
}
|
|
825
|
+
// Write content directly - terminal handles scrolling within region
|
|
826
|
+
this.write(content);
|
|
827
|
+
// Track position for later reference
|
|
828
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
829
|
+
this.contentRow += newlines;
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
writeLock.unlock();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Enable scroll region (no-op in floating mode).
|
|
837
|
+
*/
|
|
838
|
+
enableScrollRegion() {
|
|
839
|
+
// No-op: using pure floating approach
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Disable scroll region (no-op in floating mode).
|
|
843
|
+
*/
|
|
844
|
+
disableScrollRegion() {
|
|
845
|
+
// No-op: using pure floating approach
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Calculate chat box height.
|
|
849
|
+
*/
|
|
850
|
+
getChatBoxHeight() {
|
|
851
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* @deprecated Use streamContent() instead
|
|
855
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
856
|
+
*/
|
|
857
|
+
registerOutputInterceptor(_display) {
|
|
858
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* @deprecated Use streamContent() instead
|
|
862
|
+
* Write content above the floating chat box.
|
|
863
|
+
*/
|
|
864
|
+
writeToScrollRegion(content) {
|
|
865
|
+
this.streamContent(content);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Clear the entire terminal screen and reset content position.
|
|
869
|
+
* This removes all content including the launching command.
|
|
870
|
+
*/
|
|
871
|
+
clearScreen() {
|
|
872
|
+
writeLock.lock('clearScreen');
|
|
873
|
+
try {
|
|
874
|
+
this.write(ESC.HOME);
|
|
875
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
876
|
+
this.contentRow = 1;
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
writeLock.unlock();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Reset content position to row 1.
|
|
884
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
885
|
+
*/
|
|
886
|
+
resetContentPosition() {
|
|
887
|
+
this.contentRow = 1;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Set the content row explicitly (used after banner is written).
|
|
891
|
+
* This tells the input where content should start flowing from.
|
|
892
|
+
*/
|
|
893
|
+
setContentRow(row) {
|
|
894
|
+
this.contentRow = Math.max(1, row);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Get the current content row position.
|
|
898
|
+
*/
|
|
899
|
+
getContentRow() {
|
|
900
|
+
return this.contentRow;
|
|
802
901
|
}
|
|
803
902
|
/**
|
|
804
903
|
* Dispose and clean up
|
|
@@ -806,20 +905,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
806
905
|
dispose() {
|
|
807
906
|
if (this.disposed)
|
|
808
907
|
return;
|
|
809
|
-
// Clean up streaming render timer
|
|
810
|
-
if (this.streamingRenderTimer) {
|
|
811
|
-
clearInterval(this.streamingRenderTimer);
|
|
812
|
-
this.streamingRenderTimer = null;
|
|
813
|
-
}
|
|
814
|
-
// Clean up output interceptor
|
|
815
|
-
if (this.outputInterceptorCleanup) {
|
|
816
|
-
this.outputInterceptorCleanup();
|
|
817
|
-
this.outputInterceptorCleanup = undefined;
|
|
818
|
-
}
|
|
819
|
-
// Reset scroll region before disposing
|
|
820
|
-
this.write('\x1b[r');
|
|
821
908
|
this.disposed = true;
|
|
822
909
|
this.enabled = false;
|
|
910
|
+
this.disableScrollRegion();
|
|
911
|
+
this.resetStreamingRenderThrottle();
|
|
823
912
|
this.disableBracketedPaste();
|
|
824
913
|
this.buffer = '';
|
|
825
914
|
this.queue = [];
|
|
@@ -924,22 +1013,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
924
1013
|
this.toggleEditMode();
|
|
925
1014
|
return true;
|
|
926
1015
|
}
|
|
927
|
-
|
|
928
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
929
|
-
this.togglePasteExpansion();
|
|
930
|
-
}
|
|
931
|
-
else {
|
|
932
|
-
this.toggleThinking();
|
|
933
|
-
}
|
|
934
|
-
return true;
|
|
935
|
-
case 'escape':
|
|
936
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
937
|
-
if (this.mode === 'streaming') {
|
|
938
|
-
this.emit('interrupt');
|
|
939
|
-
}
|
|
940
|
-
else if (this.buffer.length > 0) {
|
|
941
|
-
this.clear();
|
|
942
|
-
}
|
|
1016
|
+
this.insertText(' ');
|
|
943
1017
|
return true;
|
|
944
1018
|
}
|
|
945
1019
|
return false;
|
|
@@ -957,7 +1031,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
957
1031
|
this.insertPlainText(chunk, insertPos);
|
|
958
1032
|
this.cursor = insertPos + chunk.length;
|
|
959
1033
|
this.emit('change', this.buffer);
|
|
960
|
-
this.updateSuggestions();
|
|
961
1034
|
this.scheduleRender();
|
|
962
1035
|
}
|
|
963
1036
|
insertNewline() {
|
|
@@ -982,7 +1055,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
982
1055
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
983
1056
|
}
|
|
984
1057
|
this.emit('change', this.buffer);
|
|
985
|
-
this.updateSuggestions();
|
|
986
1058
|
this.scheduleRender();
|
|
987
1059
|
}
|
|
988
1060
|
deleteForward() {
|
|
@@ -1210,13 +1282,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1210
1282
|
timestamp: Date.now(),
|
|
1211
1283
|
});
|
|
1212
1284
|
this.emit('queue', text);
|
|
1213
|
-
this.clear(); // Clear immediately for queued input
|
|
1285
|
+
this.clear(); // Clear immediately for queued input
|
|
1214
1286
|
}
|
|
1215
1287
|
else {
|
|
1216
|
-
// In idle mode, clear the input
|
|
1217
|
-
// The
|
|
1218
|
-
|
|
1219
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1288
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1289
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1290
|
+
this.clear();
|
|
1220
1291
|
this.emit('submit', text);
|
|
1221
1292
|
}
|
|
1222
1293
|
}
|
|
@@ -1233,7 +1304,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1233
1304
|
if (available <= 0)
|
|
1234
1305
|
return;
|
|
1235
1306
|
const chunk = clean.slice(0, available);
|
|
1236
|
-
|
|
1307
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1308
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1309
|
+
if (isMultiline && !isShortMultiline) {
|
|
1237
1310
|
this.insertPastePlaceholder(chunk);
|
|
1238
1311
|
}
|
|
1239
1312
|
else {
|
|
@@ -1369,17 +1442,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1369
1442
|
this.shiftPlaceholders(position, text.length);
|
|
1370
1443
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1371
1444
|
}
|
|
1445
|
+
shouldInlineMultiline(content) {
|
|
1446
|
+
const lines = content.split('\n').length;
|
|
1447
|
+
const maxInlineLines = 4;
|
|
1448
|
+
const maxInlineChars = 240;
|
|
1449
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1450
|
+
}
|
|
1372
1451
|
findPlaceholderAt(position) {
|
|
1373
1452
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1374
1453
|
}
|
|
1375
|
-
buildPlaceholder(
|
|
1454
|
+
buildPlaceholder(lineCount) {
|
|
1376
1455
|
const id = ++this.pasteCounter;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
const preview = summary.preview.length > 30
|
|
1380
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1381
|
-
: summary.preview;
|
|
1382
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1456
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1457
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1383
1458
|
return { id, placeholder };
|
|
1384
1459
|
}
|
|
1385
1460
|
insertPastePlaceholder(content) {
|
|
@@ -1387,67 +1462,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1387
1462
|
if (available <= 0)
|
|
1388
1463
|
return;
|
|
1389
1464
|
const cleanContent = content.slice(0, available);
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
if (summary.lineCount < 5) {
|
|
1393
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1394
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1395
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1396
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1465
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1466
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1400
1467
|
const insertPos = this.cursor;
|
|
1401
1468
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1402
1469
|
this.pastePlaceholders.push({
|
|
1403
1470
|
id,
|
|
1404
1471
|
content: cleanContent,
|
|
1405
|
-
lineCount
|
|
1472
|
+
lineCount,
|
|
1406
1473
|
placeholder,
|
|
1407
1474
|
start: insertPos,
|
|
1408
1475
|
end: insertPos + placeholder.length,
|
|
1409
|
-
summary,
|
|
1410
|
-
expanded: false,
|
|
1411
1476
|
});
|
|
1412
1477
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1413
1478
|
this.cursor = insertPos + placeholder.length;
|
|
1414
1479
|
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1417
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1418
|
-
*/
|
|
1419
|
-
togglePasteExpansion() {
|
|
1420
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1421
|
-
if (!placeholder)
|
|
1422
|
-
return false;
|
|
1423
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1424
|
-
// Update the placeholder text in buffer
|
|
1425
|
-
const newPlaceholder = placeholder.expanded
|
|
1426
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1427
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1428
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1429
|
-
// Update buffer
|
|
1430
|
-
this.buffer =
|
|
1431
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1432
|
-
newPlaceholder +
|
|
1433
|
-
this.buffer.slice(placeholder.end);
|
|
1434
|
-
// Update placeholder tracking
|
|
1435
|
-
placeholder.placeholder = newPlaceholder;
|
|
1436
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1437
|
-
// Shift other placeholders
|
|
1438
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1439
|
-
this.scheduleRender();
|
|
1440
|
-
return true;
|
|
1441
|
-
}
|
|
1442
|
-
buildExpandedPlaceholder(ph) {
|
|
1443
|
-
const lines = ph.content.split('\n');
|
|
1444
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1445
|
-
const lastLines = lines.length > 5
|
|
1446
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1447
|
-
: '';
|
|
1448
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1449
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1450
|
-
}
|
|
1451
1480
|
deletePlaceholder(placeholder) {
|
|
1452
1481
|
const length = placeholder.end - placeholder.start;
|
|
1453
1482
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1455,7 +1484,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1455
1484
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1456
1485
|
this.cursor = placeholder.start;
|
|
1457
1486
|
}
|
|
1458
|
-
updateContextUsage(value) {
|
|
1487
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1488
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1489
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1490
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1491
|
+
}
|
|
1459
1492
|
if (value === null || !Number.isFinite(value)) {
|
|
1460
1493
|
this.contextUsage = null;
|
|
1461
1494
|
}
|
|
@@ -1482,6 +1515,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1482
1515
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1483
1516
|
this.setEditMode(next);
|
|
1484
1517
|
}
|
|
1518
|
+
scheduleStreamingRender(delayMs) {
|
|
1519
|
+
if (this.streamingRenderTimer)
|
|
1520
|
+
return;
|
|
1521
|
+
const wait = Math.max(16, delayMs);
|
|
1522
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1523
|
+
this.streamingRenderTimer = null;
|
|
1524
|
+
this.render();
|
|
1525
|
+
}, wait);
|
|
1526
|
+
}
|
|
1527
|
+
resetStreamingRenderThrottle() {
|
|
1528
|
+
if (this.streamingRenderTimer) {
|
|
1529
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1530
|
+
this.streamingRenderTimer = null;
|
|
1531
|
+
}
|
|
1532
|
+
this.lastStreamingRender = 0;
|
|
1533
|
+
}
|
|
1485
1534
|
scheduleRender() {
|
|
1486
1535
|
if (!this.canRender())
|
|
1487
1536
|
return;
|