erosolar-cli 1.7.329 → 1.7.330
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 -164
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts +2 -0
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +40 -9
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +1 -4
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +149 -117
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +659 -521
- 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
|
-
|
|
597
|
+
// Thinking indicator
|
|
598
|
+
if (this.metaThinkingMs !== null) {
|
|
599
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
695
600
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
699
|
-
}
|
|
700
|
-
if (parts.length === 0) {
|
|
701
|
-
return ''; // Empty status bar when idle
|
|
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,62 +763,245 @@ 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
|
-
// For now, just re-render at current position - display should update us
|
|
804
|
-
this.renderFloatingInputArea();
|
|
805
|
-
});
|
|
806
|
-
},
|
|
807
|
-
});
|
|
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();
|
|
808
791
|
}
|
|
809
792
|
/**
|
|
810
|
-
*
|
|
793
|
+
* Exit streaming mode and restore normal operation.
|
|
811
794
|
*/
|
|
812
|
-
|
|
813
|
-
|
|
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
|
+
// Float below content when there's room, otherwise pin to bottom
|
|
817
|
+
const pinnedRow = rows - chatBoxHeight + 1;
|
|
818
|
+
const floatingRow = this.contentRow + 1;
|
|
819
|
+
const chatBoxStartRow = Math.max(1, Math.min(floatingRow, pinnedRow));
|
|
820
|
+
const metaLines = this.buildMetaLines(width);
|
|
821
|
+
const modeControls = this.buildModeControls(width);
|
|
822
|
+
const divider = theme.ui.muted('─'.repeat(width));
|
|
823
|
+
// Get input area content
|
|
824
|
+
const lines = this.buffer.split('\n');
|
|
825
|
+
const maxDisplayLines = 3;
|
|
826
|
+
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
827
|
+
const visibleLines = lines.slice(Math.max(0, lines.length - displayLines));
|
|
828
|
+
writeLock.lock('renderChatBoxAtBottom');
|
|
829
|
+
try {
|
|
830
|
+
this.write(ESC.SAVE);
|
|
831
|
+
this.write(ESC.HIDE);
|
|
832
|
+
let row = chatBoxStartRow;
|
|
833
|
+
// Meta lines
|
|
834
|
+
for (const line of metaLines) {
|
|
835
|
+
this.write(ESC.TO(row++, 1));
|
|
836
|
+
this.write(ESC.CLEAR_LINE);
|
|
837
|
+
this.write(line);
|
|
838
|
+
}
|
|
839
|
+
// Divider
|
|
840
|
+
this.write(ESC.TO(row++, 1));
|
|
841
|
+
this.write(ESC.CLEAR_LINE);
|
|
842
|
+
this.write(divider);
|
|
843
|
+
// Input area
|
|
844
|
+
for (const line of visibleLines) {
|
|
845
|
+
this.write(ESC.TO(row++, 1));
|
|
846
|
+
this.write(ESC.CLEAR_LINE);
|
|
847
|
+
this.write(this.config.promptChar);
|
|
848
|
+
this.write(line);
|
|
849
|
+
}
|
|
850
|
+
// Mode controls
|
|
851
|
+
this.write(ESC.TO(row, 1));
|
|
852
|
+
this.write(ESC.CLEAR_LINE);
|
|
853
|
+
this.write(modeControls);
|
|
854
|
+
this.write(ESC.RESTORE);
|
|
855
|
+
this.write(ESC.SHOW);
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
writeLock.unlock();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Stream content within the scroll region.
|
|
863
|
+
* Content is written directly and scrolls naturally.
|
|
864
|
+
*/
|
|
865
|
+
streamContent(content) {
|
|
866
|
+
if (!content)
|
|
814
867
|
return;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
this.
|
|
868
|
+
writeLock.lock('streamContent');
|
|
869
|
+
try {
|
|
870
|
+
// Write content - scroll region handles scrolling
|
|
871
|
+
this.write(content);
|
|
872
|
+
// Track newlines
|
|
873
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
874
|
+
this.contentRow += newlines;
|
|
819
875
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
this.outputInterceptorCleanup();
|
|
823
|
-
this.outputInterceptorCleanup = undefined;
|
|
876
|
+
finally {
|
|
877
|
+
writeLock.unlock();
|
|
824
878
|
}
|
|
825
|
-
//
|
|
826
|
-
this.
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
879
|
+
// Throttle chat box updates during streaming
|
|
880
|
+
this.scheduleStreamingRender(200);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Enable scroll region (no-op in floating mode).
|
|
884
|
+
*/
|
|
885
|
+
enableScrollRegion() {
|
|
886
|
+
// No-op: using pure floating approach
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Disable scroll region (no-op in floating mode).
|
|
890
|
+
*/
|
|
891
|
+
disableScrollRegion() {
|
|
892
|
+
// No-op: using pure floating approach
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Calculate chat box height.
|
|
896
|
+
*/
|
|
897
|
+
getChatBoxHeight() {
|
|
898
|
+
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* @deprecated Use streamContent() instead
|
|
902
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
903
|
+
*/
|
|
904
|
+
registerOutputInterceptor(_display) {
|
|
905
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Write content above the floating chat box.
|
|
909
|
+
* Works both during streaming and when idle.
|
|
910
|
+
*/
|
|
911
|
+
writeToScrollRegion(content) {
|
|
912
|
+
if (!content)
|
|
913
|
+
return;
|
|
914
|
+
writeLock.lock('writeToScrollRegion');
|
|
915
|
+
try {
|
|
916
|
+
// Position cursor at content row and write
|
|
917
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
918
|
+
this.write(content);
|
|
919
|
+
// Track newlines
|
|
920
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
921
|
+
this.contentRow += newlines;
|
|
830
922
|
}
|
|
923
|
+
finally {
|
|
924
|
+
writeLock.unlock();
|
|
925
|
+
}
|
|
926
|
+
// Re-render chat box below new content (only when not streaming)
|
|
927
|
+
if (!this.scrollRegionActive) {
|
|
928
|
+
this.forceRender();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Enter alternate screen buffer and clear it.
|
|
933
|
+
* This gives us full control over the terminal without affecting user's history.
|
|
934
|
+
*/
|
|
935
|
+
enterAlternateScreen() {
|
|
936
|
+
writeLock.lock('enterAltScreen');
|
|
937
|
+
try {
|
|
938
|
+
this.write(ESC.ENTER_ALT_SCREEN);
|
|
939
|
+
this.write(ESC.HOME);
|
|
940
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
941
|
+
this.contentRow = 1;
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
writeLock.unlock();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Exit alternate screen buffer.
|
|
949
|
+
* Restores the user's previous terminal content.
|
|
950
|
+
*/
|
|
951
|
+
exitAlternateScreen() {
|
|
952
|
+
writeLock.lock('exitAltScreen');
|
|
953
|
+
try {
|
|
954
|
+
this.write(ESC.EXIT_ALT_SCREEN);
|
|
955
|
+
}
|
|
956
|
+
finally {
|
|
957
|
+
writeLock.unlock();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Clear the entire terminal screen and reset content position.
|
|
962
|
+
* This removes all content including the launching command.
|
|
963
|
+
*/
|
|
964
|
+
clearScreen() {
|
|
965
|
+
writeLock.lock('clearScreen');
|
|
966
|
+
try {
|
|
967
|
+
this.write(ESC.HOME);
|
|
968
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
969
|
+
this.contentRow = 1;
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
writeLock.unlock();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Reset content position to row 1.
|
|
977
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
978
|
+
*/
|
|
979
|
+
resetContentPosition() {
|
|
980
|
+
this.contentRow = 1;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Set the content row explicitly (used after banner is written).
|
|
984
|
+
* This tells the input where content should start flowing from.
|
|
985
|
+
*/
|
|
986
|
+
setContentRow(row) {
|
|
987
|
+
this.contentRow = Math.max(1, row);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Get the current content row position.
|
|
991
|
+
*/
|
|
992
|
+
getContentRow() {
|
|
993
|
+
return this.contentRow;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Dispose and clean up
|
|
997
|
+
*/
|
|
998
|
+
dispose() {
|
|
999
|
+
if (this.disposed)
|
|
1000
|
+
return;
|
|
831
1001
|
this.disposed = true;
|
|
832
1002
|
this.enabled = false;
|
|
1003
|
+
this.disableScrollRegion();
|
|
1004
|
+
this.resetStreamingRenderThrottle();
|
|
833
1005
|
this.disableBracketedPaste();
|
|
834
1006
|
this.buffer = '';
|
|
835
1007
|
this.queue = [];
|
|
@@ -934,22 +1106,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
934
1106
|
this.toggleEditMode();
|
|
935
1107
|
return true;
|
|
936
1108
|
}
|
|
937
|
-
|
|
938
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
939
|
-
this.togglePasteExpansion();
|
|
940
|
-
}
|
|
941
|
-
else {
|
|
942
|
-
this.toggleThinking();
|
|
943
|
-
}
|
|
944
|
-
return true;
|
|
945
|
-
case 'escape':
|
|
946
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
947
|
-
if (this.mode === 'streaming') {
|
|
948
|
-
this.emit('interrupt');
|
|
949
|
-
}
|
|
950
|
-
else if (this.buffer.length > 0) {
|
|
951
|
-
this.clear();
|
|
952
|
-
}
|
|
1109
|
+
this.insertText(' ');
|
|
953
1110
|
return true;
|
|
954
1111
|
}
|
|
955
1112
|
return false;
|
|
@@ -967,7 +1124,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
967
1124
|
this.insertPlainText(chunk, insertPos);
|
|
968
1125
|
this.cursor = insertPos + chunk.length;
|
|
969
1126
|
this.emit('change', this.buffer);
|
|
970
|
-
this.updateSuggestions();
|
|
971
1127
|
this.scheduleRender();
|
|
972
1128
|
}
|
|
973
1129
|
insertNewline() {
|
|
@@ -992,7 +1148,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
992
1148
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
993
1149
|
}
|
|
994
1150
|
this.emit('change', this.buffer);
|
|
995
|
-
this.updateSuggestions();
|
|
996
1151
|
this.scheduleRender();
|
|
997
1152
|
}
|
|
998
1153
|
deleteForward() {
|
|
@@ -1220,13 +1375,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1220
1375
|
timestamp: Date.now(),
|
|
1221
1376
|
});
|
|
1222
1377
|
this.emit('queue', text);
|
|
1223
|
-
this.clear(); // Clear immediately for queued input
|
|
1378
|
+
this.clear(); // Clear immediately for queued input
|
|
1224
1379
|
}
|
|
1225
1380
|
else {
|
|
1226
|
-
// In idle mode, clear the input
|
|
1227
|
-
// The
|
|
1228
|
-
|
|
1229
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1381
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1382
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1383
|
+
this.clear();
|
|
1230
1384
|
this.emit('submit', text);
|
|
1231
1385
|
}
|
|
1232
1386
|
}
|
|
@@ -1243,7 +1397,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1243
1397
|
if (available <= 0)
|
|
1244
1398
|
return;
|
|
1245
1399
|
const chunk = clean.slice(0, available);
|
|
1246
|
-
|
|
1400
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1401
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1402
|
+
if (isMultiline && !isShortMultiline) {
|
|
1247
1403
|
this.insertPastePlaceholder(chunk);
|
|
1248
1404
|
}
|
|
1249
1405
|
else {
|
|
@@ -1379,17 +1535,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1379
1535
|
this.shiftPlaceholders(position, text.length);
|
|
1380
1536
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1381
1537
|
}
|
|
1538
|
+
shouldInlineMultiline(content) {
|
|
1539
|
+
const lines = content.split('\n').length;
|
|
1540
|
+
const maxInlineLines = 4;
|
|
1541
|
+
const maxInlineChars = 240;
|
|
1542
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1543
|
+
}
|
|
1382
1544
|
findPlaceholderAt(position) {
|
|
1383
1545
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1384
1546
|
}
|
|
1385
|
-
buildPlaceholder(
|
|
1547
|
+
buildPlaceholder(lineCount) {
|
|
1386
1548
|
const id = ++this.pasteCounter;
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
const preview = summary.preview.length > 30
|
|
1390
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1391
|
-
: summary.preview;
|
|
1392
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1549
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1550
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1393
1551
|
return { id, placeholder };
|
|
1394
1552
|
}
|
|
1395
1553
|
insertPastePlaceholder(content) {
|
|
@@ -1397,67 +1555,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1397
1555
|
if (available <= 0)
|
|
1398
1556
|
return;
|
|
1399
1557
|
const cleanContent = content.slice(0, available);
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
if (summary.lineCount < 5) {
|
|
1403
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1404
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1405
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1406
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1558
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1559
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1410
1560
|
const insertPos = this.cursor;
|
|
1411
1561
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1412
1562
|
this.pastePlaceholders.push({
|
|
1413
1563
|
id,
|
|
1414
1564
|
content: cleanContent,
|
|
1415
|
-
lineCount
|
|
1565
|
+
lineCount,
|
|
1416
1566
|
placeholder,
|
|
1417
1567
|
start: insertPos,
|
|
1418
1568
|
end: insertPos + placeholder.length,
|
|
1419
|
-
summary,
|
|
1420
|
-
expanded: false,
|
|
1421
1569
|
});
|
|
1422
1570
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1423
1571
|
this.cursor = insertPos + placeholder.length;
|
|
1424
1572
|
}
|
|
1425
|
-
/**
|
|
1426
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1427
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1428
|
-
*/
|
|
1429
|
-
togglePasteExpansion() {
|
|
1430
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1431
|
-
if (!placeholder)
|
|
1432
|
-
return false;
|
|
1433
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1434
|
-
// Update the placeholder text in buffer
|
|
1435
|
-
const newPlaceholder = placeholder.expanded
|
|
1436
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1437
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1438
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1439
|
-
// Update buffer
|
|
1440
|
-
this.buffer =
|
|
1441
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1442
|
-
newPlaceholder +
|
|
1443
|
-
this.buffer.slice(placeholder.end);
|
|
1444
|
-
// Update placeholder tracking
|
|
1445
|
-
placeholder.placeholder = newPlaceholder;
|
|
1446
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1447
|
-
// Shift other placeholders
|
|
1448
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1449
|
-
this.scheduleRender();
|
|
1450
|
-
return true;
|
|
1451
|
-
}
|
|
1452
|
-
buildExpandedPlaceholder(ph) {
|
|
1453
|
-
const lines = ph.content.split('\n');
|
|
1454
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1455
|
-
const lastLines = lines.length > 5
|
|
1456
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1457
|
-
: '';
|
|
1458
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1459
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1460
|
-
}
|
|
1461
1573
|
deletePlaceholder(placeholder) {
|
|
1462
1574
|
const length = placeholder.end - placeholder.start;
|
|
1463
1575
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1465,7 +1577,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1465
1577
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1466
1578
|
this.cursor = placeholder.start;
|
|
1467
1579
|
}
|
|
1468
|
-
updateContextUsage(value) {
|
|
1580
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1581
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1582
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1583
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1584
|
+
}
|
|
1469
1585
|
if (value === null || !Number.isFinite(value)) {
|
|
1470
1586
|
this.contextUsage = null;
|
|
1471
1587
|
}
|
|
@@ -1492,6 +1608,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
1492
1608
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1493
1609
|
this.setEditMode(next);
|
|
1494
1610
|
}
|
|
1611
|
+
scheduleStreamingRender(delayMs) {
|
|
1612
|
+
if (this.streamingRenderTimer)
|
|
1613
|
+
return;
|
|
1614
|
+
const wait = Math.max(16, delayMs);
|
|
1615
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1616
|
+
this.streamingRenderTimer = null;
|
|
1617
|
+
// During streaming, only update chat box (not full render)
|
|
1618
|
+
if (this.scrollRegionActive) {
|
|
1619
|
+
this.renderChatBoxAtBottom();
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
this.render();
|
|
1623
|
+
}
|
|
1624
|
+
}, wait);
|
|
1625
|
+
}
|
|
1626
|
+
resetStreamingRenderThrottle() {
|
|
1627
|
+
if (this.streamingRenderTimer) {
|
|
1628
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1629
|
+
this.streamingRenderTimer = null;
|
|
1630
|
+
}
|
|
1631
|
+
this.lastStreamingRender = 0;
|
|
1632
|
+
}
|
|
1495
1633
|
scheduleRender() {
|
|
1496
1634
|
if (!this.canRender())
|
|
1497
1635
|
return;
|