erosolar-cli 1.7.318 → 1.7.320
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 +134 -116
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +532 -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,120 @@ 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 bottom-pinned chat box.
|
|
764
|
+
*
|
|
765
|
+
* Approach:
|
|
766
|
+
* 1. Save cursor state
|
|
767
|
+
* 2. Set scroll region to content area (excludes chat box)
|
|
768
|
+
* 3. Position cursor at saved content row
|
|
769
|
+
* 4. Write content (terminal scrolls within region as needed)
|
|
770
|
+
* 5. Save new position and restore full scroll region
|
|
771
|
+
* 6. Throttle chat box updates for performance
|
|
772
|
+
*
|
|
773
|
+
* Chat box stays pinned at bottom of terminal.
|
|
780
774
|
*/
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
775
|
+
streamContent(content) {
|
|
776
|
+
if (!content)
|
|
777
|
+
return;
|
|
778
|
+
writeLock.lock('streamContent');
|
|
779
|
+
try {
|
|
780
|
+
const { rows } = this.getSize();
|
|
781
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
782
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
783
|
+
// Set scroll region to content area only (excludes chat box at bottom)
|
|
784
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
785
|
+
// Position cursor at current content row
|
|
786
|
+
// If we've reached the scroll boundary, stay at scrollEnd and let terminal scroll
|
|
787
|
+
const targetRow = Math.min(this.contentRow, scrollEnd);
|
|
788
|
+
this.write(ESC.TO(targetRow, 1));
|
|
789
|
+
// Write content - terminal auto-scrolls within the region
|
|
790
|
+
this.write(content);
|
|
791
|
+
// Count newlines and advance content row
|
|
792
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
793
|
+
this.contentRow += newlines;
|
|
794
|
+
// Cap at scroll end (content will scroll up when exceeding)
|
|
795
|
+
if (this.contentRow > scrollEnd) {
|
|
796
|
+
this.contentRow = scrollEnd;
|
|
797
|
+
}
|
|
798
|
+
// Reset scroll region to full terminal
|
|
799
|
+
this.write(ESC.RESET_SCROLL);
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
writeLock.unlock();
|
|
803
|
+
}
|
|
804
|
+
// Throttle chat box re-renders during streaming - only update periodically
|
|
805
|
+
this.scheduleStreamingRender(100);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Enable scroll region (no-op in floating mode).
|
|
809
|
+
*/
|
|
810
|
+
enableScrollRegion() {
|
|
811
|
+
// No-op: using pure floating approach
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Disable scroll region (no-op in floating mode).
|
|
815
|
+
*/
|
|
816
|
+
disableScrollRegion() {
|
|
817
|
+
// No-op: using pure floating approach
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Calculate chat box height.
|
|
821
|
+
*/
|
|
822
|
+
getChatBoxHeight() {
|
|
823
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* @deprecated Use streamContent() instead
|
|
827
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
828
|
+
*/
|
|
829
|
+
registerOutputInterceptor(_display) {
|
|
830
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* @deprecated Use streamContent() instead
|
|
834
|
+
* Write content above the floating chat box.
|
|
835
|
+
*/
|
|
836
|
+
writeToScrollRegion(content) {
|
|
837
|
+
this.streamContent(content);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Clear the entire terminal screen and reset content position.
|
|
841
|
+
* This removes all content including the launching command.
|
|
842
|
+
*/
|
|
843
|
+
clearScreen() {
|
|
844
|
+
writeLock.lock('clearScreen');
|
|
845
|
+
try {
|
|
846
|
+
this.write(ESC.HOME);
|
|
847
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
848
|
+
this.contentRow = 1;
|
|
849
|
+
}
|
|
850
|
+
finally {
|
|
851
|
+
writeLock.unlock();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Reset content position to row 1.
|
|
856
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
857
|
+
*/
|
|
858
|
+
resetContentPosition() {
|
|
859
|
+
this.contentRow = 1;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Set the content row explicitly (used after banner is written).
|
|
863
|
+
* This tells the input where content should start flowing from.
|
|
864
|
+
*/
|
|
865
|
+
setContentRow(row) {
|
|
866
|
+
this.contentRow = Math.max(1, row);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get the current content row position.
|
|
870
|
+
*/
|
|
871
|
+
getContentRow() {
|
|
872
|
+
return this.contentRow;
|
|
796
873
|
}
|
|
797
874
|
/**
|
|
798
875
|
* Dispose and clean up
|
|
@@ -800,20 +877,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
800
877
|
dispose() {
|
|
801
878
|
if (this.disposed)
|
|
802
879
|
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
880
|
this.disposed = true;
|
|
816
881
|
this.enabled = false;
|
|
882
|
+
this.disableScrollRegion();
|
|
883
|
+
this.resetStreamingRenderThrottle();
|
|
817
884
|
this.disableBracketedPaste();
|
|
818
885
|
this.buffer = '';
|
|
819
886
|
this.queue = [];
|
|
@@ -918,22 +985,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
918
985
|
this.toggleEditMode();
|
|
919
986
|
return true;
|
|
920
987
|
}
|
|
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
|
-
}
|
|
988
|
+
this.insertText(' ');
|
|
937
989
|
return true;
|
|
938
990
|
}
|
|
939
991
|
return false;
|
|
@@ -951,7 +1003,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
951
1003
|
this.insertPlainText(chunk, insertPos);
|
|
952
1004
|
this.cursor = insertPos + chunk.length;
|
|
953
1005
|
this.emit('change', this.buffer);
|
|
954
|
-
this.updateSuggestions();
|
|
955
1006
|
this.scheduleRender();
|
|
956
1007
|
}
|
|
957
1008
|
insertNewline() {
|
|
@@ -976,7 +1027,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
976
1027
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
977
1028
|
}
|
|
978
1029
|
this.emit('change', this.buffer);
|
|
979
|
-
this.updateSuggestions();
|
|
980
1030
|
this.scheduleRender();
|
|
981
1031
|
}
|
|
982
1032
|
deleteForward() {
|
|
@@ -1204,13 +1254,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1204
1254
|
timestamp: Date.now(),
|
|
1205
1255
|
});
|
|
1206
1256
|
this.emit('queue', text);
|
|
1207
|
-
this.clear(); // Clear immediately for queued input
|
|
1257
|
+
this.clear(); // Clear immediately for queued input
|
|
1208
1258
|
}
|
|
1209
1259
|
else {
|
|
1210
|
-
// In idle mode, clear the input
|
|
1211
|
-
// The
|
|
1212
|
-
|
|
1213
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1260
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1261
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1262
|
+
this.clear();
|
|
1214
1263
|
this.emit('submit', text);
|
|
1215
1264
|
}
|
|
1216
1265
|
}
|
|
@@ -1227,7 +1276,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1227
1276
|
if (available <= 0)
|
|
1228
1277
|
return;
|
|
1229
1278
|
const chunk = clean.slice(0, available);
|
|
1230
|
-
|
|
1279
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1280
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1281
|
+
if (isMultiline && !isShortMultiline) {
|
|
1231
1282
|
this.insertPastePlaceholder(chunk);
|
|
1232
1283
|
}
|
|
1233
1284
|
else {
|
|
@@ -1363,17 +1414,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1363
1414
|
this.shiftPlaceholders(position, text.length);
|
|
1364
1415
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1365
1416
|
}
|
|
1417
|
+
shouldInlineMultiline(content) {
|
|
1418
|
+
const lines = content.split('\n').length;
|
|
1419
|
+
const maxInlineLines = 4;
|
|
1420
|
+
const maxInlineChars = 240;
|
|
1421
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1422
|
+
}
|
|
1366
1423
|
findPlaceholderAt(position) {
|
|
1367
1424
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1368
1425
|
}
|
|
1369
|
-
buildPlaceholder(
|
|
1426
|
+
buildPlaceholder(lineCount) {
|
|
1370
1427
|
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}"`;
|
|
1428
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1429
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1377
1430
|
return { id, placeholder };
|
|
1378
1431
|
}
|
|
1379
1432
|
insertPastePlaceholder(content) {
|
|
@@ -1381,67 +1434,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1381
1434
|
if (available <= 0)
|
|
1382
1435
|
return;
|
|
1383
1436
|
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);
|
|
1437
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1438
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1394
1439
|
const insertPos = this.cursor;
|
|
1395
1440
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1396
1441
|
this.pastePlaceholders.push({
|
|
1397
1442
|
id,
|
|
1398
1443
|
content: cleanContent,
|
|
1399
|
-
lineCount
|
|
1444
|
+
lineCount,
|
|
1400
1445
|
placeholder,
|
|
1401
1446
|
start: insertPos,
|
|
1402
1447
|
end: insertPos + placeholder.length,
|
|
1403
|
-
summary,
|
|
1404
|
-
expanded: false,
|
|
1405
1448
|
});
|
|
1406
1449
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1407
1450
|
this.cursor = insertPos + placeholder.length;
|
|
1408
1451
|
}
|
|
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
1452
|
deletePlaceholder(placeholder) {
|
|
1446
1453
|
const length = placeholder.end - placeholder.start;
|
|
1447
1454
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1449,7 +1456,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1449
1456
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1450
1457
|
this.cursor = placeholder.start;
|
|
1451
1458
|
}
|
|
1452
|
-
updateContextUsage(value) {
|
|
1459
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1460
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1461
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1462
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1463
|
+
}
|
|
1453
1464
|
if (value === null || !Number.isFinite(value)) {
|
|
1454
1465
|
this.contextUsage = null;
|
|
1455
1466
|
}
|
|
@@ -1476,6 +1487,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1476
1487
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1477
1488
|
this.setEditMode(next);
|
|
1478
1489
|
}
|
|
1490
|
+
scheduleStreamingRender(delayMs) {
|
|
1491
|
+
if (this.streamingRenderTimer)
|
|
1492
|
+
return;
|
|
1493
|
+
const wait = Math.max(16, delayMs);
|
|
1494
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1495
|
+
this.streamingRenderTimer = null;
|
|
1496
|
+
this.render();
|
|
1497
|
+
}, wait);
|
|
1498
|
+
}
|
|
1499
|
+
resetStreamingRenderThrottle() {
|
|
1500
|
+
if (this.streamingRenderTimer) {
|
|
1501
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1502
|
+
this.streamingRenderTimer = null;
|
|
1503
|
+
}
|
|
1504
|
+
this.lastStreamingRender = 0;
|
|
1505
|
+
}
|
|
1479
1506
|
scheduleRender() {
|
|
1480
1507
|
if (!this.canRender())
|
|
1481
1508
|
return;
|