erosolar-cli 1.7.317 → 1.7.319
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 +229 -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 +132 -117
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +525 -534
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +61 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +73 -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,297 +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.
|
|
208
|
-
*
|
|
209
|
-
* Layout:
|
|
210
|
-
* 1. Clear screen
|
|
211
|
-
* 2. Write banner at top
|
|
212
|
-
* 3. Track content end position
|
|
213
|
-
* 4. Render floating input area below banner
|
|
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 and track where it ends
|
|
225
|
-
let bannerLines = 0;
|
|
226
|
-
if (this.bannerContent) {
|
|
227
|
-
const lines = this.bannerContent.split('\n');
|
|
228
|
-
bannerLines = lines.length + 2; // +2 for the trailing \n\n
|
|
229
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
230
|
-
}
|
|
231
|
-
// Set content end row so input renders right after banner
|
|
232
|
-
this.contentEndRow = bannerLines > 0 ? bannerLines : 1;
|
|
233
|
-
// Mark initialized
|
|
234
|
-
this.unifiedUIInitialized = true;
|
|
235
|
-
// Render floating input area below the banner
|
|
236
|
-
this.renderFloatingInputArea();
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Clear the input area at its tracked position.
|
|
240
|
-
* Returns true if something was cleared.
|
|
241
|
-
*/
|
|
242
|
-
clearInputArea() {
|
|
243
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
244
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
245
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
246
|
-
this.write(ESC.CLEAR_LINE);
|
|
247
|
-
}
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Reset input area tracking state.
|
|
254
|
-
*/
|
|
255
|
-
resetInputAreaTracking() {
|
|
256
|
-
this.inputAreaStartRow = 0;
|
|
257
|
-
this.flowModeRenderedLines = 0;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Render chat box - Claude Code style floating input.
|
|
261
|
-
* Clean, minimal design with dividers and mode controls.
|
|
262
|
-
*/
|
|
263
|
-
renderFloatingInputArea() {
|
|
264
|
-
const { rows, cols } = this.getSize();
|
|
265
|
-
const divider = '─'.repeat(cols);
|
|
266
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
267
|
-
// Lines needed: divider + input + divider + controls = 4 lines minimum
|
|
268
|
-
const linesNeeded = 4;
|
|
269
|
-
// FIRST: Clear any previously rendered chat box
|
|
270
|
-
this.clearInputArea();
|
|
271
|
-
// Hide cursor during render
|
|
272
|
-
this.write(ESC.HIDE);
|
|
273
|
-
// Calculate where to render - ALWAYS float right below content
|
|
274
|
-
let startRow;
|
|
275
|
-
if (this.contentEndRow > 0) {
|
|
276
|
-
startRow = this.contentEndRow + 1;
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
startRow = 1;
|
|
280
|
-
}
|
|
281
|
-
// Clamp to terminal bounds
|
|
282
|
-
const maxStartRow = rows - linesNeeded + 1;
|
|
283
|
-
startRow = Math.min(startRow, maxStartRow);
|
|
284
|
-
startRow = Math.max(1, startRow);
|
|
285
|
-
// Track this position
|
|
286
|
-
this.inputAreaStartRow = startRow;
|
|
287
|
-
let currentRow = startRow;
|
|
288
|
-
// Helper to write a line (clears then writes)
|
|
289
|
-
const writeLine = (content) => {
|
|
290
|
-
this.write(ESC.TO(currentRow, 1));
|
|
291
|
-
this.write(ESC.CLEAR_LINE);
|
|
292
|
-
this.write(content);
|
|
293
|
-
currentRow++;
|
|
294
|
-
};
|
|
295
|
-
// Top divider
|
|
296
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
297
|
-
// Input line with > prompt
|
|
298
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 3);
|
|
299
|
-
const displayLine = lines[0] ?? '';
|
|
300
|
-
const inputRow = currentRow;
|
|
301
|
-
writeLine(`${DIM}>${R} ${displayLine}`);
|
|
302
|
-
// Bottom divider
|
|
303
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
304
|
-
// Mode controls line - Claude Code style
|
|
305
|
-
this.write(ESC.TO(currentRow, 1));
|
|
306
|
-
this.write(ESC.CLEAR_LINE);
|
|
307
|
-
this.write(this.buildClaudeStyleControls(cols));
|
|
308
|
-
// Track lines rendered
|
|
309
|
-
this.flowModeRenderedLines = currentRow - startRow + 1;
|
|
310
|
-
// Position cursor in input line
|
|
311
|
-
this.write(ESC.TO(inputRow, 3 + cursorCol)); // "> " = 2 chars + 1 for position
|
|
312
|
-
// Show cursor
|
|
313
|
-
this.write(ESC.SHOW);
|
|
314
|
-
// Update tracking
|
|
315
|
-
this.lastRenderContent = this.buffer;
|
|
316
|
-
this.lastRenderCursor = this.cursor;
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Build Claude Code style controls line.
|
|
320
|
-
* Shows: edit mode indicator (shift+tab to cycle)
|
|
321
|
-
*/
|
|
322
|
-
buildClaudeStyleControls(cols) {
|
|
323
|
-
const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
|
|
324
|
-
// Edit mode indicator
|
|
325
|
-
let editModeText;
|
|
326
|
-
if (this.editMode === 'display-edits') {
|
|
327
|
-
editModeText = `${GREEN}⏵⏵${R} accept edits on`;
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
|
|
331
|
-
}
|
|
332
|
-
// Build controls line
|
|
333
|
-
const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
|
|
334
|
-
// Add thinking mode if enabled
|
|
335
|
-
if (this.thinkingEnabled) {
|
|
336
|
-
parts.push(`${CYAN}💭${R}`);
|
|
337
|
-
}
|
|
338
|
-
// Add context usage if available
|
|
339
|
-
if (this.contextUsage !== null) {
|
|
340
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
341
|
-
if (rem < 10) {
|
|
342
|
-
parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
|
|
343
|
-
}
|
|
344
|
-
else if (rem < 25) {
|
|
345
|
-
parts.push(`${YELLOW}ctx ${rem}%${R}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return parts.join(` ${DIM}·${R} `);
|
|
349
|
-
}
|
|
350
200
|
/**
|
|
351
201
|
* Set the input mode
|
|
352
202
|
*
|
|
353
|
-
*
|
|
354
|
-
* During streaming: chat box is hidden, content streams freely.
|
|
355
|
-
* After streaming: chat box re-appears below content.
|
|
203
|
+
* Content flows naturally - no scroll region pinning.
|
|
356
204
|
*/
|
|
357
205
|
setMode(mode) {
|
|
358
206
|
const prevMode = this.mode;
|
|
359
207
|
this.mode = mode;
|
|
360
208
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
361
|
-
|
|
362
|
-
this.streamingStartTime = Date.now();
|
|
363
|
-
// Ensure unified UI is initialized
|
|
364
|
-
if (!this.unifiedUIInitialized) {
|
|
365
|
-
this.initializeUnifiedUI();
|
|
366
|
-
}
|
|
367
|
-
// Clear chat box at start of streaming - content will flow freely
|
|
368
|
-
this.clearInputArea();
|
|
369
|
-
}
|
|
370
|
-
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
371
|
-
// Stop streaming render timer (if any)
|
|
372
|
-
if (this.streamingRenderTimer) {
|
|
373
|
-
clearInterval(this.streamingRenderTimer);
|
|
374
|
-
this.streamingRenderTimer = null;
|
|
375
|
-
}
|
|
376
|
-
// Reset streaming time
|
|
377
|
-
this.streamingStartTime = null;
|
|
378
|
-
// Re-render floating input area below content
|
|
209
|
+
this.resetStreamingRenderThrottle();
|
|
379
210
|
this.renderDirty = true;
|
|
380
|
-
this.
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Set the row where content ends (for idle mode positioning).
|
|
385
|
-
* Input area will render starting from this row + 1.
|
|
386
|
-
*/
|
|
387
|
-
setContentEndRow(row) {
|
|
388
|
-
this.contentEndRow = Math.max(0, row);
|
|
389
|
-
this.renderDirty = true;
|
|
390
|
-
this.scheduleRender();
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Set available slash commands for auto-complete suggestions.
|
|
394
|
-
*/
|
|
395
|
-
setCommands(commands) {
|
|
396
|
-
this.commandSuggestions = commands;
|
|
397
|
-
this.updateSuggestions();
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Update filtered suggestions based on current input.
|
|
401
|
-
*/
|
|
402
|
-
updateSuggestions() {
|
|
403
|
-
const input = this.buffer.trim();
|
|
404
|
-
// Only show suggestions when input starts with "/"
|
|
405
|
-
if (!input.startsWith('/')) {
|
|
406
|
-
this.showSuggestions = false;
|
|
407
|
-
this.filteredSuggestions = [];
|
|
408
|
-
this.selectedSuggestionIndex = 0;
|
|
409
|
-
return;
|
|
211
|
+
this.render();
|
|
410
212
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
416
|
-
// Keep selection in bounds
|
|
417
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
418
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
213
|
+
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
214
|
+
// Streaming ended - render the input area
|
|
215
|
+
this.resetStreamingRenderThrottle();
|
|
216
|
+
this.forceRender();
|
|
419
217
|
}
|
|
420
218
|
}
|
|
421
219
|
/**
|
|
422
|
-
*
|
|
423
|
-
|
|
424
|
-
selectNextSuggestion() {
|
|
425
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
426
|
-
return;
|
|
427
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
428
|
-
this.renderDirty = true;
|
|
429
|
-
this.scheduleRender();
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
220
|
+
* Legacy method - no longer used (content flows naturally).
|
|
221
|
+
* @deprecated Use setContentRow instead
|
|
433
222
|
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return;
|
|
437
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
438
|
-
? this.filteredSuggestions.length - 1
|
|
439
|
-
: this.selectedSuggestionIndex - 1;
|
|
440
|
-
this.renderDirty = true;
|
|
441
|
-
this.scheduleRender();
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Accept current suggestion and insert into buffer.
|
|
445
|
-
*/
|
|
446
|
-
acceptSuggestion() {
|
|
447
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
448
|
-
return false;
|
|
449
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
450
|
-
if (!selected)
|
|
451
|
-
return false;
|
|
452
|
-
// Replace buffer with selected command
|
|
453
|
-
this.buffer = selected.command + ' ';
|
|
454
|
-
this.cursor = this.buffer.length;
|
|
455
|
-
this.showSuggestions = false;
|
|
456
|
-
this.renderDirty = true;
|
|
457
|
-
this.scheduleRender();
|
|
458
|
-
return true;
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Check if suggestions are visible.
|
|
462
|
-
*/
|
|
463
|
-
areSuggestionsVisible() {
|
|
464
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Toggle thinking/reasoning mode
|
|
468
|
-
*/
|
|
469
|
-
toggleThinking() {
|
|
470
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
471
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
472
|
-
this.scheduleRender();
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Get thinking enabled state
|
|
476
|
-
*/
|
|
477
|
-
isThinkingEnabled() {
|
|
478
|
-
return this.thinkingEnabled;
|
|
223
|
+
setPinnedHeaderLines(_count) {
|
|
224
|
+
// No-op: scroll region pinning removed
|
|
479
225
|
}
|
|
480
226
|
/**
|
|
481
227
|
* Get current mode
|
|
@@ -508,17 +254,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
508
254
|
}
|
|
509
255
|
/**
|
|
510
256
|
* Clear the buffer
|
|
511
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
512
257
|
*/
|
|
513
|
-
clear(
|
|
258
|
+
clear() {
|
|
514
259
|
this.buffer = '';
|
|
515
260
|
this.cursor = 0;
|
|
516
261
|
this.historyIndex = -1;
|
|
517
262
|
this.tempInput = '';
|
|
518
263
|
this.pastePlaceholders = [];
|
|
519
|
-
|
|
520
|
-
this.scheduleRender();
|
|
521
|
-
}
|
|
264
|
+
this.scheduleRender();
|
|
522
265
|
}
|
|
523
266
|
/**
|
|
524
267
|
* Get queued inputs
|
|
@@ -589,6 +332,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
589
332
|
this.streamingLabel = next;
|
|
590
333
|
this.scheduleRender();
|
|
591
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
|
+
}
|
|
592
366
|
/**
|
|
593
367
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
594
368
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -598,26 +372,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
598
372
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
599
373
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
600
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);
|
|
601
377
|
if (this.verificationEnabled === nextVerification &&
|
|
602
378
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
603
379
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
604
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
380
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
381
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
382
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
605
383
|
return;
|
|
606
384
|
}
|
|
607
385
|
this.verificationEnabled = nextVerification;
|
|
608
386
|
this.autoContinueEnabled = nextAutoContinue;
|
|
609
387
|
this.verificationHotkey = nextVerifyHotkey;
|
|
610
388
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
611
|
-
this.
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
615
|
-
* This is displayed persistently above the input area.
|
|
616
|
-
*/
|
|
617
|
-
setModelInfo(info) {
|
|
618
|
-
if (this.modelInfo === info)
|
|
619
|
-
return;
|
|
620
|
-
this.modelInfo = info;
|
|
389
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
390
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
621
391
|
this.scheduleRender();
|
|
622
392
|
}
|
|
623
393
|
/**
|
|
@@ -630,33 +400,158 @@ export class TerminalInput extends EventEmitter {
|
|
|
630
400
|
this.scheduleRender();
|
|
631
401
|
}
|
|
632
402
|
/**
|
|
633
|
-
*
|
|
634
|
-
|
|
635
|
-
|
|
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.
|
|
636
421
|
*/
|
|
637
422
|
render() {
|
|
638
423
|
if (!this.canRender())
|
|
639
424
|
return;
|
|
640
425
|
if (this.isRendering)
|
|
641
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
|
+
}
|
|
642
438
|
const shouldSkip = !this.renderDirty &&
|
|
643
439
|
this.buffer === this.lastRenderContent &&
|
|
644
440
|
this.cursor === this.lastRenderCursor;
|
|
645
441
|
this.renderDirty = false;
|
|
646
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
647
442
|
if (shouldSkip) {
|
|
648
443
|
return;
|
|
649
444
|
}
|
|
650
|
-
// If write lock is held, defer render
|
|
651
445
|
if (writeLock.isLocked()) {
|
|
652
446
|
writeLock.safeWrite(() => this.render());
|
|
653
447
|
return;
|
|
654
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');
|
|
655
479
|
this.isRendering = true;
|
|
656
|
-
writeLock.lock('terminalInput.render');
|
|
657
480
|
try {
|
|
658
|
-
//
|
|
659
|
-
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
|
+
}
|
|
660
555
|
}
|
|
661
556
|
finally {
|
|
662
557
|
writeLock.unlock();
|
|
@@ -664,99 +559,181 @@ export class TerminalInput extends EventEmitter {
|
|
|
664
559
|
}
|
|
665
560
|
}
|
|
666
561
|
/**
|
|
667
|
-
* Build
|
|
668
|
-
*
|
|
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.
|
|
669
565
|
*/
|
|
670
|
-
|
|
671
|
-
const maxWidth = cols - 2;
|
|
566
|
+
buildMetaLines(width) {
|
|
672
567
|
const parts = [];
|
|
673
|
-
//
|
|
674
|
-
if (this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const secs = elapsed % 60;
|
|
680
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
681
|
-
}
|
|
682
|
-
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' });
|
|
683
574
|
}
|
|
684
|
-
//
|
|
685
|
-
if (this.
|
|
686
|
-
parts.push(
|
|
575
|
+
// Elapsed time
|
|
576
|
+
if (this.metaElapsedSeconds !== null) {
|
|
577
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
687
578
|
}
|
|
688
|
-
//
|
|
689
|
-
if (this.
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// Override/warning status
|
|
694
|
-
if (this.overrideStatusMessage) {
|
|
695
|
-
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' });
|
|
696
584
|
}
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
585
|
+
// Context remaining (only show if concerning)
|
|
586
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
587
|
+
if (tokensRemaining !== null) {
|
|
588
|
+
parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
|
|
700
589
|
}
|
|
701
|
-
//
|
|
702
|
-
if (this.
|
|
703
|
-
parts.push(
|
|
590
|
+
// Thinking indicator
|
|
591
|
+
if (this.metaThinkingMs !== null) {
|
|
592
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
704
593
|
}
|
|
705
|
-
if (parts.length
|
|
706
|
-
return
|
|
594
|
+
if (!parts.length) {
|
|
595
|
+
return [];
|
|
707
596
|
}
|
|
708
|
-
|
|
709
|
-
return joined.slice(0, maxWidth);
|
|
597
|
+
return [renderStatusLine(parts, width)];
|
|
710
598
|
}
|
|
711
599
|
/**
|
|
712
|
-
* Build mode controls line
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* 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.
|
|
716
602
|
*/
|
|
717
603
|
buildModeControls(cols) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
724
|
-
if (this.editMode === 'display-edits') {
|
|
725
|
-
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' });
|
|
726
609
|
}
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
};
|
|
760
737
|
}
|
|
761
738
|
/**
|
|
762
739
|
* Force a re-render
|
|
@@ -779,50 +756,112 @@ export class TerminalInput extends EventEmitter {
|
|
|
779
756
|
handleResize() {
|
|
780
757
|
this.lastRenderContent = '';
|
|
781
758
|
this.lastRenderCursor = -1;
|
|
759
|
+
this.resetStreamingRenderThrottle();
|
|
782
760
|
this.scheduleRender();
|
|
783
761
|
}
|
|
784
762
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
763
|
+
* Stream content with scroll region approach.
|
|
764
|
+
*
|
|
765
|
+
* Uses proper terminal scroll regions:
|
|
766
|
+
* 1. Set scroll region to exclude chat box area
|
|
767
|
+
* 2. Position cursor in scroll region
|
|
768
|
+
* 3. Write content (terminal handles scrolling automatically)
|
|
769
|
+
* 4. Restore scroll region when done
|
|
770
|
+
*
|
|
771
|
+
* Chat box stays pinned at bottom without constant redraws.
|
|
788
772
|
*/
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
773
|
+
streamContent(content) {
|
|
774
|
+
if (!content)
|
|
775
|
+
return;
|
|
776
|
+
writeLock.lock('streamContent');
|
|
777
|
+
try {
|
|
778
|
+
const { rows } = this.getSize();
|
|
779
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
780
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
781
|
+
// Set scroll region to content area only (excludes chat box)
|
|
782
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
783
|
+
// Position at end of scroll region to append content
|
|
784
|
+
this.write(ESC.TO(scrollEnd, 1));
|
|
785
|
+
// Write content - terminal auto-scrolls within the region
|
|
786
|
+
this.write(content);
|
|
787
|
+
// Reset scroll region to full terminal
|
|
788
|
+
this.write(ESC.RESET_SCROLL);
|
|
789
|
+
// Track approximate content position for chat box placement
|
|
790
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
791
|
+
this.contentRow = Math.min(this.contentRow + newlines, scrollEnd);
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
writeLock.unlock();
|
|
795
|
+
}
|
|
796
|
+
// Throttle chat box re-renders during streaming - only update periodically
|
|
797
|
+
this.scheduleStreamingRender(100);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Enable scroll region (no-op in floating mode).
|
|
801
|
+
*/
|
|
802
|
+
enableScrollRegion() {
|
|
803
|
+
// No-op: using pure floating approach
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Disable scroll region (no-op in floating mode).
|
|
807
|
+
*/
|
|
808
|
+
disableScrollRegion() {
|
|
809
|
+
// No-op: using pure floating approach
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Calculate chat box height.
|
|
813
|
+
*/
|
|
814
|
+
getChatBoxHeight() {
|
|
815
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* @deprecated Use streamContent() instead
|
|
819
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
820
|
+
*/
|
|
821
|
+
registerOutputInterceptor(_display) {
|
|
822
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* @deprecated Use streamContent() instead
|
|
826
|
+
* Write content above the floating chat box.
|
|
827
|
+
*/
|
|
828
|
+
writeToScrollRegion(content) {
|
|
829
|
+
this.streamContent(content);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Clear the entire terminal screen and reset content position.
|
|
833
|
+
* This removes all content including the launching command.
|
|
834
|
+
*/
|
|
835
|
+
clearScreen() {
|
|
836
|
+
writeLock.lock('clearScreen');
|
|
837
|
+
try {
|
|
838
|
+
this.write(ESC.HOME);
|
|
839
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
840
|
+
this.contentRow = 1;
|
|
841
|
+
}
|
|
842
|
+
finally {
|
|
843
|
+
writeLock.unlock();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Reset content position to row 1.
|
|
848
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
849
|
+
*/
|
|
850
|
+
resetContentPosition() {
|
|
851
|
+
this.contentRow = 1;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Set the content row explicitly (used after banner is written).
|
|
855
|
+
* This tells the input where content should start flowing from.
|
|
856
|
+
*/
|
|
857
|
+
setContentRow(row) {
|
|
858
|
+
this.contentRow = Math.max(1, row);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Get the current content row position.
|
|
862
|
+
*/
|
|
863
|
+
getContentRow() {
|
|
864
|
+
return this.contentRow;
|
|
826
865
|
}
|
|
827
866
|
/**
|
|
828
867
|
* Dispose and clean up
|
|
@@ -830,18 +869,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
830
869
|
dispose() {
|
|
831
870
|
if (this.disposed)
|
|
832
871
|
return;
|
|
833
|
-
// Clean up streaming render timer
|
|
834
|
-
if (this.streamingRenderTimer) {
|
|
835
|
-
clearInterval(this.streamingRenderTimer);
|
|
836
|
-
this.streamingRenderTimer = null;
|
|
837
|
-
}
|
|
838
|
-
// Clean up output interceptor
|
|
839
|
-
if (this.outputInterceptorCleanup) {
|
|
840
|
-
this.outputInterceptorCleanup();
|
|
841
|
-
this.outputInterceptorCleanup = undefined;
|
|
842
|
-
}
|
|
843
872
|
this.disposed = true;
|
|
844
873
|
this.enabled = false;
|
|
874
|
+
this.disableScrollRegion();
|
|
875
|
+
this.resetStreamingRenderThrottle();
|
|
845
876
|
this.disableBracketedPaste();
|
|
846
877
|
this.buffer = '';
|
|
847
878
|
this.queue = [];
|
|
@@ -946,22 +977,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
946
977
|
this.toggleEditMode();
|
|
947
978
|
return true;
|
|
948
979
|
}
|
|
949
|
-
|
|
950
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
951
|
-
this.togglePasteExpansion();
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
this.toggleThinking();
|
|
955
|
-
}
|
|
956
|
-
return true;
|
|
957
|
-
case 'escape':
|
|
958
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
959
|
-
if (this.mode === 'streaming') {
|
|
960
|
-
this.emit('interrupt');
|
|
961
|
-
}
|
|
962
|
-
else if (this.buffer.length > 0) {
|
|
963
|
-
this.clear();
|
|
964
|
-
}
|
|
980
|
+
this.insertText(' ');
|
|
965
981
|
return true;
|
|
966
982
|
}
|
|
967
983
|
return false;
|
|
@@ -979,7 +995,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
979
995
|
this.insertPlainText(chunk, insertPos);
|
|
980
996
|
this.cursor = insertPos + chunk.length;
|
|
981
997
|
this.emit('change', this.buffer);
|
|
982
|
-
this.updateSuggestions();
|
|
983
998
|
this.scheduleRender();
|
|
984
999
|
}
|
|
985
1000
|
insertNewline() {
|
|
@@ -1004,7 +1019,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1004
1019
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1005
1020
|
}
|
|
1006
1021
|
this.emit('change', this.buffer);
|
|
1007
|
-
this.updateSuggestions();
|
|
1008
1022
|
this.scheduleRender();
|
|
1009
1023
|
}
|
|
1010
1024
|
deleteForward() {
|
|
@@ -1232,13 +1246,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1232
1246
|
timestamp: Date.now(),
|
|
1233
1247
|
});
|
|
1234
1248
|
this.emit('queue', text);
|
|
1235
|
-
this.clear(); // Clear immediately for queued input
|
|
1249
|
+
this.clear(); // Clear immediately for queued input
|
|
1236
1250
|
}
|
|
1237
1251
|
else {
|
|
1238
|
-
// In idle mode, clear the input
|
|
1239
|
-
// The
|
|
1240
|
-
|
|
1241
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1252
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1253
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1254
|
+
this.clear();
|
|
1242
1255
|
this.emit('submit', text);
|
|
1243
1256
|
}
|
|
1244
1257
|
}
|
|
@@ -1255,7 +1268,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1255
1268
|
if (available <= 0)
|
|
1256
1269
|
return;
|
|
1257
1270
|
const chunk = clean.slice(0, available);
|
|
1258
|
-
|
|
1271
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1272
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1273
|
+
if (isMultiline && !isShortMultiline) {
|
|
1259
1274
|
this.insertPastePlaceholder(chunk);
|
|
1260
1275
|
}
|
|
1261
1276
|
else {
|
|
@@ -1391,17 +1406,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1391
1406
|
this.shiftPlaceholders(position, text.length);
|
|
1392
1407
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1393
1408
|
}
|
|
1409
|
+
shouldInlineMultiline(content) {
|
|
1410
|
+
const lines = content.split('\n').length;
|
|
1411
|
+
const maxInlineLines = 4;
|
|
1412
|
+
const maxInlineChars = 240;
|
|
1413
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1414
|
+
}
|
|
1394
1415
|
findPlaceholderAt(position) {
|
|
1395
1416
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1396
1417
|
}
|
|
1397
|
-
buildPlaceholder(
|
|
1418
|
+
buildPlaceholder(lineCount) {
|
|
1398
1419
|
const id = ++this.pasteCounter;
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
const preview = summary.preview.length > 30
|
|
1402
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1403
|
-
: summary.preview;
|
|
1404
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1420
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1421
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1405
1422
|
return { id, placeholder };
|
|
1406
1423
|
}
|
|
1407
1424
|
insertPastePlaceholder(content) {
|
|
@@ -1409,67 +1426,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1409
1426
|
if (available <= 0)
|
|
1410
1427
|
return;
|
|
1411
1428
|
const cleanContent = content.slice(0, available);
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
if (summary.lineCount < 5) {
|
|
1415
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1416
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1417
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1418
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1429
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1430
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1422
1431
|
const insertPos = this.cursor;
|
|
1423
1432
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1424
1433
|
this.pastePlaceholders.push({
|
|
1425
1434
|
id,
|
|
1426
1435
|
content: cleanContent,
|
|
1427
|
-
lineCount
|
|
1436
|
+
lineCount,
|
|
1428
1437
|
placeholder,
|
|
1429
1438
|
start: insertPos,
|
|
1430
1439
|
end: insertPos + placeholder.length,
|
|
1431
|
-
summary,
|
|
1432
|
-
expanded: false,
|
|
1433
1440
|
});
|
|
1434
1441
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1435
1442
|
this.cursor = insertPos + placeholder.length;
|
|
1436
1443
|
}
|
|
1437
|
-
/**
|
|
1438
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1439
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1440
|
-
*/
|
|
1441
|
-
togglePasteExpansion() {
|
|
1442
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1443
|
-
if (!placeholder)
|
|
1444
|
-
return false;
|
|
1445
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1446
|
-
// Update the placeholder text in buffer
|
|
1447
|
-
const newPlaceholder = placeholder.expanded
|
|
1448
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1449
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1450
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1451
|
-
// Update buffer
|
|
1452
|
-
this.buffer =
|
|
1453
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1454
|
-
newPlaceholder +
|
|
1455
|
-
this.buffer.slice(placeholder.end);
|
|
1456
|
-
// Update placeholder tracking
|
|
1457
|
-
placeholder.placeholder = newPlaceholder;
|
|
1458
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1459
|
-
// Shift other placeholders
|
|
1460
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1461
|
-
this.scheduleRender();
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
buildExpandedPlaceholder(ph) {
|
|
1465
|
-
const lines = ph.content.split('\n');
|
|
1466
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1467
|
-
const lastLines = lines.length > 5
|
|
1468
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1469
|
-
: '';
|
|
1470
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1471
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1472
|
-
}
|
|
1473
1444
|
deletePlaceholder(placeholder) {
|
|
1474
1445
|
const length = placeholder.end - placeholder.start;
|
|
1475
1446
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1477,7 +1448,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1477
1448
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1478
1449
|
this.cursor = placeholder.start;
|
|
1479
1450
|
}
|
|
1480
|
-
updateContextUsage(value) {
|
|
1451
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1452
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1453
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1454
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1455
|
+
}
|
|
1481
1456
|
if (value === null || !Number.isFinite(value)) {
|
|
1482
1457
|
this.contextUsage = null;
|
|
1483
1458
|
}
|
|
@@ -1504,6 +1479,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1504
1479
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1505
1480
|
this.setEditMode(next);
|
|
1506
1481
|
}
|
|
1482
|
+
scheduleStreamingRender(delayMs) {
|
|
1483
|
+
if (this.streamingRenderTimer)
|
|
1484
|
+
return;
|
|
1485
|
+
const wait = Math.max(16, delayMs);
|
|
1486
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1487
|
+
this.streamingRenderTimer = null;
|
|
1488
|
+
this.render();
|
|
1489
|
+
}, wait);
|
|
1490
|
+
}
|
|
1491
|
+
resetStreamingRenderThrottle() {
|
|
1492
|
+
if (this.streamingRenderTimer) {
|
|
1493
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1494
|
+
this.streamingRenderTimer = null;
|
|
1495
|
+
}
|
|
1496
|
+
this.lastStreamingRender = 0;
|
|
1497
|
+
}
|
|
1507
1498
|
scheduleRender() {
|
|
1508
1499
|
if (!this.canRender())
|
|
1509
1500
|
return;
|