erosolar-cli 1.7.300 → 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 -107
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +501 -484
- 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,282 +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. Render floating input area below banner
|
|
213
|
-
*/
|
|
214
|
-
initializeUnifiedUI() {
|
|
215
|
-
if (this.unifiedUIInitialized) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
// Hide cursor during setup
|
|
219
|
-
this.write(ESC.HIDE);
|
|
220
|
-
// Clear screen and go home
|
|
221
|
-
this.write(ESC.HOME);
|
|
222
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
223
|
-
// Write banner at top
|
|
224
|
-
if (this.bannerContent) {
|
|
225
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
226
|
-
}
|
|
227
|
-
// Mark initialized
|
|
228
|
-
this.unifiedUIInitialized = true;
|
|
229
|
-
// Render floating input area
|
|
230
|
-
this.renderFloatingInputArea();
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Clear the input area at its tracked position.
|
|
234
|
-
* Returns true if something was cleared.
|
|
235
|
-
*/
|
|
236
|
-
clearInputArea() {
|
|
237
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
238
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
239
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
240
|
-
this.write(ESC.CLEAR_LINE);
|
|
241
|
-
}
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Reset input area tracking state.
|
|
248
|
-
*/
|
|
249
|
-
resetInputAreaTracking() {
|
|
250
|
-
this.inputAreaStartRow = 0;
|
|
251
|
-
this.flowModeRenderedLines = 0;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Render floating input area below current content.
|
|
255
|
-
* This is "bottom floating" - follows content, not pinned to terminal bottom.
|
|
256
|
-
* Uses absolute positioning to prevent duplicates.
|
|
257
|
-
*/
|
|
258
|
-
renderFloatingInputArea() {
|
|
259
|
-
const { rows, cols } = this.getSize();
|
|
260
|
-
const divider = '─'.repeat(cols - 1);
|
|
261
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
262
|
-
// Calculate lines needed for input area
|
|
263
|
-
const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
|
|
264
|
-
// FIRST: Clear any previously rendered input area
|
|
265
|
-
this.clearInputArea();
|
|
266
|
-
// Hide cursor during render
|
|
267
|
-
this.write(ESC.HIDE);
|
|
268
|
-
// Calculate where to render: after current content
|
|
269
|
-
// Use contentEndRow if set, otherwise estimate from terminal bottom
|
|
270
|
-
let startRow;
|
|
271
|
-
if (this.contentEndRow > 0) {
|
|
272
|
-
startRow = this.contentEndRow + 1;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
// Render near bottom, leaving space for input area
|
|
276
|
-
startRow = Math.max(1, rows - linesNeeded);
|
|
277
|
-
}
|
|
278
|
-
// Ensure we don't go past terminal bounds
|
|
279
|
-
startRow = Math.min(startRow, rows - linesNeeded + 1);
|
|
280
|
-
startRow = Math.max(1, startRow);
|
|
281
|
-
// Track this position
|
|
282
|
-
this.inputAreaStartRow = startRow;
|
|
283
|
-
let currentRow = startRow;
|
|
284
|
-
// Status bar
|
|
285
|
-
this.write(ESC.TO(currentRow, 1));
|
|
286
|
-
this.write(this.buildStatusBar(cols));
|
|
287
|
-
currentRow++;
|
|
288
|
-
// Model info line (if set)
|
|
289
|
-
if (this.modelInfo) {
|
|
290
|
-
this.write(ESC.TO(currentRow, 1));
|
|
291
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
292
|
-
if (this.contextUsage !== null) {
|
|
293
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
294
|
-
if (rem < 10)
|
|
295
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
296
|
-
else if (rem < 25)
|
|
297
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
298
|
-
else
|
|
299
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
300
|
-
}
|
|
301
|
-
this.write(modelLine);
|
|
302
|
-
currentRow++;
|
|
303
|
-
}
|
|
304
|
-
// Top divider
|
|
305
|
-
this.write(ESC.TO(currentRow, 1));
|
|
306
|
-
this.write(divider);
|
|
307
|
-
currentRow++;
|
|
308
|
-
// Input line with prompt and buffer content
|
|
309
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
310
|
-
const displayLine = lines[0] ?? '';
|
|
311
|
-
const inputRow = currentRow;
|
|
312
|
-
this.write(ESC.TO(currentRow, 1));
|
|
313
|
-
this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
314
|
-
this.write(ESC.BG_DARK + displayLine);
|
|
315
|
-
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
316
|
-
if (padding > 0)
|
|
317
|
-
this.write(' '.repeat(padding));
|
|
318
|
-
this.write(ESC.RESET);
|
|
319
|
-
currentRow++;
|
|
320
|
-
// Bottom divider
|
|
321
|
-
this.write(ESC.TO(currentRow, 1));
|
|
322
|
-
this.write(divider);
|
|
323
|
-
currentRow++;
|
|
324
|
-
// Mode controls
|
|
325
|
-
this.write(ESC.TO(currentRow, 1));
|
|
326
|
-
this.write(this.buildModeControls(cols));
|
|
327
|
-
// Track lines rendered
|
|
328
|
-
this.flowModeRenderedLines = currentRow - startRow + 1;
|
|
329
|
-
// Position cursor in input line for typing
|
|
330
|
-
this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
|
|
331
|
-
// Show cursor
|
|
332
|
-
this.write(ESC.SHOW);
|
|
333
|
-
// Update tracking
|
|
334
|
-
this.lastRenderContent = this.buffer;
|
|
335
|
-
this.lastRenderCursor = this.cursor;
|
|
336
|
-
}
|
|
337
193
|
/**
|
|
338
194
|
* Set the input mode
|
|
339
195
|
*
|
|
340
|
-
*
|
|
196
|
+
* Content flows naturally - no scroll region pinning.
|
|
341
197
|
*/
|
|
342
198
|
setMode(mode) {
|
|
343
199
|
const prevMode = this.mode;
|
|
344
200
|
this.mode = mode;
|
|
345
201
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
346
|
-
|
|
347
|
-
this.streamingStartTime = Date.now();
|
|
348
|
-
// Ensure unified UI is initialized
|
|
349
|
-
if (!this.unifiedUIInitialized) {
|
|
350
|
-
this.initializeUnifiedUI();
|
|
351
|
-
}
|
|
202
|
+
this.resetStreamingRenderThrottle();
|
|
352
203
|
this.renderDirty = true;
|
|
353
|
-
this.
|
|
204
|
+
this.render();
|
|
354
205
|
}
|
|
355
206
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.streamingRenderTimer = null;
|
|
360
|
-
}
|
|
361
|
-
// Reset streaming time
|
|
362
|
-
this.streamingStartTime = null;
|
|
363
|
-
// Re-render floating input area
|
|
364
|
-
this.renderDirty = true;
|
|
365
|
-
this.scheduleRender();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Set the row where content ends (for idle mode positioning).
|
|
370
|
-
* Input area will render starting from this row + 1.
|
|
371
|
-
*/
|
|
372
|
-
setContentEndRow(row) {
|
|
373
|
-
this.contentEndRow = Math.max(0, row);
|
|
374
|
-
this.renderDirty = true;
|
|
375
|
-
this.scheduleRender();
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Set available slash commands for auto-complete suggestions.
|
|
379
|
-
*/
|
|
380
|
-
setCommands(commands) {
|
|
381
|
-
this.commandSuggestions = commands;
|
|
382
|
-
this.updateSuggestions();
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Update filtered suggestions based on current input.
|
|
386
|
-
*/
|
|
387
|
-
updateSuggestions() {
|
|
388
|
-
const input = this.buffer.trim();
|
|
389
|
-
// Only show suggestions when input starts with "/"
|
|
390
|
-
if (!input.startsWith('/')) {
|
|
391
|
-
this.showSuggestions = false;
|
|
392
|
-
this.filteredSuggestions = [];
|
|
393
|
-
this.selectedSuggestionIndex = 0;
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const query = input.toLowerCase();
|
|
397
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
398
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
399
|
-
// Show suggestions if we have matches
|
|
400
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
401
|
-
// Keep selection in bounds
|
|
402
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
403
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
207
|
+
// Streaming ended - render the input area
|
|
208
|
+
this.resetStreamingRenderThrottle();
|
|
209
|
+
this.forceRender();
|
|
404
210
|
}
|
|
405
211
|
}
|
|
406
212
|
/**
|
|
407
|
-
*
|
|
213
|
+
* Legacy method - no longer used (content flows naturally).
|
|
214
|
+
* @deprecated Use setContentRow instead
|
|
408
215
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return;
|
|
412
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
413
|
-
this.renderDirty = true;
|
|
414
|
-
this.scheduleRender();
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
418
|
-
*/
|
|
419
|
-
selectPrevSuggestion() {
|
|
420
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
421
|
-
return;
|
|
422
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
423
|
-
? this.filteredSuggestions.length - 1
|
|
424
|
-
: this.selectedSuggestionIndex - 1;
|
|
425
|
-
this.renderDirty = true;
|
|
426
|
-
this.scheduleRender();
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Accept current suggestion and insert into buffer.
|
|
430
|
-
*/
|
|
431
|
-
acceptSuggestion() {
|
|
432
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
433
|
-
return false;
|
|
434
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
435
|
-
if (!selected)
|
|
436
|
-
return false;
|
|
437
|
-
// Replace buffer with selected command
|
|
438
|
-
this.buffer = selected.command + ' ';
|
|
439
|
-
this.cursor = this.buffer.length;
|
|
440
|
-
this.showSuggestions = false;
|
|
441
|
-
this.renderDirty = true;
|
|
442
|
-
this.scheduleRender();
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Check if suggestions are visible.
|
|
447
|
-
*/
|
|
448
|
-
areSuggestionsVisible() {
|
|
449
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Toggle thinking/reasoning mode
|
|
453
|
-
*/
|
|
454
|
-
toggleThinking() {
|
|
455
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
456
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
457
|
-
this.scheduleRender();
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Get thinking enabled state
|
|
461
|
-
*/
|
|
462
|
-
isThinkingEnabled() {
|
|
463
|
-
return this.thinkingEnabled;
|
|
216
|
+
setPinnedHeaderLines(_count) {
|
|
217
|
+
// No-op: scroll region pinning removed
|
|
464
218
|
}
|
|
465
219
|
/**
|
|
466
220
|
* Get current mode
|
|
@@ -493,17 +247,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
493
247
|
}
|
|
494
248
|
/**
|
|
495
249
|
* Clear the buffer
|
|
496
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
497
250
|
*/
|
|
498
|
-
clear(
|
|
251
|
+
clear() {
|
|
499
252
|
this.buffer = '';
|
|
500
253
|
this.cursor = 0;
|
|
501
254
|
this.historyIndex = -1;
|
|
502
255
|
this.tempInput = '';
|
|
503
256
|
this.pastePlaceholders = [];
|
|
504
|
-
|
|
505
|
-
this.scheduleRender();
|
|
506
|
-
}
|
|
257
|
+
this.scheduleRender();
|
|
507
258
|
}
|
|
508
259
|
/**
|
|
509
260
|
* Get queued inputs
|
|
@@ -574,6 +325,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
574
325
|
this.streamingLabel = next;
|
|
575
326
|
this.scheduleRender();
|
|
576
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
|
+
}
|
|
577
359
|
/**
|
|
578
360
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
579
361
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -583,26 +365,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
583
365
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
584
366
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
585
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);
|
|
586
370
|
if (this.verificationEnabled === nextVerification &&
|
|
587
371
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
588
372
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
589
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
373
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
374
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
375
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
590
376
|
return;
|
|
591
377
|
}
|
|
592
378
|
this.verificationEnabled = nextVerification;
|
|
593
379
|
this.autoContinueEnabled = nextAutoContinue;
|
|
594
380
|
this.verificationHotkey = nextVerifyHotkey;
|
|
595
381
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
596
|
-
this.
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
600
|
-
* This is displayed persistently above the input area.
|
|
601
|
-
*/
|
|
602
|
-
setModelInfo(info) {
|
|
603
|
-
if (this.modelInfo === info)
|
|
604
|
-
return;
|
|
605
|
-
this.modelInfo = info;
|
|
382
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
383
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
606
384
|
this.scheduleRender();
|
|
607
385
|
}
|
|
608
386
|
/**
|
|
@@ -615,30 +393,155 @@ export class TerminalInput extends EventEmitter {
|
|
|
615
393
|
this.scheduleRender();
|
|
616
394
|
}
|
|
617
395
|
/**
|
|
618
|
-
*
|
|
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.
|
|
619
414
|
*/
|
|
620
415
|
render() {
|
|
621
416
|
if (!this.canRender())
|
|
622
417
|
return;
|
|
623
418
|
if (this.isRendering)
|
|
624
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
|
+
}
|
|
625
431
|
const shouldSkip = !this.renderDirty &&
|
|
626
432
|
this.buffer === this.lastRenderContent &&
|
|
627
433
|
this.cursor === this.lastRenderCursor;
|
|
628
434
|
this.renderDirty = false;
|
|
629
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
630
435
|
if (shouldSkip) {
|
|
631
436
|
return;
|
|
632
437
|
}
|
|
633
|
-
// If write lock is held, defer render
|
|
634
438
|
if (writeLock.isLocked()) {
|
|
635
439
|
writeLock.safeWrite(() => this.render());
|
|
636
440
|
return;
|
|
637
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');
|
|
638
472
|
this.isRendering = true;
|
|
639
|
-
writeLock.lock('terminalInput.render');
|
|
640
473
|
try {
|
|
641
|
-
|
|
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
|
+
}
|
|
642
545
|
}
|
|
643
546
|
finally {
|
|
644
547
|
writeLock.unlock();
|
|
@@ -646,99 +549,217 @@ export class TerminalInput extends EventEmitter {
|
|
|
646
549
|
}
|
|
647
550
|
}
|
|
648
551
|
/**
|
|
649
|
-
* Build
|
|
650
|
-
*
|
|
552
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
553
|
+
* During streaming, shows model line pinned above streaming info.
|
|
651
554
|
*/
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
//
|
|
656
|
-
if (this.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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));
|
|
663
578
|
}
|
|
664
|
-
|
|
579
|
+
return lines;
|
|
665
580
|
}
|
|
666
|
-
//
|
|
667
|
-
if (this.
|
|
668
|
-
|
|
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));
|
|
669
585
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
586
|
+
const statusParts = [];
|
|
587
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
588
|
+
if (statusLabel) {
|
|
589
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
674
590
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
591
|
+
if (this.metaElapsedSeconds !== null) {
|
|
592
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
678
593
|
}
|
|
679
|
-
|
|
680
|
-
if (
|
|
681
|
-
|
|
594
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
595
|
+
if (tokensRemaining !== null) {
|
|
596
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
682
597
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
598
|
+
if (statusParts.length) {
|
|
599
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
686
600
|
}
|
|
687
|
-
|
|
688
|
-
|
|
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' });
|
|
689
606
|
}
|
|
690
|
-
|
|
691
|
-
|
|
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;
|
|
692
619
|
}
|
|
693
620
|
/**
|
|
694
|
-
* Build mode controls line
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
* 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.
|
|
698
623
|
*/
|
|
699
624
|
buildModeControls(cols) {
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
706
|
-
if (this.editMode === 'display-edits') {
|
|
707
|
-
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' });
|
|
708
630
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
742
763
|
}
|
|
743
764
|
/**
|
|
744
765
|
* Force a re-render
|
|
@@ -761,20 +782,65 @@ export class TerminalInput extends EventEmitter {
|
|
|
761
782
|
handleResize() {
|
|
762
783
|
this.lastRenderContent = '';
|
|
763
784
|
this.lastRenderCursor = -1;
|
|
785
|
+
this.resetStreamingRenderThrottle();
|
|
764
786
|
this.scheduleRender();
|
|
765
787
|
}
|
|
766
788
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
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.
|
|
769
798
|
*/
|
|
770
|
-
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
this.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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;
|
|
778
844
|
}
|
|
779
845
|
/**
|
|
780
846
|
* Dispose and clean up
|
|
@@ -782,18 +848,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
782
848
|
dispose() {
|
|
783
849
|
if (this.disposed)
|
|
784
850
|
return;
|
|
785
|
-
// Clean up streaming render timer
|
|
786
|
-
if (this.streamingRenderTimer) {
|
|
787
|
-
clearInterval(this.streamingRenderTimer);
|
|
788
|
-
this.streamingRenderTimer = null;
|
|
789
|
-
}
|
|
790
|
-
// Clean up output interceptor
|
|
791
|
-
if (this.outputInterceptorCleanup) {
|
|
792
|
-
this.outputInterceptorCleanup();
|
|
793
|
-
this.outputInterceptorCleanup = undefined;
|
|
794
|
-
}
|
|
795
851
|
this.disposed = true;
|
|
796
852
|
this.enabled = false;
|
|
853
|
+
this.resetStreamingRenderThrottle();
|
|
797
854
|
this.disableBracketedPaste();
|
|
798
855
|
this.buffer = '';
|
|
799
856
|
this.queue = [];
|
|
@@ -898,22 +955,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
898
955
|
this.toggleEditMode();
|
|
899
956
|
return true;
|
|
900
957
|
}
|
|
901
|
-
|
|
902
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
903
|
-
this.togglePasteExpansion();
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
this.toggleThinking();
|
|
907
|
-
}
|
|
908
|
-
return true;
|
|
909
|
-
case 'escape':
|
|
910
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
911
|
-
if (this.mode === 'streaming') {
|
|
912
|
-
this.emit('interrupt');
|
|
913
|
-
}
|
|
914
|
-
else if (this.buffer.length > 0) {
|
|
915
|
-
this.clear();
|
|
916
|
-
}
|
|
958
|
+
this.insertText(' ');
|
|
917
959
|
return true;
|
|
918
960
|
}
|
|
919
961
|
return false;
|
|
@@ -931,7 +973,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
931
973
|
this.insertPlainText(chunk, insertPos);
|
|
932
974
|
this.cursor = insertPos + chunk.length;
|
|
933
975
|
this.emit('change', this.buffer);
|
|
934
|
-
this.updateSuggestions();
|
|
935
976
|
this.scheduleRender();
|
|
936
977
|
}
|
|
937
978
|
insertNewline() {
|
|
@@ -956,7 +997,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
956
997
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
957
998
|
}
|
|
958
999
|
this.emit('change', this.buffer);
|
|
959
|
-
this.updateSuggestions();
|
|
960
1000
|
this.scheduleRender();
|
|
961
1001
|
}
|
|
962
1002
|
deleteForward() {
|
|
@@ -1184,13 +1224,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1184
1224
|
timestamp: Date.now(),
|
|
1185
1225
|
});
|
|
1186
1226
|
this.emit('queue', text);
|
|
1187
|
-
this.clear(); // Clear immediately for queued input
|
|
1227
|
+
this.clear(); // Clear immediately for queued input
|
|
1188
1228
|
}
|
|
1189
1229
|
else {
|
|
1190
|
-
// In idle mode, clear the input
|
|
1191
|
-
// The
|
|
1192
|
-
|
|
1193
|
-
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();
|
|
1194
1233
|
this.emit('submit', text);
|
|
1195
1234
|
}
|
|
1196
1235
|
}
|
|
@@ -1207,7 +1246,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1207
1246
|
if (available <= 0)
|
|
1208
1247
|
return;
|
|
1209
1248
|
const chunk = clean.slice(0, available);
|
|
1210
|
-
|
|
1249
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1250
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1251
|
+
if (isMultiline && !isShortMultiline) {
|
|
1211
1252
|
this.insertPastePlaceholder(chunk);
|
|
1212
1253
|
}
|
|
1213
1254
|
else {
|
|
@@ -1343,17 +1384,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1343
1384
|
this.shiftPlaceholders(position, text.length);
|
|
1344
1385
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1345
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
|
+
}
|
|
1346
1393
|
findPlaceholderAt(position) {
|
|
1347
1394
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1348
1395
|
}
|
|
1349
|
-
buildPlaceholder(
|
|
1396
|
+
buildPlaceholder(lineCount) {
|
|
1350
1397
|
const id = ++this.pasteCounter;
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
const preview = summary.preview.length > 30
|
|
1354
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1355
|
-
: summary.preview;
|
|
1356
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1398
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1399
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1357
1400
|
return { id, placeholder };
|
|
1358
1401
|
}
|
|
1359
1402
|
insertPastePlaceholder(content) {
|
|
@@ -1361,67 +1404,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1361
1404
|
if (available <= 0)
|
|
1362
1405
|
return;
|
|
1363
1406
|
const cleanContent = content.slice(0, available);
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
if (summary.lineCount < 5) {
|
|
1367
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1368
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1369
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1370
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1407
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1408
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1374
1409
|
const insertPos = this.cursor;
|
|
1375
1410
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1376
1411
|
this.pastePlaceholders.push({
|
|
1377
1412
|
id,
|
|
1378
1413
|
content: cleanContent,
|
|
1379
|
-
lineCount
|
|
1414
|
+
lineCount,
|
|
1380
1415
|
placeholder,
|
|
1381
1416
|
start: insertPos,
|
|
1382
1417
|
end: insertPos + placeholder.length,
|
|
1383
|
-
summary,
|
|
1384
|
-
expanded: false,
|
|
1385
1418
|
});
|
|
1386
1419
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1387
1420
|
this.cursor = insertPos + placeholder.length;
|
|
1388
1421
|
}
|
|
1389
|
-
/**
|
|
1390
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1391
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1392
|
-
*/
|
|
1393
|
-
togglePasteExpansion() {
|
|
1394
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1395
|
-
if (!placeholder)
|
|
1396
|
-
return false;
|
|
1397
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1398
|
-
// Update the placeholder text in buffer
|
|
1399
|
-
const newPlaceholder = placeholder.expanded
|
|
1400
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1401
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1402
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1403
|
-
// Update buffer
|
|
1404
|
-
this.buffer =
|
|
1405
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1406
|
-
newPlaceholder +
|
|
1407
|
-
this.buffer.slice(placeholder.end);
|
|
1408
|
-
// Update placeholder tracking
|
|
1409
|
-
placeholder.placeholder = newPlaceholder;
|
|
1410
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1411
|
-
// Shift other placeholders
|
|
1412
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1413
|
-
this.scheduleRender();
|
|
1414
|
-
return true;
|
|
1415
|
-
}
|
|
1416
|
-
buildExpandedPlaceholder(ph) {
|
|
1417
|
-
const lines = ph.content.split('\n');
|
|
1418
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1419
|
-
const lastLines = lines.length > 5
|
|
1420
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1421
|
-
: '';
|
|
1422
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1423
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1424
|
-
}
|
|
1425
1422
|
deletePlaceholder(placeholder) {
|
|
1426
1423
|
const length = placeholder.end - placeholder.start;
|
|
1427
1424
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1429,7 +1426,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1429
1426
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1430
1427
|
this.cursor = placeholder.start;
|
|
1431
1428
|
}
|
|
1432
|
-
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
|
+
}
|
|
1433
1434
|
if (value === null || !Number.isFinite(value)) {
|
|
1434
1435
|
this.contextUsage = null;
|
|
1435
1436
|
}
|
|
@@ -1456,6 +1457,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1456
1457
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1457
1458
|
this.setEditMode(next);
|
|
1458
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
|
+
}
|
|
1459
1476
|
scheduleRender() {
|
|
1460
1477
|
if (!this.canRender())
|
|
1461
1478
|
return;
|