erosolar-cli 1.7.309 → 1.7.310
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 +126 -113
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +553 -521
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +56 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +66 -30
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/agentConfig.d.ts +27 -0
- package/dist/subagents/agentConfig.d.ts.map +1 -0
- package/dist/subagents/agentConfig.js +89 -0
- package/dist/subagents/agentConfig.js.map +1 -0
- package/dist/subagents/agentRegistry.d.ts +33 -0
- package/dist/subagents/agentRegistry.d.ts.map +1 -0
- package/dist/subagents/agentRegistry.js +162 -0
- package/dist/subagents/agentRegistry.js.map +1 -0
- package/dist/subagents/taskRunner.d.ts +7 -1
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +180 -47
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +13 -12
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +24 -45
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +140 -259
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +6 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +0 -158
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +0 -348
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +1 -0
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +15 -25
- package/dist/ui/unified/layout.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +10 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +78 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/package.json +4 -4
- package/dist/alpha-zero/agentWrapper.d.ts +0 -84
- package/dist/alpha-zero/agentWrapper.d.ts.map +0 -1
- package/dist/alpha-zero/agentWrapper.js +0 -171
- package/dist/alpha-zero/agentWrapper.js.map +0 -1
- package/dist/alpha-zero/codeEvaluator.d.ts +0 -25
- package/dist/alpha-zero/codeEvaluator.d.ts.map +0 -1
- package/dist/alpha-zero/codeEvaluator.js +0 -273
- package/dist/alpha-zero/codeEvaluator.js.map +0 -1
- package/dist/alpha-zero/competitiveRunner.d.ts +0 -66
- package/dist/alpha-zero/competitiveRunner.d.ts.map +0 -1
- package/dist/alpha-zero/competitiveRunner.js +0 -224
- package/dist/alpha-zero/competitiveRunner.js.map +0 -1
- package/dist/alpha-zero/index.d.ts +0 -67
- package/dist/alpha-zero/index.d.ts.map +0 -1
- package/dist/alpha-zero/index.js +0 -99
- package/dist/alpha-zero/index.js.map +0 -1
- package/dist/alpha-zero/introspection.d.ts +0 -128
- package/dist/alpha-zero/introspection.d.ts.map +0 -1
- package/dist/alpha-zero/introspection.js +0 -300
- package/dist/alpha-zero/introspection.js.map +0 -1
- package/dist/alpha-zero/metricsTracker.d.ts +0 -71
- package/dist/alpha-zero/metricsTracker.d.ts.map +0 -1
- package/dist/alpha-zero/metricsTracker.js.map +0 -1
- package/dist/alpha-zero/security/core.d.ts +0 -125
- package/dist/alpha-zero/security/core.d.ts.map +0 -1
- package/dist/alpha-zero/security/core.js +0 -271
- package/dist/alpha-zero/security/core.js.map +0 -1
- package/dist/alpha-zero/security/google.d.ts +0 -125
- package/dist/alpha-zero/security/google.d.ts.map +0 -1
- package/dist/alpha-zero/security/google.js +0 -311
- package/dist/alpha-zero/security/google.js.map +0 -1
- package/dist/alpha-zero/security/googleLoader.d.ts +0 -17
- package/dist/alpha-zero/security/googleLoader.d.ts.map +0 -1
- package/dist/alpha-zero/security/googleLoader.js +0 -41
- package/dist/alpha-zero/security/googleLoader.js.map +0 -1
- package/dist/alpha-zero/security/index.d.ts +0 -29
- package/dist/alpha-zero/security/index.d.ts.map +0 -1
- package/dist/alpha-zero/security/index.js +0 -32
- package/dist/alpha-zero/security/index.js.map +0 -1
- package/dist/alpha-zero/security/simulation.d.ts +0 -124
- package/dist/alpha-zero/security/simulation.d.ts.map +0 -1
- package/dist/alpha-zero/security/simulation.js +0 -277
- package/dist/alpha-zero/security/simulation.js.map +0 -1
- package/dist/alpha-zero/selfModification.d.ts +0 -109
- package/dist/alpha-zero/selfModification.d.ts.map +0 -1
- package/dist/alpha-zero/selfModification.js +0 -233
- package/dist/alpha-zero/selfModification.js.map +0 -1
- package/dist/alpha-zero/types.d.ts +0 -170
- package/dist/alpha-zero/types.d.ts.map +0 -1
- package/dist/alpha-zero/types.js +0 -31
- package/dist/alpha-zero/types.js.map +0 -1
- package/dist/capabilities/securityTestingCapability.d.ts +0 -13
- package/dist/capabilities/securityTestingCapability.d.ts.map +0 -1
- package/dist/capabilities/securityTestingCapability.js +0 -25
- package/dist/capabilities/securityTestingCapability.js.map +0 -1
- package/dist/core/aiFlowOptimizer.d.ts +0 -26
- package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
- package/dist/core/aiFlowOptimizer.js +0 -31
- package/dist/core/aiFlowOptimizer.js.map +0 -1
- package/dist/core/aiOptimizationEngine.d.ts +0 -158
- package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
- package/dist/core/aiOptimizationEngine.js +0 -428
- package/dist/core/aiOptimizationEngine.js.map +0 -1
- package/dist/core/aiOptimizationIntegration.d.ts +0 -93
- package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
- package/dist/core/aiOptimizationIntegration.js +0 -250
- package/dist/core/aiOptimizationIntegration.js.map +0 -1
- package/dist/core/enhancedErrorRecovery.d.ts +0 -100
- package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
- package/dist/core/enhancedErrorRecovery.js +0 -345
- package/dist/core/enhancedErrorRecovery.js.map +0 -1
- package/dist/core/hooksSystem.d.ts +0 -65
- package/dist/core/hooksSystem.d.ts.map +0 -1
- package/dist/core/hooksSystem.js +0 -273
- package/dist/core/hooksSystem.js.map +0 -1
- package/dist/core/memorySystem.d.ts +0 -48
- package/dist/core/memorySystem.d.ts.map +0 -1
- package/dist/core/memorySystem.js +0 -271
- package/dist/core/memorySystem.js.map +0 -1
- package/dist/core/unified/errors.d.ts +0 -189
- package/dist/core/unified/errors.d.ts.map +0 -1
- package/dist/core/unified/errors.js +0 -497
- package/dist/core/unified/errors.js.map +0 -1
- package/dist/core/unified/index.d.ts +0 -19
- package/dist/core/unified/index.d.ts.map +0 -1
- package/dist/core/unified/index.js +0 -68
- package/dist/core/unified/index.js.map +0 -1
- package/dist/core/unified/schema.d.ts +0 -101
- package/dist/core/unified/schema.d.ts.map +0 -1
- package/dist/core/unified/schema.js +0 -350
- package/dist/core/unified/schema.js.map +0 -1
- package/dist/core/unified/toolRuntime.d.ts +0 -179
- package/dist/core/unified/toolRuntime.d.ts.map +0 -1
- package/dist/core/unified/toolRuntime.js +0 -517
- package/dist/core/unified/toolRuntime.js.map +0 -1
- package/dist/core/unified/tools.d.ts +0 -127
- package/dist/core/unified/tools.d.ts.map +0 -1
- package/dist/core/unified/tools.js +0 -1333
- package/dist/core/unified/tools.js.map +0 -1
- package/dist/core/unified/types.d.ts +0 -352
- package/dist/core/unified/types.d.ts.map +0 -1
- package/dist/core/unified/types.js +0 -12
- package/dist/core/unified/types.js.map +0 -1
- package/dist/core/unified/version.d.ts +0 -209
- package/dist/core/unified/version.d.ts.map +0 -1
- package/dist/core/unified/version.js +0 -454
- package/dist/core/unified/version.js.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +0 -3
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.js +0 -12
- package/dist/plugins/tools/security/securityPlugin.js.map +0 -1
- package/dist/security/active-stack-security.d.ts +0 -112
- package/dist/security/active-stack-security.d.ts.map +0 -1
- package/dist/security/active-stack-security.js +0 -296
- package/dist/security/active-stack-security.js.map +0 -1
- package/dist/security/advanced-persistence-research.d.ts +0 -92
- package/dist/security/advanced-persistence-research.d.ts.map +0 -1
- package/dist/security/advanced-persistence-research.js +0 -195
- package/dist/security/advanced-persistence-research.js.map +0 -1
- package/dist/security/advanced-targeting.d.ts +0 -119
- package/dist/security/advanced-targeting.d.ts.map +0 -1
- package/dist/security/advanced-targeting.js +0 -233
- package/dist/security/advanced-targeting.js.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +0 -104
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.js +0 -315
- package/dist/security/assessment/vulnerabilityAssessment.js.map +0 -1
- package/dist/security/authorization/securityAuthorization.d.ts +0 -88
- package/dist/security/authorization/securityAuthorization.d.ts.map +0 -1
- package/dist/security/authorization/securityAuthorization.js +0 -172
- package/dist/security/authorization/securityAuthorization.js.map +0 -1
- package/dist/security/comprehensive-targeting.d.ts +0 -85
- package/dist/security/comprehensive-targeting.d.ts.map +0 -1
- package/dist/security/comprehensive-targeting.js +0 -438
- package/dist/security/comprehensive-targeting.js.map +0 -1
- package/dist/security/global-security-integration.d.ts +0 -91
- package/dist/security/global-security-integration.d.ts.map +0 -1
- package/dist/security/global-security-integration.js +0 -218
- package/dist/security/global-security-integration.js.map +0 -1
- package/dist/security/index.d.ts +0 -38
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/index.js +0 -47
- package/dist/security/index.js.map +0 -1
- package/dist/security/persistence-analyzer.d.ts +0 -56
- package/dist/security/persistence-analyzer.d.ts.map +0 -1
- package/dist/security/persistence-analyzer.js +0 -187
- package/dist/security/persistence-analyzer.js.map +0 -1
- package/dist/security/persistence-cli.d.ts +0 -36
- package/dist/security/persistence-cli.d.ts.map +0 -1
- package/dist/security/persistence-cli.js +0 -160
- package/dist/security/persistence-cli.js.map +0 -1
- package/dist/security/persistence-research.d.ts +0 -92
- package/dist/security/persistence-research.d.ts.map +0 -1
- package/dist/security/persistence-research.js +0 -364
- package/dist/security/persistence-research.js.map +0 -1
- package/dist/security/research/persistenceResearch.d.ts +0 -97
- package/dist/security/research/persistenceResearch.d.ts.map +0 -1
- package/dist/security/research/persistenceResearch.js +0 -282
- package/dist/security/research/persistenceResearch.js.map +0 -1
- package/dist/security/security-integration.d.ts +0 -74
- package/dist/security/security-integration.d.ts.map +0 -1
- package/dist/security/security-integration.js +0 -137
- package/dist/security/security-integration.js.map +0 -1
- package/dist/security/security-testing-framework.d.ts +0 -112
- package/dist/security/security-testing-framework.d.ts.map +0 -1
- package/dist/security/security-testing-framework.js +0 -364
- package/dist/security/security-testing-framework.js.map +0 -1
- package/dist/security/simulation/attackSimulation.d.ts +0 -93
- package/dist/security/simulation/attackSimulation.d.ts.map +0 -1
- package/dist/security/simulation/attackSimulation.js +0 -341
- package/dist/security/simulation/attackSimulation.js.map +0 -1
- package/dist/security/strategic-operations.d.ts +0 -100
- package/dist/security/strategic-operations.d.ts.map +0 -1
- package/dist/security/strategic-operations.js +0 -276
- package/dist/security/strategic-operations.js.map +0 -1
- package/dist/security/tool-security-wrapper.d.ts +0 -58
- package/dist/security/tool-security-wrapper.d.ts.map +0 -1
- package/dist/security/tool-security-wrapper.js +0 -156
- package/dist/security/tool-security-wrapper.js.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.js +0 -322
- package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
- package/dist/shell/inputQueueManager.d.ts +0 -144
- package/dist/shell/inputQueueManager.d.ts.map +0 -1
- package/dist/shell/inputQueueManager.js +0 -290
- package/dist/shell/inputQueueManager.js.map +0 -1
- package/dist/shell/metricsTracker.d.ts +0 -60
- package/dist/shell/metricsTracker.d.ts.map +0 -1
- package/dist/shell/metricsTracker.js +0 -119
- package/dist/shell/metricsTracker.js.map +0 -1
- package/dist/shell/streamingOutputManager.d.ts +0 -115
- package/dist/shell/streamingOutputManager.d.ts.map +0 -1
- package/dist/shell/streamingOutputManager.js +0 -225
- package/dist/shell/streamingOutputManager.js.map +0 -1
- package/dist/tools/securityTools.d.ts +0 -22
- package/dist/tools/securityTools.d.ts.map +0 -1
- package/dist/tools/securityTools.js +0 -448
- package/dist/tools/securityTools.js.map +0 -1
- package/dist/ui/persistentPrompt.d.ts +0 -50
- package/dist/ui/persistentPrompt.d.ts.map +0 -1
- package/dist/ui/persistentPrompt.js +0 -92
- package/dist/ui/persistentPrompt.js.map +0 -1
- package/dist/ui/terminalUISchema.d.ts +0 -195
- package/dist/ui/terminalUISchema.d.ts.map +0 -1
- package/dist/ui/terminalUISchema.js +0 -113
- package/dist/ui/terminalUISchema.js.map +0 -1
- package/scripts/deploy-security-capabilities.js +0 -178
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - Hybrid floating/scroll approach:
|
|
7
|
+
* - Initially: chat box floats below content
|
|
8
|
+
* - When terminal fills: scroll region activates, chat box pins to bottom
|
|
6
9
|
* - Native bracketed paste support (no heuristics)
|
|
7
10
|
* - Clean cursor model with render-time wrapping
|
|
8
11
|
* - State machine for different input modes
|
|
9
12
|
* - No readline dependency for display
|
|
10
13
|
*/
|
|
11
14
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
15
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
16
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import {
|
|
17
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
18
|
+
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
19
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
15
20
|
// ANSI escape codes
|
|
16
21
|
const ESC = {
|
|
17
22
|
// Cursor control
|
|
@@ -21,12 +26,12 @@ const ESC = {
|
|
|
21
26
|
SHOW: '\x1b[?25h',
|
|
22
27
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
23
28
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
-
// Screen control
|
|
25
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
-
HOME: '\x1b[H',
|
|
27
29
|
// Line control
|
|
28
30
|
CLEAR_LINE: '\x1b[2K',
|
|
29
31
|
CLEAR_TO_END: '\x1b[0J',
|
|
32
|
+
// Scroll region
|
|
33
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
34
|
+
RESET_SCROLL: '\x1b[r',
|
|
30
35
|
// Style
|
|
31
36
|
RESET: '\x1b[0m',
|
|
32
37
|
DIM: '\x1b[2m',
|
|
@@ -66,47 +71,46 @@ export class TerminalInput extends EventEmitter {
|
|
|
66
71
|
statusMessage = null;
|
|
67
72
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
68
73
|
streamingLabel = null; // Streaming progress indicator
|
|
74
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
75
|
+
metaTokensUsed = null; // Optional token usage
|
|
76
|
+
metaTokenLimit = null; // Optional token window
|
|
77
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
78
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
69
79
|
lastRenderContent = '';
|
|
70
80
|
lastRenderCursor = -1;
|
|
71
81
|
renderDirty = false;
|
|
72
82
|
isRendering = false;
|
|
73
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
74
|
-
inputAreaStartRow = 0; // Track absolute row position of input area
|
|
75
|
-
contentEndRow = 0; // Row where content ends (chat box renders below this)
|
|
76
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
77
|
-
commandSuggestions = [];
|
|
78
|
-
filteredSuggestions = [];
|
|
79
|
-
selectedSuggestionIndex = 0;
|
|
80
|
-
showSuggestions = false;
|
|
81
83
|
// Lifecycle
|
|
82
84
|
disposed = false;
|
|
83
85
|
enabled = true;
|
|
84
86
|
contextUsage = null;
|
|
87
|
+
contextAutoCompactThreshold = 90;
|
|
88
|
+
// Track current content row (starts at top, moves down)
|
|
89
|
+
contentRow = 1;
|
|
90
|
+
// Track if scroll region is currently active
|
|
91
|
+
scrollRegionActive = false;
|
|
92
|
+
thinkingModeLabel = null;
|
|
85
93
|
editMode = 'display-edits';
|
|
86
94
|
verificationEnabled = true;
|
|
87
95
|
autoContinueEnabled = false;
|
|
88
96
|
verificationHotkey = 'alt+v';
|
|
89
97
|
autoContinueHotkey = 'alt+c';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
98
|
+
thinkingHotkey = '/thinking';
|
|
99
|
+
modelLabel = null;
|
|
100
|
+
providerLabel = null;
|
|
101
|
+
// Streaming render throttle
|
|
102
|
+
lastStreamingRender = 0;
|
|
103
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
97
104
|
streamingRenderTimer = null;
|
|
98
|
-
// Unified UI initialization flag
|
|
99
|
-
unifiedUIInitialized = false;
|
|
100
105
|
constructor(writeStream = process.stdout, config = {}) {
|
|
101
106
|
super();
|
|
102
107
|
this.out = writeStream;
|
|
103
|
-
// Use schema defaults for configuration consistency
|
|
104
108
|
this.config = {
|
|
105
|
-
maxLines: config.maxLines ??
|
|
106
|
-
maxLength: config.maxLength ??
|
|
109
|
+
maxLines: config.maxLines ?? 1000,
|
|
110
|
+
maxLength: config.maxLength ?? 10000,
|
|
107
111
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
108
|
-
promptChar: config.promptChar ??
|
|
109
|
-
continuationChar: config.continuationChar ??
|
|
112
|
+
promptChar: config.promptChar ?? '> ',
|
|
113
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
110
114
|
};
|
|
111
115
|
}
|
|
112
116
|
// ===========================================================================
|
|
@@ -185,302 +189,36 @@ export class TerminalInput extends EventEmitter {
|
|
|
185
189
|
if (handled)
|
|
186
190
|
return;
|
|
187
191
|
}
|
|
188
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
189
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
190
|
-
this.emit('showHelp');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
192
|
// Insert printable characters
|
|
194
193
|
if (str && !key?.ctrl && !key?.meta) {
|
|
195
194
|
this.insertText(str);
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
199
|
-
bannerContent = null;
|
|
200
|
-
/**
|
|
201
|
-
* Set banner content to be written when unified UI initializes.
|
|
202
|
-
*/
|
|
203
|
-
setBannerContent(content) {
|
|
204
|
-
this.bannerContent = content;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Initialize the unified UI system.
|
|
208
|
-
*
|
|
209
|
-
* Layout:
|
|
210
|
-
* 1. Clear screen
|
|
211
|
-
* 2. Write banner at top
|
|
212
|
-
* 3. Track content end position
|
|
213
|
-
* 4. Render floating input area below banner
|
|
214
|
-
*/
|
|
215
|
-
initializeUnifiedUI() {
|
|
216
|
-
if (this.unifiedUIInitialized) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Hide cursor during setup
|
|
220
|
-
this.write(ESC.HIDE);
|
|
221
|
-
// Clear screen and go home
|
|
222
|
-
this.write(ESC.HOME);
|
|
223
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
224
|
-
// Write banner at top and track where it ends
|
|
225
|
-
let bannerLines = 0;
|
|
226
|
-
if (this.bannerContent) {
|
|
227
|
-
const lines = this.bannerContent.split('\n');
|
|
228
|
-
bannerLines = lines.length + 2; // +2 for the trailing \n\n
|
|
229
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
230
|
-
}
|
|
231
|
-
// Set content end row so input renders right after banner
|
|
232
|
-
this.contentEndRow = bannerLines > 0 ? bannerLines : 1;
|
|
233
|
-
// Mark initialized
|
|
234
|
-
this.unifiedUIInitialized = true;
|
|
235
|
-
// Render floating input area below the banner
|
|
236
|
-
this.renderFloatingInputArea();
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Clear the input area at its tracked position.
|
|
240
|
-
* Returns true if something was cleared.
|
|
241
|
-
*/
|
|
242
|
-
clearInputArea() {
|
|
243
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
244
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
245
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
246
|
-
this.write(ESC.CLEAR_LINE);
|
|
247
|
-
}
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Reset input area tracking state.
|
|
254
|
-
*/
|
|
255
|
-
resetInputAreaTracking() {
|
|
256
|
-
this.inputAreaStartRow = 0;
|
|
257
|
-
this.flowModeRenderedLines = 0;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Render chat box - UNIFIED floating approach.
|
|
261
|
-
* Both during and after streaming: chat box floats right below content.
|
|
262
|
-
* Uses scroll region during streaming to protect chat box from content overwrites.
|
|
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 chat box
|
|
269
|
-
const linesNeeded = 5 + (this.modelInfo ? 1 : 0);
|
|
270
|
-
// FIRST: Clear any previously rendered chat box
|
|
271
|
-
this.clearInputArea();
|
|
272
|
-
// Hide cursor during render
|
|
273
|
-
this.write(ESC.HIDE);
|
|
274
|
-
// Calculate where to render - ALWAYS float right below content
|
|
275
|
-
let startRow;
|
|
276
|
-
if (this.contentEndRow > 0) {
|
|
277
|
-
// Float right below content (no wasted space)
|
|
278
|
-
startRow = this.contentEndRow + 1;
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
// Default: start at row 1 (top of screen)
|
|
282
|
-
startRow = 1;
|
|
283
|
-
}
|
|
284
|
-
// Clamp to ensure chat box fits in terminal
|
|
285
|
-
// (never start so low that chat box would extend past terminal bottom)
|
|
286
|
-
const maxStartRow = rows - linesNeeded + 1;
|
|
287
|
-
startRow = Math.min(startRow, maxStartRow);
|
|
288
|
-
startRow = Math.max(1, startRow);
|
|
289
|
-
// During streaming: set scroll region to protect chat box area
|
|
290
|
-
// Content writes in rows 1 to (startRow - 1), chat box at startRow onwards
|
|
291
|
-
if (this.mode === 'streaming' && startRow > 1) {
|
|
292
|
-
this.write(`\x1b[1;${startRow - 1}r`); // Set scroll region
|
|
293
|
-
}
|
|
294
|
-
else if (this.mode !== 'streaming') {
|
|
295
|
-
this.write('\x1b[r'); // Reset scroll region when not streaming
|
|
296
|
-
}
|
|
297
|
-
// Track this position
|
|
298
|
-
this.inputAreaStartRow = startRow;
|
|
299
|
-
let currentRow = startRow;
|
|
300
|
-
// Status bar
|
|
301
|
-
this.write(ESC.TO(currentRow, 1));
|
|
302
|
-
this.write(this.buildStatusBar(cols));
|
|
303
|
-
currentRow++;
|
|
304
|
-
// Model info line (if set)
|
|
305
|
-
if (this.modelInfo) {
|
|
306
|
-
this.write(ESC.TO(currentRow, 1));
|
|
307
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
308
|
-
if (this.contextUsage !== null) {
|
|
309
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
310
|
-
if (rem < 10)
|
|
311
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
312
|
-
else if (rem < 25)
|
|
313
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
314
|
-
else
|
|
315
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
316
|
-
}
|
|
317
|
-
this.write(modelLine);
|
|
318
|
-
currentRow++;
|
|
319
|
-
}
|
|
320
|
-
// Top divider
|
|
321
|
-
this.write(ESC.TO(currentRow, 1));
|
|
322
|
-
this.write(divider);
|
|
323
|
-
currentRow++;
|
|
324
|
-
// Input line with prompt and buffer content
|
|
325
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
326
|
-
const displayLine = lines[0] ?? '';
|
|
327
|
-
const inputRow = currentRow;
|
|
328
|
-
this.write(ESC.TO(currentRow, 1));
|
|
329
|
-
this.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
330
|
-
this.write(ESC.BG_DARK + displayLine);
|
|
331
|
-
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
332
|
-
if (padding > 0)
|
|
333
|
-
this.write(' '.repeat(padding));
|
|
334
|
-
this.write(ESC.RESET);
|
|
335
|
-
currentRow++;
|
|
336
|
-
// Bottom divider
|
|
337
|
-
this.write(ESC.TO(currentRow, 1));
|
|
338
|
-
this.write(divider);
|
|
339
|
-
currentRow++;
|
|
340
|
-
// Mode controls
|
|
341
|
-
this.write(ESC.TO(currentRow, 1));
|
|
342
|
-
this.write(this.buildModeControls(cols));
|
|
343
|
-
// Track lines rendered
|
|
344
|
-
this.flowModeRenderedLines = currentRow - startRow + 1;
|
|
345
|
-
// Position cursor in input line for typing
|
|
346
|
-
this.write(ESC.TO(inputRow, this.config.promptChar.length + 1 + cursorCol));
|
|
347
|
-
// Show cursor
|
|
348
|
-
this.write(ESC.SHOW);
|
|
349
|
-
// Update tracking
|
|
350
|
-
this.lastRenderContent = this.buffer;
|
|
351
|
-
this.lastRenderCursor = this.cursor;
|
|
352
|
-
}
|
|
353
197
|
/**
|
|
354
198
|
* Set the input mode
|
|
355
199
|
*
|
|
356
|
-
*
|
|
357
|
-
* During streaming: scroll region protects chat box area.
|
|
358
|
-
* After streaming: scroll region reset, chat box floats below final content.
|
|
200
|
+
* Content flows naturally - no scroll region pinning.
|
|
359
201
|
*/
|
|
360
202
|
setMode(mode) {
|
|
361
203
|
const prevMode = this.mode;
|
|
362
204
|
this.mode = mode;
|
|
363
205
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
364
|
-
|
|
365
|
-
this.streamingStartTime = Date.now();
|
|
366
|
-
// Ensure unified UI is initialized
|
|
367
|
-
if (!this.unifiedUIInitialized) {
|
|
368
|
-
this.initializeUnifiedUI();
|
|
369
|
-
}
|
|
206
|
+
this.resetStreamingRenderThrottle();
|
|
370
207
|
this.renderDirty = true;
|
|
371
|
-
this.
|
|
208
|
+
this.render();
|
|
372
209
|
}
|
|
373
210
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.streamingRenderTimer = null;
|
|
378
|
-
}
|
|
379
|
-
// Reset streaming time
|
|
380
|
-
this.streamingStartTime = null;
|
|
381
|
-
// CRITICAL: Reset scroll region when leaving streaming mode
|
|
382
|
-
this.write('\x1b[r');
|
|
383
|
-
// Re-render floating input area below content
|
|
384
|
-
this.renderDirty = true;
|
|
385
|
-
this.scheduleRender();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Set the row where content ends (for idle mode positioning).
|
|
390
|
-
* Input area will render starting from this row + 1.
|
|
391
|
-
*/
|
|
392
|
-
setContentEndRow(row) {
|
|
393
|
-
this.contentEndRow = Math.max(0, row);
|
|
394
|
-
this.renderDirty = true;
|
|
395
|
-
this.scheduleRender();
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Set available slash commands for auto-complete suggestions.
|
|
399
|
-
*/
|
|
400
|
-
setCommands(commands) {
|
|
401
|
-
this.commandSuggestions = commands;
|
|
402
|
-
this.updateSuggestions();
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Update filtered suggestions based on current input.
|
|
406
|
-
*/
|
|
407
|
-
updateSuggestions() {
|
|
408
|
-
const input = this.buffer.trim();
|
|
409
|
-
// Only show suggestions when input starts with "/"
|
|
410
|
-
if (!input.startsWith('/')) {
|
|
411
|
-
this.showSuggestions = false;
|
|
412
|
-
this.filteredSuggestions = [];
|
|
413
|
-
this.selectedSuggestionIndex = 0;
|
|
414
|
-
return;
|
|
211
|
+
// Streaming ended - render the input area
|
|
212
|
+
this.resetStreamingRenderThrottle();
|
|
213
|
+
this.forceRender();
|
|
415
214
|
}
|
|
416
|
-
const query = input.toLowerCase();
|
|
417
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
418
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
419
|
-
// Show suggestions if we have matches
|
|
420
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
421
|
-
// Keep selection in bounds
|
|
422
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
423
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Select next suggestion (arrow down / tab).
|
|
428
|
-
*/
|
|
429
|
-
selectNextSuggestion() {
|
|
430
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
431
|
-
return;
|
|
432
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
433
|
-
this.renderDirty = true;
|
|
434
|
-
this.scheduleRender();
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
438
|
-
*/
|
|
439
|
-
selectPrevSuggestion() {
|
|
440
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
441
|
-
return;
|
|
442
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
443
|
-
? this.filteredSuggestions.length - 1
|
|
444
|
-
: this.selectedSuggestionIndex - 1;
|
|
445
|
-
this.renderDirty = true;
|
|
446
|
-
this.scheduleRender();
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Accept current suggestion and insert into buffer.
|
|
450
|
-
*/
|
|
451
|
-
acceptSuggestion() {
|
|
452
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
453
|
-
return false;
|
|
454
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
455
|
-
if (!selected)
|
|
456
|
-
return false;
|
|
457
|
-
// Replace buffer with selected command
|
|
458
|
-
this.buffer = selected.command + ' ';
|
|
459
|
-
this.cursor = this.buffer.length;
|
|
460
|
-
this.showSuggestions = false;
|
|
461
|
-
this.renderDirty = true;
|
|
462
|
-
this.scheduleRender();
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Check if suggestions are visible.
|
|
467
|
-
*/
|
|
468
|
-
areSuggestionsVisible() {
|
|
469
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
470
215
|
}
|
|
471
216
|
/**
|
|
472
|
-
*
|
|
217
|
+
* Legacy method - no longer used (content flows naturally).
|
|
218
|
+
* @deprecated Use setContentRow instead
|
|
473
219
|
*/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
477
|
-
this.scheduleRender();
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Get thinking enabled state
|
|
481
|
-
*/
|
|
482
|
-
isThinkingEnabled() {
|
|
483
|
-
return this.thinkingEnabled;
|
|
220
|
+
setPinnedHeaderLines(_count) {
|
|
221
|
+
// No-op: scroll region pinning removed
|
|
484
222
|
}
|
|
485
223
|
/**
|
|
486
224
|
* Get current mode
|
|
@@ -513,17 +251,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
513
251
|
}
|
|
514
252
|
/**
|
|
515
253
|
* Clear the buffer
|
|
516
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
517
254
|
*/
|
|
518
|
-
clear(
|
|
255
|
+
clear() {
|
|
519
256
|
this.buffer = '';
|
|
520
257
|
this.cursor = 0;
|
|
521
258
|
this.historyIndex = -1;
|
|
522
259
|
this.tempInput = '';
|
|
523
260
|
this.pastePlaceholders = [];
|
|
524
|
-
|
|
525
|
-
this.scheduleRender();
|
|
526
|
-
}
|
|
261
|
+
this.scheduleRender();
|
|
527
262
|
}
|
|
528
263
|
/**
|
|
529
264
|
* Get queued inputs
|
|
@@ -594,6 +329,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
594
329
|
this.streamingLabel = next;
|
|
595
330
|
this.scheduleRender();
|
|
596
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
334
|
+
*/
|
|
335
|
+
setMetaStatus(meta) {
|
|
336
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
337
|
+
? Math.floor(meta.elapsedSeconds)
|
|
338
|
+
: null;
|
|
339
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
340
|
+
? Math.floor(meta.tokensUsed)
|
|
341
|
+
: null;
|
|
342
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
343
|
+
? Math.floor(meta.tokenLimit)
|
|
344
|
+
: null;
|
|
345
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
346
|
+
? Math.floor(meta.thinkingMs)
|
|
347
|
+
: null;
|
|
348
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
349
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
350
|
+
this.metaTokensUsed === nextTokens &&
|
|
351
|
+
this.metaTokenLimit === nextLimit &&
|
|
352
|
+
this.metaThinkingMs === nextThinking &&
|
|
353
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
357
|
+
this.metaTokensUsed = nextTokens;
|
|
358
|
+
this.metaTokenLimit = nextLimit;
|
|
359
|
+
this.metaThinkingMs = nextThinking;
|
|
360
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
361
|
+
this.scheduleRender();
|
|
362
|
+
}
|
|
597
363
|
/**
|
|
598
364
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
599
365
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -603,26 +369,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
603
369
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
604
370
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
605
371
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
372
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
373
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
606
374
|
if (this.verificationEnabled === nextVerification &&
|
|
607
375
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
608
376
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
609
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
377
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
378
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
379
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
610
380
|
return;
|
|
611
381
|
}
|
|
612
382
|
this.verificationEnabled = nextVerification;
|
|
613
383
|
this.autoContinueEnabled = nextAutoContinue;
|
|
614
384
|
this.verificationHotkey = nextVerifyHotkey;
|
|
615
385
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
616
|
-
this.
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
620
|
-
* This is displayed persistently above the input area.
|
|
621
|
-
*/
|
|
622
|
-
setModelInfo(info) {
|
|
623
|
-
if (this.modelInfo === info)
|
|
624
|
-
return;
|
|
625
|
-
this.modelInfo = info;
|
|
386
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
387
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
626
388
|
this.scheduleRender();
|
|
627
389
|
}
|
|
628
390
|
/**
|
|
@@ -635,33 +397,159 @@ export class TerminalInput extends EventEmitter {
|
|
|
635
397
|
this.scheduleRender();
|
|
636
398
|
}
|
|
637
399
|
/**
|
|
638
|
-
*
|
|
639
|
-
|
|
640
|
-
|
|
400
|
+
* Surface model/provider context in the controls bar.
|
|
401
|
+
*/
|
|
402
|
+
setModelContext(options) {
|
|
403
|
+
const nextModel = options.model?.trim() || null;
|
|
404
|
+
const nextProvider = options.provider?.trim() || null;
|
|
405
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.modelLabel = nextModel;
|
|
409
|
+
this.providerLabel = nextProvider;
|
|
410
|
+
this.scheduleRender();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Render the floating input area at contentRow.
|
|
414
|
+
*
|
|
415
|
+
* The chat box "floats" - it renders right below the last streamed content.
|
|
416
|
+
* As content is added, contentRow advances, and the chat box moves down.
|
|
417
|
+
* No scroll regions - pure floating behavior.
|
|
641
418
|
*/
|
|
642
419
|
render() {
|
|
643
420
|
if (!this.canRender())
|
|
644
421
|
return;
|
|
645
422
|
if (this.isRendering)
|
|
646
423
|
return;
|
|
424
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
425
|
+
// During streaming, throttle re-renders
|
|
426
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
427
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
428
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
429
|
+
if (waitMs > 0) {
|
|
430
|
+
this.renderDirty = true;
|
|
431
|
+
this.scheduleStreamingRender(waitMs);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
647
435
|
const shouldSkip = !this.renderDirty &&
|
|
648
436
|
this.buffer === this.lastRenderContent &&
|
|
649
437
|
this.cursor === this.lastRenderCursor;
|
|
650
438
|
this.renderDirty = false;
|
|
651
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
652
439
|
if (shouldSkip) {
|
|
653
440
|
return;
|
|
654
441
|
}
|
|
655
|
-
// If write lock is held, defer render
|
|
656
442
|
if (writeLock.isLocked()) {
|
|
657
443
|
writeLock.safeWrite(() => this.render());
|
|
658
444
|
return;
|
|
659
445
|
}
|
|
446
|
+
this.renderFloatingInputArea();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Core floating input area renderer.
|
|
450
|
+
* Chat box always floats at contentRow (below streamed content).
|
|
451
|
+
* This creates "persistent bottom floating" behavior.
|
|
452
|
+
*/
|
|
453
|
+
renderFloatingInputArea() {
|
|
454
|
+
const { rows, cols } = this.getSize();
|
|
455
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
456
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
457
|
+
// Wrap buffer into display lines
|
|
458
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
459
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
460
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
461
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
462
|
+
// Calculate display window (keep cursor visible)
|
|
463
|
+
let startLine = 0;
|
|
464
|
+
if (lines.length > displayLines) {
|
|
465
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
466
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
467
|
+
}
|
|
468
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
469
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
470
|
+
// Chat box height (must match getChatBoxHeight calculation)
|
|
471
|
+
const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
|
|
472
|
+
// Unified floating: chat box always at contentRow + 1
|
|
473
|
+
// When scroll region is active, contentRow is capped at maxContentRow
|
|
474
|
+
// so chat box ends up at the bottom but still "floats" below content
|
|
475
|
+
const chatBoxStartRow = this.contentRow + 1;
|
|
476
|
+
writeLock.lock('terminalInput.renderFloating');
|
|
660
477
|
this.isRendering = true;
|
|
661
|
-
writeLock.lock('terminalInput.render');
|
|
662
478
|
try {
|
|
663
|
-
//
|
|
664
|
-
this.
|
|
479
|
+
// Hide cursor during render
|
|
480
|
+
this.write(ESC.HIDE);
|
|
481
|
+
this.write(ESC.RESET);
|
|
482
|
+
// Clear the chat box area
|
|
483
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
484
|
+
const row = chatBoxStartRow + i;
|
|
485
|
+
if (row <= rows) {
|
|
486
|
+
this.write(ESC.TO(row, 1));
|
|
487
|
+
this.write(ESC.CLEAR_LINE);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
let currentRow = chatBoxStartRow;
|
|
491
|
+
// Meta/status header
|
|
492
|
+
for (const metaLine of metaLines) {
|
|
493
|
+
this.write(ESC.TO(currentRow, 1));
|
|
494
|
+
this.write(metaLine);
|
|
495
|
+
currentRow += 1;
|
|
496
|
+
}
|
|
497
|
+
// Separator line
|
|
498
|
+
this.write(ESC.TO(currentRow, 1));
|
|
499
|
+
this.write(renderDivider(cols - 2));
|
|
500
|
+
currentRow += 1;
|
|
501
|
+
// Render input lines
|
|
502
|
+
let finalRow = currentRow;
|
|
503
|
+
let finalCol = 3;
|
|
504
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
505
|
+
const rowNum = currentRow + i;
|
|
506
|
+
this.write(ESC.TO(rowNum, 1));
|
|
507
|
+
const line = visibleLines[i] ?? '';
|
|
508
|
+
const isFirstLine = (startLine + i) === 0;
|
|
509
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
510
|
+
this.write(ESC.BG_DARK);
|
|
511
|
+
this.write(ESC.DIM);
|
|
512
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
513
|
+
this.write(ESC.RESET);
|
|
514
|
+
this.write(ESC.BG_DARK);
|
|
515
|
+
if (isCursorLine) {
|
|
516
|
+
const col = Math.min(cursorCol, line.length);
|
|
517
|
+
const before = line.slice(0, col);
|
|
518
|
+
const at = col < line.length ? line[col] : ' ';
|
|
519
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
520
|
+
this.write(before);
|
|
521
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
522
|
+
this.write(at);
|
|
523
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
524
|
+
this.write(after);
|
|
525
|
+
finalRow = rowNum;
|
|
526
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
this.write(line);
|
|
530
|
+
}
|
|
531
|
+
// Pad to edge
|
|
532
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
533
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
534
|
+
if (padding > 0)
|
|
535
|
+
this.write(' '.repeat(padding));
|
|
536
|
+
this.write(ESC.RESET);
|
|
537
|
+
}
|
|
538
|
+
// Mode controls line
|
|
539
|
+
const controlRow = currentRow + visibleLines.length;
|
|
540
|
+
this.write(ESC.TO(controlRow, 1));
|
|
541
|
+
this.write(this.buildModeControls(cols));
|
|
542
|
+
// Position cursor in input box
|
|
543
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
544
|
+
this.write(ESC.SHOW);
|
|
545
|
+
// Update state
|
|
546
|
+
this.lastRenderContent = this.buffer;
|
|
547
|
+
this.lastRenderCursor = this.cursor;
|
|
548
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
549
|
+
if (this.streamingRenderTimer) {
|
|
550
|
+
clearTimeout(this.streamingRenderTimer);
|
|
551
|
+
this.streamingRenderTimer = null;
|
|
552
|
+
}
|
|
665
553
|
}
|
|
666
554
|
finally {
|
|
667
555
|
writeLock.unlock();
|
|
@@ -669,99 +557,217 @@ export class TerminalInput extends EventEmitter {
|
|
|
669
557
|
}
|
|
670
558
|
}
|
|
671
559
|
/**
|
|
672
|
-
* Build
|
|
673
|
-
*
|
|
560
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
561
|
+
* During streaming, shows model line pinned above streaming info.
|
|
674
562
|
*/
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
//
|
|
679
|
-
if (this.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
563
|
+
buildMetaLines(width) {
|
|
564
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
565
|
+
const lines = [];
|
|
566
|
+
// Model line should ALWAYS be shown (pinned above streaming content)
|
|
567
|
+
if (this.modelLabel) {
|
|
568
|
+
const modelText = this.providerLabel
|
|
569
|
+
? `model ${this.modelLabel} @ ${this.providerLabel}`
|
|
570
|
+
: `model ${this.modelLabel}`;
|
|
571
|
+
lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
|
|
572
|
+
}
|
|
573
|
+
// During streaming, add a compact status line with essential info
|
|
574
|
+
if (streamingActive) {
|
|
575
|
+
const parts = [];
|
|
576
|
+
// Essential streaming info
|
|
577
|
+
if (this.metaThinkingMs !== null) {
|
|
578
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
686
579
|
}
|
|
687
|
-
|
|
580
|
+
if (this.metaElapsedSeconds !== null) {
|
|
581
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
582
|
+
}
|
|
583
|
+
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
584
|
+
if (parts.length) {
|
|
585
|
+
lines.push(renderStatusLine(parts, width));
|
|
586
|
+
}
|
|
587
|
+
return lines;
|
|
688
588
|
}
|
|
689
|
-
//
|
|
690
|
-
if (this.
|
|
691
|
-
|
|
589
|
+
// Non-streaming: show full status info (model line already added above)
|
|
590
|
+
if (this.metaThinkingMs !== null) {
|
|
591
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
592
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
692
593
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
594
|
+
const statusParts = [];
|
|
595
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
596
|
+
if (statusLabel) {
|
|
597
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
697
598
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
599
|
+
if (this.metaElapsedSeconds !== null) {
|
|
600
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
701
601
|
}
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
602
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
603
|
+
if (tokensRemaining !== null) {
|
|
604
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
705
605
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
606
|
+
if (statusParts.length) {
|
|
607
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
608
|
+
}
|
|
609
|
+
const usageParts = [];
|
|
610
|
+
if (this.metaTokensUsed !== null) {
|
|
611
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
612
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
613
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
614
|
+
}
|
|
615
|
+
if (this.contextUsage !== null) {
|
|
616
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
617
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
618
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
619
|
+
}
|
|
620
|
+
if (this.queue.length > 0) {
|
|
621
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
709
622
|
}
|
|
710
|
-
if (
|
|
711
|
-
|
|
623
|
+
if (usageParts.length) {
|
|
624
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
712
625
|
}
|
|
713
|
-
|
|
714
|
-
return joined.slice(0, maxWidth);
|
|
626
|
+
return lines;
|
|
715
627
|
}
|
|
716
628
|
/**
|
|
717
|
-
* Build mode controls line
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
629
|
+
* Build Claude Code style mode controls line.
|
|
630
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
721
631
|
*/
|
|
722
632
|
buildModeControls(cols) {
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
729
|
-
if (this.editMode === 'display-edits') {
|
|
730
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
633
|
+
const width = Math.max(8, cols - 2);
|
|
634
|
+
const leftParts = [];
|
|
635
|
+
const rightParts = [];
|
|
636
|
+
if (this.streamingLabel) {
|
|
637
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
731
638
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
639
|
+
if (this.overrideStatusMessage) {
|
|
640
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
641
|
+
}
|
|
642
|
+
if (this.statusMessage) {
|
|
643
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
644
|
+
}
|
|
645
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
646
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
647
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
648
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
649
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
650
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
651
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
652
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
653
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
654
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
655
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
656
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
657
|
+
}
|
|
658
|
+
if (this.buffer.includes('\n')) {
|
|
659
|
+
const lineCount = this.buffer.split('\n').length;
|
|
660
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
661
|
+
}
|
|
662
|
+
if (this.pastePlaceholders.length > 0) {
|
|
663
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
664
|
+
leftParts.push({
|
|
665
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
666
|
+
tone: 'info',
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const contextRemaining = this.computeContextRemaining();
|
|
670
|
+
if (this.thinkingModeLabel) {
|
|
671
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
672
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
673
|
+
}
|
|
674
|
+
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
675
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
676
|
+
if (this.modelLabel && !streamingActive) {
|
|
677
|
+
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
678
|
+
rightParts.push({ text: modelText, tone: 'muted' });
|
|
679
|
+
}
|
|
680
|
+
if (contextRemaining !== null) {
|
|
681
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
682
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
683
|
+
? 'Context auto-compact imminent'
|
|
684
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
685
|
+
rightParts.push({ text: label, tone });
|
|
686
|
+
}
|
|
687
|
+
if (!rightParts.length || width < 60) {
|
|
688
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
689
|
+
return renderStatusLine(merged, width);
|
|
690
|
+
}
|
|
691
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
692
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
693
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
694
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
695
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
696
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
697
|
+
}
|
|
698
|
+
formatHotkey(hotkey) {
|
|
699
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
700
|
+
if (!normalized)
|
|
701
|
+
return hotkey;
|
|
702
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
703
|
+
const map = {
|
|
704
|
+
shift: '⇧',
|
|
705
|
+
sh: '⇧',
|
|
706
|
+
alt: '⌥',
|
|
707
|
+
option: '⌥',
|
|
708
|
+
opt: '⌥',
|
|
709
|
+
ctrl: '⌃',
|
|
710
|
+
control: '⌃',
|
|
711
|
+
cmd: '⌘',
|
|
712
|
+
meta: '⌘',
|
|
713
|
+
};
|
|
714
|
+
const formatted = parts
|
|
715
|
+
.map((part) => {
|
|
716
|
+
const symbol = map[part];
|
|
717
|
+
if (symbol)
|
|
718
|
+
return symbol;
|
|
719
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
720
|
+
})
|
|
721
|
+
.join('');
|
|
722
|
+
return formatted || hotkey;
|
|
723
|
+
}
|
|
724
|
+
computeContextRemaining() {
|
|
725
|
+
if (this.contextUsage === null) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
729
|
+
}
|
|
730
|
+
computeTokensRemaining() {
|
|
731
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
735
|
+
return this.formatTokenCount(remaining);
|
|
736
|
+
}
|
|
737
|
+
formatElapsedLabel(seconds) {
|
|
738
|
+
if (seconds < 60) {
|
|
739
|
+
return `${seconds}s`;
|
|
740
|
+
}
|
|
741
|
+
const mins = Math.floor(seconds / 60);
|
|
742
|
+
const secs = seconds % 60;
|
|
743
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
744
|
+
}
|
|
745
|
+
formatTokenCount(value) {
|
|
746
|
+
if (!Number.isFinite(value)) {
|
|
747
|
+
return `${value}`;
|
|
748
|
+
}
|
|
749
|
+
if (value >= 1_000_000) {
|
|
750
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
751
|
+
}
|
|
752
|
+
if (value >= 1_000) {
|
|
753
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
754
|
+
}
|
|
755
|
+
return `${Math.round(value)}`;
|
|
756
|
+
}
|
|
757
|
+
visibleLength(value) {
|
|
758
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
759
|
+
return value.replace(ansiPattern, '').length;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
763
|
+
* needing a TTY. Not used by production code.
|
|
764
|
+
*/
|
|
765
|
+
getDebugUiSnapshot(width) {
|
|
766
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
767
|
+
return {
|
|
768
|
+
meta: this.buildMetaLines(cols - 2),
|
|
769
|
+
controls: this.buildModeControls(cols),
|
|
770
|
+
};
|
|
765
771
|
}
|
|
766
772
|
/**
|
|
767
773
|
* Force a re-render
|
|
@@ -784,32 +790,108 @@ export class TerminalInput extends EventEmitter {
|
|
|
784
790
|
handleResize() {
|
|
785
791
|
this.lastRenderContent = '';
|
|
786
792
|
this.lastRenderCursor = -1;
|
|
793
|
+
this.resetStreamingRenderThrottle();
|
|
787
794
|
this.scheduleRender();
|
|
788
795
|
}
|
|
789
796
|
/**
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
797
|
+
* Stream content with floating chat box.
|
|
798
|
+
*
|
|
799
|
+
* Clean approach - no scroll regions, just cursor positioning:
|
|
800
|
+
* 1. Save cursor state
|
|
801
|
+
* 2. Clear chat box area (it will be re-rendered)
|
|
802
|
+
* 3. Position at contentRow
|
|
803
|
+
* 4. Write content
|
|
804
|
+
* 5. Advance contentRow
|
|
805
|
+
* 6. Re-render chat box
|
|
793
806
|
*/
|
|
794
|
-
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
807
|
+
streamContent(content) {
|
|
808
|
+
if (!content)
|
|
809
|
+
return;
|
|
810
|
+
writeLock.lock('streamContent');
|
|
811
|
+
try {
|
|
812
|
+
// Save cursor and hide it
|
|
813
|
+
this.write(ESC.SAVE);
|
|
814
|
+
this.write(ESC.HIDE);
|
|
815
|
+
// Clear the chat box area first (it will be re-rendered after)
|
|
816
|
+
const { rows, cols } = this.getSize();
|
|
817
|
+
const chatBoxHeight = 6; // Approximate
|
|
818
|
+
const chatBoxStart = this.contentRow + 1;
|
|
819
|
+
for (let i = 0; i < chatBoxHeight && chatBoxStart + i <= rows; i++) {
|
|
820
|
+
this.write(ESC.TO(chatBoxStart + i, 1));
|
|
821
|
+
this.write(ESC.CLEAR_LINE);
|
|
822
|
+
}
|
|
823
|
+
// Position at contentRow and write content
|
|
824
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
825
|
+
this.write(content);
|
|
826
|
+
// Count newlines and advance contentRow
|
|
827
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
828
|
+
this.contentRow += newlines;
|
|
829
|
+
// Cap contentRow to leave room for chat box
|
|
830
|
+
const maxContentRow = Math.max(1, rows - chatBoxHeight);
|
|
831
|
+
if (this.contentRow > maxContentRow) {
|
|
832
|
+
this.contentRow = maxContentRow;
|
|
833
|
+
}
|
|
834
|
+
// Restore cursor
|
|
835
|
+
this.write(ESC.RESTORE);
|
|
836
|
+
this.write(ESC.SHOW);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
writeLock.unlock();
|
|
840
|
+
}
|
|
841
|
+
// Re-render chat box at new position
|
|
842
|
+
this.forceRender();
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Enable scroll region (no-op in floating mode).
|
|
846
|
+
*/
|
|
847
|
+
enableScrollRegion() {
|
|
848
|
+
// No-op: using pure floating approach
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Disable scroll region (no-op in floating mode).
|
|
852
|
+
*/
|
|
853
|
+
disableScrollRegion() {
|
|
854
|
+
// No-op: using pure floating approach
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Calculate chat box height.
|
|
858
|
+
*/
|
|
859
|
+
getChatBoxHeight() {
|
|
860
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* @deprecated Use streamContent() instead
|
|
864
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
865
|
+
*/
|
|
866
|
+
registerOutputInterceptor(_display) {
|
|
867
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* @deprecated Use streamContent() instead
|
|
871
|
+
* Write content above the floating chat box.
|
|
872
|
+
*/
|
|
873
|
+
writeToScrollRegion(content) {
|
|
874
|
+
this.streamContent(content);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Reset content position to row 1.
|
|
878
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
879
|
+
*/
|
|
880
|
+
resetContentPosition() {
|
|
881
|
+
this.contentRow = 1;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Set the content row explicitly (used after banner is written).
|
|
885
|
+
* This tells the input where content should start flowing from.
|
|
886
|
+
*/
|
|
887
|
+
setContentRow(row) {
|
|
888
|
+
this.contentRow = Math.max(1, row);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Get the current content row position.
|
|
892
|
+
*/
|
|
893
|
+
getContentRow() {
|
|
894
|
+
return this.contentRow;
|
|
813
895
|
}
|
|
814
896
|
/**
|
|
815
897
|
* Dispose and clean up
|
|
@@ -817,20 +899,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
817
899
|
dispose() {
|
|
818
900
|
if (this.disposed)
|
|
819
901
|
return;
|
|
820
|
-
// Clean up streaming render timer
|
|
821
|
-
if (this.streamingRenderTimer) {
|
|
822
|
-
clearInterval(this.streamingRenderTimer);
|
|
823
|
-
this.streamingRenderTimer = null;
|
|
824
|
-
}
|
|
825
|
-
// Clean up output interceptor
|
|
826
|
-
if (this.outputInterceptorCleanup) {
|
|
827
|
-
this.outputInterceptorCleanup();
|
|
828
|
-
this.outputInterceptorCleanup = undefined;
|
|
829
|
-
}
|
|
830
|
-
// Reset scroll region before disposing
|
|
831
|
-
this.write('\x1b[r');
|
|
832
902
|
this.disposed = true;
|
|
833
903
|
this.enabled = false;
|
|
904
|
+
this.disableScrollRegion();
|
|
905
|
+
this.resetStreamingRenderThrottle();
|
|
834
906
|
this.disableBracketedPaste();
|
|
835
907
|
this.buffer = '';
|
|
836
908
|
this.queue = [];
|
|
@@ -935,22 +1007,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
935
1007
|
this.toggleEditMode();
|
|
936
1008
|
return true;
|
|
937
1009
|
}
|
|
938
|
-
|
|
939
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
940
|
-
this.togglePasteExpansion();
|
|
941
|
-
}
|
|
942
|
-
else {
|
|
943
|
-
this.toggleThinking();
|
|
944
|
-
}
|
|
945
|
-
return true;
|
|
946
|
-
case 'escape':
|
|
947
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
948
|
-
if (this.mode === 'streaming') {
|
|
949
|
-
this.emit('interrupt');
|
|
950
|
-
}
|
|
951
|
-
else if (this.buffer.length > 0) {
|
|
952
|
-
this.clear();
|
|
953
|
-
}
|
|
1010
|
+
this.insertText(' ');
|
|
954
1011
|
return true;
|
|
955
1012
|
}
|
|
956
1013
|
return false;
|
|
@@ -968,7 +1025,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
968
1025
|
this.insertPlainText(chunk, insertPos);
|
|
969
1026
|
this.cursor = insertPos + chunk.length;
|
|
970
1027
|
this.emit('change', this.buffer);
|
|
971
|
-
this.updateSuggestions();
|
|
972
1028
|
this.scheduleRender();
|
|
973
1029
|
}
|
|
974
1030
|
insertNewline() {
|
|
@@ -993,7 +1049,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
993
1049
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
994
1050
|
}
|
|
995
1051
|
this.emit('change', this.buffer);
|
|
996
|
-
this.updateSuggestions();
|
|
997
1052
|
this.scheduleRender();
|
|
998
1053
|
}
|
|
999
1054
|
deleteForward() {
|
|
@@ -1221,13 +1276,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1221
1276
|
timestamp: Date.now(),
|
|
1222
1277
|
});
|
|
1223
1278
|
this.emit('queue', text);
|
|
1224
|
-
this.clear(); // Clear immediately for queued input
|
|
1279
|
+
this.clear(); // Clear immediately for queued input
|
|
1225
1280
|
}
|
|
1226
1281
|
else {
|
|
1227
|
-
// In idle mode, clear the input
|
|
1228
|
-
// The
|
|
1229
|
-
|
|
1230
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1282
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1283
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1284
|
+
this.clear();
|
|
1231
1285
|
this.emit('submit', text);
|
|
1232
1286
|
}
|
|
1233
1287
|
}
|
|
@@ -1244,7 +1298,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1244
1298
|
if (available <= 0)
|
|
1245
1299
|
return;
|
|
1246
1300
|
const chunk = clean.slice(0, available);
|
|
1247
|
-
|
|
1301
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1302
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1303
|
+
if (isMultiline && !isShortMultiline) {
|
|
1248
1304
|
this.insertPastePlaceholder(chunk);
|
|
1249
1305
|
}
|
|
1250
1306
|
else {
|
|
@@ -1380,17 +1436,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1380
1436
|
this.shiftPlaceholders(position, text.length);
|
|
1381
1437
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1382
1438
|
}
|
|
1439
|
+
shouldInlineMultiline(content) {
|
|
1440
|
+
const lines = content.split('\n').length;
|
|
1441
|
+
const maxInlineLines = 4;
|
|
1442
|
+
const maxInlineChars = 240;
|
|
1443
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1444
|
+
}
|
|
1383
1445
|
findPlaceholderAt(position) {
|
|
1384
1446
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1385
1447
|
}
|
|
1386
|
-
buildPlaceholder(
|
|
1448
|
+
buildPlaceholder(lineCount) {
|
|
1387
1449
|
const id = ++this.pasteCounter;
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
const preview = summary.preview.length > 30
|
|
1391
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1392
|
-
: summary.preview;
|
|
1393
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1450
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1451
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1394
1452
|
return { id, placeholder };
|
|
1395
1453
|
}
|
|
1396
1454
|
insertPastePlaceholder(content) {
|
|
@@ -1398,67 +1456,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1398
1456
|
if (available <= 0)
|
|
1399
1457
|
return;
|
|
1400
1458
|
const cleanContent = content.slice(0, available);
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
if (summary.lineCount < 5) {
|
|
1404
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1405
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1406
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1407
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1459
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1460
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1411
1461
|
const insertPos = this.cursor;
|
|
1412
1462
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1413
1463
|
this.pastePlaceholders.push({
|
|
1414
1464
|
id,
|
|
1415
1465
|
content: cleanContent,
|
|
1416
|
-
lineCount
|
|
1466
|
+
lineCount,
|
|
1417
1467
|
placeholder,
|
|
1418
1468
|
start: insertPos,
|
|
1419
1469
|
end: insertPos + placeholder.length,
|
|
1420
|
-
summary,
|
|
1421
|
-
expanded: false,
|
|
1422
1470
|
});
|
|
1423
1471
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1424
1472
|
this.cursor = insertPos + placeholder.length;
|
|
1425
1473
|
}
|
|
1426
|
-
/**
|
|
1427
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1428
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1429
|
-
*/
|
|
1430
|
-
togglePasteExpansion() {
|
|
1431
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1432
|
-
if (!placeholder)
|
|
1433
|
-
return false;
|
|
1434
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1435
|
-
// Update the placeholder text in buffer
|
|
1436
|
-
const newPlaceholder = placeholder.expanded
|
|
1437
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1438
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1439
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1440
|
-
// Update buffer
|
|
1441
|
-
this.buffer =
|
|
1442
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1443
|
-
newPlaceholder +
|
|
1444
|
-
this.buffer.slice(placeholder.end);
|
|
1445
|
-
// Update placeholder tracking
|
|
1446
|
-
placeholder.placeholder = newPlaceholder;
|
|
1447
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1448
|
-
// Shift other placeholders
|
|
1449
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1450
|
-
this.scheduleRender();
|
|
1451
|
-
return true;
|
|
1452
|
-
}
|
|
1453
|
-
buildExpandedPlaceholder(ph) {
|
|
1454
|
-
const lines = ph.content.split('\n');
|
|
1455
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1456
|
-
const lastLines = lines.length > 5
|
|
1457
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1458
|
-
: '';
|
|
1459
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1460
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1461
|
-
}
|
|
1462
1474
|
deletePlaceholder(placeholder) {
|
|
1463
1475
|
const length = placeholder.end - placeholder.start;
|
|
1464
1476
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1466,7 +1478,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1466
1478
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1467
1479
|
this.cursor = placeholder.start;
|
|
1468
1480
|
}
|
|
1469
|
-
updateContextUsage(value) {
|
|
1481
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1482
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1483
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1484
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1485
|
+
}
|
|
1470
1486
|
if (value === null || !Number.isFinite(value)) {
|
|
1471
1487
|
this.contextUsage = null;
|
|
1472
1488
|
}
|
|
@@ -1493,6 +1509,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1493
1509
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1494
1510
|
this.setEditMode(next);
|
|
1495
1511
|
}
|
|
1512
|
+
scheduleStreamingRender(delayMs) {
|
|
1513
|
+
if (this.streamingRenderTimer)
|
|
1514
|
+
return;
|
|
1515
|
+
const wait = Math.max(16, delayMs);
|
|
1516
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1517
|
+
this.streamingRenderTimer = null;
|
|
1518
|
+
this.render();
|
|
1519
|
+
}, wait);
|
|
1520
|
+
}
|
|
1521
|
+
resetStreamingRenderThrottle() {
|
|
1522
|
+
if (this.streamingRenderTimer) {
|
|
1523
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1524
|
+
this.streamingRenderTimer = null;
|
|
1525
|
+
}
|
|
1526
|
+
this.lastStreamingRender = 0;
|
|
1527
|
+
}
|
|
1496
1528
|
scheduleRender() {
|
|
1497
1529
|
if (!this.canRender())
|
|
1498
1530
|
return;
|