erosolar-cli 1.7.331 → 1.7.332
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -148
- package/dist/bin/erosolar.js +5 -21
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +56 -31
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/contracts/agent-schemas.json +0 -15
- package/dist/contracts/tools.schema.json +0 -9
- package/dist/core/agent.d.ts +2 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js.map +1 -1
- package/dist/core/customCommands.d.ts +1 -0
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +3 -0
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/hooks.d.ts +113 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +267 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/metricsTracker.d.ts +122 -0
- package/dist/core/metricsTracker.d.ts.map +1 -0
- package/dist/{alpha-zero → core}/metricsTracker.js +2 -5
- package/dist/core/metricsTracker.js.map +1 -0
- package/dist/core/securityAssessment.d.ts +91 -0
- package/dist/core/securityAssessment.d.ts.map +1 -0
- package/dist/core/securityAssessment.js +580 -0
- package/dist/core/securityAssessment.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +0 -14
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +22 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +0 -5
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +14 -3
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/validationRunner.d.ts +1 -3
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/core/verification.d.ts +137 -0
- package/dist/core/verification.d.ts.map +1 -0
- package/dist/core/verification.js +323 -0
- package/dist/core/verification.js.map +1 -0
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +21 -0
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +9 -18
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +4 -10
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +0 -2
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +2 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +16 -7
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +235 -166
- 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 +149 -117
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +677 -517
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +79 -20
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +99 -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,21 @@
|
|
|
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';
|
|
20
|
+
import { theme } from '../ui/theme.js';
|
|
15
21
|
// ANSI escape codes
|
|
16
22
|
const ESC = {
|
|
17
23
|
// Cursor control
|
|
@@ -21,14 +27,18 @@ const ESC = {
|
|
|
21
27
|
SHOW: '\x1b[?25h',
|
|
22
28
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
23
29
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
24
|
-
// Screen control
|
|
25
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
26
|
-
HOME: '\x1b[H',
|
|
27
|
-
ALT_SCREEN_ENTER: '\x1b[?1049h', // Enter alternate screen buffer
|
|
28
|
-
ALT_SCREEN_EXIT: '\x1b[?1049l', // Exit alternate screen buffer
|
|
29
30
|
// Line control
|
|
30
31
|
CLEAR_LINE: '\x1b[2K',
|
|
31
32
|
CLEAR_TO_END: '\x1b[0J',
|
|
33
|
+
// Screen control
|
|
34
|
+
HOME: '\x1b[H',
|
|
35
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
36
|
+
// Alternate screen buffer (like vim/tmux)
|
|
37
|
+
ENTER_ALT_SCREEN: '\x1b[?1049h',
|
|
38
|
+
EXIT_ALT_SCREEN: '\x1b[?1049l',
|
|
39
|
+
// Scroll region
|
|
40
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
41
|
+
RESET_SCROLL: '\x1b[r',
|
|
32
42
|
// Style
|
|
33
43
|
RESET: '\x1b[0m',
|
|
34
44
|
DIM: '\x1b[2m',
|
|
@@ -68,47 +78,46 @@ export class TerminalInput extends EventEmitter {
|
|
|
68
78
|
statusMessage = null;
|
|
69
79
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
70
80
|
streamingLabel = null; // Streaming progress indicator
|
|
81
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
82
|
+
metaTokensUsed = null; // Optional token usage
|
|
83
|
+
metaTokenLimit = null; // Optional token window
|
|
84
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
85
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
71
86
|
lastRenderContent = '';
|
|
72
87
|
lastRenderCursor = -1;
|
|
73
88
|
renderDirty = false;
|
|
74
89
|
isRendering = false;
|
|
75
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
76
|
-
inputAreaStartRow = 0; // Track absolute row position of input area
|
|
77
|
-
contentEndRow = 0; // Row where content ends (chat box renders below this)
|
|
78
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
79
|
-
commandSuggestions = [];
|
|
80
|
-
filteredSuggestions = [];
|
|
81
|
-
selectedSuggestionIndex = 0;
|
|
82
|
-
showSuggestions = false;
|
|
83
90
|
// Lifecycle
|
|
84
91
|
disposed = false;
|
|
85
92
|
enabled = true;
|
|
86
93
|
contextUsage = null;
|
|
94
|
+
contextAutoCompactThreshold = 90;
|
|
95
|
+
// Track current content row (starts at top, moves down)
|
|
96
|
+
contentRow = 1;
|
|
97
|
+
// Track if scroll region is currently active
|
|
98
|
+
scrollRegionActive = false;
|
|
99
|
+
thinkingModeLabel = null;
|
|
87
100
|
editMode = 'display-edits';
|
|
88
101
|
verificationEnabled = true;
|
|
89
102
|
autoContinueEnabled = false;
|
|
90
103
|
verificationHotkey = 'alt+v';
|
|
91
104
|
autoContinueHotkey = 'alt+c';
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
105
|
+
thinkingHotkey = '/thinking';
|
|
106
|
+
modelLabel = null;
|
|
107
|
+
providerLabel = null;
|
|
108
|
+
// Streaming render throttle
|
|
109
|
+
lastStreamingRender = 0;
|
|
110
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
99
111
|
streamingRenderTimer = null;
|
|
100
|
-
// Unified UI initialization flag
|
|
101
|
-
unifiedUIInitialized = false;
|
|
102
112
|
constructor(writeStream = process.stdout, config = {}) {
|
|
103
113
|
super();
|
|
104
114
|
this.out = writeStream;
|
|
105
|
-
// Use schema defaults for configuration consistency
|
|
106
115
|
this.config = {
|
|
107
|
-
maxLines: config.maxLines ??
|
|
108
|
-
maxLength: config.maxLength ??
|
|
116
|
+
maxLines: config.maxLines ?? 1000,
|
|
117
|
+
maxLength: config.maxLength ?? 10000,
|
|
109
118
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
110
|
-
promptChar: config.promptChar ??
|
|
111
|
-
continuationChar: config.continuationChar ??
|
|
119
|
+
promptChar: config.promptChar ?? '> ',
|
|
120
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
112
121
|
};
|
|
113
122
|
}
|
|
114
123
|
// ===========================================================================
|
|
@@ -187,290 +196,36 @@ export class TerminalInput extends EventEmitter {
|
|
|
187
196
|
if (handled)
|
|
188
197
|
return;
|
|
189
198
|
}
|
|
190
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
191
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
192
|
-
this.emit('showHelp');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
199
|
// Insert printable characters
|
|
196
200
|
if (str && !key?.ctrl && !key?.meta) {
|
|
197
201
|
this.insertText(str);
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
201
|
-
bannerContent = null;
|
|
202
|
-
/**
|
|
203
|
-
* Set banner content to be written when unified UI initializes.
|
|
204
|
-
*/
|
|
205
|
-
setBannerContent(content) {
|
|
206
|
-
this.bannerContent = content;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Initialize the unified UI system with BOTTOM PINNED chat box.
|
|
210
|
-
*
|
|
211
|
-
* Layout:
|
|
212
|
-
* 1. Clear screen
|
|
213
|
-
* 2. Write banner at top
|
|
214
|
-
* 3. Set content cursor row after banner
|
|
215
|
-
* 4. Render chat box at bottom (sets up scroll region)
|
|
216
|
-
*/
|
|
217
|
-
initializeUnifiedUI() {
|
|
218
|
-
if (this.unifiedUIInitialized) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
// Enter alternate screen buffer for complete terminal control
|
|
222
|
-
this.write(ESC.ALT_SCREEN_ENTER);
|
|
223
|
-
// Hide cursor during setup
|
|
224
|
-
this.write(ESC.HIDE);
|
|
225
|
-
// Clear screen and go home (in alternate buffer)
|
|
226
|
-
this.write(ESC.HOME);
|
|
227
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
228
|
-
// Write banner at top
|
|
229
|
-
let bannerLines = 0;
|
|
230
|
-
if (this.bannerContent) {
|
|
231
|
-
const lines = this.bannerContent.split('\n');
|
|
232
|
-
bannerLines = lines.length + 2; // +2 for trailing \n\n
|
|
233
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
234
|
-
}
|
|
235
|
-
// Set content cursor row after banner
|
|
236
|
-
this.contentCursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
|
|
237
|
-
// Content ends at same row initially (no content yet)
|
|
238
|
-
this.contentEndRow = this.contentCursorRow - 1;
|
|
239
|
-
// Mark initialized
|
|
240
|
-
this.unifiedUIInitialized = true;
|
|
241
|
-
// Render floating chat box below content
|
|
242
|
-
this.renderFloatingInputArea();
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Clear the input area at its tracked position.
|
|
246
|
-
* Returns true if something was cleared.
|
|
247
|
-
*/
|
|
248
|
-
clearInputArea() {
|
|
249
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
250
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
251
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
252
|
-
this.write(ESC.CLEAR_LINE);
|
|
253
|
-
}
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Reset input area tracking state.
|
|
260
|
-
*/
|
|
261
|
-
resetInputAreaTracking() {
|
|
262
|
-
this.inputAreaStartRow = 0;
|
|
263
|
-
this.flowModeRenderedLines = 0;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Render chat box - FLOATING below content (no scroll regions).
|
|
267
|
-
* Chat box appears right after content and moves down as content grows.
|
|
268
|
-
* Content and banner scroll naturally off the top of the screen.
|
|
269
|
-
*/
|
|
270
|
-
renderFloatingInputArea() {
|
|
271
|
-
const { cols } = this.getSize();
|
|
272
|
-
const divider = '─'.repeat(cols);
|
|
273
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
274
|
-
// Chat box is 4 lines: divider + input + divider + controls
|
|
275
|
-
const chatBoxHeight = 4;
|
|
276
|
-
// Chat box starts right after content
|
|
277
|
-
const chatBoxStartRow = this.contentEndRow + 1;
|
|
278
|
-
// Hide cursor during render
|
|
279
|
-
this.write(ESC.HIDE);
|
|
280
|
-
// Track position
|
|
281
|
-
this.inputAreaStartRow = chatBoxStartRow;
|
|
282
|
-
let currentRow = chatBoxStartRow;
|
|
283
|
-
// Helper to write a line at absolute position (clears then writes)
|
|
284
|
-
const writeLine = (content) => {
|
|
285
|
-
this.write(ESC.TO(currentRow, 1));
|
|
286
|
-
this.write(ESC.CLEAR_LINE);
|
|
287
|
-
this.write(content);
|
|
288
|
-
currentRow++;
|
|
289
|
-
};
|
|
290
|
-
// Top divider
|
|
291
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
292
|
-
// Input line with > prompt
|
|
293
|
-
const { lines, cursorCol } = this.wrapBuffer(cols - 3);
|
|
294
|
-
const displayLine = lines[0] ?? '';
|
|
295
|
-
const inputRow = currentRow;
|
|
296
|
-
writeLine(`${DIM}>${R} ${displayLine}`);
|
|
297
|
-
// Bottom divider
|
|
298
|
-
writeLine(`${DIM}${divider}${R}`);
|
|
299
|
-
// Mode controls line - Claude Code style
|
|
300
|
-
this.write(ESC.TO(currentRow, 1));
|
|
301
|
-
this.write(ESC.CLEAR_LINE);
|
|
302
|
-
this.write(this.buildClaudeStyleControls(cols));
|
|
303
|
-
// Track lines rendered
|
|
304
|
-
this.flowModeRenderedLines = chatBoxHeight;
|
|
305
|
-
// Position cursor at input line for typing
|
|
306
|
-
this.write(ESC.TO(inputRow, 3 + cursorCol));
|
|
307
|
-
// Show cursor
|
|
308
|
-
this.write(ESC.SHOW);
|
|
309
|
-
// Update tracking
|
|
310
|
-
this.lastRenderContent = this.buffer;
|
|
311
|
-
this.lastRenderCursor = this.cursor;
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Build Claude Code style controls line.
|
|
315
|
-
* Shows: edit mode indicator (shift+tab to cycle)
|
|
316
|
-
*/
|
|
317
|
-
buildClaudeStyleControls(cols) {
|
|
318
|
-
const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
|
|
319
|
-
// Edit mode indicator
|
|
320
|
-
let editModeText;
|
|
321
|
-
if (this.editMode === 'display-edits') {
|
|
322
|
-
editModeText = `${GREEN}⏵⏵${R} accept edits on`;
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
|
|
326
|
-
}
|
|
327
|
-
// Build controls line
|
|
328
|
-
const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
|
|
329
|
-
// Add thinking mode if enabled
|
|
330
|
-
if (this.thinkingEnabled) {
|
|
331
|
-
parts.push(`${CYAN}💭${R}`);
|
|
332
|
-
}
|
|
333
|
-
// Add context usage if available
|
|
334
|
-
if (this.contextUsage !== null) {
|
|
335
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
336
|
-
if (rem < 10) {
|
|
337
|
-
parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
|
|
338
|
-
}
|
|
339
|
-
else if (rem < 25) {
|
|
340
|
-
parts.push(`${YELLOW}ctx ${rem}%${R}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return parts.join(` ${DIM}·${R} `);
|
|
344
|
-
}
|
|
345
204
|
/**
|
|
346
205
|
* Set the input mode
|
|
347
206
|
*
|
|
348
|
-
*
|
|
349
|
-
* Scroll region protects chat box, content scrolls above it.
|
|
207
|
+
* Content flows naturally - no scroll region pinning.
|
|
350
208
|
*/
|
|
351
209
|
setMode(mode) {
|
|
352
210
|
const prevMode = this.mode;
|
|
353
211
|
this.mode = mode;
|
|
354
212
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
355
|
-
|
|
356
|
-
this.streamingStartTime = Date.now();
|
|
357
|
-
// Ensure unified UI is initialized
|
|
358
|
-
if (!this.unifiedUIInitialized) {
|
|
359
|
-
this.initializeUnifiedUI();
|
|
360
|
-
}
|
|
361
|
-
// Re-render to ensure scroll region is set correctly
|
|
213
|
+
this.resetStreamingRenderThrottle();
|
|
362
214
|
this.renderDirty = true;
|
|
363
|
-
this.
|
|
215
|
+
this.render();
|
|
364
216
|
}
|
|
365
217
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.streamingRenderTimer = null;
|
|
370
|
-
}
|
|
371
|
-
// Reset streaming time
|
|
372
|
-
this.streamingStartTime = null;
|
|
373
|
-
// Re-render chat box
|
|
374
|
-
this.renderDirty = true;
|
|
375
|
-
this.scheduleRender();
|
|
218
|
+
// Streaming ended - render the input area
|
|
219
|
+
this.resetStreamingRenderThrottle();
|
|
220
|
+
this.forceRender();
|
|
376
221
|
}
|
|
377
222
|
}
|
|
378
223
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*/
|
|
382
|
-
setContentEndRow(row) {
|
|
383
|
-
this.contentEndRow = Math.max(0, row);
|
|
384
|
-
this.renderDirty = true;
|
|
385
|
-
this.scheduleRender();
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Set available slash commands for auto-complete suggestions.
|
|
389
|
-
*/
|
|
390
|
-
setCommands(commands) {
|
|
391
|
-
this.commandSuggestions = commands;
|
|
392
|
-
this.updateSuggestions();
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Update filtered suggestions based on current input.
|
|
396
|
-
*/
|
|
397
|
-
updateSuggestions() {
|
|
398
|
-
const input = this.buffer.trim();
|
|
399
|
-
// Only show suggestions when input starts with "/"
|
|
400
|
-
if (!input.startsWith('/')) {
|
|
401
|
-
this.showSuggestions = false;
|
|
402
|
-
this.filteredSuggestions = [];
|
|
403
|
-
this.selectedSuggestionIndex = 0;
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
const query = input.toLowerCase();
|
|
407
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
408
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
409
|
-
// Show suggestions if we have matches
|
|
410
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
411
|
-
// Keep selection in bounds
|
|
412
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
413
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Select next suggestion (arrow down / tab).
|
|
418
|
-
*/
|
|
419
|
-
selectNextSuggestion() {
|
|
420
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
421
|
-
return;
|
|
422
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
423
|
-
this.renderDirty = true;
|
|
424
|
-
this.scheduleRender();
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
428
|
-
*/
|
|
429
|
-
selectPrevSuggestion() {
|
|
430
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
431
|
-
return;
|
|
432
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
433
|
-
? this.filteredSuggestions.length - 1
|
|
434
|
-
: this.selectedSuggestionIndex - 1;
|
|
435
|
-
this.renderDirty = true;
|
|
436
|
-
this.scheduleRender();
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Accept current suggestion and insert into buffer.
|
|
440
|
-
*/
|
|
441
|
-
acceptSuggestion() {
|
|
442
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
443
|
-
return false;
|
|
444
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
445
|
-
if (!selected)
|
|
446
|
-
return false;
|
|
447
|
-
// Replace buffer with selected command
|
|
448
|
-
this.buffer = selected.command + ' ';
|
|
449
|
-
this.cursor = this.buffer.length;
|
|
450
|
-
this.showSuggestions = false;
|
|
451
|
-
this.renderDirty = true;
|
|
452
|
-
this.scheduleRender();
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Check if suggestions are visible.
|
|
224
|
+
* Legacy method - no longer used (content flows naturally).
|
|
225
|
+
* @deprecated Use setContentRow instead
|
|
457
226
|
*/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Toggle thinking/reasoning mode
|
|
463
|
-
*/
|
|
464
|
-
toggleThinking() {
|
|
465
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
466
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
467
|
-
this.scheduleRender();
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Get thinking enabled state
|
|
471
|
-
*/
|
|
472
|
-
isThinkingEnabled() {
|
|
473
|
-
return this.thinkingEnabled;
|
|
227
|
+
setPinnedHeaderLines(_count) {
|
|
228
|
+
// No-op: scroll region pinning removed
|
|
474
229
|
}
|
|
475
230
|
/**
|
|
476
231
|
* Get current mode
|
|
@@ -503,17 +258,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
503
258
|
}
|
|
504
259
|
/**
|
|
505
260
|
* Clear the buffer
|
|
506
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
507
261
|
*/
|
|
508
|
-
clear(
|
|
262
|
+
clear() {
|
|
509
263
|
this.buffer = '';
|
|
510
264
|
this.cursor = 0;
|
|
511
265
|
this.historyIndex = -1;
|
|
512
266
|
this.tempInput = '';
|
|
513
267
|
this.pastePlaceholders = [];
|
|
514
|
-
|
|
515
|
-
this.scheduleRender();
|
|
516
|
-
}
|
|
268
|
+
this.scheduleRender();
|
|
517
269
|
}
|
|
518
270
|
/**
|
|
519
271
|
* Get queued inputs
|
|
@@ -584,6 +336,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
584
336
|
this.streamingLabel = next;
|
|
585
337
|
this.scheduleRender();
|
|
586
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
341
|
+
*/
|
|
342
|
+
setMetaStatus(meta) {
|
|
343
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
344
|
+
? Math.floor(meta.elapsedSeconds)
|
|
345
|
+
: null;
|
|
346
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
347
|
+
? Math.floor(meta.tokensUsed)
|
|
348
|
+
: null;
|
|
349
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
350
|
+
? Math.floor(meta.tokenLimit)
|
|
351
|
+
: null;
|
|
352
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
353
|
+
? Math.floor(meta.thinkingMs)
|
|
354
|
+
: null;
|
|
355
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
356
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
357
|
+
this.metaTokensUsed === nextTokens &&
|
|
358
|
+
this.metaTokenLimit === nextLimit &&
|
|
359
|
+
this.metaThinkingMs === nextThinking &&
|
|
360
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
364
|
+
this.metaTokensUsed = nextTokens;
|
|
365
|
+
this.metaTokenLimit = nextLimit;
|
|
366
|
+
this.metaThinkingMs = nextThinking;
|
|
367
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
368
|
+
this.scheduleRender();
|
|
369
|
+
}
|
|
587
370
|
/**
|
|
588
371
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
589
372
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -593,26 +376,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
593
376
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
594
377
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
595
378
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
379
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
380
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
596
381
|
if (this.verificationEnabled === nextVerification &&
|
|
597
382
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
598
383
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
599
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
384
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
385
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
386
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
600
387
|
return;
|
|
601
388
|
}
|
|
602
389
|
this.verificationEnabled = nextVerification;
|
|
603
390
|
this.autoContinueEnabled = nextAutoContinue;
|
|
604
391
|
this.verificationHotkey = nextVerifyHotkey;
|
|
605
392
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
606
|
-
this.
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
610
|
-
* This is displayed persistently above the input area.
|
|
611
|
-
*/
|
|
612
|
-
setModelInfo(info) {
|
|
613
|
-
if (this.modelInfo === info)
|
|
614
|
-
return;
|
|
615
|
-
this.modelInfo = info;
|
|
393
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
394
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
616
395
|
this.scheduleRender();
|
|
617
396
|
}
|
|
618
397
|
/**
|
|
@@ -625,33 +404,161 @@ export class TerminalInput extends EventEmitter {
|
|
|
625
404
|
this.scheduleRender();
|
|
626
405
|
}
|
|
627
406
|
/**
|
|
628
|
-
*
|
|
629
|
-
|
|
630
|
-
|
|
407
|
+
* Surface model/provider context in the controls bar.
|
|
408
|
+
*/
|
|
409
|
+
setModelContext(options) {
|
|
410
|
+
const nextModel = options.model?.trim() || null;
|
|
411
|
+
const nextProvider = options.provider?.trim() || null;
|
|
412
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.modelLabel = nextModel;
|
|
416
|
+
this.providerLabel = nextProvider;
|
|
417
|
+
this.scheduleRender();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Render the floating input area at contentRow.
|
|
421
|
+
*
|
|
422
|
+
* The chat box "floats" - it renders right below the last streamed content.
|
|
423
|
+
* As content is added, contentRow advances, and the chat box moves down.
|
|
424
|
+
* No scroll regions - pure floating behavior.
|
|
631
425
|
*/
|
|
632
426
|
render() {
|
|
633
427
|
if (!this.canRender())
|
|
634
428
|
return;
|
|
635
429
|
if (this.isRendering)
|
|
636
430
|
return;
|
|
431
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
432
|
+
// During streaming, throttle re-renders
|
|
433
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
434
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
435
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
436
|
+
if (waitMs > 0) {
|
|
437
|
+
this.renderDirty = true;
|
|
438
|
+
this.scheduleStreamingRender(waitMs);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
637
442
|
const shouldSkip = !this.renderDirty &&
|
|
638
443
|
this.buffer === this.lastRenderContent &&
|
|
639
444
|
this.cursor === this.lastRenderCursor;
|
|
640
445
|
this.renderDirty = false;
|
|
641
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
642
446
|
if (shouldSkip) {
|
|
643
447
|
return;
|
|
644
448
|
}
|
|
645
|
-
// If write lock is held, defer render
|
|
646
449
|
if (writeLock.isLocked()) {
|
|
647
450
|
writeLock.safeWrite(() => this.render());
|
|
648
451
|
return;
|
|
649
452
|
}
|
|
453
|
+
this.renderFloatingInputArea();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Core floating input area renderer.
|
|
457
|
+
* Chat box always floats at contentRow (below streamed content).
|
|
458
|
+
* This creates "persistent bottom floating" behavior.
|
|
459
|
+
*/
|
|
460
|
+
renderFloatingInputArea() {
|
|
461
|
+
const { rows, cols } = this.getSize();
|
|
462
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
463
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
464
|
+
// Wrap buffer into display lines
|
|
465
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
466
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
467
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
468
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
469
|
+
// Calculate display window (keep cursor visible)
|
|
470
|
+
let startLine = 0;
|
|
471
|
+
if (lines.length > displayLines) {
|
|
472
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
473
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
474
|
+
}
|
|
475
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
476
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
477
|
+
// Chat box height (must match getChatBoxHeight calculation)
|
|
478
|
+
const chatBoxHeight = metaLines.length + 1 + displayLines + 1;
|
|
479
|
+
// During streaming, use renderChatBoxAtBottom instead
|
|
480
|
+
if (this.scrollRegionActive) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Float chat box below content (not pinned to bottom)
|
|
484
|
+
const chatBoxStartRow = Math.max(1, this.contentRow + 1);
|
|
485
|
+
writeLock.lock('terminalInput.renderFloating');
|
|
650
486
|
this.isRendering = true;
|
|
651
|
-
writeLock.lock('terminalInput.render');
|
|
652
487
|
try {
|
|
653
|
-
//
|
|
654
|
-
this.
|
|
488
|
+
// Hide cursor during render
|
|
489
|
+
this.write(ESC.HIDE);
|
|
490
|
+
this.write(ESC.RESET);
|
|
491
|
+
// Clear the chat box area
|
|
492
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
493
|
+
const row = chatBoxStartRow + i;
|
|
494
|
+
if (row <= rows) {
|
|
495
|
+
this.write(ESC.TO(row, 1));
|
|
496
|
+
this.write(ESC.CLEAR_LINE);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
let currentRow = chatBoxStartRow;
|
|
500
|
+
// Meta/status header
|
|
501
|
+
for (const metaLine of metaLines) {
|
|
502
|
+
this.write(ESC.TO(currentRow, 1));
|
|
503
|
+
this.write(metaLine);
|
|
504
|
+
currentRow += 1;
|
|
505
|
+
}
|
|
506
|
+
// Separator line
|
|
507
|
+
this.write(ESC.TO(currentRow, 1));
|
|
508
|
+
this.write(renderDivider(cols - 2));
|
|
509
|
+
currentRow += 1;
|
|
510
|
+
// Render input lines
|
|
511
|
+
let finalRow = currentRow;
|
|
512
|
+
let finalCol = 3;
|
|
513
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
514
|
+
const rowNum = currentRow + i;
|
|
515
|
+
this.write(ESC.TO(rowNum, 1));
|
|
516
|
+
const line = visibleLines[i] ?? '';
|
|
517
|
+
const isFirstLine = (startLine + i) === 0;
|
|
518
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
519
|
+
this.write(ESC.BG_DARK);
|
|
520
|
+
this.write(ESC.DIM);
|
|
521
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
522
|
+
this.write(ESC.RESET);
|
|
523
|
+
this.write(ESC.BG_DARK);
|
|
524
|
+
if (isCursorLine) {
|
|
525
|
+
const col = Math.min(cursorCol, line.length);
|
|
526
|
+
const before = line.slice(0, col);
|
|
527
|
+
const at = col < line.length ? line[col] : ' ';
|
|
528
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
529
|
+
this.write(before);
|
|
530
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
531
|
+
this.write(at);
|
|
532
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
533
|
+
this.write(after);
|
|
534
|
+
finalRow = rowNum;
|
|
535
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
this.write(line);
|
|
539
|
+
}
|
|
540
|
+
// Pad to edge
|
|
541
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
542
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
543
|
+
if (padding > 0)
|
|
544
|
+
this.write(' '.repeat(padding));
|
|
545
|
+
this.write(ESC.RESET);
|
|
546
|
+
}
|
|
547
|
+
// Mode controls line
|
|
548
|
+
const controlRow = currentRow + visibleLines.length;
|
|
549
|
+
this.write(ESC.TO(controlRow, 1));
|
|
550
|
+
this.write(this.buildModeControls(cols));
|
|
551
|
+
// Position cursor in input box
|
|
552
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
553
|
+
this.write(ESC.SHOW);
|
|
554
|
+
// Update state
|
|
555
|
+
this.lastRenderContent = this.buffer;
|
|
556
|
+
this.lastRenderCursor = this.cursor;
|
|
557
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
558
|
+
if (this.streamingRenderTimer) {
|
|
559
|
+
clearTimeout(this.streamingRenderTimer);
|
|
560
|
+
this.streamingRenderTimer = null;
|
|
561
|
+
}
|
|
655
562
|
}
|
|
656
563
|
finally {
|
|
657
564
|
writeLock.unlock();
|
|
@@ -659,99 +566,181 @@ export class TerminalInput extends EventEmitter {
|
|
|
659
566
|
}
|
|
660
567
|
}
|
|
661
568
|
/**
|
|
662
|
-
* Build
|
|
663
|
-
*
|
|
569
|
+
* Build compact meta line above the divider.
|
|
570
|
+
* Shows model/provider and key metrics in a single line.
|
|
571
|
+
* Status message is shown in mode controls to avoid duplication.
|
|
664
572
|
*/
|
|
665
|
-
|
|
666
|
-
const maxWidth = cols - 2;
|
|
573
|
+
buildMetaLines(width) {
|
|
667
574
|
const parts = [];
|
|
668
|
-
//
|
|
669
|
-
if (this.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const secs = elapsed % 60;
|
|
675
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
676
|
-
}
|
|
677
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
575
|
+
// Model/provider info
|
|
576
|
+
if (this.modelLabel) {
|
|
577
|
+
const modelText = this.providerLabel
|
|
578
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
579
|
+
: this.modelLabel;
|
|
580
|
+
parts.push({ text: modelText, tone: 'info' });
|
|
678
581
|
}
|
|
679
|
-
//
|
|
680
|
-
if (this.
|
|
681
|
-
parts.push(
|
|
582
|
+
// Elapsed time
|
|
583
|
+
if (this.metaElapsedSeconds !== null) {
|
|
584
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
682
585
|
}
|
|
683
|
-
//
|
|
684
|
-
if (this.
|
|
685
|
-
const
|
|
686
|
-
|
|
586
|
+
// Token usage (compact)
|
|
587
|
+
if (this.metaTokensUsed !== null) {
|
|
588
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
589
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
590
|
+
parts.push({ text: `${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
687
591
|
}
|
|
688
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
592
|
+
// Context remaining (only show if concerning)
|
|
593
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
594
|
+
if (tokensRemaining !== null) {
|
|
595
|
+
parts.push({ text: `↓${tokensRemaining}`, tone: 'muted' });
|
|
691
596
|
}
|
|
692
|
-
//
|
|
693
|
-
if (this.
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
// Multi-line indicator
|
|
697
|
-
if (this.buffer.includes('\n')) {
|
|
698
|
-
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
597
|
+
// Thinking indicator
|
|
598
|
+
if (this.metaThinkingMs !== null) {
|
|
599
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
699
600
|
}
|
|
700
|
-
if (parts.length
|
|
701
|
-
return
|
|
601
|
+
if (!parts.length) {
|
|
602
|
+
return [];
|
|
702
603
|
}
|
|
703
|
-
|
|
704
|
-
return joined.slice(0, maxWidth);
|
|
604
|
+
return [renderStatusLine(parts, width)];
|
|
705
605
|
}
|
|
706
606
|
/**
|
|
707
|
-
* Build mode controls line
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
607
|
+
* Build Claude Code style mode controls line.
|
|
608
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
711
609
|
*/
|
|
712
610
|
buildModeControls(cols) {
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
719
|
-
if (this.editMode === 'display-edits') {
|
|
720
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
611
|
+
const width = Math.max(8, cols - 2);
|
|
612
|
+
const leftParts = [];
|
|
613
|
+
const rightParts = [];
|
|
614
|
+
if (this.streamingLabel) {
|
|
615
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
721
616
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
617
|
+
if (this.overrideStatusMessage) {
|
|
618
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
619
|
+
}
|
|
620
|
+
if (this.statusMessage) {
|
|
621
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
622
|
+
}
|
|
623
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
624
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
625
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
626
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
627
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
628
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
629
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
630
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
631
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
632
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
633
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
634
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
635
|
+
}
|
|
636
|
+
if (this.buffer.includes('\n')) {
|
|
637
|
+
const lineCount = this.buffer.split('\n').length;
|
|
638
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
639
|
+
}
|
|
640
|
+
if (this.pastePlaceholders.length > 0) {
|
|
641
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
642
|
+
leftParts.push({
|
|
643
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
644
|
+
tone: 'info',
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
const contextRemaining = this.computeContextRemaining();
|
|
648
|
+
if (this.thinkingModeLabel) {
|
|
649
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
650
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
651
|
+
}
|
|
652
|
+
// Model info is now in meta lines only - no duplication here
|
|
653
|
+
if (contextRemaining !== null) {
|
|
654
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
655
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
656
|
+
? 'Context auto-compact imminent'
|
|
657
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
658
|
+
rightParts.push({ text: label, tone });
|
|
659
|
+
}
|
|
660
|
+
if (!rightParts.length || width < 60) {
|
|
661
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
662
|
+
return renderStatusLine(merged, width);
|
|
663
|
+
}
|
|
664
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
665
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
666
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
667
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
668
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
669
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
670
|
+
}
|
|
671
|
+
formatHotkey(hotkey) {
|
|
672
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
673
|
+
if (!normalized)
|
|
674
|
+
return hotkey;
|
|
675
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
676
|
+
const map = {
|
|
677
|
+
shift: '⇧',
|
|
678
|
+
sh: '⇧',
|
|
679
|
+
alt: '⌥',
|
|
680
|
+
option: '⌥',
|
|
681
|
+
opt: '⌥',
|
|
682
|
+
ctrl: '⌃',
|
|
683
|
+
control: '⌃',
|
|
684
|
+
cmd: '⌘',
|
|
685
|
+
meta: '⌘',
|
|
686
|
+
};
|
|
687
|
+
const formatted = parts
|
|
688
|
+
.map((part) => {
|
|
689
|
+
const symbol = map[part];
|
|
690
|
+
if (symbol)
|
|
691
|
+
return symbol;
|
|
692
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
693
|
+
})
|
|
694
|
+
.join('');
|
|
695
|
+
return formatted || hotkey;
|
|
696
|
+
}
|
|
697
|
+
computeContextRemaining() {
|
|
698
|
+
if (this.contextUsage === null) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
702
|
+
}
|
|
703
|
+
computeTokensRemaining() {
|
|
704
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
708
|
+
return this.formatTokenCount(remaining);
|
|
709
|
+
}
|
|
710
|
+
formatElapsedLabel(seconds) {
|
|
711
|
+
if (seconds < 60) {
|
|
712
|
+
return `${seconds}s`;
|
|
713
|
+
}
|
|
714
|
+
const mins = Math.floor(seconds / 60);
|
|
715
|
+
const secs = seconds % 60;
|
|
716
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
717
|
+
}
|
|
718
|
+
formatTokenCount(value) {
|
|
719
|
+
if (!Number.isFinite(value)) {
|
|
720
|
+
return `${value}`;
|
|
721
|
+
}
|
|
722
|
+
if (value >= 1_000_000) {
|
|
723
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
724
|
+
}
|
|
725
|
+
if (value >= 1_000) {
|
|
726
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
727
|
+
}
|
|
728
|
+
return `${Math.round(value)}`;
|
|
729
|
+
}
|
|
730
|
+
visibleLength(value) {
|
|
731
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
732
|
+
return value.replace(ansiPattern, '').length;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
736
|
+
* needing a TTY. Not used by production code.
|
|
737
|
+
*/
|
|
738
|
+
getDebugUiSnapshot(width) {
|
|
739
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
740
|
+
return {
|
|
741
|
+
meta: this.buildMetaLines(cols - 2),
|
|
742
|
+
controls: this.buildModeControls(cols),
|
|
743
|
+
};
|
|
755
744
|
}
|
|
756
745
|
/**
|
|
757
746
|
* Force a re-render
|
|
@@ -774,58 +763,263 @@ export class TerminalInput extends EventEmitter {
|
|
|
774
763
|
handleResize() {
|
|
775
764
|
this.lastRenderContent = '';
|
|
776
765
|
this.lastRenderCursor = -1;
|
|
766
|
+
this.resetStreamingRenderThrottle();
|
|
777
767
|
this.scheduleRender();
|
|
778
768
|
}
|
|
779
|
-
// Track current content row for writing
|
|
780
|
-
contentCursorRow = 1;
|
|
781
769
|
/**
|
|
782
|
-
*
|
|
783
|
-
*
|
|
770
|
+
* Enter streaming mode with scroll region.
|
|
771
|
+
* Sets up terminal scroll region to exclude chat box.
|
|
784
772
|
*/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
});
|
|
773
|
+
enterStreamingScrollRegion() {
|
|
774
|
+
const { rows } = this.getSize();
|
|
775
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
776
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
777
|
+
writeLock.lock('enterStreamingScrollRegion');
|
|
778
|
+
try {
|
|
779
|
+
// Set scroll region for content area
|
|
780
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
781
|
+
// Position cursor at current content row
|
|
782
|
+
this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
|
|
783
|
+
this.scrollRegionActive = true;
|
|
784
|
+
this.setStatusMessage('esc to interrupt');
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
writeLock.unlock();
|
|
788
|
+
}
|
|
789
|
+
// Render chat box at bottom (outside scroll region)
|
|
790
|
+
this.renderChatBoxAtBottom();
|
|
804
791
|
}
|
|
805
792
|
/**
|
|
806
|
-
*
|
|
793
|
+
* Exit streaming mode and restore normal operation.
|
|
807
794
|
*/
|
|
808
|
-
|
|
809
|
-
|
|
795
|
+
exitStreamingScrollRegion() {
|
|
796
|
+
writeLock.lock('exitStreamingScrollRegion');
|
|
797
|
+
try {
|
|
798
|
+
// Reset scroll region to full terminal
|
|
799
|
+
this.write(ESC.RESET_SCROLL);
|
|
800
|
+
this.scrollRegionActive = false;
|
|
801
|
+
this.setStatusMessage('Ready for prompts');
|
|
802
|
+
}
|
|
803
|
+
finally {
|
|
804
|
+
writeLock.unlock();
|
|
805
|
+
}
|
|
806
|
+
// Final render
|
|
807
|
+
this.forceRender();
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Render just the chat box at the bottom of terminal.
|
|
811
|
+
* Used during streaming to update status without affecting content.
|
|
812
|
+
*/
|
|
813
|
+
renderChatBoxAtBottom() {
|
|
814
|
+
const { rows, cols: width } = this.getSize();
|
|
815
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
816
|
+
const pinnedRow = rows - chatBoxHeight + 1;
|
|
817
|
+
// During streaming: ALWAYS pin to absolute bottom (outside scroll region)
|
|
818
|
+
// When idle: float below content when there's room
|
|
819
|
+
let chatBoxStartRow;
|
|
820
|
+
if (this.scrollRegionActive) {
|
|
821
|
+
chatBoxStartRow = Math.max(1, pinnedRow);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const floatingRow = this.contentRow + 1;
|
|
825
|
+
chatBoxStartRow = Math.max(1, Math.min(floatingRow, pinnedRow));
|
|
826
|
+
}
|
|
827
|
+
const metaLines = this.buildMetaLines(width);
|
|
828
|
+
const modeControls = this.buildModeControls(width);
|
|
829
|
+
const divider = theme.ui.muted('─'.repeat(width));
|
|
830
|
+
// Get input area content
|
|
831
|
+
const lines = this.buffer.split('\n');
|
|
832
|
+
const maxDisplayLines = 3;
|
|
833
|
+
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
834
|
+
const visibleLines = lines.slice(Math.max(0, lines.length - displayLines));
|
|
835
|
+
const scrollEnd = rows - chatBoxHeight;
|
|
836
|
+
writeLock.lock('renderChatBoxAtBottom');
|
|
837
|
+
try {
|
|
838
|
+
this.write(ESC.SAVE);
|
|
839
|
+
this.write(ESC.HIDE);
|
|
840
|
+
// Temporarily reset scroll region to write chat box cleanly
|
|
841
|
+
if (this.scrollRegionActive) {
|
|
842
|
+
this.write(ESC.RESET_SCROLL);
|
|
843
|
+
}
|
|
844
|
+
let row = chatBoxStartRow;
|
|
845
|
+
// Meta lines (single line)
|
|
846
|
+
for (const line of metaLines) {
|
|
847
|
+
this.write(ESC.TO(row++, 1));
|
|
848
|
+
this.write(ESC.CLEAR_LINE);
|
|
849
|
+
this.write(line);
|
|
850
|
+
}
|
|
851
|
+
// Divider
|
|
852
|
+
this.write(ESC.TO(row++, 1));
|
|
853
|
+
this.write(ESC.CLEAR_LINE);
|
|
854
|
+
this.write(divider);
|
|
855
|
+
// Input area
|
|
856
|
+
for (const line of visibleLines) {
|
|
857
|
+
this.write(ESC.TO(row++, 1));
|
|
858
|
+
this.write(ESC.CLEAR_LINE);
|
|
859
|
+
this.write(this.config.promptChar);
|
|
860
|
+
this.write(line);
|
|
861
|
+
}
|
|
862
|
+
// Mode controls
|
|
863
|
+
this.write(ESC.TO(row, 1));
|
|
864
|
+
this.write(ESC.CLEAR_LINE);
|
|
865
|
+
this.write(modeControls);
|
|
866
|
+
// Restore scroll region and cursor position for content
|
|
867
|
+
if (this.scrollRegionActive) {
|
|
868
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
869
|
+
// Position cursor at content row for next write
|
|
870
|
+
this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
|
|
871
|
+
}
|
|
872
|
+
this.write(ESC.RESTORE);
|
|
873
|
+
this.write(ESC.SHOW);
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
writeLock.unlock();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Stream content within the scroll region.
|
|
881
|
+
* Content is written directly and scrolls naturally.
|
|
882
|
+
*/
|
|
883
|
+
streamContent(content) {
|
|
884
|
+
if (!content)
|
|
810
885
|
return;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
this.
|
|
886
|
+
writeLock.lock('streamContent');
|
|
887
|
+
try {
|
|
888
|
+
// Write content - scroll region handles scrolling
|
|
889
|
+
this.write(content);
|
|
890
|
+
// Track newlines
|
|
891
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
892
|
+
this.contentRow += newlines;
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
writeLock.unlock();
|
|
896
|
+
}
|
|
897
|
+
// Throttle chat box updates during streaming
|
|
898
|
+
this.scheduleStreamingRender(200);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Enable scroll region (no-op in floating mode).
|
|
902
|
+
*/
|
|
903
|
+
enableScrollRegion() {
|
|
904
|
+
// No-op: using pure floating approach
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Disable scroll region (no-op in floating mode).
|
|
908
|
+
*/
|
|
909
|
+
disableScrollRegion() {
|
|
910
|
+
// No-op: using pure floating approach
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Calculate chat box height.
|
|
914
|
+
*/
|
|
915
|
+
getChatBoxHeight() {
|
|
916
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* @deprecated Use streamContent() instead
|
|
920
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
921
|
+
*/
|
|
922
|
+
registerOutputInterceptor(_display) {
|
|
923
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Write content above the floating chat box.
|
|
927
|
+
* Works both during streaming and when idle.
|
|
928
|
+
*/
|
|
929
|
+
writeToScrollRegion(content) {
|
|
930
|
+
if (!content)
|
|
931
|
+
return;
|
|
932
|
+
writeLock.lock('writeToScrollRegion');
|
|
933
|
+
try {
|
|
934
|
+
// Position cursor at content row and write
|
|
935
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
936
|
+
this.write(content);
|
|
937
|
+
// Track newlines
|
|
938
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
939
|
+
this.contentRow += newlines;
|
|
940
|
+
}
|
|
941
|
+
finally {
|
|
942
|
+
writeLock.unlock();
|
|
943
|
+
}
|
|
944
|
+
// Re-render chat box below new content (only when not streaming)
|
|
945
|
+
if (!this.scrollRegionActive) {
|
|
946
|
+
this.forceRender();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Enter alternate screen buffer and clear it.
|
|
951
|
+
* This gives us full control over the terminal without affecting user's history.
|
|
952
|
+
*/
|
|
953
|
+
enterAlternateScreen() {
|
|
954
|
+
writeLock.lock('enterAltScreen');
|
|
955
|
+
try {
|
|
956
|
+
this.write(ESC.ENTER_ALT_SCREEN);
|
|
957
|
+
this.write(ESC.HOME);
|
|
958
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
959
|
+
this.contentRow = 1;
|
|
960
|
+
}
|
|
961
|
+
finally {
|
|
962
|
+
writeLock.unlock();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Exit alternate screen buffer.
|
|
967
|
+
* Restores the user's previous terminal content.
|
|
968
|
+
*/
|
|
969
|
+
exitAlternateScreen() {
|
|
970
|
+
writeLock.lock('exitAltScreen');
|
|
971
|
+
try {
|
|
972
|
+
this.write(ESC.EXIT_ALT_SCREEN);
|
|
815
973
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
974
|
+
finally {
|
|
975
|
+
writeLock.unlock();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Clear the entire terminal screen and reset content position.
|
|
980
|
+
* This removes all content including the launching command.
|
|
981
|
+
*/
|
|
982
|
+
clearScreen() {
|
|
983
|
+
writeLock.lock('clearScreen');
|
|
984
|
+
try {
|
|
985
|
+
this.write(ESC.HOME);
|
|
986
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
987
|
+
this.contentRow = 1;
|
|
820
988
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
// Exit alternate screen buffer (restores main terminal)
|
|
824
|
-
if (this.unifiedUIInitialized) {
|
|
825
|
-
this.write(ESC.ALT_SCREEN_EXIT);
|
|
989
|
+
finally {
|
|
990
|
+
writeLock.unlock();
|
|
826
991
|
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Reset content position to row 1.
|
|
995
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
996
|
+
*/
|
|
997
|
+
resetContentPosition() {
|
|
998
|
+
this.contentRow = 1;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Set the content row explicitly (used after banner is written).
|
|
1002
|
+
* This tells the input where content should start flowing from.
|
|
1003
|
+
*/
|
|
1004
|
+
setContentRow(row) {
|
|
1005
|
+
this.contentRow = Math.max(1, row);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Get the current content row position.
|
|
1009
|
+
*/
|
|
1010
|
+
getContentRow() {
|
|
1011
|
+
return this.contentRow;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Dispose and clean up
|
|
1015
|
+
*/
|
|
1016
|
+
dispose() {
|
|
1017
|
+
if (this.disposed)
|
|
1018
|
+
return;
|
|
827
1019
|
this.disposed = true;
|
|
828
1020
|
this.enabled = false;
|
|
1021
|
+
this.disableScrollRegion();
|
|
1022
|
+
this.resetStreamingRenderThrottle();
|
|
829
1023
|
this.disableBracketedPaste();
|
|
830
1024
|
this.buffer = '';
|
|
831
1025
|
this.queue = [];
|
|
@@ -930,22 +1124,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
930
1124
|
this.toggleEditMode();
|
|
931
1125
|
return true;
|
|
932
1126
|
}
|
|
933
|
-
|
|
934
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
935
|
-
this.togglePasteExpansion();
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
this.toggleThinking();
|
|
939
|
-
}
|
|
940
|
-
return true;
|
|
941
|
-
case 'escape':
|
|
942
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
943
|
-
if (this.mode === 'streaming') {
|
|
944
|
-
this.emit('interrupt');
|
|
945
|
-
}
|
|
946
|
-
else if (this.buffer.length > 0) {
|
|
947
|
-
this.clear();
|
|
948
|
-
}
|
|
1127
|
+
this.insertText(' ');
|
|
949
1128
|
return true;
|
|
950
1129
|
}
|
|
951
1130
|
return false;
|
|
@@ -963,7 +1142,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
963
1142
|
this.insertPlainText(chunk, insertPos);
|
|
964
1143
|
this.cursor = insertPos + chunk.length;
|
|
965
1144
|
this.emit('change', this.buffer);
|
|
966
|
-
this.updateSuggestions();
|
|
967
1145
|
this.scheduleRender();
|
|
968
1146
|
}
|
|
969
1147
|
insertNewline() {
|
|
@@ -988,7 +1166,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
988
1166
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
989
1167
|
}
|
|
990
1168
|
this.emit('change', this.buffer);
|
|
991
|
-
this.updateSuggestions();
|
|
992
1169
|
this.scheduleRender();
|
|
993
1170
|
}
|
|
994
1171
|
deleteForward() {
|
|
@@ -1216,13 +1393,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1216
1393
|
timestamp: Date.now(),
|
|
1217
1394
|
});
|
|
1218
1395
|
this.emit('queue', text);
|
|
1219
|
-
this.clear(); // Clear immediately for queued input
|
|
1396
|
+
this.clear(); // Clear immediately for queued input
|
|
1220
1397
|
}
|
|
1221
1398
|
else {
|
|
1222
|
-
// In idle mode, clear the input
|
|
1223
|
-
// The
|
|
1224
|
-
|
|
1225
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1399
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1400
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1401
|
+
this.clear();
|
|
1226
1402
|
this.emit('submit', text);
|
|
1227
1403
|
}
|
|
1228
1404
|
}
|
|
@@ -1239,7 +1415,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1239
1415
|
if (available <= 0)
|
|
1240
1416
|
return;
|
|
1241
1417
|
const chunk = clean.slice(0, available);
|
|
1242
|
-
|
|
1418
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1419
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1420
|
+
if (isMultiline && !isShortMultiline) {
|
|
1243
1421
|
this.insertPastePlaceholder(chunk);
|
|
1244
1422
|
}
|
|
1245
1423
|
else {
|
|
@@ -1375,17 +1553,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1375
1553
|
this.shiftPlaceholders(position, text.length);
|
|
1376
1554
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1377
1555
|
}
|
|
1556
|
+
shouldInlineMultiline(content) {
|
|
1557
|
+
const lines = content.split('\n').length;
|
|
1558
|
+
const maxInlineLines = 4;
|
|
1559
|
+
const maxInlineChars = 240;
|
|
1560
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1561
|
+
}
|
|
1378
1562
|
findPlaceholderAt(position) {
|
|
1379
1563
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1380
1564
|
}
|
|
1381
|
-
buildPlaceholder(
|
|
1565
|
+
buildPlaceholder(lineCount) {
|
|
1382
1566
|
const id = ++this.pasteCounter;
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
const preview = summary.preview.length > 30
|
|
1386
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1387
|
-
: summary.preview;
|
|
1388
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1567
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1568
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1389
1569
|
return { id, placeholder };
|
|
1390
1570
|
}
|
|
1391
1571
|
insertPastePlaceholder(content) {
|
|
@@ -1393,67 +1573,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1393
1573
|
if (available <= 0)
|
|
1394
1574
|
return;
|
|
1395
1575
|
const cleanContent = content.slice(0, available);
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
if (summary.lineCount < 5) {
|
|
1399
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1400
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1401
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1402
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1576
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1577
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1406
1578
|
const insertPos = this.cursor;
|
|
1407
1579
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1408
1580
|
this.pastePlaceholders.push({
|
|
1409
1581
|
id,
|
|
1410
1582
|
content: cleanContent,
|
|
1411
|
-
lineCount
|
|
1583
|
+
lineCount,
|
|
1412
1584
|
placeholder,
|
|
1413
1585
|
start: insertPos,
|
|
1414
1586
|
end: insertPos + placeholder.length,
|
|
1415
|
-
summary,
|
|
1416
|
-
expanded: false,
|
|
1417
1587
|
});
|
|
1418
1588
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1419
1589
|
this.cursor = insertPos + placeholder.length;
|
|
1420
1590
|
}
|
|
1421
|
-
/**
|
|
1422
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1423
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1424
|
-
*/
|
|
1425
|
-
togglePasteExpansion() {
|
|
1426
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1427
|
-
if (!placeholder)
|
|
1428
|
-
return false;
|
|
1429
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1430
|
-
// Update the placeholder text in buffer
|
|
1431
|
-
const newPlaceholder = placeholder.expanded
|
|
1432
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1433
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1434
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1435
|
-
// Update buffer
|
|
1436
|
-
this.buffer =
|
|
1437
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1438
|
-
newPlaceholder +
|
|
1439
|
-
this.buffer.slice(placeholder.end);
|
|
1440
|
-
// Update placeholder tracking
|
|
1441
|
-
placeholder.placeholder = newPlaceholder;
|
|
1442
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1443
|
-
// Shift other placeholders
|
|
1444
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1445
|
-
this.scheduleRender();
|
|
1446
|
-
return true;
|
|
1447
|
-
}
|
|
1448
|
-
buildExpandedPlaceholder(ph) {
|
|
1449
|
-
const lines = ph.content.split('\n');
|
|
1450
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1451
|
-
const lastLines = lines.length > 5
|
|
1452
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1453
|
-
: '';
|
|
1454
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1455
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1456
|
-
}
|
|
1457
1591
|
deletePlaceholder(placeholder) {
|
|
1458
1592
|
const length = placeholder.end - placeholder.start;
|
|
1459
1593
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1461,7 +1595,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1461
1595
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1462
1596
|
this.cursor = placeholder.start;
|
|
1463
1597
|
}
|
|
1464
|
-
updateContextUsage(value) {
|
|
1598
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1599
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1600
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1601
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1602
|
+
}
|
|
1465
1603
|
if (value === null || !Number.isFinite(value)) {
|
|
1466
1604
|
this.contextUsage = null;
|
|
1467
1605
|
}
|
|
@@ -1488,6 +1626,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
1488
1626
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1489
1627
|
this.setEditMode(next);
|
|
1490
1628
|
}
|
|
1629
|
+
scheduleStreamingRender(delayMs) {
|
|
1630
|
+
if (this.streamingRenderTimer)
|
|
1631
|
+
return;
|
|
1632
|
+
const wait = Math.max(16, delayMs);
|
|
1633
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1634
|
+
this.streamingRenderTimer = null;
|
|
1635
|
+
// During streaming, only update chat box (not full render)
|
|
1636
|
+
if (this.scrollRegionActive) {
|
|
1637
|
+
this.renderChatBoxAtBottom();
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
this.render();
|
|
1641
|
+
}
|
|
1642
|
+
}, wait);
|
|
1643
|
+
}
|
|
1644
|
+
resetStreamingRenderThrottle() {
|
|
1645
|
+
if (this.streamingRenderTimer) {
|
|
1646
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1647
|
+
this.streamingRenderTimer = null;
|
|
1648
|
+
}
|
|
1649
|
+
this.lastStreamingRender = 0;
|
|
1650
|
+
}
|
|
1491
1651
|
scheduleRender() {
|
|
1492
1652
|
if (!this.canRender())
|
|
1493
1653
|
return;
|