erosolar-cli 1.7.321 → 1.7.323
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 +135 -115
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +527 -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
|
-
*
|
|
220
|
+
* Legacy method - no longer used (content flows naturally).
|
|
221
|
+
* @deprecated Use setContentRow instead
|
|
382
222
|
*/
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
397
|
-
*/
|
|
398
|
-
updateSuggestions() {
|
|
399
|
-
const input = this.buffer.trim();
|
|
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,162 @@ 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
|
+
// During streaming, don't render the chat box - let content flow naturally
|
|
476
|
+
if (this.scrollRegionActive) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// When not streaming, position chat box after content (floating)
|
|
480
|
+
// This allows banner and history to scroll naturally
|
|
481
|
+
const chatBoxStartRow = Math.max(1, this.contentRow + 1);
|
|
482
|
+
writeLock.lock('terminalInput.renderFloating');
|
|
651
483
|
this.isRendering = true;
|
|
652
|
-
writeLock.lock('terminalInput.render');
|
|
653
484
|
try {
|
|
654
|
-
//
|
|
655
|
-
this.
|
|
485
|
+
// Hide cursor during render
|
|
486
|
+
this.write(ESC.HIDE);
|
|
487
|
+
this.write(ESC.RESET);
|
|
488
|
+
// Clear the chat box area
|
|
489
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
490
|
+
const row = chatBoxStartRow + i;
|
|
491
|
+
if (row <= rows) {
|
|
492
|
+
this.write(ESC.TO(row, 1));
|
|
493
|
+
this.write(ESC.CLEAR_LINE);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
let currentRow = chatBoxStartRow;
|
|
497
|
+
// Meta/status header
|
|
498
|
+
for (const metaLine of metaLines) {
|
|
499
|
+
this.write(ESC.TO(currentRow, 1));
|
|
500
|
+
this.write(metaLine);
|
|
501
|
+
currentRow += 1;
|
|
502
|
+
}
|
|
503
|
+
// Separator line
|
|
504
|
+
this.write(ESC.TO(currentRow, 1));
|
|
505
|
+
this.write(renderDivider(cols - 2));
|
|
506
|
+
currentRow += 1;
|
|
507
|
+
// Render input lines
|
|
508
|
+
let finalRow = currentRow;
|
|
509
|
+
let finalCol = 3;
|
|
510
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
511
|
+
const rowNum = currentRow + i;
|
|
512
|
+
this.write(ESC.TO(rowNum, 1));
|
|
513
|
+
const line = visibleLines[i] ?? '';
|
|
514
|
+
const isFirstLine = (startLine + i) === 0;
|
|
515
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
516
|
+
this.write(ESC.BG_DARK);
|
|
517
|
+
this.write(ESC.DIM);
|
|
518
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
519
|
+
this.write(ESC.RESET);
|
|
520
|
+
this.write(ESC.BG_DARK);
|
|
521
|
+
if (isCursorLine) {
|
|
522
|
+
const col = Math.min(cursorCol, line.length);
|
|
523
|
+
const before = line.slice(0, col);
|
|
524
|
+
const at = col < line.length ? line[col] : ' ';
|
|
525
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
526
|
+
this.write(before);
|
|
527
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
528
|
+
this.write(at);
|
|
529
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
530
|
+
this.write(after);
|
|
531
|
+
finalRow = rowNum;
|
|
532
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
this.write(line);
|
|
536
|
+
}
|
|
537
|
+
// Pad to edge
|
|
538
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
539
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
540
|
+
if (padding > 0)
|
|
541
|
+
this.write(' '.repeat(padding));
|
|
542
|
+
this.write(ESC.RESET);
|
|
543
|
+
}
|
|
544
|
+
// Mode controls line
|
|
545
|
+
const controlRow = currentRow + visibleLines.length;
|
|
546
|
+
this.write(ESC.TO(controlRow, 1));
|
|
547
|
+
this.write(this.buildModeControls(cols));
|
|
548
|
+
// Position cursor in input box
|
|
549
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
550
|
+
this.write(ESC.SHOW);
|
|
551
|
+
// Update state
|
|
552
|
+
this.lastRenderContent = this.buffer;
|
|
553
|
+
this.lastRenderCursor = this.cursor;
|
|
554
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
555
|
+
if (this.streamingRenderTimer) {
|
|
556
|
+
clearTimeout(this.streamingRenderTimer);
|
|
557
|
+
this.streamingRenderTimer = null;
|
|
558
|
+
}
|
|
656
559
|
}
|
|
657
560
|
finally {
|
|
658
561
|
writeLock.unlock();
|
|
@@ -660,99 +563,181 @@ export class TerminalInput extends EventEmitter {
|
|
|
660
563
|
}
|
|
661
564
|
}
|
|
662
565
|
/**
|
|
663
|
-
* Build
|
|
664
|
-
*
|
|
566
|
+
* Build compact meta line above the divider.
|
|
567
|
+
* Shows model/provider and key metrics in a single line.
|
|
568
|
+
* Status message is shown in mode controls to avoid duplication.
|
|
665
569
|
*/
|
|
666
|
-
|
|
667
|
-
const maxWidth = cols - 2;
|
|
570
|
+
buildMetaLines(width) {
|
|
668
571
|
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
|
|
572
|
+
// Model/provider info
|
|
573
|
+
if (this.modelLabel) {
|
|
574
|
+
const modelText = this.providerLabel
|
|
575
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
576
|
+
: this.modelLabel;
|
|
577
|
+
parts.push({ text: modelText, tone: 'info' });
|
|
679
578
|
}
|
|
680
|
-
//
|
|
681
|
-
if (this.
|
|
682
|
-
parts.push(
|
|
579
|
+
// Elapsed time
|
|
580
|
+
if (this.metaElapsedSeconds !== null) {
|
|
581
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
683
582
|
}
|
|
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
|
|
583
|
+
// Token usage (compact)
|
|
584
|
+
if (this.metaTokensUsed !== null) {
|
|
585
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
586
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
587
|
+
parts.push({ text: `${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
692
588
|
}
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
589
|
+
// Context remaining (only show if concerning)
|
|
590
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
591
|
+
if (tokensRemaining !== null) {
|
|
592
|
+
parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
|
|
696
593
|
}
|
|
697
|
-
//
|
|
698
|
-
if (this.
|
|
699
|
-
parts.push(
|
|
594
|
+
// Thinking indicator
|
|
595
|
+
if (this.metaThinkingMs !== null) {
|
|
596
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
700
597
|
}
|
|
701
|
-
if (parts.length
|
|
702
|
-
return
|
|
598
|
+
if (!parts.length) {
|
|
599
|
+
return [];
|
|
703
600
|
}
|
|
704
|
-
|
|
705
|
-
return joined.slice(0, maxWidth);
|
|
601
|
+
return [renderStatusLine(parts, width)];
|
|
706
602
|
}
|
|
707
603
|
/**
|
|
708
|
-
* Build mode controls line
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
604
|
+
* Build Claude Code style mode controls line.
|
|
605
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
712
606
|
*/
|
|
713
607
|
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}`);
|
|
608
|
+
const width = Math.max(8, cols - 2);
|
|
609
|
+
const leftParts = [];
|
|
610
|
+
const rightParts = [];
|
|
611
|
+
if (this.streamingLabel) {
|
|
612
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
722
613
|
}
|
|
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
|
-
|
|
614
|
+
if (this.overrideStatusMessage) {
|
|
615
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
616
|
+
}
|
|
617
|
+
if (this.statusMessage) {
|
|
618
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
619
|
+
}
|
|
620
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
621
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
622
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
623
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
624
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
625
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
626
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
627
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
628
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
629
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
630
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
631
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
632
|
+
}
|
|
633
|
+
if (this.buffer.includes('\n')) {
|
|
634
|
+
const lineCount = this.buffer.split('\n').length;
|
|
635
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
636
|
+
}
|
|
637
|
+
if (this.pastePlaceholders.length > 0) {
|
|
638
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
639
|
+
leftParts.push({
|
|
640
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
641
|
+
tone: 'info',
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const contextRemaining = this.computeContextRemaining();
|
|
645
|
+
if (this.thinkingModeLabel) {
|
|
646
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
647
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
648
|
+
}
|
|
649
|
+
// Model info is now in meta lines only - no duplication here
|
|
650
|
+
if (contextRemaining !== null) {
|
|
651
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
652
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
653
|
+
? 'Context auto-compact imminent'
|
|
654
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
655
|
+
rightParts.push({ text: label, tone });
|
|
656
|
+
}
|
|
657
|
+
if (!rightParts.length || width < 60) {
|
|
658
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
659
|
+
return renderStatusLine(merged, width);
|
|
660
|
+
}
|
|
661
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
662
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
663
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
664
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
665
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
666
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
667
|
+
}
|
|
668
|
+
formatHotkey(hotkey) {
|
|
669
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
670
|
+
if (!normalized)
|
|
671
|
+
return hotkey;
|
|
672
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
673
|
+
const map = {
|
|
674
|
+
shift: '⇧',
|
|
675
|
+
sh: '⇧',
|
|
676
|
+
alt: '⌥',
|
|
677
|
+
option: '⌥',
|
|
678
|
+
opt: '⌥',
|
|
679
|
+
ctrl: '⌃',
|
|
680
|
+
control: '⌃',
|
|
681
|
+
cmd: '⌘',
|
|
682
|
+
meta: '⌘',
|
|
683
|
+
};
|
|
684
|
+
const formatted = parts
|
|
685
|
+
.map((part) => {
|
|
686
|
+
const symbol = map[part];
|
|
687
|
+
if (symbol)
|
|
688
|
+
return symbol;
|
|
689
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
690
|
+
})
|
|
691
|
+
.join('');
|
|
692
|
+
return formatted || hotkey;
|
|
693
|
+
}
|
|
694
|
+
computeContextRemaining() {
|
|
695
|
+
if (this.contextUsage === null) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
699
|
+
}
|
|
700
|
+
computeTokensRemaining() {
|
|
701
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
705
|
+
return this.formatTokenCount(remaining);
|
|
706
|
+
}
|
|
707
|
+
formatElapsedLabel(seconds) {
|
|
708
|
+
if (seconds < 60) {
|
|
709
|
+
return `${seconds}s`;
|
|
710
|
+
}
|
|
711
|
+
const mins = Math.floor(seconds / 60);
|
|
712
|
+
const secs = seconds % 60;
|
|
713
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
714
|
+
}
|
|
715
|
+
formatTokenCount(value) {
|
|
716
|
+
if (!Number.isFinite(value)) {
|
|
717
|
+
return `${value}`;
|
|
718
|
+
}
|
|
719
|
+
if (value >= 1_000_000) {
|
|
720
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
721
|
+
}
|
|
722
|
+
if (value >= 1_000) {
|
|
723
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
724
|
+
}
|
|
725
|
+
return `${Math.round(value)}`;
|
|
726
|
+
}
|
|
727
|
+
visibleLength(value) {
|
|
728
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
729
|
+
return value.replace(ansiPattern, '').length;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
733
|
+
* needing a TTY. Not used by production code.
|
|
734
|
+
*/
|
|
735
|
+
getDebugUiSnapshot(width) {
|
|
736
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
737
|
+
return {
|
|
738
|
+
meta: this.buildMetaLines(cols - 2),
|
|
739
|
+
controls: this.buildModeControls(cols),
|
|
740
|
+
};
|
|
756
741
|
}
|
|
757
742
|
/**
|
|
758
743
|
* Force a re-render
|
|
@@ -775,30 +760,111 @@ export class TerminalInput extends EventEmitter {
|
|
|
775
760
|
handleResize() {
|
|
776
761
|
this.lastRenderContent = '';
|
|
777
762
|
this.lastRenderCursor = -1;
|
|
763
|
+
this.resetStreamingRenderThrottle();
|
|
778
764
|
this.scheduleRender();
|
|
779
765
|
}
|
|
780
766
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
767
|
+
* Enter streaming mode - just mark that we're streaming.
|
|
768
|
+
* No scroll regions - we'll use simple sequential output.
|
|
783
769
|
*/
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
770
|
+
enterStreamingScrollRegion() {
|
|
771
|
+
this.scrollRegionActive = true;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Exit streaming mode and render final chat box.
|
|
775
|
+
*/
|
|
776
|
+
exitStreamingScrollRegion() {
|
|
777
|
+
this.scrollRegionActive = false;
|
|
778
|
+
// Add newline after content, then render chat box
|
|
779
|
+
this.write('\n');
|
|
780
|
+
this.forceRender();
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Stream content - simple sequential write.
|
|
784
|
+
* Content flows naturally, terminal handles scrolling.
|
|
785
|
+
* No escape codes, no scroll regions - just write.
|
|
786
|
+
*/
|
|
787
|
+
streamContent(content) {
|
|
788
|
+
if (!content)
|
|
789
|
+
return;
|
|
790
|
+
writeLock.lock('streamContent');
|
|
791
|
+
try {
|
|
792
|
+
// Just write content directly to stdout
|
|
793
|
+
this.write(content);
|
|
794
|
+
// Track newlines for contentRow (informational only)
|
|
795
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
796
|
+
this.contentRow += newlines;
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
writeLock.unlock();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Enable scroll region (no-op in floating mode).
|
|
804
|
+
*/
|
|
805
|
+
enableScrollRegion() {
|
|
806
|
+
// No-op: using pure floating approach
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Disable scroll region (no-op in floating mode).
|
|
810
|
+
*/
|
|
811
|
+
disableScrollRegion() {
|
|
812
|
+
// No-op: using pure floating approach
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Calculate chat box height.
|
|
816
|
+
*/
|
|
817
|
+
getChatBoxHeight() {
|
|
818
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* @deprecated Use streamContent() instead
|
|
822
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
823
|
+
*/
|
|
824
|
+
registerOutputInterceptor(_display) {
|
|
825
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* @deprecated Use streamContent() instead
|
|
829
|
+
* Write content above the floating chat box.
|
|
830
|
+
*/
|
|
831
|
+
writeToScrollRegion(content) {
|
|
832
|
+
this.streamContent(content);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Clear the entire terminal screen and reset content position.
|
|
836
|
+
* This removes all content including the launching command.
|
|
837
|
+
*/
|
|
838
|
+
clearScreen() {
|
|
839
|
+
writeLock.lock('clearScreen');
|
|
840
|
+
try {
|
|
841
|
+
this.write(ESC.HOME);
|
|
842
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
843
|
+
this.contentRow = 1;
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
writeLock.unlock();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Reset content position to row 1.
|
|
851
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
852
|
+
*/
|
|
853
|
+
resetContentPosition() {
|
|
854
|
+
this.contentRow = 1;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Set the content row explicitly (used after banner is written).
|
|
858
|
+
* This tells the input where content should start flowing from.
|
|
859
|
+
*/
|
|
860
|
+
setContentRow(row) {
|
|
861
|
+
this.contentRow = Math.max(1, row);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Get the current content row position.
|
|
865
|
+
*/
|
|
866
|
+
getContentRow() {
|
|
867
|
+
return this.contentRow;
|
|
802
868
|
}
|
|
803
869
|
/**
|
|
804
870
|
* Dispose and clean up
|
|
@@ -806,20 +872,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
806
872
|
dispose() {
|
|
807
873
|
if (this.disposed)
|
|
808
874
|
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
875
|
this.disposed = true;
|
|
822
876
|
this.enabled = false;
|
|
877
|
+
this.disableScrollRegion();
|
|
878
|
+
this.resetStreamingRenderThrottle();
|
|
823
879
|
this.disableBracketedPaste();
|
|
824
880
|
this.buffer = '';
|
|
825
881
|
this.queue = [];
|
|
@@ -924,22 +980,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
924
980
|
this.toggleEditMode();
|
|
925
981
|
return true;
|
|
926
982
|
}
|
|
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
|
-
}
|
|
983
|
+
this.insertText(' ');
|
|
943
984
|
return true;
|
|
944
985
|
}
|
|
945
986
|
return false;
|
|
@@ -957,7 +998,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
957
998
|
this.insertPlainText(chunk, insertPos);
|
|
958
999
|
this.cursor = insertPos + chunk.length;
|
|
959
1000
|
this.emit('change', this.buffer);
|
|
960
|
-
this.updateSuggestions();
|
|
961
1001
|
this.scheduleRender();
|
|
962
1002
|
}
|
|
963
1003
|
insertNewline() {
|
|
@@ -982,7 +1022,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
982
1022
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
983
1023
|
}
|
|
984
1024
|
this.emit('change', this.buffer);
|
|
985
|
-
this.updateSuggestions();
|
|
986
1025
|
this.scheduleRender();
|
|
987
1026
|
}
|
|
988
1027
|
deleteForward() {
|
|
@@ -1210,13 +1249,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1210
1249
|
timestamp: Date.now(),
|
|
1211
1250
|
});
|
|
1212
1251
|
this.emit('queue', text);
|
|
1213
|
-
this.clear(); // Clear immediately for queued input
|
|
1252
|
+
this.clear(); // Clear immediately for queued input
|
|
1214
1253
|
}
|
|
1215
1254
|
else {
|
|
1216
|
-
// In idle mode, clear the input
|
|
1217
|
-
// The
|
|
1218
|
-
|
|
1219
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1255
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1256
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1257
|
+
this.clear();
|
|
1220
1258
|
this.emit('submit', text);
|
|
1221
1259
|
}
|
|
1222
1260
|
}
|
|
@@ -1233,7 +1271,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1233
1271
|
if (available <= 0)
|
|
1234
1272
|
return;
|
|
1235
1273
|
const chunk = clean.slice(0, available);
|
|
1236
|
-
|
|
1274
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1275
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1276
|
+
if (isMultiline && !isShortMultiline) {
|
|
1237
1277
|
this.insertPastePlaceholder(chunk);
|
|
1238
1278
|
}
|
|
1239
1279
|
else {
|
|
@@ -1369,17 +1409,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1369
1409
|
this.shiftPlaceholders(position, text.length);
|
|
1370
1410
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1371
1411
|
}
|
|
1412
|
+
shouldInlineMultiline(content) {
|
|
1413
|
+
const lines = content.split('\n').length;
|
|
1414
|
+
const maxInlineLines = 4;
|
|
1415
|
+
const maxInlineChars = 240;
|
|
1416
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1417
|
+
}
|
|
1372
1418
|
findPlaceholderAt(position) {
|
|
1373
1419
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1374
1420
|
}
|
|
1375
|
-
buildPlaceholder(
|
|
1421
|
+
buildPlaceholder(lineCount) {
|
|
1376
1422
|
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}"`;
|
|
1423
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1424
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1383
1425
|
return { id, placeholder };
|
|
1384
1426
|
}
|
|
1385
1427
|
insertPastePlaceholder(content) {
|
|
@@ -1387,67 +1429,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1387
1429
|
if (available <= 0)
|
|
1388
1430
|
return;
|
|
1389
1431
|
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);
|
|
1432
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1433
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1400
1434
|
const insertPos = this.cursor;
|
|
1401
1435
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1402
1436
|
this.pastePlaceholders.push({
|
|
1403
1437
|
id,
|
|
1404
1438
|
content: cleanContent,
|
|
1405
|
-
lineCount
|
|
1439
|
+
lineCount,
|
|
1406
1440
|
placeholder,
|
|
1407
1441
|
start: insertPos,
|
|
1408
1442
|
end: insertPos + placeholder.length,
|
|
1409
|
-
summary,
|
|
1410
|
-
expanded: false,
|
|
1411
1443
|
});
|
|
1412
1444
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1413
1445
|
this.cursor = insertPos + placeholder.length;
|
|
1414
1446
|
}
|
|
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
1447
|
deletePlaceholder(placeholder) {
|
|
1452
1448
|
const length = placeholder.end - placeholder.start;
|
|
1453
1449
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1455,7 +1451,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1455
1451
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1456
1452
|
this.cursor = placeholder.start;
|
|
1457
1453
|
}
|
|
1458
|
-
updateContextUsage(value) {
|
|
1454
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1455
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1456
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1457
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1458
|
+
}
|
|
1459
1459
|
if (value === null || !Number.isFinite(value)) {
|
|
1460
1460
|
this.contextUsage = null;
|
|
1461
1461
|
}
|
|
@@ -1482,6 +1482,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1482
1482
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1483
1483
|
this.setEditMode(next);
|
|
1484
1484
|
}
|
|
1485
|
+
scheduleStreamingRender(delayMs) {
|
|
1486
|
+
if (this.streamingRenderTimer)
|
|
1487
|
+
return;
|
|
1488
|
+
const wait = Math.max(16, delayMs);
|
|
1489
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1490
|
+
this.streamingRenderTimer = null;
|
|
1491
|
+
this.render();
|
|
1492
|
+
}, wait);
|
|
1493
|
+
}
|
|
1494
|
+
resetStreamingRenderThrottle() {
|
|
1495
|
+
if (this.streamingRenderTimer) {
|
|
1496
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1497
|
+
this.streamingRenderTimer = null;
|
|
1498
|
+
}
|
|
1499
|
+
this.lastStreamingRender = 0;
|
|
1500
|
+
}
|
|
1485
1501
|
scheduleRender() {
|
|
1486
1502
|
if (!this.canRender())
|
|
1487
1503
|
return;
|