erosolar-cli 1.7.301 → 1.7.302
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 +3 -1
- 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 +223 -163
- 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 +111 -108
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +501 -490
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +47 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +53 -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,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - Content flows naturally from top to bottom (no scroll region pinning)
|
|
6
7
|
* - Native bracketed paste support (no heuristics)
|
|
7
8
|
* - Clean cursor model with render-time wrapping
|
|
8
9
|
* - State machine for different input modes
|
|
9
10
|
* - No readline dependency for display
|
|
10
11
|
*/
|
|
11
12
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
13
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
14
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import {
|
|
15
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
16
|
+
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
17
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
15
18
|
// ANSI escape codes
|
|
16
19
|
const ESC = {
|
|
17
20
|
// Cursor control
|
|
@@ -21,12 +24,12 @@ const ESC = {
|
|
|
21
24
|
SHOW: '\x1b[?25h',
|
|
22
25
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
23
26
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
-
// Screen control
|
|
25
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
-
HOME: '\x1b[H',
|
|
27
27
|
// Line control
|
|
28
28
|
CLEAR_LINE: '\x1b[2K',
|
|
29
29
|
CLEAR_TO_END: '\x1b[0J',
|
|
30
|
+
// Scroll region
|
|
31
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
32
|
+
RESET_SCROLL: '\x1b[r',
|
|
30
33
|
// Style
|
|
31
34
|
RESET: '\x1b[0m',
|
|
32
35
|
DIM: '\x1b[2m',
|
|
@@ -66,47 +69,44 @@ export class TerminalInput extends EventEmitter {
|
|
|
66
69
|
statusMessage = null;
|
|
67
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
68
71
|
streamingLabel = null; // Streaming progress indicator
|
|
72
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
73
|
+
metaTokensUsed = null; // Optional token usage
|
|
74
|
+
metaTokenLimit = null; // Optional token window
|
|
75
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
76
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
69
77
|
lastRenderContent = '';
|
|
70
78
|
lastRenderCursor = -1;
|
|
71
79
|
renderDirty = false;
|
|
72
80
|
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 (for idle mode positioning)
|
|
76
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
77
|
-
commandSuggestions = [];
|
|
78
|
-
filteredSuggestions = [];
|
|
79
|
-
selectedSuggestionIndex = 0;
|
|
80
|
-
showSuggestions = false;
|
|
81
81
|
// Lifecycle
|
|
82
82
|
disposed = false;
|
|
83
83
|
enabled = true;
|
|
84
84
|
contextUsage = null;
|
|
85
|
+
contextAutoCompactThreshold = 90;
|
|
86
|
+
// Track current content row in scroll region (starts at top, moves down)
|
|
87
|
+
contentRow = 1;
|
|
88
|
+
thinkingModeLabel = null;
|
|
85
89
|
editMode = 'display-edits';
|
|
86
90
|
verificationEnabled = true;
|
|
87
91
|
autoContinueEnabled = false;
|
|
88
92
|
verificationHotkey = 'alt+v';
|
|
89
93
|
autoContinueHotkey = 'alt+c';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
94
|
+
thinkingHotkey = '/thinking';
|
|
95
|
+
modelLabel = null;
|
|
96
|
+
providerLabel = null;
|
|
97
|
+
// Streaming render throttle
|
|
98
|
+
lastStreamingRender = 0;
|
|
99
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
97
100
|
streamingRenderTimer = null;
|
|
98
|
-
// Unified UI initialization flag
|
|
99
|
-
unifiedUIInitialized = false;
|
|
100
101
|
constructor(writeStream = process.stdout, config = {}) {
|
|
101
102
|
super();
|
|
102
103
|
this.out = writeStream;
|
|
103
|
-
// Use schema defaults for configuration consistency
|
|
104
104
|
this.config = {
|
|
105
|
-
maxLines: config.maxLines ??
|
|
106
|
-
maxLength: config.maxLength ??
|
|
105
|
+
maxLines: config.maxLines ?? 1000,
|
|
106
|
+
maxLength: config.maxLength ?? 10000,
|
|
107
107
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
108
|
-
promptChar: config.promptChar ??
|
|
109
|
-
continuationChar: config.continuationChar ??
|
|
108
|
+
promptChar: config.promptChar ?? '> ',
|
|
109
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
// ===========================================================================
|
|
@@ -185,288 +185,36 @@ export class TerminalInput extends EventEmitter {
|
|
|
185
185
|
if (handled)
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
189
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
190
|
-
this.emit('showHelp');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
188
|
// Insert printable characters
|
|
194
189
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
190
|
this.insertText(str);
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
199
|
-
bannerContent = null;
|
|
200
|
-
/**
|
|
201
|
-
* Set banner content to be written when unified UI initializes.
|
|
202
|
-
*/
|
|
203
|
-
setBannerContent(content) {
|
|
204
|
-
this.bannerContent = content;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Initialize the unified UI system.
|
|
208
|
-
*
|
|
209
|
-
* Layout:
|
|
210
|
-
* 1. Clear screen
|
|
211
|
-
* 2. Write banner at top
|
|
212
|
-
* 3. Track content end position
|
|
213
|
-
* 4. Render floating input area below banner
|
|
214
|
-
*/
|
|
215
|
-
initializeUnifiedUI() {
|
|
216
|
-
if (this.unifiedUIInitialized) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Hide cursor during setup
|
|
220
|
-
this.write(ESC.HIDE);
|
|
221
|
-
// Clear screen and go home
|
|
222
|
-
this.write(ESC.HOME);
|
|
223
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
224
|
-
// Write banner at top and track where it ends
|
|
225
|
-
let bannerLines = 0;
|
|
226
|
-
if (this.bannerContent) {
|
|
227
|
-
const lines = this.bannerContent.split('\n');
|
|
228
|
-
bannerLines = lines.length + 2; // +2 for the trailing \n\n
|
|
229
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
230
|
-
}
|
|
231
|
-
// Set content end row so input renders right after banner
|
|
232
|
-
this.contentEndRow = bannerLines > 0 ? bannerLines : 1;
|
|
233
|
-
// Mark initialized
|
|
234
|
-
this.unifiedUIInitialized = true;
|
|
235
|
-
// Render floating input area below the banner
|
|
236
|
-
this.renderFloatingInputArea();
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Clear the input area at its tracked position.
|
|
240
|
-
* Returns true if something was cleared.
|
|
241
|
-
*/
|
|
242
|
-
clearInputArea() {
|
|
243
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
244
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
245
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
246
|
-
this.write(ESC.CLEAR_LINE);
|
|
247
|
-
}
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Reset input area tracking state.
|
|
254
|
-
*/
|
|
255
|
-
resetInputAreaTracking() {
|
|
256
|
-
this.inputAreaStartRow = 0;
|
|
257
|
-
this.flowModeRenderedLines = 0;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Render floating input area below current content.
|
|
261
|
-
* This is "bottom floating" - follows content, not pinned to terminal bottom.
|
|
262
|
-
* Uses absolute positioning to prevent duplicates.
|
|
263
|
-
*/
|
|
264
|
-
renderFloatingInputArea() {
|
|
265
|
-
const { rows, cols } = this.getSize();
|
|
266
|
-
const divider = '─'.repeat(cols - 1);
|
|
267
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
268
|
-
// Calculate lines needed for input area
|
|
269
|
-
const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
|
|
270
|
-
// FIRST: Clear any previously rendered input area
|
|
271
|
-
this.clearInputArea();
|
|
272
|
-
// Hide cursor during render
|
|
273
|
-
this.write(ESC.HIDE);
|
|
274
|
-
// Calculate where to render: after current content
|
|
275
|
-
// Use contentEndRow if set, otherwise estimate from terminal bottom
|
|
276
|
-
let startRow;
|
|
277
|
-
if (this.contentEndRow > 0) {
|
|
278
|
-
startRow = this.contentEndRow + 1;
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
// Render near bottom, leaving space for input area
|
|
282
|
-
startRow = Math.max(1, rows - linesNeeded);
|
|
283
|
-
}
|
|
284
|
-
// Ensure we don't go past terminal bounds
|
|
285
|
-
startRow = Math.min(startRow, rows - linesNeeded + 1);
|
|
286
|
-
startRow = Math.max(1, startRow);
|
|
287
|
-
// Track this position
|
|
288
|
-
this.inputAreaStartRow = startRow;
|
|
289
|
-
let currentRow = startRow;
|
|
290
|
-
// Status bar
|
|
291
|
-
this.write(ESC.TO(currentRow, 1));
|
|
292
|
-
this.write(this.buildStatusBar(cols));
|
|
293
|
-
currentRow++;
|
|
294
|
-
// Model info line (if set)
|
|
295
|
-
if (this.modelInfo) {
|
|
296
|
-
this.write(ESC.TO(currentRow, 1));
|
|
297
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
298
|
-
if (this.contextUsage !== null) {
|
|
299
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
300
|
-
if (rem < 10)
|
|
301
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
302
|
-
else if (rem < 25)
|
|
303
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
304
|
-
else
|
|
305
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
306
|
-
}
|
|
307
|
-
this.write(modelLine);
|
|
308
|
-
currentRow++;
|
|
309
|
-
}
|
|
310
|
-
// Top divider
|
|
311
|
-
this.write(ESC.TO(currentRow, 1));
|
|
312
|
-
this.write(divider);
|
|
313
|
-
currentRow++;
|
|
314
|
-
// Input line with prompt and buffer content
|
|
315
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
316
|
-
const displayLine = lines[0] ?? '';
|
|
317
|
-
const inputRow = currentRow;
|
|
318
|
-
this.write(ESC.TO(currentRow, 1));
|
|
319
|
-
this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
320
|
-
this.write(ESC.BG_DARK + displayLine);
|
|
321
|
-
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
322
|
-
if (padding > 0)
|
|
323
|
-
this.write(' '.repeat(padding));
|
|
324
|
-
this.write(ESC.RESET);
|
|
325
|
-
currentRow++;
|
|
326
|
-
// Bottom divider
|
|
327
|
-
this.write(ESC.TO(currentRow, 1));
|
|
328
|
-
this.write(divider);
|
|
329
|
-
currentRow++;
|
|
330
|
-
// Mode controls
|
|
331
|
-
this.write(ESC.TO(currentRow, 1));
|
|
332
|
-
this.write(this.buildModeControls(cols));
|
|
333
|
-
// Track lines rendered
|
|
334
|
-
this.flowModeRenderedLines = currentRow - startRow + 1;
|
|
335
|
-
// Position cursor in input line for typing
|
|
336
|
-
this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
|
|
337
|
-
// Show cursor
|
|
338
|
-
this.write(ESC.SHOW);
|
|
339
|
-
// Update tracking
|
|
340
|
-
this.lastRenderContent = this.buffer;
|
|
341
|
-
this.lastRenderCursor = this.cursor;
|
|
342
|
-
}
|
|
343
193
|
/**
|
|
344
194
|
* Set the input mode
|
|
345
195
|
*
|
|
346
|
-
*
|
|
196
|
+
* Content flows naturally - no scroll region pinning.
|
|
347
197
|
*/
|
|
348
198
|
setMode(mode) {
|
|
349
199
|
const prevMode = this.mode;
|
|
350
200
|
this.mode = mode;
|
|
351
201
|
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
|
-
}
|
|
202
|
+
this.resetStreamingRenderThrottle();
|
|
358
203
|
this.renderDirty = true;
|
|
359
|
-
this.
|
|
204
|
+
this.render();
|
|
360
205
|
}
|
|
361
206
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.streamingRenderTimer = null;
|
|
366
|
-
}
|
|
367
|
-
// Reset streaming time
|
|
368
|
-
this.streamingStartTime = null;
|
|
369
|
-
// Re-render floating input area
|
|
370
|
-
this.renderDirty = true;
|
|
371
|
-
this.scheduleRender();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Set the row where content ends (for idle mode positioning).
|
|
376
|
-
* Input area will render starting from this row + 1.
|
|
377
|
-
*/
|
|
378
|
-
setContentEndRow(row) {
|
|
379
|
-
this.contentEndRow = Math.max(0, row);
|
|
380
|
-
this.renderDirty = true;
|
|
381
|
-
this.scheduleRender();
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Set available slash commands for auto-complete suggestions.
|
|
385
|
-
*/
|
|
386
|
-
setCommands(commands) {
|
|
387
|
-
this.commandSuggestions = commands;
|
|
388
|
-
this.updateSuggestions();
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Update filtered suggestions based on current input.
|
|
392
|
-
*/
|
|
393
|
-
updateSuggestions() {
|
|
394
|
-
const input = this.buffer.trim();
|
|
395
|
-
// Only show suggestions when input starts with "/"
|
|
396
|
-
if (!input.startsWith('/')) {
|
|
397
|
-
this.showSuggestions = false;
|
|
398
|
-
this.filteredSuggestions = [];
|
|
399
|
-
this.selectedSuggestionIndex = 0;
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
const query = input.toLowerCase();
|
|
403
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
404
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
405
|
-
// Show suggestions if we have matches
|
|
406
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
407
|
-
// Keep selection in bounds
|
|
408
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
409
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
207
|
+
// Streaming ended - render the input area
|
|
208
|
+
this.resetStreamingRenderThrottle();
|
|
209
|
+
this.forceRender();
|
|
410
210
|
}
|
|
411
211
|
}
|
|
412
212
|
/**
|
|
413
|
-
*
|
|
213
|
+
* Legacy method - no longer used (content flows naturally).
|
|
214
|
+
* @deprecated Use setContentRow instead
|
|
414
215
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return;
|
|
418
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
419
|
-
this.renderDirty = true;
|
|
420
|
-
this.scheduleRender();
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
424
|
-
*/
|
|
425
|
-
selectPrevSuggestion() {
|
|
426
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
427
|
-
return;
|
|
428
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
429
|
-
? this.filteredSuggestions.length - 1
|
|
430
|
-
: this.selectedSuggestionIndex - 1;
|
|
431
|
-
this.renderDirty = true;
|
|
432
|
-
this.scheduleRender();
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Accept current suggestion and insert into buffer.
|
|
436
|
-
*/
|
|
437
|
-
acceptSuggestion() {
|
|
438
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
439
|
-
return false;
|
|
440
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
441
|
-
if (!selected)
|
|
442
|
-
return false;
|
|
443
|
-
// Replace buffer with selected command
|
|
444
|
-
this.buffer = selected.command + ' ';
|
|
445
|
-
this.cursor = this.buffer.length;
|
|
446
|
-
this.showSuggestions = false;
|
|
447
|
-
this.renderDirty = true;
|
|
448
|
-
this.scheduleRender();
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Check if suggestions are visible.
|
|
453
|
-
*/
|
|
454
|
-
areSuggestionsVisible() {
|
|
455
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Toggle thinking/reasoning mode
|
|
459
|
-
*/
|
|
460
|
-
toggleThinking() {
|
|
461
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
462
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
463
|
-
this.scheduleRender();
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Get thinking enabled state
|
|
467
|
-
*/
|
|
468
|
-
isThinkingEnabled() {
|
|
469
|
-
return this.thinkingEnabled;
|
|
216
|
+
setPinnedHeaderLines(_count) {
|
|
217
|
+
// No-op: scroll region pinning removed
|
|
470
218
|
}
|
|
471
219
|
/**
|
|
472
220
|
* Get current mode
|
|
@@ -499,17 +247,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
499
247
|
}
|
|
500
248
|
/**
|
|
501
249
|
* Clear the buffer
|
|
502
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
503
250
|
*/
|
|
504
|
-
clear(
|
|
251
|
+
clear() {
|
|
505
252
|
this.buffer = '';
|
|
506
253
|
this.cursor = 0;
|
|
507
254
|
this.historyIndex = -1;
|
|
508
255
|
this.tempInput = '';
|
|
509
256
|
this.pastePlaceholders = [];
|
|
510
|
-
|
|
511
|
-
this.scheduleRender();
|
|
512
|
-
}
|
|
257
|
+
this.scheduleRender();
|
|
513
258
|
}
|
|
514
259
|
/**
|
|
515
260
|
* Get queued inputs
|
|
@@ -580,6 +325,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
580
325
|
this.streamingLabel = next;
|
|
581
326
|
this.scheduleRender();
|
|
582
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
330
|
+
*/
|
|
331
|
+
setMetaStatus(meta) {
|
|
332
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
333
|
+
? Math.floor(meta.elapsedSeconds)
|
|
334
|
+
: null;
|
|
335
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
336
|
+
? Math.floor(meta.tokensUsed)
|
|
337
|
+
: null;
|
|
338
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
339
|
+
? Math.floor(meta.tokenLimit)
|
|
340
|
+
: null;
|
|
341
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
342
|
+
? Math.floor(meta.thinkingMs)
|
|
343
|
+
: null;
|
|
344
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
345
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
346
|
+
this.metaTokensUsed === nextTokens &&
|
|
347
|
+
this.metaTokenLimit === nextLimit &&
|
|
348
|
+
this.metaThinkingMs === nextThinking &&
|
|
349
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
353
|
+
this.metaTokensUsed = nextTokens;
|
|
354
|
+
this.metaTokenLimit = nextLimit;
|
|
355
|
+
this.metaThinkingMs = nextThinking;
|
|
356
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
357
|
+
this.scheduleRender();
|
|
358
|
+
}
|
|
583
359
|
/**
|
|
584
360
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
585
361
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -589,26 +365,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
589
365
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
590
366
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
591
367
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
368
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
369
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
592
370
|
if (this.verificationEnabled === nextVerification &&
|
|
593
371
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
594
372
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
595
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
373
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
374
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
375
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
596
376
|
return;
|
|
597
377
|
}
|
|
598
378
|
this.verificationEnabled = nextVerification;
|
|
599
379
|
this.autoContinueEnabled = nextAutoContinue;
|
|
600
380
|
this.verificationHotkey = nextVerifyHotkey;
|
|
601
381
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
602
|
-
this.
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
606
|
-
* This is displayed persistently above the input area.
|
|
607
|
-
*/
|
|
608
|
-
setModelInfo(info) {
|
|
609
|
-
if (this.modelInfo === info)
|
|
610
|
-
return;
|
|
611
|
-
this.modelInfo = info;
|
|
382
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
383
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
612
384
|
this.scheduleRender();
|
|
613
385
|
}
|
|
614
386
|
/**
|
|
@@ -621,30 +393,155 @@ export class TerminalInput extends EventEmitter {
|
|
|
621
393
|
this.scheduleRender();
|
|
622
394
|
}
|
|
623
395
|
/**
|
|
624
|
-
*
|
|
396
|
+
* Surface model/provider context in the controls bar.
|
|
397
|
+
*/
|
|
398
|
+
setModelContext(options) {
|
|
399
|
+
const nextModel = options.model?.trim() || null;
|
|
400
|
+
const nextProvider = options.provider?.trim() || null;
|
|
401
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.modelLabel = nextModel;
|
|
405
|
+
this.providerLabel = nextProvider;
|
|
406
|
+
this.scheduleRender();
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Render the floating input area at contentRow.
|
|
410
|
+
*
|
|
411
|
+
* The chat box "floats" - it renders right below the last streamed content.
|
|
412
|
+
* As content is added, contentRow advances, and the chat box moves down.
|
|
413
|
+
* No scroll regions - pure floating behavior.
|
|
625
414
|
*/
|
|
626
415
|
render() {
|
|
627
416
|
if (!this.canRender())
|
|
628
417
|
return;
|
|
629
418
|
if (this.isRendering)
|
|
630
419
|
return;
|
|
420
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
421
|
+
// During streaming, throttle re-renders
|
|
422
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
423
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
424
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
425
|
+
if (waitMs > 0) {
|
|
426
|
+
this.renderDirty = true;
|
|
427
|
+
this.scheduleStreamingRender(waitMs);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
631
431
|
const shouldSkip = !this.renderDirty &&
|
|
632
432
|
this.buffer === this.lastRenderContent &&
|
|
633
433
|
this.cursor === this.lastRenderCursor;
|
|
634
434
|
this.renderDirty = false;
|
|
635
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
636
435
|
if (shouldSkip) {
|
|
637
436
|
return;
|
|
638
437
|
}
|
|
639
|
-
// If write lock is held, defer render
|
|
640
438
|
if (writeLock.isLocked()) {
|
|
641
439
|
writeLock.safeWrite(() => this.render());
|
|
642
440
|
return;
|
|
643
441
|
}
|
|
442
|
+
this.renderFloatingInputArea();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Core floating input area renderer.
|
|
446
|
+
* Chat box always floats at contentRow (below streamed content).
|
|
447
|
+
* This creates "persistent bottom floating" behavior.
|
|
448
|
+
*/
|
|
449
|
+
renderFloatingInputArea() {
|
|
450
|
+
const { rows, cols } = this.getSize();
|
|
451
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
452
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
453
|
+
// Wrap buffer into display lines
|
|
454
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
455
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
456
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
457
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
458
|
+
// Calculate display window (keep cursor visible)
|
|
459
|
+
let startLine = 0;
|
|
460
|
+
if (lines.length > displayLines) {
|
|
461
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
462
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
463
|
+
}
|
|
464
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
465
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
466
|
+
// Chat box height
|
|
467
|
+
const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
|
|
468
|
+
// Chat box floats at contentRow - right below the last content
|
|
469
|
+
// This is "persistent bottom floating" - always at bottom of content
|
|
470
|
+
const chatBoxStartRow = this.contentRow;
|
|
471
|
+
writeLock.lock('terminalInput.renderFloating');
|
|
644
472
|
this.isRendering = true;
|
|
645
|
-
writeLock.lock('terminalInput.render');
|
|
646
473
|
try {
|
|
647
|
-
|
|
474
|
+
// Hide cursor during render
|
|
475
|
+
this.write(ESC.HIDE);
|
|
476
|
+
this.write(ESC.RESET);
|
|
477
|
+
// Clear the chat box area
|
|
478
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
479
|
+
this.write(ESC.TO(chatBoxStartRow + i, 1));
|
|
480
|
+
this.write(ESC.CLEAR_LINE);
|
|
481
|
+
}
|
|
482
|
+
let currentRow = chatBoxStartRow;
|
|
483
|
+
// Meta/status header
|
|
484
|
+
for (const metaLine of metaLines) {
|
|
485
|
+
this.write(ESC.TO(currentRow, 1));
|
|
486
|
+
this.write(metaLine);
|
|
487
|
+
currentRow += 1;
|
|
488
|
+
}
|
|
489
|
+
// Separator line
|
|
490
|
+
this.write(ESC.TO(currentRow, 1));
|
|
491
|
+
this.write(renderDivider(cols - 2));
|
|
492
|
+
currentRow += 1;
|
|
493
|
+
// Render input lines
|
|
494
|
+
let finalRow = currentRow;
|
|
495
|
+
let finalCol = 3;
|
|
496
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
497
|
+
const rowNum = currentRow + i;
|
|
498
|
+
this.write(ESC.TO(rowNum, 1));
|
|
499
|
+
const line = visibleLines[i] ?? '';
|
|
500
|
+
const isFirstLine = (startLine + i) === 0;
|
|
501
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
502
|
+
this.write(ESC.BG_DARK);
|
|
503
|
+
this.write(ESC.DIM);
|
|
504
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
505
|
+
this.write(ESC.RESET);
|
|
506
|
+
this.write(ESC.BG_DARK);
|
|
507
|
+
if (isCursorLine) {
|
|
508
|
+
const col = Math.min(cursorCol, line.length);
|
|
509
|
+
const before = line.slice(0, col);
|
|
510
|
+
const at = col < line.length ? line[col] : ' ';
|
|
511
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
512
|
+
this.write(before);
|
|
513
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
514
|
+
this.write(at);
|
|
515
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
516
|
+
this.write(after);
|
|
517
|
+
finalRow = rowNum;
|
|
518
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
this.write(line);
|
|
522
|
+
}
|
|
523
|
+
// Pad to edge
|
|
524
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
525
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
526
|
+
if (padding > 0)
|
|
527
|
+
this.write(' '.repeat(padding));
|
|
528
|
+
this.write(ESC.RESET);
|
|
529
|
+
}
|
|
530
|
+
// Mode controls line
|
|
531
|
+
const controlRow = currentRow + visibleLines.length;
|
|
532
|
+
this.write(ESC.TO(controlRow, 1));
|
|
533
|
+
this.write(this.buildModeControls(cols));
|
|
534
|
+
// Position cursor in input box
|
|
535
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
536
|
+
this.write(ESC.SHOW);
|
|
537
|
+
// Update state
|
|
538
|
+
this.lastRenderContent = this.buffer;
|
|
539
|
+
this.lastRenderCursor = this.cursor;
|
|
540
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
541
|
+
if (this.streamingRenderTimer) {
|
|
542
|
+
clearTimeout(this.streamingRenderTimer);
|
|
543
|
+
this.streamingRenderTimer = null;
|
|
544
|
+
}
|
|
648
545
|
}
|
|
649
546
|
finally {
|
|
650
547
|
writeLock.unlock();
|
|
@@ -652,99 +549,217 @@ export class TerminalInput extends EventEmitter {
|
|
|
652
549
|
}
|
|
653
550
|
}
|
|
654
551
|
/**
|
|
655
|
-
* Build
|
|
656
|
-
*
|
|
552
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
553
|
+
* During streaming, shows model line pinned above streaming info.
|
|
657
554
|
*/
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
//
|
|
662
|
-
if (this.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
555
|
+
buildMetaLines(width) {
|
|
556
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
557
|
+
const lines = [];
|
|
558
|
+
// Model line should ALWAYS be shown (pinned above streaming content)
|
|
559
|
+
if (this.modelLabel) {
|
|
560
|
+
const modelText = this.providerLabel
|
|
561
|
+
? `model ${this.modelLabel} @ ${this.providerLabel}`
|
|
562
|
+
: `model ${this.modelLabel}`;
|
|
563
|
+
lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
|
|
564
|
+
}
|
|
565
|
+
// During streaming, add a compact status line with essential info
|
|
566
|
+
if (streamingActive) {
|
|
567
|
+
const parts = [];
|
|
568
|
+
// Essential streaming info
|
|
569
|
+
if (this.metaThinkingMs !== null) {
|
|
570
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
571
|
+
}
|
|
572
|
+
if (this.metaElapsedSeconds !== null) {
|
|
573
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
574
|
+
}
|
|
575
|
+
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
576
|
+
if (parts.length) {
|
|
577
|
+
lines.push(renderStatusLine(parts, width));
|
|
669
578
|
}
|
|
670
|
-
|
|
579
|
+
return lines;
|
|
671
580
|
}
|
|
672
|
-
//
|
|
673
|
-
if (this.
|
|
674
|
-
|
|
581
|
+
// Non-streaming: show full status info (model line already added above)
|
|
582
|
+
if (this.metaThinkingMs !== null) {
|
|
583
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
584
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
675
585
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
586
|
+
const statusParts = [];
|
|
587
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
588
|
+
if (statusLabel) {
|
|
589
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
680
590
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
591
|
+
if (this.metaElapsedSeconds !== null) {
|
|
592
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
684
593
|
}
|
|
685
|
-
|
|
686
|
-
if (
|
|
687
|
-
|
|
594
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
595
|
+
if (tokensRemaining !== null) {
|
|
596
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
688
597
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
598
|
+
if (statusParts.length) {
|
|
599
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
692
600
|
}
|
|
693
|
-
|
|
694
|
-
|
|
601
|
+
const usageParts = [];
|
|
602
|
+
if (this.metaTokensUsed !== null) {
|
|
603
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
604
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
605
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
695
606
|
}
|
|
696
|
-
|
|
697
|
-
|
|
607
|
+
if (this.contextUsage !== null) {
|
|
608
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
609
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
610
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
611
|
+
}
|
|
612
|
+
if (this.queue.length > 0) {
|
|
613
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
614
|
+
}
|
|
615
|
+
if (usageParts.length) {
|
|
616
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
617
|
+
}
|
|
618
|
+
return lines;
|
|
698
619
|
}
|
|
699
620
|
/**
|
|
700
|
-
* Build mode controls line
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
621
|
+
* Build Claude Code style mode controls line.
|
|
622
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
704
623
|
*/
|
|
705
624
|
buildModeControls(cols) {
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
712
|
-
if (this.editMode === 'display-edits') {
|
|
713
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
625
|
+
const width = Math.max(8, cols - 2);
|
|
626
|
+
const leftParts = [];
|
|
627
|
+
const rightParts = [];
|
|
628
|
+
if (this.streamingLabel) {
|
|
629
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
714
630
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
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
|
-
|
|
631
|
+
if (this.overrideStatusMessage) {
|
|
632
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
633
|
+
}
|
|
634
|
+
if (this.statusMessage) {
|
|
635
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
636
|
+
}
|
|
637
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
638
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
639
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
640
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
641
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
642
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
643
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
644
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
645
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
646
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
647
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
648
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
649
|
+
}
|
|
650
|
+
if (this.buffer.includes('\n')) {
|
|
651
|
+
const lineCount = this.buffer.split('\n').length;
|
|
652
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
653
|
+
}
|
|
654
|
+
if (this.pastePlaceholders.length > 0) {
|
|
655
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
656
|
+
leftParts.push({
|
|
657
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
658
|
+
tone: 'info',
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const contextRemaining = this.computeContextRemaining();
|
|
662
|
+
if (this.thinkingModeLabel) {
|
|
663
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
664
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
665
|
+
}
|
|
666
|
+
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
667
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
668
|
+
if (this.modelLabel && !streamingActive) {
|
|
669
|
+
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
670
|
+
rightParts.push({ text: modelText, tone: 'muted' });
|
|
671
|
+
}
|
|
672
|
+
if (contextRemaining !== null) {
|
|
673
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
674
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
675
|
+
? 'Context auto-compact imminent'
|
|
676
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
677
|
+
rightParts.push({ text: label, tone });
|
|
678
|
+
}
|
|
679
|
+
if (!rightParts.length || width < 60) {
|
|
680
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
681
|
+
return renderStatusLine(merged, width);
|
|
682
|
+
}
|
|
683
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
684
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
685
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
686
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
687
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
688
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
689
|
+
}
|
|
690
|
+
formatHotkey(hotkey) {
|
|
691
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
692
|
+
if (!normalized)
|
|
693
|
+
return hotkey;
|
|
694
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
695
|
+
const map = {
|
|
696
|
+
shift: '⇧',
|
|
697
|
+
sh: '⇧',
|
|
698
|
+
alt: '⌥',
|
|
699
|
+
option: '⌥',
|
|
700
|
+
opt: '⌥',
|
|
701
|
+
ctrl: '⌃',
|
|
702
|
+
control: '⌃',
|
|
703
|
+
cmd: '⌘',
|
|
704
|
+
meta: '⌘',
|
|
705
|
+
};
|
|
706
|
+
const formatted = parts
|
|
707
|
+
.map((part) => {
|
|
708
|
+
const symbol = map[part];
|
|
709
|
+
if (symbol)
|
|
710
|
+
return symbol;
|
|
711
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
712
|
+
})
|
|
713
|
+
.join('');
|
|
714
|
+
return formatted || hotkey;
|
|
715
|
+
}
|
|
716
|
+
computeContextRemaining() {
|
|
717
|
+
if (this.contextUsage === null) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
721
|
+
}
|
|
722
|
+
computeTokensRemaining() {
|
|
723
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
727
|
+
return this.formatTokenCount(remaining);
|
|
728
|
+
}
|
|
729
|
+
formatElapsedLabel(seconds) {
|
|
730
|
+
if (seconds < 60) {
|
|
731
|
+
return `${seconds}s`;
|
|
732
|
+
}
|
|
733
|
+
const mins = Math.floor(seconds / 60);
|
|
734
|
+
const secs = seconds % 60;
|
|
735
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
736
|
+
}
|
|
737
|
+
formatTokenCount(value) {
|
|
738
|
+
if (!Number.isFinite(value)) {
|
|
739
|
+
return `${value}`;
|
|
740
|
+
}
|
|
741
|
+
if (value >= 1_000_000) {
|
|
742
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
743
|
+
}
|
|
744
|
+
if (value >= 1_000) {
|
|
745
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
746
|
+
}
|
|
747
|
+
return `${Math.round(value)}`;
|
|
748
|
+
}
|
|
749
|
+
visibleLength(value) {
|
|
750
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
751
|
+
return value.replace(ansiPattern, '').length;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
755
|
+
* needing a TTY. Not used by production code.
|
|
756
|
+
*/
|
|
757
|
+
getDebugUiSnapshot(width) {
|
|
758
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
759
|
+
return {
|
|
760
|
+
meta: this.buildMetaLines(cols - 2),
|
|
761
|
+
controls: this.buildModeControls(cols),
|
|
762
|
+
};
|
|
748
763
|
}
|
|
749
764
|
/**
|
|
750
765
|
* Force a re-render
|
|
@@ -767,20 +782,65 @@ export class TerminalInput extends EventEmitter {
|
|
|
767
782
|
handleResize() {
|
|
768
783
|
this.lastRenderContent = '';
|
|
769
784
|
this.lastRenderCursor = -1;
|
|
785
|
+
this.resetStreamingRenderThrottle();
|
|
770
786
|
this.scheduleRender();
|
|
771
787
|
}
|
|
772
788
|
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
789
|
+
* Stream content above the floating chat box.
|
|
790
|
+
*
|
|
791
|
+
* This is the CLEAN method for streaming - no output interceptor needed.
|
|
792
|
+
* 1. Position cursor at contentRow
|
|
793
|
+
* 2. Write content directly (overwrites any chat box visually)
|
|
794
|
+
* 3. Advance contentRow by newlines
|
|
795
|
+
* 4. Re-render chat box at new position
|
|
796
|
+
*
|
|
797
|
+
* The chat box always re-renders after content, so it appears to "float" below.
|
|
775
798
|
*/
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
this.
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
799
|
+
streamContent(content) {
|
|
800
|
+
if (!content)
|
|
801
|
+
return;
|
|
802
|
+
// Position cursor at contentRow and write content
|
|
803
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
804
|
+
this.write(content);
|
|
805
|
+
// Advance contentRow by number of newlines
|
|
806
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
807
|
+
this.contentRow += newlines;
|
|
808
|
+
// Re-render chat box at new position (below the content just written)
|
|
809
|
+
this.forceRender();
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* @deprecated Use streamContent() instead
|
|
813
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
814
|
+
*/
|
|
815
|
+
registerOutputInterceptor(_display) {
|
|
816
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* @deprecated Use streamContent() instead
|
|
820
|
+
* Write content above the floating chat box.
|
|
821
|
+
*/
|
|
822
|
+
writeToScrollRegion(content) {
|
|
823
|
+
this.streamContent(content);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Reset content position to row 1.
|
|
827
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
828
|
+
*/
|
|
829
|
+
resetContentPosition() {
|
|
830
|
+
this.contentRow = 1;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Set the content row explicitly (used after banner is written).
|
|
834
|
+
* This tells the input where content should start flowing from.
|
|
835
|
+
*/
|
|
836
|
+
setContentRow(row) {
|
|
837
|
+
this.contentRow = Math.max(1, row);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Get the current content row position.
|
|
841
|
+
*/
|
|
842
|
+
getContentRow() {
|
|
843
|
+
return this.contentRow;
|
|
784
844
|
}
|
|
785
845
|
/**
|
|
786
846
|
* Dispose and clean up
|
|
@@ -788,18 +848,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
788
848
|
dispose() {
|
|
789
849
|
if (this.disposed)
|
|
790
850
|
return;
|
|
791
|
-
// Clean up streaming render timer
|
|
792
|
-
if (this.streamingRenderTimer) {
|
|
793
|
-
clearInterval(this.streamingRenderTimer);
|
|
794
|
-
this.streamingRenderTimer = null;
|
|
795
|
-
}
|
|
796
|
-
// Clean up output interceptor
|
|
797
|
-
if (this.outputInterceptorCleanup) {
|
|
798
|
-
this.outputInterceptorCleanup();
|
|
799
|
-
this.outputInterceptorCleanup = undefined;
|
|
800
|
-
}
|
|
801
851
|
this.disposed = true;
|
|
802
852
|
this.enabled = false;
|
|
853
|
+
this.resetStreamingRenderThrottle();
|
|
803
854
|
this.disableBracketedPaste();
|
|
804
855
|
this.buffer = '';
|
|
805
856
|
this.queue = [];
|
|
@@ -904,22 +955,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
904
955
|
this.toggleEditMode();
|
|
905
956
|
return true;
|
|
906
957
|
}
|
|
907
|
-
|
|
908
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
909
|
-
this.togglePasteExpansion();
|
|
910
|
-
}
|
|
911
|
-
else {
|
|
912
|
-
this.toggleThinking();
|
|
913
|
-
}
|
|
914
|
-
return true;
|
|
915
|
-
case 'escape':
|
|
916
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
917
|
-
if (this.mode === 'streaming') {
|
|
918
|
-
this.emit('interrupt');
|
|
919
|
-
}
|
|
920
|
-
else if (this.buffer.length > 0) {
|
|
921
|
-
this.clear();
|
|
922
|
-
}
|
|
958
|
+
this.insertText(' ');
|
|
923
959
|
return true;
|
|
924
960
|
}
|
|
925
961
|
return false;
|
|
@@ -937,7 +973,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
937
973
|
this.insertPlainText(chunk, insertPos);
|
|
938
974
|
this.cursor = insertPos + chunk.length;
|
|
939
975
|
this.emit('change', this.buffer);
|
|
940
|
-
this.updateSuggestions();
|
|
941
976
|
this.scheduleRender();
|
|
942
977
|
}
|
|
943
978
|
insertNewline() {
|
|
@@ -962,7 +997,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
962
997
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
963
998
|
}
|
|
964
999
|
this.emit('change', this.buffer);
|
|
965
|
-
this.updateSuggestions();
|
|
966
1000
|
this.scheduleRender();
|
|
967
1001
|
}
|
|
968
1002
|
deleteForward() {
|
|
@@ -1190,13 +1224,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1190
1224
|
timestamp: Date.now(),
|
|
1191
1225
|
});
|
|
1192
1226
|
this.emit('queue', text);
|
|
1193
|
-
this.clear(); // Clear immediately for queued input
|
|
1227
|
+
this.clear(); // Clear immediately for queued input
|
|
1194
1228
|
}
|
|
1195
1229
|
else {
|
|
1196
|
-
// In idle mode, clear the input
|
|
1197
|
-
// The
|
|
1198
|
-
|
|
1199
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1230
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1231
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1232
|
+
this.clear();
|
|
1200
1233
|
this.emit('submit', text);
|
|
1201
1234
|
}
|
|
1202
1235
|
}
|
|
@@ -1213,7 +1246,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1213
1246
|
if (available <= 0)
|
|
1214
1247
|
return;
|
|
1215
1248
|
const chunk = clean.slice(0, available);
|
|
1216
|
-
|
|
1249
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1250
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1251
|
+
if (isMultiline && !isShortMultiline) {
|
|
1217
1252
|
this.insertPastePlaceholder(chunk);
|
|
1218
1253
|
}
|
|
1219
1254
|
else {
|
|
@@ -1349,17 +1384,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1349
1384
|
this.shiftPlaceholders(position, text.length);
|
|
1350
1385
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1351
1386
|
}
|
|
1387
|
+
shouldInlineMultiline(content) {
|
|
1388
|
+
const lines = content.split('\n').length;
|
|
1389
|
+
const maxInlineLines = 4;
|
|
1390
|
+
const maxInlineChars = 240;
|
|
1391
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1392
|
+
}
|
|
1352
1393
|
findPlaceholderAt(position) {
|
|
1353
1394
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1354
1395
|
}
|
|
1355
|
-
buildPlaceholder(
|
|
1396
|
+
buildPlaceholder(lineCount) {
|
|
1356
1397
|
const id = ++this.pasteCounter;
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
const preview = summary.preview.length > 30
|
|
1360
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1361
|
-
: summary.preview;
|
|
1362
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1398
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1399
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1363
1400
|
return { id, placeholder };
|
|
1364
1401
|
}
|
|
1365
1402
|
insertPastePlaceholder(content) {
|
|
@@ -1367,67 +1404,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1367
1404
|
if (available <= 0)
|
|
1368
1405
|
return;
|
|
1369
1406
|
const cleanContent = content.slice(0, available);
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
if (summary.lineCount < 5) {
|
|
1373
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1374
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1375
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1376
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1407
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1408
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1380
1409
|
const insertPos = this.cursor;
|
|
1381
1410
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1382
1411
|
this.pastePlaceholders.push({
|
|
1383
1412
|
id,
|
|
1384
1413
|
content: cleanContent,
|
|
1385
|
-
lineCount
|
|
1414
|
+
lineCount,
|
|
1386
1415
|
placeholder,
|
|
1387
1416
|
start: insertPos,
|
|
1388
1417
|
end: insertPos + placeholder.length,
|
|
1389
|
-
summary,
|
|
1390
|
-
expanded: false,
|
|
1391
1418
|
});
|
|
1392
1419
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1393
1420
|
this.cursor = insertPos + placeholder.length;
|
|
1394
1421
|
}
|
|
1395
|
-
/**
|
|
1396
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1397
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1398
|
-
*/
|
|
1399
|
-
togglePasteExpansion() {
|
|
1400
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1401
|
-
if (!placeholder)
|
|
1402
|
-
return false;
|
|
1403
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1404
|
-
// Update the placeholder text in buffer
|
|
1405
|
-
const newPlaceholder = placeholder.expanded
|
|
1406
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1407
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1408
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1409
|
-
// Update buffer
|
|
1410
|
-
this.buffer =
|
|
1411
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1412
|
-
newPlaceholder +
|
|
1413
|
-
this.buffer.slice(placeholder.end);
|
|
1414
|
-
// Update placeholder tracking
|
|
1415
|
-
placeholder.placeholder = newPlaceholder;
|
|
1416
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1417
|
-
// Shift other placeholders
|
|
1418
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1419
|
-
this.scheduleRender();
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
buildExpandedPlaceholder(ph) {
|
|
1423
|
-
const lines = ph.content.split('\n');
|
|
1424
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1425
|
-
const lastLines = lines.length > 5
|
|
1426
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1427
|
-
: '';
|
|
1428
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1429
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1430
|
-
}
|
|
1431
1422
|
deletePlaceholder(placeholder) {
|
|
1432
1423
|
const length = placeholder.end - placeholder.start;
|
|
1433
1424
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1435,7 +1426,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1435
1426
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1436
1427
|
this.cursor = placeholder.start;
|
|
1437
1428
|
}
|
|
1438
|
-
updateContextUsage(value) {
|
|
1429
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1430
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1431
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1432
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1433
|
+
}
|
|
1439
1434
|
if (value === null || !Number.isFinite(value)) {
|
|
1440
1435
|
this.contextUsage = null;
|
|
1441
1436
|
}
|
|
@@ -1462,6 +1457,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1462
1457
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1463
1458
|
this.setEditMode(next);
|
|
1464
1459
|
}
|
|
1460
|
+
scheduleStreamingRender(delayMs) {
|
|
1461
|
+
if (this.streamingRenderTimer)
|
|
1462
|
+
return;
|
|
1463
|
+
const wait = Math.max(16, delayMs);
|
|
1464
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1465
|
+
this.streamingRenderTimer = null;
|
|
1466
|
+
this.render();
|
|
1467
|
+
}, wait);
|
|
1468
|
+
}
|
|
1469
|
+
resetStreamingRenderThrottle() {
|
|
1470
|
+
if (this.streamingRenderTimer) {
|
|
1471
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1472
|
+
this.streamingRenderTimer = null;
|
|
1473
|
+
}
|
|
1474
|
+
this.lastStreamingRender = 0;
|
|
1475
|
+
}
|
|
1465
1476
|
scheduleRender() {
|
|
1466
1477
|
if (!this.canRender())
|
|
1467
1478
|
return;
|