erosolar-cli 1.7.318 → 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 -116
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +524 -505
- 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,289 +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. Render chat box at bottom (sets up scroll region)
|
|
213
|
-
* 4. Position cursor after banner for content
|
|
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
|
-
// Mark initialized
|
|
232
|
-
this.unifiedUIInitialized = true;
|
|
233
|
-
// Render chat box at bottom (this also sets scroll region)
|
|
234
|
-
this.renderFloatingInputArea();
|
|
235
|
-
// Position cursor after banner, ready for content
|
|
236
|
-
const cursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
|
|
237
|
-
this.write(ESC.TO(cursorRow, 1));
|
|
238
|
-
this.write(ESC.SHOW);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Clear the input area at its tracked position.
|
|
242
|
-
* Returns true if something was cleared.
|
|
243
|
-
*/
|
|
244
|
-
clearInputArea() {
|
|
245
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
246
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
247
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
248
|
-
this.write(ESC.CLEAR_LINE);
|
|
249
|
-
}
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Reset input area tracking state.
|
|
256
|
-
*/
|
|
257
|
-
resetInputAreaTracking() {
|
|
258
|
-
this.inputAreaStartRow = 0;
|
|
259
|
-
this.flowModeRenderedLines = 0;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Render chat box - BOTTOM PINNED with scroll region (SSE).
|
|
263
|
-
* Chat box is always at terminal bottom, content scrolls above it.
|
|
264
|
-
*/
|
|
265
|
-
renderFloatingInputArea() {
|
|
266
|
-
const { rows, cols } = this.getSize();
|
|
267
|
-
const divider = '─'.repeat(cols);
|
|
268
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
269
|
-
// Chat box is 4 lines: divider + input + divider + controls
|
|
270
|
-
const chatBoxHeight = 4;
|
|
271
|
-
const chatBoxStartRow = rows - chatBoxHeight + 1;
|
|
272
|
-
// Set scroll region to protect chat box area
|
|
273
|
-
// Content scrolls in rows 1 to (chatBoxStartRow - 1)
|
|
274
|
-
this.write(`\x1b[1;${chatBoxStartRow - 1}r`);
|
|
275
|
-
// Hide cursor during render
|
|
276
|
-
this.write(ESC.HIDE);
|
|
277
|
-
// Track position
|
|
278
|
-
this.inputAreaStartRow = chatBoxStartRow;
|
|
279
|
-
let currentRow = chatBoxStartRow;
|
|
280
|
-
// Helper to write a line at absolute position (clears then writes)
|
|
281
|
-
const writeLine = (content) => {
|
|
282
|
-
this.write(ESC.TO(currentRow, 1));
|
|
283
|
-
this.write(ESC.CLEAR_LINE);
|
|
284
|
-
this.write(content);
|
|
285
|
-
currentRow++;
|
|
286
|
-
};
|
|
287
|
-
// Top divider
|
|
288
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
289
|
-
// Input line with > prompt
|
|
290
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 3);
|
|
291
|
-
const displayLine = lines[0] ?? '';
|
|
292
|
-
const inputRow = currentRow;
|
|
293
|
-
writeLine(`${DIM}>${R} ${displayLine}`);
|
|
294
|
-
// Bottom divider
|
|
295
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
296
|
-
// Mode controls line - Claude Code style
|
|
297
|
-
this.write(ESC.TO(currentRow, 1));
|
|
298
|
-
this.write(ESC.CLEAR_LINE);
|
|
299
|
-
this.write(this.buildClaudeStyleControls(cols));
|
|
300
|
-
// Track lines rendered
|
|
301
|
-
this.flowModeRenderedLines = chatBoxHeight;
|
|
302
|
-
// Position cursor in input line
|
|
303
|
-
this.write(ESC.TO(inputRow, 3 + cursorCol)); // "> " = 2 chars + 1 for position
|
|
304
|
-
// Show cursor
|
|
305
|
-
this.write(ESC.SHOW);
|
|
306
|
-
// Update tracking
|
|
307
|
-
this.lastRenderContent = this.buffer;
|
|
308
|
-
this.lastRenderCursor = this.cursor;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Build Claude Code style controls line.
|
|
312
|
-
* Shows: edit mode indicator (shift+tab to cycle)
|
|
313
|
-
*/
|
|
314
|
-
buildClaudeStyleControls(cols) {
|
|
315
|
-
const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
|
|
316
|
-
// Edit mode indicator
|
|
317
|
-
let editModeText;
|
|
318
|
-
if (this.editMode === 'display-edits') {
|
|
319
|
-
editModeText = `${GREEN}⏵⏵${R} accept edits on`;
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
|
|
323
|
-
}
|
|
324
|
-
// Build controls line
|
|
325
|
-
const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
|
|
326
|
-
// Add thinking mode if enabled
|
|
327
|
-
if (this.thinkingEnabled) {
|
|
328
|
-
parts.push(`${CYAN}💭${R}`);
|
|
329
|
-
}
|
|
330
|
-
// Add context usage if available
|
|
331
|
-
if (this.contextUsage !== null) {
|
|
332
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
333
|
-
if (rem < 10) {
|
|
334
|
-
parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
|
|
335
|
-
}
|
|
336
|
-
else if (rem < 25) {
|
|
337
|
-
parts.push(`${YELLOW}ctx ${rem}%${R}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return parts.join(` ${DIM}·${R} `);
|
|
341
|
-
}
|
|
342
200
|
/**
|
|
343
201
|
* Set the input mode
|
|
344
202
|
*
|
|
345
|
-
*
|
|
346
|
-
* Scroll region protects chat box, content scrolls above it.
|
|
203
|
+
* Content flows naturally - no scroll region pinning.
|
|
347
204
|
*/
|
|
348
205
|
setMode(mode) {
|
|
349
206
|
const prevMode = this.mode;
|
|
350
207
|
this.mode = mode;
|
|
351
208
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
352
|
-
|
|
353
|
-
this.streamingStartTime = Date.now();
|
|
354
|
-
// Ensure unified UI is initialized
|
|
355
|
-
if (!this.unifiedUIInitialized) {
|
|
356
|
-
this.initializeUnifiedUI();
|
|
357
|
-
}
|
|
358
|
-
// Re-render to ensure scroll region is set correctly
|
|
209
|
+
this.resetStreamingRenderThrottle();
|
|
359
210
|
this.renderDirty = true;
|
|
360
|
-
this.
|
|
211
|
+
this.render();
|
|
361
212
|
}
|
|
362
213
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
this.streamingRenderTimer = null;
|
|
367
|
-
}
|
|
368
|
-
// Reset streaming time
|
|
369
|
-
this.streamingStartTime = null;
|
|
370
|
-
// Re-render chat box
|
|
371
|
-
this.renderDirty = true;
|
|
372
|
-
this.scheduleRender();
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Set the row where content ends (for idle mode positioning).
|
|
377
|
-
* Input area will render starting from this row + 1.
|
|
378
|
-
*/
|
|
379
|
-
setContentEndRow(row) {
|
|
380
|
-
this.contentEndRow = Math.max(0, row);
|
|
381
|
-
this.renderDirty = true;
|
|
382
|
-
this.scheduleRender();
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Set available slash commands for auto-complete suggestions.
|
|
386
|
-
*/
|
|
387
|
-
setCommands(commands) {
|
|
388
|
-
this.commandSuggestions = commands;
|
|
389
|
-
this.updateSuggestions();
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Update filtered suggestions based on current input.
|
|
393
|
-
*/
|
|
394
|
-
updateSuggestions() {
|
|
395
|
-
const input = this.buffer.trim();
|
|
396
|
-
// Only show suggestions when input starts with "/"
|
|
397
|
-
if (!input.startsWith('/')) {
|
|
398
|
-
this.showSuggestions = false;
|
|
399
|
-
this.filteredSuggestions = [];
|
|
400
|
-
this.selectedSuggestionIndex = 0;
|
|
401
|
-
return;
|
|
214
|
+
// Streaming ended - render the input area
|
|
215
|
+
this.resetStreamingRenderThrottle();
|
|
216
|
+
this.forceRender();
|
|
402
217
|
}
|
|
403
|
-
const query = input.toLowerCase();
|
|
404
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
405
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
406
|
-
// Show suggestions if we have matches
|
|
407
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
408
|
-
// Keep selection in bounds
|
|
409
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
410
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Select next suggestion (arrow down / tab).
|
|
415
|
-
*/
|
|
416
|
-
selectNextSuggestion() {
|
|
417
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
418
|
-
return;
|
|
419
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
420
|
-
this.renderDirty = true;
|
|
421
|
-
this.scheduleRender();
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
425
|
-
*/
|
|
426
|
-
selectPrevSuggestion() {
|
|
427
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
428
|
-
return;
|
|
429
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
430
|
-
? this.filteredSuggestions.length - 1
|
|
431
|
-
: this.selectedSuggestionIndex - 1;
|
|
432
|
-
this.renderDirty = true;
|
|
433
|
-
this.scheduleRender();
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Accept current suggestion and insert into buffer.
|
|
437
|
-
*/
|
|
438
|
-
acceptSuggestion() {
|
|
439
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
440
|
-
return false;
|
|
441
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
442
|
-
if (!selected)
|
|
443
|
-
return false;
|
|
444
|
-
// Replace buffer with selected command
|
|
445
|
-
this.buffer = selected.command + ' ';
|
|
446
|
-
this.cursor = this.buffer.length;
|
|
447
|
-
this.showSuggestions = false;
|
|
448
|
-
this.renderDirty = true;
|
|
449
|
-
this.scheduleRender();
|
|
450
|
-
return true;
|
|
451
218
|
}
|
|
452
219
|
/**
|
|
453
|
-
*
|
|
220
|
+
* Legacy method - no longer used (content flows naturally).
|
|
221
|
+
* @deprecated Use setContentRow instead
|
|
454
222
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Toggle thinking/reasoning mode
|
|
460
|
-
*/
|
|
461
|
-
toggleThinking() {
|
|
462
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
463
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
464
|
-
this.scheduleRender();
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Get thinking enabled state
|
|
468
|
-
*/
|
|
469
|
-
isThinkingEnabled() {
|
|
470
|
-
return this.thinkingEnabled;
|
|
223
|
+
setPinnedHeaderLines(_count) {
|
|
224
|
+
// No-op: scroll region pinning removed
|
|
471
225
|
}
|
|
472
226
|
/**
|
|
473
227
|
* Get current mode
|
|
@@ -500,17 +254,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
500
254
|
}
|
|
501
255
|
/**
|
|
502
256
|
* Clear the buffer
|
|
503
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
504
257
|
*/
|
|
505
|
-
clear(
|
|
258
|
+
clear() {
|
|
506
259
|
this.buffer = '';
|
|
507
260
|
this.cursor = 0;
|
|
508
261
|
this.historyIndex = -1;
|
|
509
262
|
this.tempInput = '';
|
|
510
263
|
this.pastePlaceholders = [];
|
|
511
|
-
|
|
512
|
-
this.scheduleRender();
|
|
513
|
-
}
|
|
264
|
+
this.scheduleRender();
|
|
514
265
|
}
|
|
515
266
|
/**
|
|
516
267
|
* Get queued inputs
|
|
@@ -581,6 +332,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
581
332
|
this.streamingLabel = next;
|
|
582
333
|
this.scheduleRender();
|
|
583
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
|
+
}
|
|
584
366
|
/**
|
|
585
367
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
586
368
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -590,26 +372,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
590
372
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
591
373
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
592
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);
|
|
593
377
|
if (this.verificationEnabled === nextVerification &&
|
|
594
378
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
595
379
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
596
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
380
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
381
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
382
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
597
383
|
return;
|
|
598
384
|
}
|
|
599
385
|
this.verificationEnabled = nextVerification;
|
|
600
386
|
this.autoContinueEnabled = nextAutoContinue;
|
|
601
387
|
this.verificationHotkey = nextVerifyHotkey;
|
|
602
388
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
603
|
-
this.
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
607
|
-
* This is displayed persistently above the input area.
|
|
608
|
-
*/
|
|
609
|
-
setModelInfo(info) {
|
|
610
|
-
if (this.modelInfo === info)
|
|
611
|
-
return;
|
|
612
|
-
this.modelInfo = info;
|
|
389
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
390
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
613
391
|
this.scheduleRender();
|
|
614
392
|
}
|
|
615
393
|
/**
|
|
@@ -622,33 +400,158 @@ export class TerminalInput extends EventEmitter {
|
|
|
622
400
|
this.scheduleRender();
|
|
623
401
|
}
|
|
624
402
|
/**
|
|
625
|
-
*
|
|
626
|
-
|
|
627
|
-
|
|
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.
|
|
628
421
|
*/
|
|
629
422
|
render() {
|
|
630
423
|
if (!this.canRender())
|
|
631
424
|
return;
|
|
632
425
|
if (this.isRendering)
|
|
633
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
|
+
}
|
|
634
438
|
const shouldSkip = !this.renderDirty &&
|
|
635
439
|
this.buffer === this.lastRenderContent &&
|
|
636
440
|
this.cursor === this.lastRenderCursor;
|
|
637
441
|
this.renderDirty = false;
|
|
638
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
639
442
|
if (shouldSkip) {
|
|
640
443
|
return;
|
|
641
444
|
}
|
|
642
|
-
// If write lock is held, defer render
|
|
643
445
|
if (writeLock.isLocked()) {
|
|
644
446
|
writeLock.safeWrite(() => this.render());
|
|
645
447
|
return;
|
|
646
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');
|
|
647
479
|
this.isRendering = true;
|
|
648
|
-
writeLock.lock('terminalInput.render');
|
|
649
480
|
try {
|
|
650
|
-
//
|
|
651
|
-
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
|
+
}
|
|
652
555
|
}
|
|
653
556
|
finally {
|
|
654
557
|
writeLock.unlock();
|
|
@@ -656,99 +559,181 @@ export class TerminalInput extends EventEmitter {
|
|
|
656
559
|
}
|
|
657
560
|
}
|
|
658
561
|
/**
|
|
659
|
-
* Build
|
|
660
|
-
*
|
|
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.
|
|
661
565
|
*/
|
|
662
|
-
|
|
663
|
-
const maxWidth = cols - 2;
|
|
566
|
+
buildMetaLines(width) {
|
|
664
567
|
const parts = [];
|
|
665
|
-
//
|
|
666
|
-
if (this.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const secs = elapsed % 60;
|
|
672
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
673
|
-
}
|
|
674
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
675
|
-
}
|
|
676
|
-
// Queue indicator during streaming
|
|
677
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
678
|
-
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
568
|
+
// Model/provider info
|
|
569
|
+
if (this.modelLabel) {
|
|
570
|
+
const modelText = this.providerLabel
|
|
571
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
572
|
+
: this.modelLabel;
|
|
573
|
+
parts.push({ text: modelText, tone: 'info' });
|
|
679
574
|
}
|
|
680
|
-
//
|
|
681
|
-
if (this.
|
|
682
|
-
|
|
683
|
-
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
575
|
+
// Elapsed time
|
|
576
|
+
if (this.metaElapsedSeconds !== null) {
|
|
577
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
684
578
|
}
|
|
685
|
-
//
|
|
686
|
-
if (this.
|
|
687
|
-
|
|
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' });
|
|
688
584
|
}
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
585
|
+
// Context remaining (only show if concerning)
|
|
586
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
587
|
+
if (tokensRemaining !== null) {
|
|
588
|
+
parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
|
|
692
589
|
}
|
|
693
|
-
//
|
|
694
|
-
if (this.
|
|
695
|
-
parts.push(
|
|
590
|
+
// Thinking indicator
|
|
591
|
+
if (this.metaThinkingMs !== null) {
|
|
592
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
696
593
|
}
|
|
697
|
-
if (parts.length
|
|
698
|
-
return
|
|
594
|
+
if (!parts.length) {
|
|
595
|
+
return [];
|
|
699
596
|
}
|
|
700
|
-
|
|
701
|
-
return joined.slice(0, maxWidth);
|
|
597
|
+
return [renderStatusLine(parts, width)];
|
|
702
598
|
}
|
|
703
599
|
/**
|
|
704
|
-
* Build mode controls line
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* 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.
|
|
708
602
|
*/
|
|
709
603
|
buildModeControls(cols) {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
716
|
-
if (this.editMode === 'display-edits') {
|
|
717
|
-
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' });
|
|
718
609
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
752
737
|
}
|
|
753
738
|
/**
|
|
754
739
|
* Force a re-render
|
|
@@ -771,28 +756,112 @@ export class TerminalInput extends EventEmitter {
|
|
|
771
756
|
handleResize() {
|
|
772
757
|
this.lastRenderContent = '';
|
|
773
758
|
this.lastRenderCursor = -1;
|
|
759
|
+
this.resetStreamingRenderThrottle();
|
|
774
760
|
this.scheduleRender();
|
|
775
761
|
}
|
|
776
762
|
/**
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
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.
|
|
780
772
|
*/
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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;
|
|
796
865
|
}
|
|
797
866
|
/**
|
|
798
867
|
* Dispose and clean up
|
|
@@ -800,20 +869,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
800
869
|
dispose() {
|
|
801
870
|
if (this.disposed)
|
|
802
871
|
return;
|
|
803
|
-
// Clean up streaming render timer
|
|
804
|
-
if (this.streamingRenderTimer) {
|
|
805
|
-
clearInterval(this.streamingRenderTimer);
|
|
806
|
-
this.streamingRenderTimer = null;
|
|
807
|
-
}
|
|
808
|
-
// Clean up output interceptor
|
|
809
|
-
if (this.outputInterceptorCleanup) {
|
|
810
|
-
this.outputInterceptorCleanup();
|
|
811
|
-
this.outputInterceptorCleanup = undefined;
|
|
812
|
-
}
|
|
813
|
-
// Reset scroll region before disposing
|
|
814
|
-
this.write('\x1b[r');
|
|
815
872
|
this.disposed = true;
|
|
816
873
|
this.enabled = false;
|
|
874
|
+
this.disableScrollRegion();
|
|
875
|
+
this.resetStreamingRenderThrottle();
|
|
817
876
|
this.disableBracketedPaste();
|
|
818
877
|
this.buffer = '';
|
|
819
878
|
this.queue = [];
|
|
@@ -918,22 +977,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
918
977
|
this.toggleEditMode();
|
|
919
978
|
return true;
|
|
920
979
|
}
|
|
921
|
-
|
|
922
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
923
|
-
this.togglePasteExpansion();
|
|
924
|
-
}
|
|
925
|
-
else {
|
|
926
|
-
this.toggleThinking();
|
|
927
|
-
}
|
|
928
|
-
return true;
|
|
929
|
-
case 'escape':
|
|
930
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
931
|
-
if (this.mode === 'streaming') {
|
|
932
|
-
this.emit('interrupt');
|
|
933
|
-
}
|
|
934
|
-
else if (this.buffer.length > 0) {
|
|
935
|
-
this.clear();
|
|
936
|
-
}
|
|
980
|
+
this.insertText(' ');
|
|
937
981
|
return true;
|
|
938
982
|
}
|
|
939
983
|
return false;
|
|
@@ -951,7 +995,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
951
995
|
this.insertPlainText(chunk, insertPos);
|
|
952
996
|
this.cursor = insertPos + chunk.length;
|
|
953
997
|
this.emit('change', this.buffer);
|
|
954
|
-
this.updateSuggestions();
|
|
955
998
|
this.scheduleRender();
|
|
956
999
|
}
|
|
957
1000
|
insertNewline() {
|
|
@@ -976,7 +1019,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
976
1019
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
977
1020
|
}
|
|
978
1021
|
this.emit('change', this.buffer);
|
|
979
|
-
this.updateSuggestions();
|
|
980
1022
|
this.scheduleRender();
|
|
981
1023
|
}
|
|
982
1024
|
deleteForward() {
|
|
@@ -1204,13 +1246,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1204
1246
|
timestamp: Date.now(),
|
|
1205
1247
|
});
|
|
1206
1248
|
this.emit('queue', text);
|
|
1207
|
-
this.clear(); // Clear immediately for queued input
|
|
1249
|
+
this.clear(); // Clear immediately for queued input
|
|
1208
1250
|
}
|
|
1209
1251
|
else {
|
|
1210
|
-
// In idle mode, clear the input
|
|
1211
|
-
// The
|
|
1212
|
-
|
|
1213
|
-
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();
|
|
1214
1255
|
this.emit('submit', text);
|
|
1215
1256
|
}
|
|
1216
1257
|
}
|
|
@@ -1227,7 +1268,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1227
1268
|
if (available <= 0)
|
|
1228
1269
|
return;
|
|
1229
1270
|
const chunk = clean.slice(0, available);
|
|
1230
|
-
|
|
1271
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1272
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1273
|
+
if (isMultiline && !isShortMultiline) {
|
|
1231
1274
|
this.insertPastePlaceholder(chunk);
|
|
1232
1275
|
}
|
|
1233
1276
|
else {
|
|
@@ -1363,17 +1406,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1363
1406
|
this.shiftPlaceholders(position, text.length);
|
|
1364
1407
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1365
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
|
+
}
|
|
1366
1415
|
findPlaceholderAt(position) {
|
|
1367
1416
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1368
1417
|
}
|
|
1369
|
-
buildPlaceholder(
|
|
1418
|
+
buildPlaceholder(lineCount) {
|
|
1370
1419
|
const id = ++this.pasteCounter;
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
const preview = summary.preview.length > 30
|
|
1374
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1375
|
-
: summary.preview;
|
|
1376
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1420
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1421
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1377
1422
|
return { id, placeholder };
|
|
1378
1423
|
}
|
|
1379
1424
|
insertPastePlaceholder(content) {
|
|
@@ -1381,67 +1426,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1381
1426
|
if (available <= 0)
|
|
1382
1427
|
return;
|
|
1383
1428
|
const cleanContent = content.slice(0, available);
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
if (summary.lineCount < 5) {
|
|
1387
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1388
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1389
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1390
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1429
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1430
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1394
1431
|
const insertPos = this.cursor;
|
|
1395
1432
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1396
1433
|
this.pastePlaceholders.push({
|
|
1397
1434
|
id,
|
|
1398
1435
|
content: cleanContent,
|
|
1399
|
-
lineCount
|
|
1436
|
+
lineCount,
|
|
1400
1437
|
placeholder,
|
|
1401
1438
|
start: insertPos,
|
|
1402
1439
|
end: insertPos + placeholder.length,
|
|
1403
|
-
summary,
|
|
1404
|
-
expanded: false,
|
|
1405
1440
|
});
|
|
1406
1441
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1407
1442
|
this.cursor = insertPos + placeholder.length;
|
|
1408
1443
|
}
|
|
1409
|
-
/**
|
|
1410
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1411
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1412
|
-
*/
|
|
1413
|
-
togglePasteExpansion() {
|
|
1414
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1415
|
-
if (!placeholder)
|
|
1416
|
-
return false;
|
|
1417
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1418
|
-
// Update the placeholder text in buffer
|
|
1419
|
-
const newPlaceholder = placeholder.expanded
|
|
1420
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1421
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1422
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1423
|
-
// Update buffer
|
|
1424
|
-
this.buffer =
|
|
1425
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1426
|
-
newPlaceholder +
|
|
1427
|
-
this.buffer.slice(placeholder.end);
|
|
1428
|
-
// Update placeholder tracking
|
|
1429
|
-
placeholder.placeholder = newPlaceholder;
|
|
1430
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1431
|
-
// Shift other placeholders
|
|
1432
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1433
|
-
this.scheduleRender();
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
|
-
buildExpandedPlaceholder(ph) {
|
|
1437
|
-
const lines = ph.content.split('\n');
|
|
1438
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1439
|
-
const lastLines = lines.length > 5
|
|
1440
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1441
|
-
: '';
|
|
1442
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1443
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1444
|
-
}
|
|
1445
1444
|
deletePlaceholder(placeholder) {
|
|
1446
1445
|
const length = placeholder.end - placeholder.start;
|
|
1447
1446
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1449,7 +1448,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1449
1448
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1450
1449
|
this.cursor = placeholder.start;
|
|
1451
1450
|
}
|
|
1452
|
-
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
|
+
}
|
|
1453
1456
|
if (value === null || !Number.isFinite(value)) {
|
|
1454
1457
|
this.contextUsage = null;
|
|
1455
1458
|
}
|
|
@@ -1476,6 +1479,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1476
1479
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1477
1480
|
this.setEditMode(next);
|
|
1478
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
|
+
}
|
|
1479
1498
|
scheduleRender() {
|
|
1480
1499
|
if (!this.canRender())
|
|
1481
1500
|
return;
|