erosolar-cli 1.7.356 → 1.7.357
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/sessionStore.d.ts +2 -0
- package/dist/core/sessionStore.d.ts.map +1 -1
- package/dist/core/sessionStore.js +1 -0
- package/dist/core/sessionStore.js.map +1 -1
- 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 +41 -7
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +399 -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 +82 -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 +250 -125
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +1071 -612
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +106 -24
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +137 -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 +200 -49
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +7 -1
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +42 -18
- 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 +148 -274
- 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 +20 -0
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +105 -216
- 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,24 @@
|
|
|
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
|
|
13
|
+
* - Text selection enabled: mouse tracking disabled by default to preserve
|
|
14
|
+
* native terminal text selection and copy/paste functionality
|
|
15
|
+
* - Scrollback navigation via keyboard: PageUp/PageDown, Ctrl+Home/End
|
|
10
16
|
*/
|
|
11
17
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
18
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
19
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import {
|
|
20
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
21
|
+
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
22
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
23
|
+
import { theme } from '../ui/theme.js';
|
|
15
24
|
// ANSI escape codes
|
|
16
25
|
const ESC = {
|
|
17
26
|
// Cursor control
|
|
@@ -21,14 +30,18 @@ const ESC = {
|
|
|
21
30
|
SHOW: '\x1b[?25h',
|
|
22
31
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
23
32
|
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
33
|
// Line control
|
|
30
34
|
CLEAR_LINE: '\x1b[2K',
|
|
31
35
|
CLEAR_TO_END: '\x1b[0J',
|
|
36
|
+
// Screen control
|
|
37
|
+
HOME: '\x1b[H',
|
|
38
|
+
CLEAR_SCREEN: '\x1b[2J',
|
|
39
|
+
// Alternate screen buffer (like vim/tmux)
|
|
40
|
+
ENTER_ALT_SCREEN: '\x1b[?1049h',
|
|
41
|
+
EXIT_ALT_SCREEN: '\x1b[?1049l',
|
|
42
|
+
// Scroll region
|
|
43
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
44
|
+
RESET_SCROLL: '\x1b[r',
|
|
32
45
|
// Style
|
|
33
46
|
RESET: '\x1b[0m',
|
|
34
47
|
DIM: '\x1b[2m',
|
|
@@ -40,6 +53,12 @@ const ESC = {
|
|
|
40
53
|
PASTE_DISABLE: '\x1b[?2004l',
|
|
41
54
|
PASTE_START: '\x1b[200~',
|
|
42
55
|
PASTE_END: '\x1b[201~',
|
|
56
|
+
// Mouse tracking - Button events only (allows Shift+drag for text selection)
|
|
57
|
+
// Mode 1000: Track button presses/releases (wheel events included)
|
|
58
|
+
// Mode 1006: SGR extended format for better coordinate handling
|
|
59
|
+
// Note: NOT using mode 1003 (all motion) to preserve terminal text selection
|
|
60
|
+
MOUSE_ENABLE: '\x1b[?1000h\x1b[?1006h', // Enable button tracking + SGR mode
|
|
61
|
+
MOUSE_DISABLE: '\x1b[?1006l\x1b[?1000l', // Disable mouse tracking
|
|
43
62
|
};
|
|
44
63
|
/**
|
|
45
64
|
* Unified terminal input handler
|
|
@@ -68,49 +87,54 @@ export class TerminalInput extends EventEmitter {
|
|
|
68
87
|
statusMessage = null;
|
|
69
88
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
70
89
|
streamingLabel = null; // Streaming progress indicator
|
|
90
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
91
|
+
metaTokensUsed = null; // Optional token usage
|
|
92
|
+
metaTokenLimit = null; // Optional token window
|
|
93
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
94
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
71
95
|
lastRenderContent = '';
|
|
72
96
|
lastRenderCursor = -1;
|
|
73
97
|
renderDirty = false;
|
|
74
98
|
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
99
|
// Lifecycle
|
|
84
100
|
disposed = false;
|
|
85
101
|
enabled = true;
|
|
86
102
|
contextUsage = null;
|
|
103
|
+
contextAutoCompactThreshold = 90;
|
|
104
|
+
// Track current content row (starts at top, moves down)
|
|
105
|
+
contentRow = 1;
|
|
106
|
+
// Track if scroll region is currently active
|
|
107
|
+
scrollRegionActive = false;
|
|
108
|
+
thinkingModeLabel = null;
|
|
109
|
+
// Scrollback buffer
|
|
110
|
+
scrollbackBuffer = [];
|
|
111
|
+
maxScrollbackLines = 10000;
|
|
112
|
+
scrollbackOffset = 0; // 0 = at bottom (live), > 0 = scrolled up
|
|
113
|
+
isInScrollbackMode = false;
|
|
114
|
+
scrollIndicatorFrame = 0; // For animated scroll indicator
|
|
115
|
+
// Alternate screen state
|
|
116
|
+
alternateScreenActive = false;
|
|
87
117
|
editMode = 'display-edits';
|
|
88
118
|
verificationEnabled = true;
|
|
89
119
|
autoContinueEnabled = false;
|
|
90
120
|
verificationHotkey = 'alt+v';
|
|
91
121
|
autoContinueHotkey = 'alt+c';
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
122
|
+
thinkingHotkey = '/thinking';
|
|
123
|
+
modelLabel = null;
|
|
124
|
+
providerLabel = null;
|
|
125
|
+
// Streaming render throttle
|
|
126
|
+
lastStreamingRender = 0;
|
|
127
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
99
128
|
streamingRenderTimer = null;
|
|
100
|
-
// Reference to display module for getting line counts during streaming
|
|
101
|
-
displayRef = null;
|
|
102
|
-
// Unified UI initialization flag
|
|
103
|
-
unifiedUIInitialized = false;
|
|
104
129
|
constructor(writeStream = process.stdout, config = {}) {
|
|
105
130
|
super();
|
|
106
131
|
this.out = writeStream;
|
|
107
|
-
// Use schema defaults for configuration consistency
|
|
108
132
|
this.config = {
|
|
109
|
-
maxLines: config.maxLines ??
|
|
110
|
-
maxLength: config.maxLength ??
|
|
133
|
+
maxLines: config.maxLines ?? 1000,
|
|
134
|
+
maxLength: config.maxLength ?? 10000,
|
|
111
135
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
112
|
-
promptChar: config.promptChar ??
|
|
113
|
-
continuationChar: config.continuationChar ??
|
|
136
|
+
promptChar: config.promptChar ?? '> ',
|
|
137
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
114
138
|
};
|
|
115
139
|
}
|
|
116
140
|
// ===========================================================================
|
|
@@ -133,10 +157,47 @@ export class TerminalInput extends EventEmitter {
|
|
|
133
157
|
}
|
|
134
158
|
}
|
|
135
159
|
/**
|
|
136
|
-
*
|
|
137
|
-
|
|
160
|
+
* Enable mouse tracking in terminal
|
|
161
|
+
*/
|
|
162
|
+
enableMouseTracking() {
|
|
163
|
+
if (this.isTTY()) {
|
|
164
|
+
this.write(ESC.MOUSE_ENABLE);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Disable mouse tracking
|
|
169
|
+
*/
|
|
170
|
+
disableMouseTracking() {
|
|
171
|
+
if (this.isTTY()) {
|
|
172
|
+
this.write(ESC.MOUSE_DISABLE);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Process raw terminal data (handles bracketed paste sequences, mouse events, and escape sequences)
|
|
177
|
+
* Returns true if the data was consumed (paste sequence, mouse event, etc.)
|
|
138
178
|
*/
|
|
139
179
|
processRawData(data) {
|
|
180
|
+
// Check for mouse events (SGR mode: \x1b[<button;x;yM or m)
|
|
181
|
+
const mouseMatch = data.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
182
|
+
if (mouseMatch) {
|
|
183
|
+
const button = parseInt(mouseMatch[1], 10);
|
|
184
|
+
const x = parseInt(mouseMatch[2], 10);
|
|
185
|
+
const y = parseInt(mouseMatch[3], 10);
|
|
186
|
+
const action = mouseMatch[4]; // 'M' = press, 'm' = release
|
|
187
|
+
this.handleMouseEvent(button, x, y, action);
|
|
188
|
+
// Remove mouse event from data and return remaining
|
|
189
|
+
const mouseEventEnd = data.indexOf(action) + 1;
|
|
190
|
+
const remaining = data.slice(mouseEventEnd);
|
|
191
|
+
return { consumed: true, passthrough: remaining };
|
|
192
|
+
}
|
|
193
|
+
// Filter out arrow key escape sequences (they're handled by keypress events)
|
|
194
|
+
// Arrow keys: \x1b[A (up), \x1b[B (down), \x1b[C (right), \x1b[D (left)
|
|
195
|
+
const arrowMatch = data.match(/\x1b\[[ABCD]/);
|
|
196
|
+
if (arrowMatch) {
|
|
197
|
+
// Arrow keys should be handled by keypress handler, strip them from passthrough
|
|
198
|
+
const filtered = data.replace(/\x1b\[[ABCD]/g, '');
|
|
199
|
+
return { consumed: filtered.length !== data.length, passthrough: filtered };
|
|
200
|
+
}
|
|
140
201
|
// Check for paste start
|
|
141
202
|
if (data.includes(ESC.PASTE_START)) {
|
|
142
203
|
this.isPasting = true;
|
|
@@ -167,6 +228,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
167
228
|
}
|
|
168
229
|
return { consumed: false, passthrough: data };
|
|
169
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Handle mouse events (button, x, y coordinates, action)
|
|
233
|
+
*/
|
|
234
|
+
handleMouseEvent(button, _x, _y, _action) {
|
|
235
|
+
// Mouse wheel events: button 64 = scroll up, button 65 = scroll down
|
|
236
|
+
if (button === 64) {
|
|
237
|
+
// Scroll up (3 lines per wheel tick)
|
|
238
|
+
this.scrollUp(3);
|
|
239
|
+
}
|
|
240
|
+
else if (button === 65) {
|
|
241
|
+
// Scroll down (3 lines per wheel tick)
|
|
242
|
+
this.scrollDown(3);
|
|
243
|
+
}
|
|
244
|
+
// Ignore other mouse events (clicks, drags, etc.) for now
|
|
245
|
+
}
|
|
170
246
|
/**
|
|
171
247
|
* Handle a keypress event
|
|
172
248
|
*/
|
|
@@ -189,258 +265,36 @@ export class TerminalInput extends EventEmitter {
|
|
|
189
265
|
if (handled)
|
|
190
266
|
return;
|
|
191
267
|
}
|
|
192
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
193
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
194
|
-
this.emit('showHelp');
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
268
|
// Insert printable characters
|
|
198
269
|
if (str && !key?.ctrl && !key?.meta) {
|
|
199
270
|
this.insertText(str);
|
|
200
271
|
}
|
|
201
272
|
}
|
|
202
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
203
|
-
bannerContent = null;
|
|
204
|
-
/**
|
|
205
|
-
* Set banner content to be written when unified UI initializes.
|
|
206
|
-
*/
|
|
207
|
-
setBannerContent(content) {
|
|
208
|
-
this.bannerContent = content;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Initialize the unified UI system with BOTTOM PINNED chat box.
|
|
212
|
-
*
|
|
213
|
-
* Layout:
|
|
214
|
-
* 1. Clear screen
|
|
215
|
-
* 2. Write banner at top
|
|
216
|
-
* 3. Set content cursor row after banner
|
|
217
|
-
* 4. Render chat box at bottom (sets up scroll region)
|
|
218
|
-
*/
|
|
219
|
-
initializeUnifiedUI() {
|
|
220
|
-
if (this.unifiedUIInitialized) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
// Enter alternate screen buffer for complete terminal control
|
|
224
|
-
this.write(ESC.ALT_SCREEN_ENTER);
|
|
225
|
-
// Hide cursor during setup
|
|
226
|
-
this.write(ESC.HIDE);
|
|
227
|
-
// Clear screen and go home (in alternate buffer)
|
|
228
|
-
this.write(ESC.HOME);
|
|
229
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
230
|
-
// Write banner at top
|
|
231
|
-
let bannerLines = 0;
|
|
232
|
-
if (this.bannerContent) {
|
|
233
|
-
const lines = this.bannerContent.split('\n');
|
|
234
|
-
bannerLines = lines.length + 2; // +2 for trailing \n\n
|
|
235
|
-
process.stdout.write(this.bannerContent + '\n\n');
|
|
236
|
-
}
|
|
237
|
-
// Set content cursor row after banner
|
|
238
|
-
this.contentCursorRow = bannerLines > 0 ? bannerLines + 1 : 1;
|
|
239
|
-
// Content ends at same row initially (no content yet)
|
|
240
|
-
this.contentEndRow = this.contentCursorRow - 1;
|
|
241
|
-
// Mark initialized
|
|
242
|
-
this.unifiedUIInitialized = true;
|
|
243
|
-
// Render floating chat box below content
|
|
244
|
-
this.renderFloatingInputArea();
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Clear the input area at its tracked position.
|
|
248
|
-
* Returns true if something was cleared.
|
|
249
|
-
*/
|
|
250
|
-
clearInputArea() {
|
|
251
|
-
if (this.inputAreaStartRow > 0 && this.flowModeRenderedLines > 0) {
|
|
252
|
-
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
253
|
-
this.write(ESC.TO(this.inputAreaStartRow + i, 1));
|
|
254
|
-
this.write(ESC.CLEAR_LINE);
|
|
255
|
-
}
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Reset input area tracking state.
|
|
262
|
-
*/
|
|
263
|
-
resetInputAreaTracking() {
|
|
264
|
-
this.inputAreaStartRow = 0;
|
|
265
|
-
this.flowModeRenderedLines = 0;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Render chat box - FLOATING below content (no scroll regions).
|
|
269
|
-
* Chat box appears right after content and moves down as content grows.
|
|
270
|
-
* Content and banner scroll naturally off the top of the screen.
|
|
271
|
-
*/
|
|
272
|
-
renderFloatingInputArea() {
|
|
273
|
-
const { cols, rows } = this.getSize();
|
|
274
|
-
const layout = this.buildInputLayout(cols);
|
|
275
|
-
// Clamp start row so the full UI stays visible, even when content exceeds viewport
|
|
276
|
-
const chatBoxHeight = layout.lines.length;
|
|
277
|
-
const chatBoxStartRow = Math.max(1, Math.min(this.contentEndRow + 1, rows - chatBoxHeight + 1));
|
|
278
|
-
// Save cursor position (content writer), render, then restore to avoid disrupting output
|
|
279
|
-
this.write(ESC.SAVE);
|
|
280
|
-
this.write(ESC.HIDE);
|
|
281
|
-
this.inputAreaStartRow = chatBoxStartRow;
|
|
282
|
-
let currentRow = chatBoxStartRow;
|
|
283
|
-
// Render each line with clearing to avoid ghosting
|
|
284
|
-
for (const line of layout.lines) {
|
|
285
|
-
this.write(ESC.TO(currentRow, 1));
|
|
286
|
-
this.write(ESC.CLEAR_LINE);
|
|
287
|
-
this.write(this.fitToWidth(line, cols));
|
|
288
|
-
currentRow++;
|
|
289
|
-
}
|
|
290
|
-
this.flowModeRenderedLines = chatBoxHeight;
|
|
291
|
-
this.lastRenderContent = this.buffer;
|
|
292
|
-
this.lastRenderCursor = this.cursor;
|
|
293
|
-
// Restore cursor for content writes; caret is rendered virtually inside the prompt
|
|
294
|
-
this.write(ESC.RESTORE);
|
|
295
|
-
this.write(ESC.SHOW);
|
|
296
|
-
}
|
|
297
273
|
/**
|
|
298
274
|
* Set the input mode
|
|
299
275
|
*
|
|
300
|
-
*
|
|
301
|
-
* Scroll region protects chat box, content scrolls above it.
|
|
276
|
+
* Content flows naturally - no scroll region pinning.
|
|
302
277
|
*/
|
|
303
278
|
setMode(mode) {
|
|
304
279
|
const prevMode = this.mode;
|
|
305
280
|
this.mode = mode;
|
|
306
281
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
307
|
-
|
|
308
|
-
this.streamingStartTime = Date.now();
|
|
309
|
-
// Ensure unified UI is initialized
|
|
310
|
-
if (!this.unifiedUIInitialized) {
|
|
311
|
-
this.initializeUnifiedUI();
|
|
312
|
-
}
|
|
313
|
-
// Start periodic render timer to keep chat box updated during streaming
|
|
314
|
-
// This updates contentEndRow from display and re-renders chat box
|
|
315
|
-
if (!this.streamingRenderTimer) {
|
|
316
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
317
|
-
// Update contentEndRow from display's line count
|
|
318
|
-
if (this.displayRef?.getTotalWrittenLines) {
|
|
319
|
-
const next = this.displayRef.getTotalWrittenLines();
|
|
320
|
-
if (typeof next === 'number' && Number.isFinite(next) && next !== this.contentEndRow) {
|
|
321
|
-
this.contentEndRow = next;
|
|
322
|
-
this.renderDirty = true;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Re-render chat box at updated position
|
|
326
|
-
if (this.renderDirty) {
|
|
327
|
-
this.render();
|
|
328
|
-
}
|
|
329
|
-
}, 120); // Update periodically without overwhelming output
|
|
330
|
-
}
|
|
331
|
-
// Initial render
|
|
282
|
+
this.resetStreamingRenderThrottle();
|
|
332
283
|
this.renderDirty = true;
|
|
333
|
-
this.
|
|
284
|
+
this.render();
|
|
334
285
|
}
|
|
335
286
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
this.streamingRenderTimer = null;
|
|
340
|
-
}
|
|
341
|
-
// Reset streaming time
|
|
342
|
-
this.streamingStartTime = null;
|
|
343
|
-
// Final render with accurate position
|
|
344
|
-
this.renderDirty = true;
|
|
345
|
-
this.scheduleRender();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Set the row where content ends (for idle mode positioning).
|
|
350
|
-
* Input area will render starting from this row + 1.
|
|
351
|
-
*/
|
|
352
|
-
setContentEndRow(row) {
|
|
353
|
-
this.contentEndRow = Math.max(0, row);
|
|
354
|
-
this.renderDirty = true;
|
|
355
|
-
this.scheduleRender();
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Set available slash commands for auto-complete suggestions.
|
|
359
|
-
*/
|
|
360
|
-
setCommands(commands) {
|
|
361
|
-
this.commandSuggestions = commands;
|
|
362
|
-
this.updateSuggestions();
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Update filtered suggestions based on current input.
|
|
366
|
-
*/
|
|
367
|
-
updateSuggestions() {
|
|
368
|
-
const input = this.buffer.trim();
|
|
369
|
-
// Only show suggestions when input starts with "/"
|
|
370
|
-
if (!input.startsWith('/')) {
|
|
371
|
-
this.showSuggestions = false;
|
|
372
|
-
this.filteredSuggestions = [];
|
|
373
|
-
this.selectedSuggestionIndex = 0;
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const query = input.toLowerCase();
|
|
377
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
378
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
379
|
-
// Show suggestions if we have matches
|
|
380
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
381
|
-
// Keep selection in bounds
|
|
382
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
383
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
287
|
+
// Streaming ended - render the input area
|
|
288
|
+
this.resetStreamingRenderThrottle();
|
|
289
|
+
this.forceRender();
|
|
384
290
|
}
|
|
385
291
|
}
|
|
386
292
|
/**
|
|
387
|
-
*
|
|
388
|
-
|
|
389
|
-
selectNextSuggestion() {
|
|
390
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
391
|
-
return;
|
|
392
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
393
|
-
this.renderDirty = true;
|
|
394
|
-
this.scheduleRender();
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
398
|
-
*/
|
|
399
|
-
selectPrevSuggestion() {
|
|
400
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
401
|
-
return;
|
|
402
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
403
|
-
? this.filteredSuggestions.length - 1
|
|
404
|
-
: this.selectedSuggestionIndex - 1;
|
|
405
|
-
this.renderDirty = true;
|
|
406
|
-
this.scheduleRender();
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Accept current suggestion and insert into buffer.
|
|
410
|
-
*/
|
|
411
|
-
acceptSuggestion() {
|
|
412
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
413
|
-
return false;
|
|
414
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
415
|
-
if (!selected)
|
|
416
|
-
return false;
|
|
417
|
-
// Replace buffer with selected command
|
|
418
|
-
this.buffer = selected.command + ' ';
|
|
419
|
-
this.cursor = this.buffer.length;
|
|
420
|
-
this.showSuggestions = false;
|
|
421
|
-
this.renderDirty = true;
|
|
422
|
-
this.scheduleRender();
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Check if suggestions are visible.
|
|
427
|
-
*/
|
|
428
|
-
areSuggestionsVisible() {
|
|
429
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Toggle thinking/reasoning mode
|
|
433
|
-
*/
|
|
434
|
-
toggleThinking() {
|
|
435
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
436
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
437
|
-
this.scheduleRender();
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Get thinking enabled state
|
|
293
|
+
* Legacy method - no longer used (content flows naturally).
|
|
294
|
+
* @deprecated Use setContentRow instead
|
|
441
295
|
*/
|
|
442
|
-
|
|
443
|
-
|
|
296
|
+
setPinnedHeaderLines(_count) {
|
|
297
|
+
// No-op: scroll region pinning removed
|
|
444
298
|
}
|
|
445
299
|
/**
|
|
446
300
|
* Get current mode
|
|
@@ -473,17 +327,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
473
327
|
}
|
|
474
328
|
/**
|
|
475
329
|
* Clear the buffer
|
|
476
|
-
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
477
330
|
*/
|
|
478
|
-
clear(
|
|
331
|
+
clear() {
|
|
479
332
|
this.buffer = '';
|
|
480
333
|
this.cursor = 0;
|
|
481
334
|
this.historyIndex = -1;
|
|
482
335
|
this.tempInput = '';
|
|
483
336
|
this.pastePlaceholders = [];
|
|
484
|
-
|
|
485
|
-
this.scheduleRender();
|
|
486
|
-
}
|
|
337
|
+
this.scheduleRender();
|
|
487
338
|
}
|
|
488
339
|
/**
|
|
489
340
|
* Get queued inputs
|
|
@@ -554,6 +405,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
554
405
|
this.streamingLabel = next;
|
|
555
406
|
this.scheduleRender();
|
|
556
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
410
|
+
*/
|
|
411
|
+
setMetaStatus(meta) {
|
|
412
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
413
|
+
? Math.floor(meta.elapsedSeconds)
|
|
414
|
+
: null;
|
|
415
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
416
|
+
? Math.floor(meta.tokensUsed)
|
|
417
|
+
: null;
|
|
418
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
419
|
+
? Math.floor(meta.tokenLimit)
|
|
420
|
+
: null;
|
|
421
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
422
|
+
? Math.floor(meta.thinkingMs)
|
|
423
|
+
: null;
|
|
424
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
425
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
426
|
+
this.metaTokensUsed === nextTokens &&
|
|
427
|
+
this.metaTokenLimit === nextLimit &&
|
|
428
|
+
this.metaThinkingMs === nextThinking &&
|
|
429
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
433
|
+
this.metaTokensUsed = nextTokens;
|
|
434
|
+
this.metaTokenLimit = nextLimit;
|
|
435
|
+
this.metaThinkingMs = nextThinking;
|
|
436
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
437
|
+
this.scheduleRender();
|
|
438
|
+
}
|
|
557
439
|
/**
|
|
558
440
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
559
441
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -563,26 +445,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
563
445
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
564
446
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
565
447
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
448
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
449
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
566
450
|
if (this.verificationEnabled === nextVerification &&
|
|
567
451
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
568
452
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
569
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
453
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
454
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
455
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
570
456
|
return;
|
|
571
457
|
}
|
|
572
458
|
this.verificationEnabled = nextVerification;
|
|
573
459
|
this.autoContinueEnabled = nextAutoContinue;
|
|
574
460
|
this.verificationHotkey = nextVerifyHotkey;
|
|
575
461
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
576
|
-
this.
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
580
|
-
* This is displayed persistently above the input area.
|
|
581
|
-
*/
|
|
582
|
-
setModelInfo(info) {
|
|
583
|
-
if (this.modelInfo === info)
|
|
584
|
-
return;
|
|
585
|
-
this.modelInfo = info;
|
|
462
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
463
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
586
464
|
this.scheduleRender();
|
|
587
465
|
}
|
|
588
466
|
/**
|
|
@@ -595,33 +473,174 @@ export class TerminalInput extends EventEmitter {
|
|
|
595
473
|
this.scheduleRender();
|
|
596
474
|
}
|
|
597
475
|
/**
|
|
598
|
-
*
|
|
599
|
-
|
|
600
|
-
|
|
476
|
+
* Surface model/provider context in the controls bar.
|
|
477
|
+
*/
|
|
478
|
+
setModelContext(options) {
|
|
479
|
+
const nextModel = options.model?.trim() || null;
|
|
480
|
+
const nextProvider = options.provider?.trim() || null;
|
|
481
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.modelLabel = nextModel;
|
|
485
|
+
this.providerLabel = nextProvider;
|
|
486
|
+
this.scheduleRender();
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Render the floating input area at contentRow.
|
|
490
|
+
*
|
|
491
|
+
* The chat box "floats" - it renders right below the last streamed content.
|
|
492
|
+
* As content is added, contentRow advances, and the chat box moves down.
|
|
493
|
+
* No scroll regions - pure floating behavior.
|
|
601
494
|
*/
|
|
602
495
|
render() {
|
|
603
496
|
if (!this.canRender())
|
|
604
497
|
return;
|
|
605
498
|
if (this.isRendering)
|
|
606
499
|
return;
|
|
500
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
501
|
+
// During streaming, throttle re-renders
|
|
502
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
503
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
504
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
505
|
+
if (waitMs > 0) {
|
|
506
|
+
this.renderDirty = true;
|
|
507
|
+
this.scheduleStreamingRender(waitMs);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
607
511
|
const shouldSkip = !this.renderDirty &&
|
|
608
512
|
this.buffer === this.lastRenderContent &&
|
|
609
513
|
this.cursor === this.lastRenderCursor;
|
|
610
514
|
this.renderDirty = false;
|
|
611
|
-
// Skip if nothing changed (unless explicitly forced)
|
|
612
515
|
if (shouldSkip) {
|
|
613
516
|
return;
|
|
614
517
|
}
|
|
615
|
-
// If write lock is held, defer render
|
|
616
518
|
if (writeLock.isLocked()) {
|
|
617
519
|
writeLock.safeWrite(() => this.render());
|
|
618
520
|
return;
|
|
619
521
|
}
|
|
522
|
+
this.renderPinnedChatBox();
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Unified scroll region renderer.
|
|
526
|
+
* Chat box is ALWAYS pinned at the bottom of the terminal.
|
|
527
|
+
* Content scrolls in the region above the chat box.
|
|
528
|
+
*/
|
|
529
|
+
renderPinnedChatBox() {
|
|
530
|
+
const { rows, cols } = this.getSize();
|
|
531
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
532
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
533
|
+
// Wrap buffer into display lines
|
|
534
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
535
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
536
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
537
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
538
|
+
// Calculate display window (keep cursor visible)
|
|
539
|
+
let startLine = 0;
|
|
540
|
+
if (lines.length > displayLines) {
|
|
541
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
542
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
543
|
+
}
|
|
544
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
545
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
546
|
+
// Chat box height
|
|
547
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
548
|
+
// ALWAYS pin chat box at absolute bottom
|
|
549
|
+
const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
|
|
550
|
+
const scrollEnd = chatBoxStartRow - 1;
|
|
551
|
+
writeLock.lock('terminalInput.renderPinned');
|
|
620
552
|
this.isRendering = true;
|
|
621
|
-
writeLock.lock('terminalInput.render');
|
|
622
553
|
try {
|
|
623
|
-
|
|
624
|
-
this.
|
|
554
|
+
this.write(ESC.SAVE);
|
|
555
|
+
this.write(ESC.HIDE);
|
|
556
|
+
// Temporarily reset scroll region to write chat box cleanly
|
|
557
|
+
if (this.scrollRegionActive) {
|
|
558
|
+
this.write(ESC.RESET_SCROLL);
|
|
559
|
+
}
|
|
560
|
+
// Clear the chat box area
|
|
561
|
+
for (let i = 0; i < chatBoxHeight; i++) {
|
|
562
|
+
const row = chatBoxStartRow + i;
|
|
563
|
+
if (row <= rows) {
|
|
564
|
+
this.write(ESC.TO(row, 1));
|
|
565
|
+
this.write(ESC.CLEAR_LINE);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
let currentRow = chatBoxStartRow;
|
|
569
|
+
// Render scroll/status indicator on the left (Claude Code style)
|
|
570
|
+
const scrollIndicator = this.buildScrollIndicator();
|
|
571
|
+
// Meta/status header with scroll indicator
|
|
572
|
+
for (const metaLine of metaLines) {
|
|
573
|
+
this.write(ESC.TO(currentRow, 1));
|
|
574
|
+
this.write(metaLine);
|
|
575
|
+
currentRow += 1;
|
|
576
|
+
}
|
|
577
|
+
// Separator line with scroll status
|
|
578
|
+
this.write(ESC.TO(currentRow, 1));
|
|
579
|
+
const dividerLabel = scrollIndicator || undefined;
|
|
580
|
+
this.write(renderDivider(cols - 2, dividerLabel));
|
|
581
|
+
currentRow += 1;
|
|
582
|
+
// Render input lines
|
|
583
|
+
let finalRow = currentRow;
|
|
584
|
+
let finalCol = 3;
|
|
585
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
586
|
+
const rowNum = currentRow + i;
|
|
587
|
+
this.write(ESC.TO(rowNum, 1));
|
|
588
|
+
const line = visibleLines[i] ?? '';
|
|
589
|
+
const isFirstLine = (startLine + i) === 0;
|
|
590
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
591
|
+
this.write(ESC.BG_DARK);
|
|
592
|
+
this.write(ESC.DIM);
|
|
593
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
594
|
+
this.write(ESC.RESET);
|
|
595
|
+
this.write(ESC.BG_DARK);
|
|
596
|
+
if (isCursorLine) {
|
|
597
|
+
const col = Math.min(cursorCol, line.length);
|
|
598
|
+
const before = line.slice(0, col);
|
|
599
|
+
const at = col < line.length ? line[col] : ' ';
|
|
600
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
601
|
+
this.write(before);
|
|
602
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
603
|
+
this.write(at);
|
|
604
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
605
|
+
this.write(after);
|
|
606
|
+
finalRow = rowNum;
|
|
607
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
this.write(line);
|
|
611
|
+
}
|
|
612
|
+
// Pad to edge
|
|
613
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
614
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
615
|
+
if (padding > 0)
|
|
616
|
+
this.write(' '.repeat(padding));
|
|
617
|
+
this.write(ESC.RESET);
|
|
618
|
+
}
|
|
619
|
+
// Mode controls line with all keyboard shortcuts
|
|
620
|
+
const controlRow = currentRow + visibleLines.length;
|
|
621
|
+
this.write(ESC.TO(controlRow, 1));
|
|
622
|
+
this.write(this.buildModeControls(cols));
|
|
623
|
+
// Restore scroll region and cursor
|
|
624
|
+
if (this.scrollRegionActive) {
|
|
625
|
+
// Restore scroll region
|
|
626
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
627
|
+
// Restore cursor to where it was before rendering (preserves column position)
|
|
628
|
+
this.write(ESC.RESTORE);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
// Not streaming - position cursor in input box
|
|
632
|
+
this.write(ESC.RESTORE);
|
|
633
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
634
|
+
}
|
|
635
|
+
this.write(ESC.SHOW);
|
|
636
|
+
// Update state
|
|
637
|
+
this.lastRenderContent = this.buffer;
|
|
638
|
+
this.lastRenderCursor = this.cursor;
|
|
639
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
640
|
+
if (this.streamingRenderTimer) {
|
|
641
|
+
clearTimeout(this.streamingRenderTimer);
|
|
642
|
+
this.streamingRenderTimer = null;
|
|
643
|
+
}
|
|
625
644
|
}
|
|
626
645
|
finally {
|
|
627
646
|
writeLock.unlock();
|
|
@@ -629,211 +648,179 @@ export class TerminalInput extends EventEmitter {
|
|
|
629
648
|
}
|
|
630
649
|
}
|
|
631
650
|
/**
|
|
632
|
-
* Build
|
|
651
|
+
* Build compact meta line above the divider.
|
|
652
|
+
* Shows model/provider and key metrics in a single line.
|
|
633
653
|
*/
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return visibleLines.map((lineText, index) => {
|
|
666
|
-
const hasCursor = caretLine === index;
|
|
667
|
-
const cursorIndex = hasCursor ? cursorCol : null;
|
|
668
|
-
const prefix = index === 0 ? prompt : continuation;
|
|
669
|
-
return this.renderPromptLine(prefix, lineText, cursorIndex, textWidth, width);
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
buildSuggestions(width) {
|
|
673
|
-
if (!this.areSuggestionsVisible()) {
|
|
654
|
+
buildMetaLines(width) {
|
|
655
|
+
const leftParts = [];
|
|
656
|
+
const rightParts = [];
|
|
657
|
+
// Model/provider info (left side)
|
|
658
|
+
if (this.modelLabel) {
|
|
659
|
+
const modelText = this.providerLabel
|
|
660
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
661
|
+
: this.modelLabel;
|
|
662
|
+
leftParts.push({ text: modelText, tone: 'info' });
|
|
663
|
+
}
|
|
664
|
+
// Elapsed time (right side)
|
|
665
|
+
if (this.metaElapsedSeconds !== null) {
|
|
666
|
+
rightParts.push({ text: `⏱ ${this.formatElapsedLabel(this.metaElapsedSeconds)}`, tone: 'muted' });
|
|
667
|
+
}
|
|
668
|
+
// Token usage (right side)
|
|
669
|
+
if (this.metaTokensUsed !== null) {
|
|
670
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
671
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
672
|
+
rightParts.push({ text: `⊛ ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
673
|
+
}
|
|
674
|
+
// Context remaining warning
|
|
675
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
676
|
+
if (tokensRemaining !== null) {
|
|
677
|
+
const contextPct = this.contextUsage !== null ? `${100 - this.contextUsage}%` : '';
|
|
678
|
+
rightParts.push({ text: `↓${tokensRemaining} ${contextPct}`, tone: this.contextUsage && this.contextUsage > 80 ? 'warn' : 'muted' });
|
|
679
|
+
}
|
|
680
|
+
// Thinking indicator
|
|
681
|
+
if (this.metaThinkingMs !== null) {
|
|
682
|
+
leftParts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
683
|
+
}
|
|
684
|
+
if (!leftParts.length && !rightParts.length) {
|
|
674
685
|
return [];
|
|
675
686
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
lines.push(this.fitToWidth(`${bullet} ${body}`, width));
|
|
687
|
-
}
|
|
688
|
-
return lines;
|
|
689
|
-
}
|
|
690
|
-
renderPromptLine(prefix, text, cursorCol, textWidth, totalWidth) {
|
|
691
|
-
const visibleTextWidth = Math.max(1, textWidth);
|
|
692
|
-
const padded = (text ?? '').slice(0, visibleTextWidth).padEnd(visibleTextWidth, ' ');
|
|
693
|
-
let rendered = padded;
|
|
694
|
-
if (cursorCol !== null && cursorCol >= 0) {
|
|
695
|
-
const safeCol = Math.max(0, Math.min(cursorCol, Math.max(0, visibleTextWidth - 1)));
|
|
696
|
-
const before = padded.slice(0, safeCol);
|
|
697
|
-
const caretChar = padded[safeCol] ?? ' ';
|
|
698
|
-
const after = padded.slice(safeCol + 1);
|
|
699
|
-
rendered = `${before}${UI_COLORS.reverse}${caretChar || ' '}${UI_COLORS.reset}${after}`;
|
|
700
|
-
}
|
|
701
|
-
return this.fitToWidth(`${prefix}${rendered}`, totalWidth);
|
|
687
|
+
// Render left and right aligned
|
|
688
|
+
if (!rightParts.length || width < 50) {
|
|
689
|
+
return [renderStatusLine([...leftParts, ...rightParts], width)];
|
|
690
|
+
}
|
|
691
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.5));
|
|
692
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
693
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
694
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
695
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
696
|
+
return [`${leftText}${' '.repeat(spacing)}${rightText}`];
|
|
702
697
|
}
|
|
703
|
-
|
|
704
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Build mode controls line with status and key shortcuts.
|
|
700
|
+
* Compact single line with essential info.
|
|
701
|
+
*/
|
|
702
|
+
buildModeControls(cols) {
|
|
703
|
+
const width = Math.max(8, cols - 2);
|
|
704
|
+
const parts = [];
|
|
705
|
+
// Streaming indicator
|
|
706
|
+
if (this.streamingLabel) {
|
|
707
|
+
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
708
|
+
}
|
|
709
|
+
// Interrupt hint (during streaming)
|
|
710
|
+
if (this.mode === 'streaming' || this.scrollRegionActive) {
|
|
711
|
+
parts.push({ text: `[Esc] stop`, tone: 'warn' });
|
|
712
|
+
}
|
|
713
|
+
// Scrollback indicator
|
|
714
|
+
if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
|
|
715
|
+
parts.push({ text: `↕${this.scrollbackOffset}L`, tone: 'info' });
|
|
716
|
+
}
|
|
717
|
+
else if (this.scrollbackBuffer.length > 50) {
|
|
718
|
+
parts.push({ text: `↕${this.scrollbackBuffer.length}L`, tone: 'muted' });
|
|
719
|
+
}
|
|
720
|
+
// Toggle states (compact)
|
|
721
|
+
const editIcon = this.editMode === 'display-edits' ? '✓' : '?';
|
|
722
|
+
const verifyIcon = this.verificationEnabled ? '✓' : '○';
|
|
723
|
+
const autoIcon = this.autoContinueEnabled ? '✓' : '○';
|
|
724
|
+
parts.push({
|
|
725
|
+
text: `edits${editIcon} verify${verifyIcon} auto${autoIcon}`,
|
|
726
|
+
tone: 'muted'
|
|
727
|
+
});
|
|
728
|
+
// Context remaining
|
|
729
|
+
const contextRemaining = this.computeContextRemaining();
|
|
730
|
+
if (contextRemaining !== null && contextRemaining <= 50) {
|
|
731
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'info';
|
|
732
|
+
parts.push({ text: `${contextRemaining}%`, tone });
|
|
733
|
+
}
|
|
734
|
+
// Model info (when not streaming)
|
|
735
|
+
if (this.modelLabel && !this.streamingLabel) {
|
|
736
|
+
const shortModel = this.modelLabel.length > 20 ? this.modelLabel.slice(0, 18) + '..' : this.modelLabel;
|
|
737
|
+
parts.push({ text: shortModel, tone: 'muted' });
|
|
738
|
+
}
|
|
739
|
+
return renderStatusLine(parts, width);
|
|
740
|
+
}
|
|
741
|
+
formatHotkey(hotkey) {
|
|
742
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
743
|
+
if (!normalized)
|
|
744
|
+
return hotkey;
|
|
745
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
746
|
+
// Use readable key names instead of symbols for better terminal compatibility
|
|
747
|
+
const map = {
|
|
748
|
+
shift: 'Shift',
|
|
749
|
+
sh: 'Shift',
|
|
750
|
+
alt: 'Alt',
|
|
751
|
+
option: 'Alt',
|
|
752
|
+
opt: 'Alt',
|
|
753
|
+
ctrl: 'Ctrl',
|
|
754
|
+
control: 'Ctrl',
|
|
755
|
+
cmd: 'Cmd',
|
|
756
|
+
meta: 'Cmd',
|
|
757
|
+
esc: 'Esc',
|
|
758
|
+
escape: 'Esc',
|
|
759
|
+
tab: 'Tab',
|
|
760
|
+
return: 'Enter',
|
|
761
|
+
enter: 'Enter',
|
|
762
|
+
pageup: 'PgUp',
|
|
763
|
+
pagedown: 'PgDn',
|
|
764
|
+
home: 'Home',
|
|
765
|
+
end: 'End',
|
|
766
|
+
};
|
|
767
|
+
const formatted = parts
|
|
768
|
+
.map((part) => {
|
|
769
|
+
const label = map[part];
|
|
770
|
+
if (label)
|
|
771
|
+
return label;
|
|
772
|
+
return part.length === 1 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1);
|
|
773
|
+
})
|
|
774
|
+
.join('+');
|
|
775
|
+
return `[${formatted}]`;
|
|
705
776
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (visible === target) {
|
|
710
|
-
return value;
|
|
711
|
-
}
|
|
712
|
-
if (visible < target) {
|
|
713
|
-
return `${value}${' '.repeat(target - visible)}`;
|
|
777
|
+
computeContextRemaining() {
|
|
778
|
+
if (this.contextUsage === null) {
|
|
779
|
+
return null;
|
|
714
780
|
}
|
|
715
|
-
return
|
|
781
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
716
782
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
let result = '';
|
|
721
|
-
for (let i = 0; i < value.length && visible < target; i++) {
|
|
722
|
-
const char = value[i];
|
|
723
|
-
if (char === '\x1b') {
|
|
724
|
-
const match = value.slice(i).match(/^\x1b\[[0-9;]*[A-Za-z]/);
|
|
725
|
-
if (match) {
|
|
726
|
-
result += match[0];
|
|
727
|
-
i += match[0].length - 1;
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
result += char;
|
|
732
|
-
visible += 1;
|
|
783
|
+
computeTokensRemaining() {
|
|
784
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
785
|
+
return null;
|
|
733
786
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
return result;
|
|
787
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
788
|
+
return this.formatTokenCount(remaining);
|
|
738
789
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
*/
|
|
743
|
-
buildStatusBar(width) {
|
|
744
|
-
const { green: GREEN, yellow: YELLOW, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
745
|
-
const segments = [];
|
|
746
|
-
// Streaming status with elapsed time
|
|
747
|
-
if (this.mode === 'streaming' || this.streamingLabel) {
|
|
748
|
-
let label = this.streamingLabel || 'Streaming';
|
|
749
|
-
if (this.streamingStartTime) {
|
|
750
|
-
const elapsed = Math.max(0, Math.floor((Date.now() - this.streamingStartTime) / 1000));
|
|
751
|
-
const mins = Math.floor(elapsed / 60);
|
|
752
|
-
const secs = elapsed % 60;
|
|
753
|
-
const elapsedLabel = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
754
|
-
label = `${label} ${elapsedLabel}`;
|
|
755
|
-
}
|
|
756
|
-
segments.push(`${GREEN}● ${label}${R}`);
|
|
757
|
-
}
|
|
758
|
-
// Override/warning status
|
|
759
|
-
if (this.overrideStatusMessage) {
|
|
760
|
-
segments.push(`${YELLOW}⚠ ${this.overrideStatusMessage}${R}`);
|
|
761
|
-
}
|
|
762
|
-
// Primary status message
|
|
763
|
-
if (this.statusMessage) {
|
|
764
|
-
segments.push(`${CYAN}${this.statusMessage}${R}`);
|
|
790
|
+
formatElapsedLabel(seconds) {
|
|
791
|
+
if (seconds < 60) {
|
|
792
|
+
return `${seconds}s`;
|
|
765
793
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
// Model info for quick glance
|
|
775
|
-
if (this.modelInfo) {
|
|
776
|
-
segments.push(`${DIM}${this.modelInfo}${R}`);
|
|
794
|
+
const mins = Math.floor(seconds / 60);
|
|
795
|
+
const secs = seconds % 60;
|
|
796
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
797
|
+
}
|
|
798
|
+
formatTokenCount(value) {
|
|
799
|
+
if (!Number.isFinite(value)) {
|
|
800
|
+
return `${value}`;
|
|
777
801
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
segments.push(`${DIM}Type a message or / for commands${R}`);
|
|
802
|
+
if (value >= 1_000_000) {
|
|
803
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
781
804
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
segments.push(`${DIM}${this.buffer.split('\n').length}L${R}`);
|
|
805
|
+
if (value >= 1_000) {
|
|
806
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
785
807
|
}
|
|
786
|
-
|
|
787
|
-
|
|
808
|
+
return `${Math.round(value)}`;
|
|
809
|
+
}
|
|
810
|
+
visibleLength(value) {
|
|
811
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
812
|
+
return value.replace(ansiPattern, '').length;
|
|
788
813
|
}
|
|
789
814
|
/**
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
815
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
816
|
+
* needing a TTY. Not used by production code.
|
|
794
817
|
*/
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
802
|
-
if (this.editMode === 'display-edits') {
|
|
803
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
807
|
-
}
|
|
808
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
809
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
810
|
-
// Verification (green when on) - per schema.verificationMode
|
|
811
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
812
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
813
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
814
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
815
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
816
|
-
const rightParts = [];
|
|
817
|
-
if (this.contextUsage !== null) {
|
|
818
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
819
|
-
if (rem < 10)
|
|
820
|
-
rightParts.push(`${RED}⚠ ctx: ${rem}%${R}`);
|
|
821
|
-
else if (rem < 25)
|
|
822
|
-
rightParts.push(`${YELLOW}! ctx: ${rem}%${R}`);
|
|
823
|
-
else
|
|
824
|
-
rightParts.push(`${DIM}ctx: ${rem}%${R}`);
|
|
825
|
-
}
|
|
826
|
-
if (this.modelInfo) {
|
|
827
|
-
rightParts.push(`${DIM}${this.modelInfo}${R}`);
|
|
828
|
-
}
|
|
829
|
-
const rightPart = rightParts.join(`${DIM} · ${R}`);
|
|
830
|
-
const leftLen = this.visibleLength(leftPart);
|
|
831
|
-
const rightLen = this.visibleLength(rightPart);
|
|
832
|
-
if (rightPart && leftLen + rightLen < maxWidth - 2) {
|
|
833
|
-
return `${leftPart}${' '.repeat(Math.max(1, maxWidth - leftLen - rightLen))}${rightPart}`;
|
|
834
|
-
}
|
|
835
|
-
const combined = rightPart ? `${leftPart}${DIM} · ${R}${rightPart}` : leftPart;
|
|
836
|
-
return this.fitToWidth(combined, maxWidth);
|
|
818
|
+
getDebugUiSnapshot(width) {
|
|
819
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
820
|
+
return {
|
|
821
|
+
meta: this.buildMetaLines(cols - 2),
|
|
822
|
+
controls: this.buildModeControls(cols),
|
|
823
|
+
};
|
|
837
824
|
}
|
|
838
825
|
/**
|
|
839
826
|
* Force a re-render
|
|
@@ -856,65 +843,224 @@ export class TerminalInput extends EventEmitter {
|
|
|
856
843
|
handleResize() {
|
|
857
844
|
this.lastRenderContent = '';
|
|
858
845
|
this.lastRenderCursor = -1;
|
|
859
|
-
this.
|
|
846
|
+
this.resetStreamingRenderThrottle();
|
|
847
|
+
// If in scrollback mode, re-render the scrollback view with new dimensions
|
|
848
|
+
if (this.isInScrollbackMode) {
|
|
849
|
+
this.renderScrollbackView();
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
this.scheduleRender();
|
|
853
|
+
}
|
|
860
854
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const next = this.displayRef.getTotalWrittenLines();
|
|
884
|
-
if (typeof next === 'number' && Number.isFinite(next)) {
|
|
885
|
-
this.contentEndRow = next;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
this.renderDirty = true;
|
|
889
|
-
queueMicrotask(() => this.render());
|
|
890
|
-
}
|
|
891
|
-
},
|
|
892
|
-
});
|
|
855
|
+
/**
|
|
856
|
+
* Enter streaming mode with scroll region.
|
|
857
|
+
* Sets up terminal scroll region to exclude chat box.
|
|
858
|
+
*/
|
|
859
|
+
enterStreamingScrollRegion() {
|
|
860
|
+
const { rows } = this.getSize();
|
|
861
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
862
|
+
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
863
|
+
writeLock.lock('enterStreamingScrollRegion');
|
|
864
|
+
try {
|
|
865
|
+
// Set scroll region for content area (above chat box)
|
|
866
|
+
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
867
|
+
// Position cursor at current content row
|
|
868
|
+
this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
|
|
869
|
+
this.scrollRegionActive = true;
|
|
870
|
+
this.setStatusMessage('esc to interrupt');
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
writeLock.unlock();
|
|
874
|
+
}
|
|
875
|
+
// Render pinned chat box at bottom
|
|
876
|
+
this.forceRender();
|
|
893
877
|
}
|
|
894
878
|
/**
|
|
895
|
-
*
|
|
879
|
+
* Exit streaming mode and restore normal operation.
|
|
896
880
|
*/
|
|
897
|
-
|
|
898
|
-
|
|
881
|
+
exitStreamingScrollRegion() {
|
|
882
|
+
writeLock.lock('exitStreamingScrollRegion');
|
|
883
|
+
try {
|
|
884
|
+
// Reset scroll region to full terminal
|
|
885
|
+
this.write(ESC.RESET_SCROLL);
|
|
886
|
+
this.scrollRegionActive = false;
|
|
887
|
+
this.setStatusMessage('Ready for prompts');
|
|
888
|
+
}
|
|
889
|
+
finally {
|
|
890
|
+
writeLock.unlock();
|
|
891
|
+
}
|
|
892
|
+
// Final render
|
|
893
|
+
this.forceRender();
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Render chat box at bottom - now uses unified renderer.
|
|
897
|
+
* @deprecated Use renderPinnedChatBox() directly via render()/forceRender()
|
|
898
|
+
*/
|
|
899
|
+
renderChatBoxAtBottom() {
|
|
900
|
+
this.renderPinnedChatBox();
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Stream content within the scroll region.
|
|
904
|
+
* Content is written directly and scrolls naturally.
|
|
905
|
+
*/
|
|
906
|
+
streamContent(content) {
|
|
907
|
+
if (!content)
|
|
899
908
|
return;
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
909
|
+
// Capture content in scrollback buffer
|
|
910
|
+
this.addToScrollback(content);
|
|
911
|
+
writeLock.lock('streamContent');
|
|
912
|
+
try {
|
|
913
|
+
// Write content - scroll region handles scrolling
|
|
914
|
+
this.write(content);
|
|
915
|
+
// Track newlines
|
|
916
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
917
|
+
this.contentRow += newlines;
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
writeLock.unlock();
|
|
921
|
+
}
|
|
922
|
+
// Throttle chat box updates during streaming
|
|
923
|
+
this.scheduleStreamingRender(200);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Enable scroll region (no-op in floating mode).
|
|
927
|
+
*/
|
|
928
|
+
enableScrollRegion() {
|
|
929
|
+
// No-op: using pure floating approach
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Disable scroll region (no-op in floating mode).
|
|
933
|
+
*/
|
|
934
|
+
disableScrollRegion() {
|
|
935
|
+
// No-op: using pure floating approach
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Calculate chat box height.
|
|
939
|
+
*/
|
|
940
|
+
getChatBoxHeight() {
|
|
941
|
+
return 5; // Fixed: divider + input + status + buffer
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* @deprecated Use streamContent() instead
|
|
945
|
+
* Register with display's output interceptor - kept for backwards compatibility
|
|
946
|
+
*/
|
|
947
|
+
registerOutputInterceptor(_display) {
|
|
948
|
+
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Write content above the floating chat box.
|
|
952
|
+
* Works both during streaming and when idle.
|
|
953
|
+
*/
|
|
954
|
+
writeToScrollRegion(content) {
|
|
955
|
+
if (!content)
|
|
956
|
+
return;
|
|
957
|
+
// Capture content in scrollback buffer
|
|
958
|
+
this.addToScrollback(content);
|
|
959
|
+
writeLock.lock('writeToScrollRegion');
|
|
960
|
+
try {
|
|
961
|
+
// Position cursor at content row and write
|
|
962
|
+
this.write(ESC.TO(this.contentRow, 1));
|
|
963
|
+
this.write(content);
|
|
964
|
+
// Track newlines
|
|
965
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
966
|
+
this.contentRow += newlines;
|
|
904
967
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
this.outputInterceptorCleanup();
|
|
908
|
-
this.outputInterceptorCleanup = undefined;
|
|
968
|
+
finally {
|
|
969
|
+
writeLock.unlock();
|
|
909
970
|
}
|
|
910
|
-
//
|
|
911
|
-
this.
|
|
912
|
-
|
|
913
|
-
if (this.unifiedUIInitialized) {
|
|
914
|
-
this.write(ESC.ALT_SCREEN_EXIT);
|
|
971
|
+
// Re-render chat box below new content (only when not streaming)
|
|
972
|
+
if (!this.scrollRegionActive) {
|
|
973
|
+
this.forceRender();
|
|
915
974
|
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Enter alternate screen buffer and clear it.
|
|
978
|
+
* This gives us full control over the terminal without affecting user's history.
|
|
979
|
+
*/
|
|
980
|
+
enterAlternateScreen() {
|
|
981
|
+
writeLock.lock('enterAltScreen');
|
|
982
|
+
try {
|
|
983
|
+
this.write(ESC.ENTER_ALT_SCREEN);
|
|
984
|
+
this.write(ESC.HOME);
|
|
985
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
986
|
+
this.contentRow = 1;
|
|
987
|
+
this.alternateScreenActive = true;
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
writeLock.unlock();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Exit alternate screen buffer.
|
|
995
|
+
* Restores the user's previous terminal content.
|
|
996
|
+
*/
|
|
997
|
+
exitAlternateScreen() {
|
|
998
|
+
writeLock.lock('exitAltScreen');
|
|
999
|
+
try {
|
|
1000
|
+
this.write(ESC.EXIT_ALT_SCREEN);
|
|
1001
|
+
this.alternateScreenActive = false;
|
|
1002
|
+
}
|
|
1003
|
+
finally {
|
|
1004
|
+
writeLock.unlock();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Check if alternate screen buffer is currently active.
|
|
1009
|
+
*/
|
|
1010
|
+
isAlternateScreenActive() {
|
|
1011
|
+
return this.alternateScreenActive;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Get a snapshot of the scrollback buffer (for display on exit).
|
|
1015
|
+
*/
|
|
1016
|
+
getScrollbackSnapshot() {
|
|
1017
|
+
return [...this.scrollbackBuffer];
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Clear the entire terminal screen and reset content position.
|
|
1021
|
+
* This removes all content including the launching command.
|
|
1022
|
+
*/
|
|
1023
|
+
clearScreen() {
|
|
1024
|
+
writeLock.lock('clearScreen');
|
|
1025
|
+
try {
|
|
1026
|
+
this.write(ESC.HOME);
|
|
1027
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
1028
|
+
this.contentRow = 1;
|
|
1029
|
+
}
|
|
1030
|
+
finally {
|
|
1031
|
+
writeLock.unlock();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Reset content position to row 1.
|
|
1036
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
1037
|
+
*/
|
|
1038
|
+
resetContentPosition() {
|
|
1039
|
+
this.contentRow = 1;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Set the content row explicitly (used after banner is written).
|
|
1043
|
+
* This tells the input where content should start flowing from.
|
|
1044
|
+
*/
|
|
1045
|
+
setContentRow(row) {
|
|
1046
|
+
this.contentRow = Math.max(1, row);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get the current content row position.
|
|
1050
|
+
*/
|
|
1051
|
+
getContentRow() {
|
|
1052
|
+
return this.contentRow;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Dispose and clean up
|
|
1056
|
+
*/
|
|
1057
|
+
dispose() {
|
|
1058
|
+
if (this.disposed)
|
|
1059
|
+
return;
|
|
916
1060
|
this.disposed = true;
|
|
917
1061
|
this.enabled = false;
|
|
1062
|
+
this.disableScrollRegion();
|
|
1063
|
+
this.resetStreamingRenderThrottle();
|
|
918
1064
|
this.disableBracketedPaste();
|
|
919
1065
|
this.buffer = '';
|
|
920
1066
|
this.queue = [];
|
|
@@ -956,8 +1102,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
956
1102
|
break;
|
|
957
1103
|
}
|
|
958
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Handle Alt/Meta key combinations for mode toggles and navigation.
|
|
1107
|
+
* All major erosolar-cli features accessible via keyboard shortcuts.
|
|
1108
|
+
*/
|
|
959
1109
|
handleMetaKey(key) {
|
|
960
1110
|
switch (key.name) {
|
|
1111
|
+
// === CURSOR MOVEMENT ===
|
|
961
1112
|
case 'left':
|
|
962
1113
|
case 'b':
|
|
963
1114
|
this.moveCursorWordLeft();
|
|
@@ -972,14 +1123,96 @@ export class TerminalInput extends EventEmitter {
|
|
|
972
1123
|
case 'return':
|
|
973
1124
|
this.insertNewline();
|
|
974
1125
|
break;
|
|
1126
|
+
// === MODE TOGGLES ===
|
|
975
1127
|
case 'v':
|
|
1128
|
+
// Alt+V: Toggle verification mode (auto-tests after edits)
|
|
976
1129
|
this.emit('toggleVerify');
|
|
977
1130
|
break;
|
|
978
1131
|
case 'c':
|
|
1132
|
+
// Alt+C: Toggle auto-continue mode
|
|
979
1133
|
this.emit('toggleAutoContinue');
|
|
980
1134
|
break;
|
|
1135
|
+
case 't':
|
|
1136
|
+
// Alt+T: Toggle/cycle thinking mode
|
|
1137
|
+
this.emit('toggleThinking');
|
|
1138
|
+
break;
|
|
1139
|
+
case 'e':
|
|
1140
|
+
// Alt+E: Toggle edit permission mode (ask/auto)
|
|
1141
|
+
this.toggleEditMode();
|
|
1142
|
+
this.emit('toggleEditMode');
|
|
1143
|
+
break;
|
|
1144
|
+
case 'x':
|
|
1145
|
+
// Alt+X: Clear/compact context
|
|
1146
|
+
this.emit('clearContext');
|
|
1147
|
+
break;
|
|
1148
|
+
// === SCROLLBACK NAVIGATION ===
|
|
1149
|
+
case 's':
|
|
1150
|
+
// Alt+S: Toggle scrollback mode
|
|
1151
|
+
this.toggleScrollbackMode();
|
|
1152
|
+
break;
|
|
1153
|
+
case 'up':
|
|
1154
|
+
// Alt+Up: Quick scroll up into history
|
|
1155
|
+
if (!this.isInScrollbackMode) {
|
|
1156
|
+
this.scrollUp(10);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
this.scrollUp(1);
|
|
1160
|
+
}
|
|
1161
|
+
break;
|
|
1162
|
+
case 'down':
|
|
1163
|
+
// Alt+Down: Quick scroll down
|
|
1164
|
+
if (this.isInScrollbackMode) {
|
|
1165
|
+
this.scrollDown(1);
|
|
1166
|
+
}
|
|
1167
|
+
break;
|
|
1168
|
+
case 'pageup':
|
|
1169
|
+
// Alt+PageUp: Page up in scrollback
|
|
1170
|
+
this.scrollUp(this.getPageSize());
|
|
1171
|
+
break;
|
|
1172
|
+
case 'pagedown':
|
|
1173
|
+
// Alt+PageDown: Page down in scrollback
|
|
1174
|
+
this.scrollDown(this.getPageSize());
|
|
1175
|
+
break;
|
|
1176
|
+
case 'home':
|
|
1177
|
+
// Alt+Home: Jump to top of scrollback
|
|
1178
|
+
this.scrollToTop();
|
|
1179
|
+
break;
|
|
1180
|
+
case 'end':
|
|
1181
|
+
// Alt+End: Jump to bottom (live)
|
|
1182
|
+
this.scrollToBottom();
|
|
1183
|
+
break;
|
|
981
1184
|
}
|
|
982
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Get page size for scrollback navigation.
|
|
1188
|
+
*/
|
|
1189
|
+
getPageSize() {
|
|
1190
|
+
const { rows } = this.getSize();
|
|
1191
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
1192
|
+
return Math.max(5, rows - chatBoxHeight - 2);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Build scroll indicator for the divider line (Claude Code style).
|
|
1196
|
+
* Shows scroll position when in scrollback mode, or history size hint when idle.
|
|
1197
|
+
*/
|
|
1198
|
+
buildScrollIndicator() {
|
|
1199
|
+
const bufferSize = this.scrollbackBuffer.length;
|
|
1200
|
+
// In scrollback mode - show position
|
|
1201
|
+
if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
|
|
1202
|
+
const { rows } = this.getSize();
|
|
1203
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
1204
|
+
const viewportHeight = Math.max(1, rows - chatBoxHeight);
|
|
1205
|
+
const currentPos = Math.max(0, bufferSize - this.scrollbackOffset - viewportHeight);
|
|
1206
|
+
const pct = bufferSize > 0 ? Math.round((currentPos / bufferSize) * 100) : 0;
|
|
1207
|
+
return `↑${this.scrollbackOffset} · ${pct}% · PgUp/Dn`;
|
|
1208
|
+
}
|
|
1209
|
+
// Not in scrollback - show hint if there's history
|
|
1210
|
+
if (bufferSize > 20) {
|
|
1211
|
+
const sizeLabel = bufferSize >= 1000 ? `${Math.floor(bufferSize / 1000)}k` : `${bufferSize}`;
|
|
1212
|
+
return `↕${sizeLabel}L · PgUp`;
|
|
1213
|
+
}
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
983
1216
|
handleSpecialKey(_str, key) {
|
|
984
1217
|
switch (key.name) {
|
|
985
1218
|
case 'return':
|
|
@@ -1003,38 +1236,53 @@ export class TerminalInput extends EventEmitter {
|
|
|
1003
1236
|
this.moveCursorRight();
|
|
1004
1237
|
return true;
|
|
1005
1238
|
case 'up':
|
|
1006
|
-
|
|
1239
|
+
// Ctrl+Shift+Up or Shift+Up: Quick scroll up in scrollback
|
|
1240
|
+
if ((key.ctrl && key.shift) || key.shift) {
|
|
1241
|
+
this.scrollUp(5);
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
this.handleUp();
|
|
1245
|
+
}
|
|
1007
1246
|
return true;
|
|
1008
1247
|
case 'down':
|
|
1009
|
-
|
|
1248
|
+
// Ctrl+Shift+Down or Shift+Down: Quick scroll down in scrollback
|
|
1249
|
+
if ((key.ctrl && key.shift) || key.shift) {
|
|
1250
|
+
this.scrollDown(5);
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
this.handleDown();
|
|
1254
|
+
}
|
|
1010
1255
|
return true;
|
|
1011
1256
|
case 'home':
|
|
1012
|
-
|
|
1257
|
+
// Ctrl+Home or in scrollback mode: scroll to top
|
|
1258
|
+
if (key.ctrl || this.isInScrollbackMode) {
|
|
1259
|
+
this.scrollToTop();
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
this.moveCursorToLineStart();
|
|
1263
|
+
}
|
|
1013
1264
|
return true;
|
|
1014
1265
|
case 'end':
|
|
1015
|
-
|
|
1266
|
+
// Ctrl+End or in scrollback mode: scroll to bottom (live mode)
|
|
1267
|
+
if (key.ctrl || this.isInScrollbackMode) {
|
|
1268
|
+
this.scrollToBottom();
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
this.moveCursorToLineEnd();
|
|
1272
|
+
}
|
|
1273
|
+
return true;
|
|
1274
|
+
case 'pageup':
|
|
1275
|
+
this.scrollUp(20); // Scroll up by 20 lines
|
|
1276
|
+
return true;
|
|
1277
|
+
case 'pagedown':
|
|
1278
|
+
this.scrollDown(20); // Scroll down by 20 lines
|
|
1016
1279
|
return true;
|
|
1017
1280
|
case 'tab':
|
|
1018
1281
|
if (key.shift) {
|
|
1019
1282
|
this.toggleEditMode();
|
|
1020
1283
|
return true;
|
|
1021
1284
|
}
|
|
1022
|
-
|
|
1023
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1024
|
-
this.togglePasteExpansion();
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
this.toggleThinking();
|
|
1028
|
-
}
|
|
1029
|
-
return true;
|
|
1030
|
-
case 'escape':
|
|
1031
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1032
|
-
if (this.mode === 'streaming') {
|
|
1033
|
-
this.emit('interrupt');
|
|
1034
|
-
}
|
|
1035
|
-
else if (this.buffer.length > 0) {
|
|
1036
|
-
this.clear();
|
|
1037
|
-
}
|
|
1285
|
+
this.insertText(' ');
|
|
1038
1286
|
return true;
|
|
1039
1287
|
}
|
|
1040
1288
|
return false;
|
|
@@ -1052,7 +1300,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1052
1300
|
this.insertPlainText(chunk, insertPos);
|
|
1053
1301
|
this.cursor = insertPos + chunk.length;
|
|
1054
1302
|
this.emit('change', this.buffer);
|
|
1055
|
-
this.updateSuggestions();
|
|
1056
1303
|
this.scheduleRender();
|
|
1057
1304
|
}
|
|
1058
1305
|
insertNewline() {
|
|
@@ -1077,7 +1324,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1077
1324
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1078
1325
|
}
|
|
1079
1326
|
this.emit('change', this.buffer);
|
|
1080
|
-
this.updateSuggestions();
|
|
1081
1327
|
this.scheduleRender();
|
|
1082
1328
|
}
|
|
1083
1329
|
deleteForward() {
|
|
@@ -1305,13 +1551,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1305
1551
|
timestamp: Date.now(),
|
|
1306
1552
|
});
|
|
1307
1553
|
this.emit('queue', text);
|
|
1308
|
-
this.clear(); // Clear immediately for queued input
|
|
1554
|
+
this.clear(); // Clear immediately for queued input
|
|
1309
1555
|
}
|
|
1310
1556
|
else {
|
|
1311
|
-
// In idle mode, clear the input
|
|
1312
|
-
// The
|
|
1313
|
-
|
|
1314
|
-
this.clear(true); // Skip render - streaming will handle display
|
|
1557
|
+
// In idle mode, clear the input first, then emit submit.
|
|
1558
|
+
// The prompt will be logged as a visible message by the caller.
|
|
1559
|
+
this.clear();
|
|
1315
1560
|
this.emit('submit', text);
|
|
1316
1561
|
}
|
|
1317
1562
|
}
|
|
@@ -1328,7 +1573,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1328
1573
|
if (available <= 0)
|
|
1329
1574
|
return;
|
|
1330
1575
|
const chunk = clean.slice(0, available);
|
|
1331
|
-
|
|
1576
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1577
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1578
|
+
if (isMultiline && !isShortMultiline) {
|
|
1332
1579
|
this.insertPastePlaceholder(chunk);
|
|
1333
1580
|
}
|
|
1334
1581
|
else {
|
|
@@ -1392,6 +1639,236 @@ export class TerminalInput extends EventEmitter {
|
|
|
1392
1639
|
return { lines, cursorLine, cursorCol };
|
|
1393
1640
|
}
|
|
1394
1641
|
// ===========================================================================
|
|
1642
|
+
// SCROLLBACK BUFFER
|
|
1643
|
+
// ===========================================================================
|
|
1644
|
+
/**
|
|
1645
|
+
* Add content to the scrollback buffer for history retention
|
|
1646
|
+
*/
|
|
1647
|
+
addToScrollback(content) {
|
|
1648
|
+
if (!content)
|
|
1649
|
+
return;
|
|
1650
|
+
// Split content into lines and add to buffer
|
|
1651
|
+
const lines = content.split('\n');
|
|
1652
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1653
|
+
const line = lines[i];
|
|
1654
|
+
if (line !== undefined) {
|
|
1655
|
+
// Only add non-empty lines or preserve newlines between content
|
|
1656
|
+
if (i < lines.length - 1 || line.length > 0) {
|
|
1657
|
+
this.scrollbackBuffer.push(line);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// Trim buffer if it exceeds max size
|
|
1662
|
+
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1663
|
+
this.scrollbackBuffer.shift();
|
|
1664
|
+
}
|
|
1665
|
+
// If we're in live mode (not scrolled up), keep offset at 0
|
|
1666
|
+
if (this.scrollbackOffset === 0) {
|
|
1667
|
+
this.isInScrollbackMode = false;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Scroll up by a number of lines (PageUp)
|
|
1672
|
+
*/
|
|
1673
|
+
scrollUp(lines = 10) {
|
|
1674
|
+
const { rows } = this.getSize();
|
|
1675
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
1676
|
+
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1677
|
+
// Calculate max scroll offset
|
|
1678
|
+
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1679
|
+
this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
|
|
1680
|
+
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1681
|
+
if (this.isInScrollbackMode) {
|
|
1682
|
+
this.renderScrollbackView();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Scroll down by a number of lines (PageDown)
|
|
1687
|
+
*/
|
|
1688
|
+
scrollDown(lines = 10) {
|
|
1689
|
+
this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
|
|
1690
|
+
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1691
|
+
if (this.isInScrollbackMode) {
|
|
1692
|
+
this.renderScrollbackView();
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
// Returned to live mode - force re-render
|
|
1696
|
+
this.forceRender();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Jump to the top of scrollback buffer
|
|
1701
|
+
*/
|
|
1702
|
+
scrollToTop() {
|
|
1703
|
+
const { rows } = this.getSize();
|
|
1704
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
1705
|
+
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1706
|
+
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1707
|
+
this.scrollbackOffset = maxOffset;
|
|
1708
|
+
this.isInScrollbackMode = true;
|
|
1709
|
+
this.renderScrollbackView();
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Jump to the bottom (live mode)
|
|
1713
|
+
*/
|
|
1714
|
+
scrollToBottom() {
|
|
1715
|
+
this.scrollbackOffset = 0;
|
|
1716
|
+
this.isInScrollbackMode = false;
|
|
1717
|
+
this.forceRender();
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Toggle scrollback mode on/off (Alt+S hotkey)
|
|
1721
|
+
*/
|
|
1722
|
+
toggleScrollbackMode() {
|
|
1723
|
+
if (this.isInScrollbackMode) {
|
|
1724
|
+
this.scrollToBottom();
|
|
1725
|
+
}
|
|
1726
|
+
else if (this.scrollbackBuffer.length > 0) {
|
|
1727
|
+
this.scrollUp(20);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Render the scrollback buffer view with enhanced visuals
|
|
1732
|
+
* Features:
|
|
1733
|
+
* - Visual scroll position indicator
|
|
1734
|
+
* - Progress bar showing position in history
|
|
1735
|
+
* - Keyboard navigation hints
|
|
1736
|
+
* - Animated indicators
|
|
1737
|
+
*/
|
|
1738
|
+
renderScrollbackView() {
|
|
1739
|
+
const { rows, cols } = this.getSize();
|
|
1740
|
+
const chatBoxHeight = this.getChatBoxHeight();
|
|
1741
|
+
const contentHeight = Math.max(1, rows - chatBoxHeight);
|
|
1742
|
+
writeLock.lock('renderScrollback');
|
|
1743
|
+
try {
|
|
1744
|
+
this.write(ESC.SAVE);
|
|
1745
|
+
this.write(ESC.HIDE);
|
|
1746
|
+
// Clear content area
|
|
1747
|
+
for (let i = 1; i <= contentHeight; i++) {
|
|
1748
|
+
this.write(ESC.TO(i, 1));
|
|
1749
|
+
this.write(ESC.CLEAR_LINE);
|
|
1750
|
+
}
|
|
1751
|
+
// Calculate which lines to show
|
|
1752
|
+
const totalLines = this.scrollbackBuffer.length;
|
|
1753
|
+
const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
|
|
1754
|
+
const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
|
|
1755
|
+
const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
|
|
1756
|
+
// Build header bar with navigation hints
|
|
1757
|
+
const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
|
|
1758
|
+
this.write(ESC.TO(1, 1));
|
|
1759
|
+
this.write(headerInfo);
|
|
1760
|
+
// Render visible lines with line numbers and visual guides
|
|
1761
|
+
const lineNumWidth = String(totalLines).length + 1;
|
|
1762
|
+
const contentStart = 2; // Start after header
|
|
1763
|
+
for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
|
|
1764
|
+
const line = visibleLines[i] ?? '';
|
|
1765
|
+
const lineNum = startIdx + i + 1;
|
|
1766
|
+
this.write(ESC.TO(contentStart + i, 1));
|
|
1767
|
+
// Line number gutter
|
|
1768
|
+
const numStr = String(lineNum).padStart(lineNumWidth, ' ');
|
|
1769
|
+
this.write(theme.ui.muted(`${numStr} │ `));
|
|
1770
|
+
// Content with truncation
|
|
1771
|
+
const gutterWidth = lineNumWidth + 4;
|
|
1772
|
+
const maxLen = cols - gutterWidth - 2;
|
|
1773
|
+
const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
|
|
1774
|
+
this.write(displayLine);
|
|
1775
|
+
}
|
|
1776
|
+
// Add visual scroll track on the right edge
|
|
1777
|
+
this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
|
|
1778
|
+
this.write(ESC.RESTORE);
|
|
1779
|
+
this.write(ESC.SHOW);
|
|
1780
|
+
}
|
|
1781
|
+
finally {
|
|
1782
|
+
writeLock.unlock();
|
|
1783
|
+
}
|
|
1784
|
+
// Re-render chat box
|
|
1785
|
+
this.forceRender();
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Build scrollback header with navigation hints
|
|
1789
|
+
*/
|
|
1790
|
+
buildScrollbackHeader(cols, totalLines, startIdx, endIdx) {
|
|
1791
|
+
const percentage = Math.round((endIdx / totalLines) * 100);
|
|
1792
|
+
// Animated scroll indicator
|
|
1793
|
+
const scrollFrames = ['◆', '◇', '◆', '◈'];
|
|
1794
|
+
this.scrollIndicatorFrame = (this.scrollIndicatorFrame + 1) % scrollFrames.length;
|
|
1795
|
+
const indicator = scrollFrames[this.scrollIndicatorFrame];
|
|
1796
|
+
// Build header parts
|
|
1797
|
+
const leftPart = theme.info(`${indicator} SCROLLBACK`) +
|
|
1798
|
+
theme.ui.muted(` [${startIdx + 1}-${endIdx} of ${totalLines}]`);
|
|
1799
|
+
const progressBar = this.buildProgressBar(percentage, 15);
|
|
1800
|
+
const rightPart = progressBar +
|
|
1801
|
+
theme.ui.muted(` ${percentage}%`) +
|
|
1802
|
+
theme.ui.muted(' │ ') +
|
|
1803
|
+
theme.primary('PgUp') + theme.ui.muted('/') + theme.primary('PgDn') +
|
|
1804
|
+
theme.ui.muted(' scroll · ') +
|
|
1805
|
+
theme.primary('End') + theme.ui.muted(' exit');
|
|
1806
|
+
const leftLen = this.visibleLength(leftPart);
|
|
1807
|
+
const rightLen = this.visibleLength(rightPart);
|
|
1808
|
+
const padding = Math.max(1, cols - leftLen - rightLen - 2);
|
|
1809
|
+
return `${leftPart}${' '.repeat(padding)}${rightPart}`;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Render visual scroll track on the right side
|
|
1813
|
+
*/
|
|
1814
|
+
renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx) {
|
|
1815
|
+
if (totalLines <= contentHeight || cols < 40)
|
|
1816
|
+
return;
|
|
1817
|
+
const trackHeight = contentHeight - 1; // Exclude header
|
|
1818
|
+
const viewportRatio = (endIdx - startIdx) / totalLines;
|
|
1819
|
+
const positionRatio = startIdx / Math.max(1, totalLines - (endIdx - startIdx));
|
|
1820
|
+
// Calculate thumb size and position
|
|
1821
|
+
const thumbSize = Math.max(1, Math.round(viewportRatio * trackHeight));
|
|
1822
|
+
const thumbStart = Math.round(positionRatio * (trackHeight - thumbSize));
|
|
1823
|
+
// Render track on right edge
|
|
1824
|
+
for (let i = 0; i < trackHeight; i++) {
|
|
1825
|
+
const row = 2 + i; // Start after header
|
|
1826
|
+
this.write(ESC.TO(row, cols));
|
|
1827
|
+
if (i >= thumbStart && i < thumbStart + thumbSize) {
|
|
1828
|
+
// Thumb (viewport indicator)
|
|
1829
|
+
this.write(theme.accent('█'));
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
// Track background
|
|
1833
|
+
this.write(theme.ui.muted('░'));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Build a visual progress bar
|
|
1839
|
+
*/
|
|
1840
|
+
buildProgressBar(percentage, width = 10) {
|
|
1841
|
+
const filled = Math.round((percentage / 100) * width);
|
|
1842
|
+
const empty = width - filled;
|
|
1843
|
+
const bar = theme.accent('█'.repeat(filled)) +
|
|
1844
|
+
theme.ui.muted('░'.repeat(empty));
|
|
1845
|
+
return `${theme.ui.muted('[')}${bar}${theme.ui.muted(']')}`;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Get scrollback buffer content (for persistence)
|
|
1849
|
+
*/
|
|
1850
|
+
getScrollbackBuffer() {
|
|
1851
|
+
return [...this.scrollbackBuffer];
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Load scrollback buffer (for restoration)
|
|
1855
|
+
*/
|
|
1856
|
+
loadScrollbackBuffer(lines) {
|
|
1857
|
+
this.scrollbackBuffer = [...lines];
|
|
1858
|
+
// Trim if necessary
|
|
1859
|
+
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1860
|
+
this.scrollbackBuffer.shift();
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Clear scrollback buffer
|
|
1865
|
+
*/
|
|
1866
|
+
clearScrollbackBuffer() {
|
|
1867
|
+
this.scrollbackBuffer = [];
|
|
1868
|
+
this.scrollbackOffset = 0;
|
|
1869
|
+
this.isInScrollbackMode = false;
|
|
1870
|
+
}
|
|
1871
|
+
// ===========================================================================
|
|
1395
1872
|
// UTILITIES
|
|
1396
1873
|
// ===========================================================================
|
|
1397
1874
|
getComposedLength() {
|
|
@@ -1464,17 +1941,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1464
1941
|
this.shiftPlaceholders(position, text.length);
|
|
1465
1942
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1466
1943
|
}
|
|
1944
|
+
shouldInlineMultiline(content) {
|
|
1945
|
+
const lines = content.split('\n').length;
|
|
1946
|
+
const maxInlineLines = 4;
|
|
1947
|
+
const maxInlineChars = 240;
|
|
1948
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1949
|
+
}
|
|
1467
1950
|
findPlaceholderAt(position) {
|
|
1468
1951
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1469
1952
|
}
|
|
1470
|
-
buildPlaceholder(
|
|
1953
|
+
buildPlaceholder(lineCount) {
|
|
1471
1954
|
const id = ++this.pasteCounter;
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
const preview = summary.preview.length > 30
|
|
1475
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1476
|
-
: summary.preview;
|
|
1477
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1955
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1956
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1478
1957
|
return { id, placeholder };
|
|
1479
1958
|
}
|
|
1480
1959
|
insertPastePlaceholder(content) {
|
|
@@ -1482,67 +1961,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1482
1961
|
if (available <= 0)
|
|
1483
1962
|
return;
|
|
1484
1963
|
const cleanContent = content.slice(0, available);
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
if (summary.lineCount < 5) {
|
|
1488
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1489
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1490
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1491
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1964
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1965
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1495
1966
|
const insertPos = this.cursor;
|
|
1496
1967
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1497
1968
|
this.pastePlaceholders.push({
|
|
1498
1969
|
id,
|
|
1499
1970
|
content: cleanContent,
|
|
1500
|
-
lineCount
|
|
1971
|
+
lineCount,
|
|
1501
1972
|
placeholder,
|
|
1502
1973
|
start: insertPos,
|
|
1503
1974
|
end: insertPos + placeholder.length,
|
|
1504
|
-
summary,
|
|
1505
|
-
expanded: false,
|
|
1506
1975
|
});
|
|
1507
1976
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1508
1977
|
this.cursor = insertPos + placeholder.length;
|
|
1509
1978
|
}
|
|
1510
|
-
/**
|
|
1511
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1512
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1513
|
-
*/
|
|
1514
|
-
togglePasteExpansion() {
|
|
1515
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1516
|
-
if (!placeholder)
|
|
1517
|
-
return false;
|
|
1518
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1519
|
-
// Update the placeholder text in buffer
|
|
1520
|
-
const newPlaceholder = placeholder.expanded
|
|
1521
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1522
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1523
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1524
|
-
// Update buffer
|
|
1525
|
-
this.buffer =
|
|
1526
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1527
|
-
newPlaceholder +
|
|
1528
|
-
this.buffer.slice(placeholder.end);
|
|
1529
|
-
// Update placeholder tracking
|
|
1530
|
-
placeholder.placeholder = newPlaceholder;
|
|
1531
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1532
|
-
// Shift other placeholders
|
|
1533
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1534
|
-
this.scheduleRender();
|
|
1535
|
-
return true;
|
|
1536
|
-
}
|
|
1537
|
-
buildExpandedPlaceholder(ph) {
|
|
1538
|
-
const lines = ph.content.split('\n');
|
|
1539
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1540
|
-
const lastLines = lines.length > 5
|
|
1541
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1542
|
-
: '';
|
|
1543
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1544
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1545
|
-
}
|
|
1546
1979
|
deletePlaceholder(placeholder) {
|
|
1547
1980
|
const length = placeholder.end - placeholder.start;
|
|
1548
1981
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1550,7 +1983,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1550
1983
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1551
1984
|
this.cursor = placeholder.start;
|
|
1552
1985
|
}
|
|
1553
|
-
updateContextUsage(value) {
|
|
1986
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1987
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1988
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1989
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1990
|
+
}
|
|
1554
1991
|
if (value === null || !Number.isFinite(value)) {
|
|
1555
1992
|
this.contextUsage = null;
|
|
1556
1993
|
}
|
|
@@ -1577,6 +2014,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
1577
2014
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1578
2015
|
this.setEditMode(next);
|
|
1579
2016
|
}
|
|
2017
|
+
scheduleStreamingRender(delayMs) {
|
|
2018
|
+
if (this.streamingRenderTimer)
|
|
2019
|
+
return;
|
|
2020
|
+
const wait = Math.max(16, delayMs);
|
|
2021
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
2022
|
+
this.streamingRenderTimer = null;
|
|
2023
|
+
// During streaming, only update chat box (not full render)
|
|
2024
|
+
if (this.scrollRegionActive) {
|
|
2025
|
+
this.renderChatBoxAtBottom();
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
this.render();
|
|
2029
|
+
}
|
|
2030
|
+
}, wait);
|
|
2031
|
+
}
|
|
2032
|
+
resetStreamingRenderThrottle() {
|
|
2033
|
+
if (this.streamingRenderTimer) {
|
|
2034
|
+
clearTimeout(this.streamingRenderTimer);
|
|
2035
|
+
this.streamingRenderTimer = null;
|
|
2036
|
+
}
|
|
2037
|
+
this.lastStreamingRender = 0;
|
|
2038
|
+
}
|
|
1580
2039
|
scheduleRender() {
|
|
1581
2040
|
if (!this.canRender())
|
|
1582
2041
|
return;
|