erosolar-cli 1.7.345 → 1.7.346
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 +148 -24
- package/dist/alpha-zero/agentWrapper.d.ts +84 -0
- package/dist/alpha-zero/agentWrapper.d.ts.map +1 -0
- package/dist/alpha-zero/agentWrapper.js +171 -0
- package/dist/alpha-zero/agentWrapper.js.map +1 -0
- package/dist/alpha-zero/codeEvaluator.d.ts +25 -0
- package/dist/alpha-zero/codeEvaluator.d.ts.map +1 -0
- package/dist/alpha-zero/codeEvaluator.js +273 -0
- package/dist/alpha-zero/codeEvaluator.js.map +1 -0
- package/dist/alpha-zero/competitiveRunner.d.ts +66 -0
- package/dist/alpha-zero/competitiveRunner.d.ts.map +1 -0
- package/dist/alpha-zero/competitiveRunner.js +224 -0
- package/dist/alpha-zero/competitiveRunner.js.map +1 -0
- package/dist/alpha-zero/index.d.ts +67 -0
- package/dist/alpha-zero/index.d.ts.map +1 -0
- package/dist/alpha-zero/index.js +99 -0
- package/dist/alpha-zero/index.js.map +1 -0
- package/dist/alpha-zero/introspection.d.ts +128 -0
- package/dist/alpha-zero/introspection.d.ts.map +1 -0
- package/dist/alpha-zero/introspection.js +300 -0
- package/dist/alpha-zero/introspection.js.map +1 -0
- package/dist/alpha-zero/metricsTracker.d.ts +71 -0
- package/dist/alpha-zero/metricsTracker.d.ts.map +1 -0
- package/dist/{core → alpha-zero}/metricsTracker.js +5 -2
- package/dist/alpha-zero/metricsTracker.js.map +1 -0
- package/dist/alpha-zero/security/core.d.ts +125 -0
- package/dist/alpha-zero/security/core.d.ts.map +1 -0
- package/dist/alpha-zero/security/core.js +271 -0
- package/dist/alpha-zero/security/core.js.map +1 -0
- package/dist/alpha-zero/security/google.d.ts +125 -0
- package/dist/alpha-zero/security/google.d.ts.map +1 -0
- package/dist/alpha-zero/security/google.js +311 -0
- package/dist/alpha-zero/security/google.js.map +1 -0
- package/dist/alpha-zero/security/googleLoader.d.ts +17 -0
- package/dist/alpha-zero/security/googleLoader.d.ts.map +1 -0
- package/dist/alpha-zero/security/googleLoader.js +41 -0
- package/dist/alpha-zero/security/googleLoader.js.map +1 -0
- package/dist/alpha-zero/security/index.d.ts +29 -0
- package/dist/alpha-zero/security/index.d.ts.map +1 -0
- package/dist/alpha-zero/security/index.js +32 -0
- package/dist/alpha-zero/security/index.js.map +1 -0
- package/dist/alpha-zero/security/simulation.d.ts +124 -0
- package/dist/alpha-zero/security/simulation.d.ts.map +1 -0
- package/dist/alpha-zero/security/simulation.js +277 -0
- package/dist/alpha-zero/security/simulation.js.map +1 -0
- package/dist/alpha-zero/selfModification.d.ts +109 -0
- package/dist/alpha-zero/selfModification.d.ts.map +1 -0
- package/dist/alpha-zero/selfModification.js +233 -0
- package/dist/alpha-zero/selfModification.js.map +1 -0
- package/dist/alpha-zero/types.d.ts +170 -0
- package/dist/alpha-zero/types.d.ts.map +1 -0
- package/dist/alpha-zero/types.js +31 -0
- package/dist/alpha-zero/types.js.map +1 -0
- package/dist/bin/erosolar.js +21 -5
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +31 -56
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/capabilities/securityTestingCapability.d.ts +13 -0
- package/dist/capabilities/securityTestingCapability.d.ts.map +1 -0
- package/dist/capabilities/securityTestingCapability.js +25 -0
- package/dist/capabilities/securityTestingCapability.js.map +1 -0
- package/dist/contracts/agent-schemas.json +15 -0
- package/dist/contracts/tools.schema.json +9 -0
- 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/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/customCommands.d.ts +0 -1
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +0 -3
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/core/hooksSystem.d.ts +65 -0
- package/dist/core/hooksSystem.d.ts.map +1 -0
- package/dist/core/hooksSystem.js +273 -0
- package/dist/core/hooksSystem.js.map +1 -0
- package/dist/core/memorySystem.d.ts +48 -0
- package/dist/core/memorySystem.d.ts.map +1 -0
- package/dist/core/memorySystem.js +271 -0
- package/dist/core/memorySystem.js.map +1 -0
- package/dist/core/sessionStore.d.ts +0 -2
- package/dist/core/sessionStore.d.ts.map +1 -1
- package/dist/core/sessionStore.js +0 -1
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +14 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +1 -22
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +5 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +3 -14
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/unified/errors.d.ts +189 -0
- package/dist/core/unified/errors.d.ts.map +1 -0
- package/dist/core/unified/errors.js +497 -0
- package/dist/core/unified/errors.js.map +1 -0
- package/dist/core/unified/index.d.ts +19 -0
- package/dist/core/unified/index.d.ts.map +1 -0
- package/dist/core/unified/index.js +68 -0
- package/dist/core/unified/index.js.map +1 -0
- package/dist/core/unified/schema.d.ts +101 -0
- package/dist/core/unified/schema.d.ts.map +1 -0
- package/dist/core/unified/schema.js +350 -0
- package/dist/core/unified/schema.js.map +1 -0
- package/dist/core/unified/toolRuntime.d.ts +179 -0
- package/dist/core/unified/toolRuntime.d.ts.map +1 -0
- package/dist/core/unified/toolRuntime.js +517 -0
- package/dist/core/unified/toolRuntime.js.map +1 -0
- package/dist/core/unified/tools.d.ts +127 -0
- package/dist/core/unified/tools.d.ts.map +1 -0
- package/dist/core/unified/tools.js +1333 -0
- package/dist/core/unified/tools.js.map +1 -0
- package/dist/core/unified/types.d.ts +352 -0
- package/dist/core/unified/types.d.ts.map +1 -0
- package/dist/core/unified/types.js +12 -0
- package/dist/core/unified/types.js.map +1 -0
- package/dist/core/unified/version.d.ts +209 -0
- package/dist/core/unified/version.d.ts.map +1 -0
- package/dist/core/unified/version.js +454 -0
- package/dist/core/unified/version.js.map +1 -0
- package/dist/core/validationRunner.d.ts +3 -1
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -21
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- 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 +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +3 -0
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +1 -0
- package/dist/plugins/tools/security/securityPlugin.js +12 -0
- package/dist/plugins/tools/security/securityPlugin.js.map +1 -0
- 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/security/active-stack-security.d.ts +112 -0
- package/dist/security/active-stack-security.d.ts.map +1 -0
- package/dist/security/active-stack-security.js +296 -0
- package/dist/security/active-stack-security.js.map +1 -0
- package/dist/security/advanced-persistence-research.d.ts +92 -0
- package/dist/security/advanced-persistence-research.d.ts.map +1 -0
- package/dist/security/advanced-persistence-research.js +195 -0
- package/dist/security/advanced-persistence-research.js.map +1 -0
- package/dist/security/advanced-targeting.d.ts +119 -0
- package/dist/security/advanced-targeting.d.ts.map +1 -0
- package/dist/security/advanced-targeting.js +233 -0
- package/dist/security/advanced-targeting.js.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +104 -0
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +1 -0
- package/dist/security/assessment/vulnerabilityAssessment.js +315 -0
- package/dist/security/assessment/vulnerabilityAssessment.js.map +1 -0
- package/dist/security/authorization/securityAuthorization.d.ts +88 -0
- package/dist/security/authorization/securityAuthorization.d.ts.map +1 -0
- package/dist/security/authorization/securityAuthorization.js +172 -0
- package/dist/security/authorization/securityAuthorization.js.map +1 -0
- package/dist/security/comprehensive-targeting.d.ts +85 -0
- package/dist/security/comprehensive-targeting.d.ts.map +1 -0
- package/dist/security/comprehensive-targeting.js +438 -0
- package/dist/security/comprehensive-targeting.js.map +1 -0
- package/dist/security/global-security-integration.d.ts +91 -0
- package/dist/security/global-security-integration.d.ts.map +1 -0
- package/dist/security/global-security-integration.js +218 -0
- package/dist/security/global-security-integration.js.map +1 -0
- package/dist/security/index.d.ts +38 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +47 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/persistence-analyzer.d.ts +56 -0
- package/dist/security/persistence-analyzer.d.ts.map +1 -0
- package/dist/security/persistence-analyzer.js +187 -0
- package/dist/security/persistence-analyzer.js.map +1 -0
- package/dist/security/persistence-cli.d.ts +36 -0
- package/dist/security/persistence-cli.d.ts.map +1 -0
- package/dist/security/persistence-cli.js +160 -0
- package/dist/security/persistence-cli.js.map +1 -0
- package/dist/security/persistence-research.d.ts +92 -0
- package/dist/security/persistence-research.d.ts.map +1 -0
- package/dist/security/persistence-research.js +364 -0
- package/dist/security/persistence-research.js.map +1 -0
- package/dist/security/research/persistenceResearch.d.ts +97 -0
- package/dist/security/research/persistenceResearch.d.ts.map +1 -0
- package/dist/security/research/persistenceResearch.js +282 -0
- package/dist/security/research/persistenceResearch.js.map +1 -0
- package/dist/security/security-integration.d.ts +74 -0
- package/dist/security/security-integration.d.ts.map +1 -0
- package/dist/security/security-integration.js +137 -0
- package/dist/security/security-integration.js.map +1 -0
- package/dist/security/security-testing-framework.d.ts +112 -0
- package/dist/security/security-testing-framework.d.ts.map +1 -0
- package/dist/security/security-testing-framework.js +364 -0
- package/dist/security/security-testing-framework.js.map +1 -0
- package/dist/security/simulation/attackSimulation.d.ts +93 -0
- package/dist/security/simulation/attackSimulation.d.ts.map +1 -0
- package/dist/security/simulation/attackSimulation.js +341 -0
- package/dist/security/simulation/attackSimulation.js.map +1 -0
- package/dist/security/strategic-operations.d.ts +100 -0
- package/dist/security/strategic-operations.d.ts.map +1 -0
- package/dist/security/strategic-operations.js +276 -0
- package/dist/security/strategic-operations.js.map +1 -0
- package/dist/security/tool-security-wrapper.d.ts +58 -0
- package/dist/security/tool-security-wrapper.d.ts.map +1 -0
- package/dist/security/tool-security-wrapper.js +156 -0
- package/dist/security/tool-security-wrapper.js.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +7 -31
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +166 -362
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/metricsTracker.d.ts +60 -0
- package/dist/shell/metricsTracker.d.ts.map +1 -0
- package/dist/shell/metricsTracker.js +119 -0
- package/dist/shell/metricsTracker.js.map +1 -0
- package/dist/shell/shellApp.d.ts +0 -2
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +9 -82
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +120 -236
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +539 -1044
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +21 -99
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +30 -135
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/taskRunner.d.ts +1 -7
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +47 -180
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/securityTools.d.ts +22 -0
- package/dist/tools/securityTools.d.ts.map +1 -0
- package/dist/tools/securityTools.js +448 -0
- package/dist/tools/securityTools.js.map +1 -0
- package/dist/ui/ShellUIAdapter.d.ts +1 -7
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +18 -42
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +45 -24
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +259 -140
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/terminalUISchema.d.ts +195 -0
- package/dist/ui/terminalUISchema.d.ts.map +1 -0
- package/dist/ui/terminalUISchema.js +113 -0
- package/dist/ui/terminalUISchema.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +158 -0
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +348 -0
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +0 -20
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +216 -105
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +4 -4
- package/scripts/deploy-security-capabilities.js +178 -0
- package/dist/core/hooks.d.ts +0 -113
- package/dist/core/hooks.d.ts.map +0 -1
- package/dist/core/hooks.js +0 -267
- package/dist/core/hooks.js.map +0 -1
- package/dist/core/metricsTracker.d.ts +0 -122
- package/dist/core/metricsTracker.d.ts.map +0 -1
- package/dist/core/metricsTracker.js.map +0 -1
- package/dist/core/securityAssessment.d.ts +0 -91
- package/dist/core/securityAssessment.d.ts.map +0 -1
- package/dist/core/securityAssessment.js +0 -580
- package/dist/core/securityAssessment.js.map +0 -1
- package/dist/core/verification.d.ts +0 -137
- package/dist/core/verification.d.ts.map +0 -1
- package/dist/core/verification.js +0 -323
- package/dist/core/verification.js.map +0 -1
- package/dist/subagents/agentConfig.d.ts +0 -27
- package/dist/subagents/agentConfig.d.ts.map +0 -1
- package/dist/subagents/agentConfig.js +0 -89
- package/dist/subagents/agentConfig.js.map +0 -1
- package/dist/subagents/agentRegistry.d.ts +0 -33
- package/dist/subagents/agentRegistry.d.ts.map +0 -1
- package/dist/subagents/agentRegistry.js +0 -162
- package/dist/subagents/agentRegistry.js.map +0 -1
- package/dist/utils/frontmatter.d.ts +0 -10
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js +0 -78
- package/dist/utils/frontmatter.js.map +0 -1
|
@@ -3,24 +3,15 @@
|
|
|
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
|
|
9
6
|
* - Native bracketed paste support (no heuristics)
|
|
10
7
|
* - Clean cursor model with render-time wrapping
|
|
11
8
|
* - State machine for different input modes
|
|
12
9
|
* - 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
|
|
16
10
|
*/
|
|
17
11
|
import { EventEmitter } from 'node:events';
|
|
18
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
19
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
20
|
-
import {
|
|
21
|
-
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
22
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
23
|
-
import { theme } from '../ui/theme.js';
|
|
14
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
24
15
|
// ANSI escape codes
|
|
25
16
|
const ESC = {
|
|
26
17
|
// Cursor control
|
|
@@ -30,18 +21,14 @@ const ESC = {
|
|
|
30
21
|
SHOW: '\x1b[?25h',
|
|
31
22
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
32
23
|
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
|
|
33
29
|
// Line control
|
|
34
30
|
CLEAR_LINE: '\x1b[2K',
|
|
35
31
|
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',
|
|
45
32
|
// Style
|
|
46
33
|
RESET: '\x1b[0m',
|
|
47
34
|
DIM: '\x1b[2m',
|
|
@@ -53,12 +40,6 @@ const ESC = {
|
|
|
53
40
|
PASTE_DISABLE: '\x1b[?2004l',
|
|
54
41
|
PASTE_START: '\x1b[200~',
|
|
55
42
|
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
|
|
62
43
|
};
|
|
63
44
|
/**
|
|
64
45
|
* Unified terminal input handler
|
|
@@ -87,54 +68,49 @@ export class TerminalInput extends EventEmitter {
|
|
|
87
68
|
statusMessage = null;
|
|
88
69
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
89
70
|
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
|
|
95
71
|
lastRenderContent = '';
|
|
96
72
|
lastRenderCursor = -1;
|
|
97
73
|
renderDirty = false;
|
|
98
74
|
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;
|
|
99
83
|
// Lifecycle
|
|
100
84
|
disposed = false;
|
|
101
85
|
enabled = true;
|
|
102
86
|
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;
|
|
117
87
|
editMode = 'display-edits';
|
|
118
88
|
verificationEnabled = true;
|
|
119
89
|
autoContinueEnabled = false;
|
|
120
90
|
verificationHotkey = 'alt+v';
|
|
121
91
|
autoContinueHotkey = 'alt+c';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
92
|
+
// Output interceptor cleanup
|
|
93
|
+
outputInterceptorCleanup;
|
|
94
|
+
// Metrics tracking for status bar
|
|
95
|
+
streamingStartTime = null;
|
|
96
|
+
thinkingEnabled = true;
|
|
97
|
+
modelInfo = null; // Provider · Model info
|
|
98
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
128
99
|
streamingRenderTimer = null;
|
|
100
|
+
// Reference to display module for getting line counts during streaming
|
|
101
|
+
displayRef = null;
|
|
102
|
+
// Unified UI initialization flag
|
|
103
|
+
unifiedUIInitialized = false;
|
|
129
104
|
constructor(writeStream = process.stdout, config = {}) {
|
|
130
105
|
super();
|
|
131
106
|
this.out = writeStream;
|
|
107
|
+
// Use schema defaults for configuration consistency
|
|
132
108
|
this.config = {
|
|
133
|
-
maxLines: config.maxLines ??
|
|
134
|
-
maxLength: config.maxLength ??
|
|
109
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
110
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
135
111
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
136
|
-
promptChar: config.promptChar ??
|
|
137
|
-
continuationChar: config.continuationChar ??
|
|
112
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
113
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
138
114
|
};
|
|
139
115
|
}
|
|
140
116
|
// ===========================================================================
|
|
@@ -157,39 +133,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
135
|
/**
|
|
160
|
-
*
|
|
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 and mouse events)
|
|
136
|
+
* Process raw terminal data (handles bracketed paste sequences)
|
|
177
137
|
* Returns true if the data was consumed (paste sequence)
|
|
178
138
|
*/
|
|
179
139
|
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
140
|
// Check for paste start
|
|
194
141
|
if (data.includes(ESC.PASTE_START)) {
|
|
195
142
|
this.isPasting = true;
|
|
@@ -220,21 +167,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
220
167
|
}
|
|
221
168
|
return { consumed: false, passthrough: data };
|
|
222
169
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Handle mouse events (button, x, y coordinates, action)
|
|
225
|
-
*/
|
|
226
|
-
handleMouseEvent(button, _x, _y, _action) {
|
|
227
|
-
// Mouse wheel events: button 64 = scroll up, button 65 = scroll down
|
|
228
|
-
if (button === 64) {
|
|
229
|
-
// Scroll up (3 lines per wheel tick)
|
|
230
|
-
this.scrollUp(3);
|
|
231
|
-
}
|
|
232
|
-
else if (button === 65) {
|
|
233
|
-
// Scroll down (3 lines per wheel tick)
|
|
234
|
-
this.scrollDown(3);
|
|
235
|
-
}
|
|
236
|
-
// Ignore other mouse events (clicks, drags, etc.) for now
|
|
237
|
-
}
|
|
238
170
|
/**
|
|
239
171
|
* Handle a keypress event
|
|
240
172
|
*/
|
|
@@ -257,36 +189,306 @@ export class TerminalInput extends EventEmitter {
|
|
|
257
189
|
if (handled)
|
|
258
190
|
return;
|
|
259
191
|
}
|
|
192
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
193
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
194
|
+
this.emit('showHelp');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
260
197
|
// Insert printable characters
|
|
261
198
|
if (str && !key?.ctrl && !key?.meta) {
|
|
262
199
|
this.insertText(str);
|
|
263
200
|
}
|
|
264
201
|
}
|
|
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 } = this.getSize();
|
|
274
|
+
const divider = '─'.repeat(cols);
|
|
275
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
276
|
+
// Chat box is 4 lines: divider + input + divider + controls
|
|
277
|
+
const chatBoxHeight = 4;
|
|
278
|
+
// Chat box starts right after content
|
|
279
|
+
const chatBoxStartRow = this.contentEndRow + 1;
|
|
280
|
+
// Save cursor position before rendering
|
|
281
|
+
this.write('\x1b7');
|
|
282
|
+
// Hide cursor during render
|
|
283
|
+
this.write(ESC.HIDE);
|
|
284
|
+
// Track position
|
|
285
|
+
this.inputAreaStartRow = chatBoxStartRow;
|
|
286
|
+
let currentRow = chatBoxStartRow;
|
|
287
|
+
// Helper to write a line at absolute position (clears then writes)
|
|
288
|
+
const writeLine = (content) => {
|
|
289
|
+
this.write(ESC.TO(currentRow, 1));
|
|
290
|
+
this.write(ESC.CLEAR_LINE);
|
|
291
|
+
this.write(content);
|
|
292
|
+
currentRow++;
|
|
293
|
+
};
|
|
294
|
+
// Top divider
|
|
295
|
+
writeLine(`${DIM}${divider}${R}`);
|
|
296
|
+
// Input line with > prompt
|
|
297
|
+
const { lines, cursorCol } = this.wrapBuffer(cols - 3);
|
|
298
|
+
const displayLine = lines[0] ?? '';
|
|
299
|
+
const inputRow = currentRow;
|
|
300
|
+
writeLine(`${DIM}>${R} ${displayLine}`);
|
|
301
|
+
// Bottom divider
|
|
302
|
+
writeLine(`${DIM}${divider}${R}`);
|
|
303
|
+
// Mode controls line - Claude Code style
|
|
304
|
+
this.write(ESC.TO(currentRow, 1));
|
|
305
|
+
this.write(ESC.CLEAR_LINE);
|
|
306
|
+
this.write(this.buildClaudeStyleControls(cols));
|
|
307
|
+
// Track lines rendered
|
|
308
|
+
this.flowModeRenderedLines = chatBoxHeight;
|
|
309
|
+
// Restore cursor position (back to where content was being written)
|
|
310
|
+
// During streaming, this keeps cursor in content area
|
|
311
|
+
// During idle, cursor was in input area anyway
|
|
312
|
+
this.write('\x1b8');
|
|
313
|
+
// Show cursor
|
|
314
|
+
this.write(ESC.SHOW);
|
|
315
|
+
// Update tracking
|
|
316
|
+
this.lastRenderContent = this.buffer;
|
|
317
|
+
this.lastRenderCursor = this.cursor;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build Claude Code style controls line.
|
|
321
|
+
* Shows: edit mode indicator (shift+tab to cycle)
|
|
322
|
+
*/
|
|
323
|
+
buildClaudeStyleControls(cols) {
|
|
324
|
+
const { dim: DIM, green: GREEN, yellow: YELLOW, cyan: CYAN, reset: R } = UI_COLORS;
|
|
325
|
+
// Edit mode indicator
|
|
326
|
+
let editModeText;
|
|
327
|
+
if (this.editMode === 'display-edits') {
|
|
328
|
+
editModeText = `${GREEN}⏵⏵${R} accept edits on`;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
editModeText = `${YELLOW}⏸⏸${R} ask before edit`;
|
|
332
|
+
}
|
|
333
|
+
// Build controls line
|
|
334
|
+
const parts = [` ${editModeText} ${DIM}(shift+tab to cycle)${R}`];
|
|
335
|
+
// Add thinking mode if enabled
|
|
336
|
+
if (this.thinkingEnabled) {
|
|
337
|
+
parts.push(`${CYAN}💭${R}`);
|
|
338
|
+
}
|
|
339
|
+
// Add context usage if available
|
|
340
|
+
if (this.contextUsage !== null) {
|
|
341
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
342
|
+
if (rem < 10) {
|
|
343
|
+
parts.push(`${UI_COLORS.red}ctx ${rem}%${R}`);
|
|
344
|
+
}
|
|
345
|
+
else if (rem < 25) {
|
|
346
|
+
parts.push(`${YELLOW}ctx ${rem}%${R}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return parts.join(` ${DIM}·${R} `);
|
|
350
|
+
}
|
|
265
351
|
/**
|
|
266
352
|
* Set the input mode
|
|
267
353
|
*
|
|
268
|
-
*
|
|
354
|
+
* BOTTOM PINNED with SSE: Chat box stays at terminal bottom.
|
|
355
|
+
* Scroll region protects chat box, content scrolls above it.
|
|
269
356
|
*/
|
|
270
357
|
setMode(mode) {
|
|
271
358
|
const prevMode = this.mode;
|
|
272
359
|
this.mode = mode;
|
|
273
360
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
274
|
-
|
|
361
|
+
// Track streaming start time for elapsed display
|
|
362
|
+
this.streamingStartTime = Date.now();
|
|
363
|
+
// Ensure unified UI is initialized
|
|
364
|
+
if (!this.unifiedUIInitialized) {
|
|
365
|
+
this.initializeUnifiedUI();
|
|
366
|
+
}
|
|
367
|
+
// Start periodic render timer to keep chat box updated during streaming
|
|
368
|
+
// This updates contentEndRow from display and re-renders chat box
|
|
369
|
+
if (!this.streamingRenderTimer) {
|
|
370
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
371
|
+
// Update contentEndRow from display's line count
|
|
372
|
+
if (this.displayRef?.getTotalWrittenLines) {
|
|
373
|
+
this.contentEndRow = this.displayRef.getTotalWrittenLines();
|
|
374
|
+
}
|
|
375
|
+
// Re-render chat box at updated position
|
|
376
|
+
this.renderFloatingInputArea();
|
|
377
|
+
}, 100); // Update every 100ms
|
|
378
|
+
}
|
|
379
|
+
// Initial render
|
|
275
380
|
this.renderDirty = true;
|
|
276
|
-
this.
|
|
381
|
+
this.scheduleRender();
|
|
277
382
|
}
|
|
278
383
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
279
|
-
//
|
|
280
|
-
this.
|
|
281
|
-
|
|
384
|
+
// Stop streaming render timer
|
|
385
|
+
if (this.streamingRenderTimer) {
|
|
386
|
+
clearInterval(this.streamingRenderTimer);
|
|
387
|
+
this.streamingRenderTimer = null;
|
|
388
|
+
}
|
|
389
|
+
// Reset streaming time
|
|
390
|
+
this.streamingStartTime = null;
|
|
391
|
+
// Final render with accurate position
|
|
392
|
+
this.renderDirty = true;
|
|
393
|
+
this.scheduleRender();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Set the row where content ends (for idle mode positioning).
|
|
398
|
+
* Input area will render starting from this row + 1.
|
|
399
|
+
*/
|
|
400
|
+
setContentEndRow(row) {
|
|
401
|
+
this.contentEndRow = Math.max(0, row);
|
|
402
|
+
this.renderDirty = true;
|
|
403
|
+
this.scheduleRender();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Set available slash commands for auto-complete suggestions.
|
|
407
|
+
*/
|
|
408
|
+
setCommands(commands) {
|
|
409
|
+
this.commandSuggestions = commands;
|
|
410
|
+
this.updateSuggestions();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Update filtered suggestions based on current input.
|
|
414
|
+
*/
|
|
415
|
+
updateSuggestions() {
|
|
416
|
+
const input = this.buffer.trim();
|
|
417
|
+
// Only show suggestions when input starts with "/"
|
|
418
|
+
if (!input.startsWith('/')) {
|
|
419
|
+
this.showSuggestions = false;
|
|
420
|
+
this.filteredSuggestions = [];
|
|
421
|
+
this.selectedSuggestionIndex = 0;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const query = input.toLowerCase();
|
|
425
|
+
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
426
|
+
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
427
|
+
// Show suggestions if we have matches
|
|
428
|
+
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
429
|
+
// Keep selection in bounds
|
|
430
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
431
|
+
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
282
432
|
}
|
|
283
433
|
}
|
|
284
434
|
/**
|
|
285
|
-
*
|
|
286
|
-
* @deprecated Use setContentRow instead
|
|
435
|
+
* Select next suggestion (arrow down / tab).
|
|
287
436
|
*/
|
|
288
|
-
|
|
289
|
-
|
|
437
|
+
selectNextSuggestion() {
|
|
438
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
439
|
+
return;
|
|
440
|
+
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
441
|
+
this.renderDirty = true;
|
|
442
|
+
this.scheduleRender();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Select previous suggestion (arrow up / shift+tab).
|
|
446
|
+
*/
|
|
447
|
+
selectPrevSuggestion() {
|
|
448
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
449
|
+
return;
|
|
450
|
+
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
451
|
+
? this.filteredSuggestions.length - 1
|
|
452
|
+
: this.selectedSuggestionIndex - 1;
|
|
453
|
+
this.renderDirty = true;
|
|
454
|
+
this.scheduleRender();
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Accept current suggestion and insert into buffer.
|
|
458
|
+
*/
|
|
459
|
+
acceptSuggestion() {
|
|
460
|
+
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
461
|
+
return false;
|
|
462
|
+
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
463
|
+
if (!selected)
|
|
464
|
+
return false;
|
|
465
|
+
// Replace buffer with selected command
|
|
466
|
+
this.buffer = selected.command + ' ';
|
|
467
|
+
this.cursor = this.buffer.length;
|
|
468
|
+
this.showSuggestions = false;
|
|
469
|
+
this.renderDirty = true;
|
|
470
|
+
this.scheduleRender();
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Check if suggestions are visible.
|
|
475
|
+
*/
|
|
476
|
+
areSuggestionsVisible() {
|
|
477
|
+
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Toggle thinking/reasoning mode
|
|
481
|
+
*/
|
|
482
|
+
toggleThinking() {
|
|
483
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
484
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
485
|
+
this.scheduleRender();
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get thinking enabled state
|
|
489
|
+
*/
|
|
490
|
+
isThinkingEnabled() {
|
|
491
|
+
return this.thinkingEnabled;
|
|
290
492
|
}
|
|
291
493
|
/**
|
|
292
494
|
* Get current mode
|
|
@@ -319,14 +521,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
319
521
|
}
|
|
320
522
|
/**
|
|
321
523
|
* Clear the buffer
|
|
524
|
+
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
322
525
|
*/
|
|
323
|
-
clear() {
|
|
526
|
+
clear(skipRender = false) {
|
|
324
527
|
this.buffer = '';
|
|
325
528
|
this.cursor = 0;
|
|
326
529
|
this.historyIndex = -1;
|
|
327
530
|
this.tempInput = '';
|
|
328
531
|
this.pastePlaceholders = [];
|
|
329
|
-
|
|
532
|
+
if (!skipRender) {
|
|
533
|
+
this.scheduleRender();
|
|
534
|
+
}
|
|
330
535
|
}
|
|
331
536
|
/**
|
|
332
537
|
* Get queued inputs
|
|
@@ -397,37 +602,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
397
602
|
this.streamingLabel = next;
|
|
398
603
|
this.scheduleRender();
|
|
399
604
|
}
|
|
400
|
-
/**
|
|
401
|
-
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
402
|
-
*/
|
|
403
|
-
setMetaStatus(meta) {
|
|
404
|
-
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
405
|
-
? Math.floor(meta.elapsedSeconds)
|
|
406
|
-
: null;
|
|
407
|
-
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
408
|
-
? Math.floor(meta.tokensUsed)
|
|
409
|
-
: null;
|
|
410
|
-
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
411
|
-
? Math.floor(meta.tokenLimit)
|
|
412
|
-
: null;
|
|
413
|
-
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
414
|
-
? Math.floor(meta.thinkingMs)
|
|
415
|
-
: null;
|
|
416
|
-
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
417
|
-
if (this.metaElapsedSeconds === nextElapsed &&
|
|
418
|
-
this.metaTokensUsed === nextTokens &&
|
|
419
|
-
this.metaTokenLimit === nextLimit &&
|
|
420
|
-
this.metaThinkingMs === nextThinking &&
|
|
421
|
-
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
this.metaElapsedSeconds = nextElapsed;
|
|
425
|
-
this.metaTokensUsed = nextTokens;
|
|
426
|
-
this.metaTokenLimit = nextLimit;
|
|
427
|
-
this.metaThinkingMs = nextThinking;
|
|
428
|
-
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
429
|
-
this.scheduleRender();
|
|
430
|
-
}
|
|
431
605
|
/**
|
|
432
606
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
433
607
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -437,22 +611,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
437
611
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
438
612
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
439
613
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
440
|
-
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
441
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
442
614
|
if (this.verificationEnabled === nextVerification &&
|
|
443
615
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
444
616
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
445
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
446
|
-
this.thinkingHotkey === nextThinkingHotkey &&
|
|
447
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
617
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
448
618
|
return;
|
|
449
619
|
}
|
|
450
620
|
this.verificationEnabled = nextVerification;
|
|
451
621
|
this.autoContinueEnabled = nextAutoContinue;
|
|
452
622
|
this.verificationHotkey = nextVerifyHotkey;
|
|
453
623
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
454
|
-
this.
|
|
455
|
-
|
|
624
|
+
this.scheduleRender();
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
628
|
+
* This is displayed persistently above the input area.
|
|
629
|
+
*/
|
|
630
|
+
setModelInfo(info) {
|
|
631
|
+
if (this.modelInfo === info)
|
|
632
|
+
return;
|
|
633
|
+
this.modelInfo = info;
|
|
456
634
|
this.scheduleRender();
|
|
457
635
|
}
|
|
458
636
|
/**
|
|
@@ -465,171 +643,33 @@ export class TerminalInput extends EventEmitter {
|
|
|
465
643
|
this.scheduleRender();
|
|
466
644
|
}
|
|
467
645
|
/**
|
|
468
|
-
*
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const nextModel = options.model?.trim() || null;
|
|
472
|
-
const nextProvider = options.provider?.trim() || null;
|
|
473
|
-
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
this.modelLabel = nextModel;
|
|
477
|
-
this.providerLabel = nextProvider;
|
|
478
|
-
this.scheduleRender();
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Render the floating input area at contentRow.
|
|
482
|
-
*
|
|
483
|
-
* The chat box "floats" - it renders right below the last streamed content.
|
|
484
|
-
* As content is added, contentRow advances, and the chat box moves down.
|
|
485
|
-
* No scroll regions - pure floating behavior.
|
|
646
|
+
* Render the input area.
|
|
647
|
+
* During streaming: renders at terminal bottom (with scroll region)
|
|
648
|
+
* After streaming: renders floating below content
|
|
486
649
|
*/
|
|
487
650
|
render() {
|
|
488
651
|
if (!this.canRender())
|
|
489
652
|
return;
|
|
490
653
|
if (this.isRendering)
|
|
491
654
|
return;
|
|
492
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
493
|
-
// During streaming, throttle re-renders
|
|
494
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
495
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
496
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
497
|
-
if (waitMs > 0) {
|
|
498
|
-
this.renderDirty = true;
|
|
499
|
-
this.scheduleStreamingRender(waitMs);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
655
|
const shouldSkip = !this.renderDirty &&
|
|
504
656
|
this.buffer === this.lastRenderContent &&
|
|
505
657
|
this.cursor === this.lastRenderCursor;
|
|
506
658
|
this.renderDirty = false;
|
|
659
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
507
660
|
if (shouldSkip) {
|
|
508
661
|
return;
|
|
509
662
|
}
|
|
663
|
+
// If write lock is held, defer render
|
|
510
664
|
if (writeLock.isLocked()) {
|
|
511
665
|
writeLock.safeWrite(() => this.render());
|
|
512
666
|
return;
|
|
513
667
|
}
|
|
514
|
-
this.renderPinnedChatBox();
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Unified scroll region renderer.
|
|
518
|
-
* Chat box is ALWAYS pinned at the bottom of the terminal.
|
|
519
|
-
* Content scrolls in the region above the chat box.
|
|
520
|
-
*/
|
|
521
|
-
renderPinnedChatBox() {
|
|
522
|
-
const { rows, cols } = this.getSize();
|
|
523
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
524
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
525
|
-
// Wrap buffer into display lines
|
|
526
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
527
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
528
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
529
|
-
const metaLines = this.buildMetaLines(cols - 2);
|
|
530
|
-
// Calculate display window (keep cursor visible)
|
|
531
|
-
let startLine = 0;
|
|
532
|
-
if (lines.length > displayLines) {
|
|
533
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
534
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
535
|
-
}
|
|
536
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
537
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
538
|
-
// Chat box height
|
|
539
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
540
|
-
// ALWAYS pin chat box at absolute bottom
|
|
541
|
-
const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
|
|
542
|
-
const scrollEnd = chatBoxStartRow - 1;
|
|
543
|
-
writeLock.lock('terminalInput.renderPinned');
|
|
544
668
|
this.isRendering = true;
|
|
669
|
+
writeLock.lock('terminalInput.render');
|
|
545
670
|
try {
|
|
546
|
-
|
|
547
|
-
this.
|
|
548
|
-
// Temporarily reset scroll region to write chat box cleanly
|
|
549
|
-
if (this.scrollRegionActive) {
|
|
550
|
-
this.write(ESC.RESET_SCROLL);
|
|
551
|
-
}
|
|
552
|
-
// Clear the chat box area
|
|
553
|
-
for (let i = 0; i < chatBoxHeight; i++) {
|
|
554
|
-
const row = chatBoxStartRow + i;
|
|
555
|
-
if (row <= rows) {
|
|
556
|
-
this.write(ESC.TO(row, 1));
|
|
557
|
-
this.write(ESC.CLEAR_LINE);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
let currentRow = chatBoxStartRow;
|
|
561
|
-
// Meta/status header
|
|
562
|
-
for (const metaLine of metaLines) {
|
|
563
|
-
this.write(ESC.TO(currentRow, 1));
|
|
564
|
-
this.write(metaLine);
|
|
565
|
-
currentRow += 1;
|
|
566
|
-
}
|
|
567
|
-
// Separator line
|
|
568
|
-
this.write(ESC.TO(currentRow, 1));
|
|
569
|
-
this.write(renderDivider(cols - 2));
|
|
570
|
-
currentRow += 1;
|
|
571
|
-
// Render input lines
|
|
572
|
-
let finalRow = currentRow;
|
|
573
|
-
let finalCol = 3;
|
|
574
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
575
|
-
const rowNum = currentRow + i;
|
|
576
|
-
this.write(ESC.TO(rowNum, 1));
|
|
577
|
-
const line = visibleLines[i] ?? '';
|
|
578
|
-
const isFirstLine = (startLine + i) === 0;
|
|
579
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
580
|
-
this.write(ESC.BG_DARK);
|
|
581
|
-
this.write(ESC.DIM);
|
|
582
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
583
|
-
this.write(ESC.RESET);
|
|
584
|
-
this.write(ESC.BG_DARK);
|
|
585
|
-
if (isCursorLine) {
|
|
586
|
-
const col = Math.min(cursorCol, line.length);
|
|
587
|
-
const before = line.slice(0, col);
|
|
588
|
-
const at = col < line.length ? line[col] : ' ';
|
|
589
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
590
|
-
this.write(before);
|
|
591
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
592
|
-
this.write(at);
|
|
593
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
594
|
-
this.write(after);
|
|
595
|
-
finalRow = rowNum;
|
|
596
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
this.write(line);
|
|
600
|
-
}
|
|
601
|
-
// Pad to edge
|
|
602
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
603
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
604
|
-
if (padding > 0)
|
|
605
|
-
this.write(' '.repeat(padding));
|
|
606
|
-
this.write(ESC.RESET);
|
|
607
|
-
}
|
|
608
|
-
// Mode controls line with all keyboard shortcuts
|
|
609
|
-
const controlRow = currentRow + visibleLines.length;
|
|
610
|
-
this.write(ESC.TO(controlRow, 1));
|
|
611
|
-
this.write(this.buildModeControls(cols));
|
|
612
|
-
// Restore scroll region and cursor
|
|
613
|
-
if (this.scrollRegionActive) {
|
|
614
|
-
// Restore scroll region
|
|
615
|
-
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
616
|
-
// Restore cursor to where it was before rendering (preserves column position)
|
|
617
|
-
this.write(ESC.RESTORE);
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
// Not streaming - position cursor in input box
|
|
621
|
-
this.write(ESC.RESTORE);
|
|
622
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
623
|
-
}
|
|
624
|
-
this.write(ESC.SHOW);
|
|
625
|
-
// Update state
|
|
626
|
-
this.lastRenderContent = this.buffer;
|
|
627
|
-
this.lastRenderCursor = this.cursor;
|
|
628
|
-
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
629
|
-
if (this.streamingRenderTimer) {
|
|
630
|
-
clearTimeout(this.streamingRenderTimer);
|
|
631
|
-
this.streamingRenderTimer = null;
|
|
632
|
-
}
|
|
671
|
+
// Always render floating right after content (no wasted space)
|
|
672
|
+
this.renderFloatingInputArea();
|
|
633
673
|
}
|
|
634
674
|
finally {
|
|
635
675
|
writeLock.unlock();
|
|
@@ -637,231 +677,99 @@ export class TerminalInput extends EventEmitter {
|
|
|
637
677
|
}
|
|
638
678
|
}
|
|
639
679
|
/**
|
|
640
|
-
* Build
|
|
641
|
-
*
|
|
642
|
-
*/
|
|
643
|
-
buildMetaLines(width) {
|
|
644
|
-
const leftParts = [];
|
|
645
|
-
const rightParts = [];
|
|
646
|
-
// Model/provider info (left side)
|
|
647
|
-
if (this.modelLabel) {
|
|
648
|
-
const modelText = this.providerLabel
|
|
649
|
-
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
650
|
-
: this.modelLabel;
|
|
651
|
-
leftParts.push({ text: modelText, tone: 'info' });
|
|
652
|
-
}
|
|
653
|
-
// Elapsed time (right side)
|
|
654
|
-
if (this.metaElapsedSeconds !== null) {
|
|
655
|
-
rightParts.push({ text: `⏱ ${this.formatElapsedLabel(this.metaElapsedSeconds)}`, tone: 'muted' });
|
|
656
|
-
}
|
|
657
|
-
// Token usage (right side)
|
|
658
|
-
if (this.metaTokensUsed !== null) {
|
|
659
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
660
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
661
|
-
rightParts.push({ text: `⊛ ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
662
|
-
}
|
|
663
|
-
// Context remaining warning
|
|
664
|
-
const tokensRemaining = this.computeTokensRemaining();
|
|
665
|
-
if (tokensRemaining !== null) {
|
|
666
|
-
const contextPct = this.contextUsage !== null ? `${100 - this.contextUsage}%` : '';
|
|
667
|
-
rightParts.push({ text: `↓${tokensRemaining} ${contextPct}`, tone: this.contextUsage && this.contextUsage > 80 ? 'warn' : 'muted' });
|
|
668
|
-
}
|
|
669
|
-
// Thinking indicator
|
|
670
|
-
if (this.metaThinkingMs !== null) {
|
|
671
|
-
leftParts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
672
|
-
}
|
|
673
|
-
if (!leftParts.length && !rightParts.length) {
|
|
674
|
-
return [];
|
|
675
|
-
}
|
|
676
|
-
// Render left and right aligned
|
|
677
|
-
if (!rightParts.length || width < 50) {
|
|
678
|
-
return [renderStatusLine([...leftParts, ...rightParts], width)];
|
|
679
|
-
}
|
|
680
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.5));
|
|
681
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
682
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
683
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
684
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
685
|
-
return [`${leftText}${' '.repeat(spacing)}${rightText}`];
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Build mode controls line with all keyboard shortcuts.
|
|
689
|
-
* Shows status, all toggles, and contextual information.
|
|
690
|
-
* Enhanced with comprehensive feature status display.
|
|
680
|
+
* Build status bar showing streaming/ready status and key info.
|
|
681
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
691
682
|
*/
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (this.statusMessage) {
|
|
706
|
-
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
707
|
-
}
|
|
708
|
-
// === SCROLLBACK INDICATOR ===
|
|
709
|
-
if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
|
|
710
|
-
const scrollInfo = `↑${this.scrollbackOffset}`;
|
|
711
|
-
leftParts.push({ text: scrollInfo, tone: 'info' });
|
|
712
|
-
// Show scrollback help hint
|
|
713
|
-
leftParts.push({ text: `${this.formatHotkey('alt+s')}exit`, tone: 'muted' });
|
|
714
|
-
}
|
|
715
|
-
// === KEYBOARD SHORTCUTS ===
|
|
716
|
-
// Interrupt shortcut (during streaming)
|
|
717
|
-
if (this.mode === 'streaming' || this.scrollRegionActive) {
|
|
718
|
-
leftParts.push({ text: `${this.formatHotkey('esc')} stop`, tone: 'warn' });
|
|
719
|
-
}
|
|
720
|
-
// Edit mode toggle (Shift+Tab)
|
|
721
|
-
const editHotkey = this.formatHotkey('shift+tab');
|
|
722
|
-
const editIcon = this.editMode === 'display-edits' ? '✓' : '?';
|
|
723
|
-
const editLabel = this.editMode === 'display-edits' ? 'edits:auto' : 'edits:ask';
|
|
724
|
-
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
725
|
-
leftParts.push({ text: `${editHotkey}${editIcon}${editLabel}`, tone: editTone });
|
|
726
|
-
// Verification toggle (Alt+V)
|
|
727
|
-
const verifyIcon = this.verificationEnabled ? '✓' : '○';
|
|
728
|
-
const verifyHotkey = this.formatHotkey(this.verificationHotkey || 'alt+v');
|
|
729
|
-
const verifyLabel = this.verificationEnabled ? 'verify' : 'no-verify';
|
|
730
|
-
leftParts.push({ text: `${verifyHotkey}${verifyIcon}${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
731
|
-
// Auto-continue toggle (Alt+C)
|
|
732
|
-
const autoIcon = this.autoContinueEnabled ? '↻' : '○';
|
|
733
|
-
const continueHotkey = this.formatHotkey(this.autoContinueHotkey || 'alt+c');
|
|
734
|
-
const continueLabel = this.autoContinueEnabled ? 'auto' : 'manual';
|
|
735
|
-
leftParts.push({ text: `${continueHotkey}${autoIcon}${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
736
|
-
// Thinking mode toggle (if available)
|
|
737
|
-
if (this.thinkingModeLabel) {
|
|
738
|
-
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey || '/thinking');
|
|
739
|
-
rightParts.push({ text: `${thinkingHotkey}◐${this.thinkingModeLabel}`, tone: 'info' });
|
|
740
|
-
}
|
|
741
|
-
// === CONTEXTUAL INFO ===
|
|
742
|
-
// Queued commands
|
|
743
|
-
if (this.queue.length > 0) {
|
|
744
|
-
const queueIcon = this.mode === 'streaming' ? '⏳' : '▸';
|
|
745
|
-
leftParts.push({ text: `${queueIcon}${this.queue.length}queued`, tone: 'info' });
|
|
746
|
-
}
|
|
747
|
-
// Scrollback buffer size (when significant)
|
|
748
|
-
if (this.scrollbackBuffer.length > 100 && !this.isInScrollbackMode) {
|
|
749
|
-
const bufferHint = `${this.formatHotkey('alt+s')}↕${Math.floor(this.scrollbackBuffer.length / 100) * 100}+L`;
|
|
750
|
-
rightParts.push({ text: bufferHint, tone: 'muted' });
|
|
683
|
+
buildStatusBar(cols) {
|
|
684
|
+
const maxWidth = cols - 2;
|
|
685
|
+
const parts = [];
|
|
686
|
+
// Streaming status with elapsed time (left side)
|
|
687
|
+
if (this.mode === 'streaming') {
|
|
688
|
+
let statusText = '● Streaming';
|
|
689
|
+
if (this.streamingStartTime) {
|
|
690
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
691
|
+
const mins = Math.floor(elapsed / 60);
|
|
692
|
+
const secs = elapsed % 60;
|
|
693
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
694
|
+
}
|
|
695
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
751
696
|
}
|
|
752
|
-
//
|
|
753
|
-
if (this.
|
|
754
|
-
|
|
755
|
-
rightParts.push({ text: `${lineCount}L`, tone: 'muted' });
|
|
697
|
+
// Queue indicator during streaming
|
|
698
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
699
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
756
700
|
}
|
|
757
701
|
// Paste indicator
|
|
758
702
|
if (this.pastePlaceholders.length > 0) {
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
text: `paste#${latest.id}+${latest.lineCount}L`,
|
|
762
|
-
tone: 'info',
|
|
763
|
-
});
|
|
703
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
704
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
764
705
|
}
|
|
765
|
-
//
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const tone = contextRemaining <= 10 ? 'warn' : contextRemaining <= 30 ? 'info' : 'muted';
|
|
769
|
-
const icon = contextRemaining <= 10 ? '⚠' : '⊛';
|
|
770
|
-
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
771
|
-
? `${icon}compact!`
|
|
772
|
-
: `${icon}${contextRemaining}%`;
|
|
773
|
-
rightParts.push({ text: label, tone });
|
|
774
|
-
}
|
|
775
|
-
// Model/provider quick reference (compact)
|
|
776
|
-
if (this.modelLabel && !this.streamingLabel) {
|
|
777
|
-
const shortModel = this.modelLabel.length > 12 ? this.modelLabel.slice(0, 10) + '..' : this.modelLabel;
|
|
778
|
-
rightParts.push({ text: shortModel, tone: 'muted' });
|
|
779
|
-
}
|
|
780
|
-
// Render: left-aligned shortcuts, right-aligned context info
|
|
781
|
-
if (!rightParts.length || width < 60) {
|
|
782
|
-
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
783
|
-
return renderStatusLine(merged, width);
|
|
784
|
-
}
|
|
785
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.65));
|
|
786
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
787
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
788
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
789
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
790
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
791
|
-
}
|
|
792
|
-
formatHotkey(hotkey) {
|
|
793
|
-
const normalized = hotkey.trim().toLowerCase();
|
|
794
|
-
if (!normalized)
|
|
795
|
-
return hotkey;
|
|
796
|
-
const parts = normalized.split('+').filter(Boolean);
|
|
797
|
-
const map = {
|
|
798
|
-
shift: '⇧',
|
|
799
|
-
sh: '⇧',
|
|
800
|
-
alt: '⌥',
|
|
801
|
-
option: '⌥',
|
|
802
|
-
opt: '⌥',
|
|
803
|
-
ctrl: '⌃',
|
|
804
|
-
control: '⌃',
|
|
805
|
-
cmd: '⌘',
|
|
806
|
-
meta: '⌘',
|
|
807
|
-
};
|
|
808
|
-
const formatted = parts
|
|
809
|
-
.map((part) => {
|
|
810
|
-
const symbol = map[part];
|
|
811
|
-
if (symbol)
|
|
812
|
-
return symbol;
|
|
813
|
-
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
814
|
-
})
|
|
815
|
-
.join('');
|
|
816
|
-
return formatted || hotkey;
|
|
817
|
-
}
|
|
818
|
-
computeContextRemaining() {
|
|
819
|
-
if (this.contextUsage === null) {
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
823
|
-
}
|
|
824
|
-
computeTokensRemaining() {
|
|
825
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
826
|
-
return null;
|
|
706
|
+
// Override/warning status
|
|
707
|
+
if (this.overrideStatusMessage) {
|
|
708
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
827
709
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
formatElapsedLabel(seconds) {
|
|
832
|
-
if (seconds < 60) {
|
|
833
|
-
return `${seconds}s`;
|
|
710
|
+
// If idle with empty buffer, show quick shortcuts
|
|
711
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
712
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
834
713
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
formatTokenCount(value) {
|
|
840
|
-
if (!Number.isFinite(value)) {
|
|
841
|
-
return `${value}`;
|
|
842
|
-
}
|
|
843
|
-
if (value >= 1_000_000) {
|
|
844
|
-
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
714
|
+
// Multi-line indicator
|
|
715
|
+
if (this.buffer.includes('\n')) {
|
|
716
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
845
717
|
}
|
|
846
|
-
if (
|
|
847
|
-
return
|
|
718
|
+
if (parts.length === 0) {
|
|
719
|
+
return ''; // Empty status bar when idle
|
|
848
720
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
visibleLength(value) {
|
|
852
|
-
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
853
|
-
return value.replace(ansiPattern, '').length;
|
|
721
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
722
|
+
return joined.slice(0, maxWidth);
|
|
854
723
|
}
|
|
855
724
|
/**
|
|
856
|
-
*
|
|
857
|
-
*
|
|
725
|
+
* Build mode controls line showing toggles and context info.
|
|
726
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
727
|
+
*
|
|
728
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
858
729
|
*/
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
730
|
+
buildModeControls(cols) {
|
|
731
|
+
const maxWidth = cols - 2;
|
|
732
|
+
// Use schema-defined colors for consistency
|
|
733
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
734
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
735
|
+
const toggles = [];
|
|
736
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
737
|
+
if (this.editMode === 'display-edits') {
|
|
738
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
742
|
+
}
|
|
743
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
744
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
745
|
+
// Verification (green when on) - per schema.verificationMode
|
|
746
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
747
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
748
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
749
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
750
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
751
|
+
let rightPart = '';
|
|
752
|
+
if (this.contextUsage !== null) {
|
|
753
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
754
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
755
|
+
if (rem < 10)
|
|
756
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
757
|
+
else if (rem < 25)
|
|
758
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
759
|
+
else
|
|
760
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
761
|
+
}
|
|
762
|
+
// Calculate visible lengths (strip ANSI)
|
|
763
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
764
|
+
const leftLen = strip(leftPart).length;
|
|
765
|
+
const rightLen = strip(rightPart).length;
|
|
766
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
767
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
768
|
+
}
|
|
769
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
770
|
+
return `${leftPart} ${rightPart}`;
|
|
771
|
+
}
|
|
772
|
+
return leftPart;
|
|
865
773
|
}
|
|
866
774
|
/**
|
|
867
775
|
* Force a re-render
|
|
@@ -884,213 +792,31 @@ export class TerminalInput extends EventEmitter {
|
|
|
884
792
|
handleResize() {
|
|
885
793
|
this.lastRenderContent = '';
|
|
886
794
|
this.lastRenderCursor = -1;
|
|
887
|
-
this.
|
|
888
|
-
// If in scrollback mode, re-render the scrollback view with new dimensions
|
|
889
|
-
if (this.isInScrollbackMode) {
|
|
890
|
-
this.renderScrollbackView();
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
this.scheduleRender();
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Enter streaming mode with scroll region.
|
|
898
|
-
* Sets up terminal scroll region to exclude chat box.
|
|
899
|
-
*/
|
|
900
|
-
enterStreamingScrollRegion() {
|
|
901
|
-
const { rows } = this.getSize();
|
|
902
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
903
|
-
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
904
|
-
writeLock.lock('enterStreamingScrollRegion');
|
|
905
|
-
try {
|
|
906
|
-
// Set scroll region for content area (above chat box)
|
|
907
|
-
this.write(ESC.SET_SCROLL(1, scrollEnd));
|
|
908
|
-
// Position cursor at current content row
|
|
909
|
-
this.write(ESC.TO(Math.min(this.contentRow, scrollEnd), 1));
|
|
910
|
-
this.scrollRegionActive = true;
|
|
911
|
-
this.setStatusMessage('esc to interrupt');
|
|
912
|
-
}
|
|
913
|
-
finally {
|
|
914
|
-
writeLock.unlock();
|
|
915
|
-
}
|
|
916
|
-
// Render pinned chat box at bottom
|
|
917
|
-
this.forceRender();
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Exit streaming mode and restore normal operation.
|
|
921
|
-
*/
|
|
922
|
-
exitStreamingScrollRegion() {
|
|
923
|
-
writeLock.lock('exitStreamingScrollRegion');
|
|
924
|
-
try {
|
|
925
|
-
// Reset scroll region to full terminal
|
|
926
|
-
this.write(ESC.RESET_SCROLL);
|
|
927
|
-
this.scrollRegionActive = false;
|
|
928
|
-
this.setStatusMessage('Ready for prompts');
|
|
929
|
-
}
|
|
930
|
-
finally {
|
|
931
|
-
writeLock.unlock();
|
|
932
|
-
}
|
|
933
|
-
// Final render
|
|
934
|
-
this.forceRender();
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Render chat box at bottom - now uses unified renderer.
|
|
938
|
-
* @deprecated Use renderPinnedChatBox() directly via render()/forceRender()
|
|
939
|
-
*/
|
|
940
|
-
renderChatBoxAtBottom() {
|
|
941
|
-
this.renderPinnedChatBox();
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* Stream content within the scroll region.
|
|
945
|
-
* Content is written directly and scrolls naturally.
|
|
946
|
-
*/
|
|
947
|
-
streamContent(content) {
|
|
948
|
-
if (!content)
|
|
949
|
-
return;
|
|
950
|
-
// Capture content in scrollback buffer
|
|
951
|
-
this.addToScrollback(content);
|
|
952
|
-
writeLock.lock('streamContent');
|
|
953
|
-
try {
|
|
954
|
-
// Write content - scroll region handles scrolling
|
|
955
|
-
this.write(content);
|
|
956
|
-
// Track newlines
|
|
957
|
-
const newlines = (content.match(/\n/g) || []).length;
|
|
958
|
-
this.contentRow += newlines;
|
|
959
|
-
}
|
|
960
|
-
finally {
|
|
961
|
-
writeLock.unlock();
|
|
962
|
-
}
|
|
963
|
-
// Throttle chat box updates during streaming
|
|
964
|
-
this.scheduleStreamingRender(200);
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Enable scroll region (no-op in floating mode).
|
|
968
|
-
*/
|
|
969
|
-
enableScrollRegion() {
|
|
970
|
-
// No-op: using pure floating approach
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Disable scroll region (no-op in floating mode).
|
|
974
|
-
*/
|
|
975
|
-
disableScrollRegion() {
|
|
976
|
-
// No-op: using pure floating approach
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Calculate chat box height.
|
|
980
|
-
*/
|
|
981
|
-
getChatBoxHeight() {
|
|
982
|
-
return 6; // Fixed: meta + divider + input + controls + buffer
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* @deprecated Use streamContent() instead
|
|
986
|
-
* Register with display's output interceptor - kept for backwards compatibility
|
|
987
|
-
*/
|
|
988
|
-
registerOutputInterceptor(_display) {
|
|
989
|
-
// No-op: Use streamContent() for cleaner floating chat box behavior
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Write content above the floating chat box.
|
|
993
|
-
* Works both during streaming and when idle.
|
|
994
|
-
*/
|
|
995
|
-
writeToScrollRegion(content) {
|
|
996
|
-
if (!content)
|
|
997
|
-
return;
|
|
998
|
-
// Capture content in scrollback buffer
|
|
999
|
-
this.addToScrollback(content);
|
|
1000
|
-
writeLock.lock('writeToScrollRegion');
|
|
1001
|
-
try {
|
|
1002
|
-
// Position cursor at content row and write
|
|
1003
|
-
this.write(ESC.TO(this.contentRow, 1));
|
|
1004
|
-
this.write(content);
|
|
1005
|
-
// Track newlines
|
|
1006
|
-
const newlines = (content.match(/\n/g) || []).length;
|
|
1007
|
-
this.contentRow += newlines;
|
|
1008
|
-
}
|
|
1009
|
-
finally {
|
|
1010
|
-
writeLock.unlock();
|
|
1011
|
-
}
|
|
1012
|
-
// Re-render chat box below new content (only when not streaming)
|
|
1013
|
-
if (!this.scrollRegionActive) {
|
|
1014
|
-
this.forceRender();
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Enter alternate screen buffer and clear it.
|
|
1019
|
-
* This gives us full control over the terminal without affecting user's history.
|
|
1020
|
-
*/
|
|
1021
|
-
enterAlternateScreen() {
|
|
1022
|
-
writeLock.lock('enterAltScreen');
|
|
1023
|
-
try {
|
|
1024
|
-
this.write(ESC.ENTER_ALT_SCREEN);
|
|
1025
|
-
this.write(ESC.HOME);
|
|
1026
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
1027
|
-
this.contentRow = 1;
|
|
1028
|
-
this.alternateScreenActive = true;
|
|
1029
|
-
}
|
|
1030
|
-
finally {
|
|
1031
|
-
writeLock.unlock();
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Exit alternate screen buffer.
|
|
1036
|
-
* Restores the user's previous terminal content.
|
|
1037
|
-
*/
|
|
1038
|
-
exitAlternateScreen() {
|
|
1039
|
-
writeLock.lock('exitAltScreen');
|
|
1040
|
-
try {
|
|
1041
|
-
this.write(ESC.EXIT_ALT_SCREEN);
|
|
1042
|
-
this.alternateScreenActive = false;
|
|
1043
|
-
}
|
|
1044
|
-
finally {
|
|
1045
|
-
writeLock.unlock();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Check if alternate screen buffer is currently active.
|
|
1050
|
-
*/
|
|
1051
|
-
isAlternateScreenActive() {
|
|
1052
|
-
return this.alternateScreenActive;
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Get a snapshot of the scrollback buffer (for display on exit).
|
|
1056
|
-
*/
|
|
1057
|
-
getScrollbackSnapshot() {
|
|
1058
|
-
return [...this.scrollbackBuffer];
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* Clear the entire terminal screen and reset content position.
|
|
1062
|
-
* This removes all content including the launching command.
|
|
1063
|
-
*/
|
|
1064
|
-
clearScreen() {
|
|
1065
|
-
writeLock.lock('clearScreen');
|
|
1066
|
-
try {
|
|
1067
|
-
this.write(ESC.HOME);
|
|
1068
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
1069
|
-
this.contentRow = 1;
|
|
1070
|
-
}
|
|
1071
|
-
finally {
|
|
1072
|
-
writeLock.unlock();
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Reset content position to row 1.
|
|
1077
|
-
* Does NOT clear the terminal - content starts from current position.
|
|
1078
|
-
*/
|
|
1079
|
-
resetContentPosition() {
|
|
1080
|
-
this.contentRow = 1;
|
|
1081
|
-
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Set the content row explicitly (used after banner is written).
|
|
1084
|
-
* This tells the input where content should start flowing from.
|
|
1085
|
-
*/
|
|
1086
|
-
setContentRow(row) {
|
|
1087
|
-
this.contentRow = Math.max(1, row);
|
|
795
|
+
this.scheduleRender();
|
|
1088
796
|
}
|
|
797
|
+
// Track current content row for writing
|
|
798
|
+
contentCursorRow = 1;
|
|
1089
799
|
/**
|
|
1090
|
-
*
|
|
800
|
+
* Register with display's output interceptor.
|
|
801
|
+
* Clears chat box before writes, re-renders after with updated position.
|
|
1091
802
|
*/
|
|
1092
|
-
|
|
1093
|
-
|
|
803
|
+
registerOutputInterceptor(display) {
|
|
804
|
+
if (this.outputInterceptorCleanup) {
|
|
805
|
+
this.outputInterceptorCleanup();
|
|
806
|
+
}
|
|
807
|
+
// Store display reference for streaming timer to use
|
|
808
|
+
this.displayRef = display;
|
|
809
|
+
// Clear chat box before writes to make room for content
|
|
810
|
+
// Re-render is done via periodic timer during streaming, or setContentEndRow after
|
|
811
|
+
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
812
|
+
beforeWrite: () => {
|
|
813
|
+
// Clear chat box to make room for content
|
|
814
|
+
this.clearInputArea();
|
|
815
|
+
},
|
|
816
|
+
afterWrite: () => {
|
|
817
|
+
// Render is handled by streaming timer or setContentEndRow
|
|
818
|
+
},
|
|
819
|
+
});
|
|
1094
820
|
}
|
|
1095
821
|
/**
|
|
1096
822
|
* Dispose and clean up
|
|
@@ -1098,10 +824,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
1098
824
|
dispose() {
|
|
1099
825
|
if (this.disposed)
|
|
1100
826
|
return;
|
|
827
|
+
// Clean up streaming render timer
|
|
828
|
+
if (this.streamingRenderTimer) {
|
|
829
|
+
clearInterval(this.streamingRenderTimer);
|
|
830
|
+
this.streamingRenderTimer = null;
|
|
831
|
+
}
|
|
832
|
+
// Clean up output interceptor
|
|
833
|
+
if (this.outputInterceptorCleanup) {
|
|
834
|
+
this.outputInterceptorCleanup();
|
|
835
|
+
this.outputInterceptorCleanup = undefined;
|
|
836
|
+
}
|
|
837
|
+
// Reset scroll region
|
|
838
|
+
this.write('\x1b[r');
|
|
839
|
+
// Exit alternate screen buffer (restores main terminal)
|
|
840
|
+
if (this.unifiedUIInitialized) {
|
|
841
|
+
this.write(ESC.ALT_SCREEN_EXIT);
|
|
842
|
+
}
|
|
1101
843
|
this.disposed = true;
|
|
1102
844
|
this.enabled = false;
|
|
1103
|
-
this.disableScrollRegion();
|
|
1104
|
-
this.resetStreamingRenderThrottle();
|
|
1105
845
|
this.disableBracketedPaste();
|
|
1106
846
|
this.buffer = '';
|
|
1107
847
|
this.queue = [];
|
|
@@ -1165,25 +905,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1165
905
|
case 'c':
|
|
1166
906
|
this.emit('toggleAutoContinue');
|
|
1167
907
|
break;
|
|
1168
|
-
case 's':
|
|
1169
|
-
// Alt+S: Toggle scrollback mode
|
|
1170
|
-
this.toggleScrollbackMode();
|
|
1171
|
-
break;
|
|
1172
|
-
case 'up':
|
|
1173
|
-
// Alt+Up: Quick scroll up into history
|
|
1174
|
-
if (!this.isInScrollbackMode) {
|
|
1175
|
-
this.scrollUp(10);
|
|
1176
|
-
}
|
|
1177
|
-
else {
|
|
1178
|
-
this.scrollUp(1);
|
|
1179
|
-
}
|
|
1180
|
-
break;
|
|
1181
|
-
case 'down':
|
|
1182
|
-
// Alt+Down: Quick scroll down
|
|
1183
|
-
if (this.isInScrollbackMode) {
|
|
1184
|
-
this.scrollDown(1);
|
|
1185
|
-
}
|
|
1186
|
-
break;
|
|
1187
908
|
}
|
|
1188
909
|
}
|
|
1189
910
|
handleSpecialKey(_str, key) {
|
|
@@ -1209,53 +930,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
1209
930
|
this.moveCursorRight();
|
|
1210
931
|
return true;
|
|
1211
932
|
case 'up':
|
|
1212
|
-
|
|
1213
|
-
if ((key.ctrl && key.shift) || key.shift) {
|
|
1214
|
-
this.scrollUp(5);
|
|
1215
|
-
}
|
|
1216
|
-
else {
|
|
1217
|
-
this.handleUp();
|
|
1218
|
-
}
|
|
933
|
+
this.handleUp();
|
|
1219
934
|
return true;
|
|
1220
935
|
case 'down':
|
|
1221
|
-
|
|
1222
|
-
if ((key.ctrl && key.shift) || key.shift) {
|
|
1223
|
-
this.scrollDown(5);
|
|
1224
|
-
}
|
|
1225
|
-
else {
|
|
1226
|
-
this.handleDown();
|
|
1227
|
-
}
|
|
936
|
+
this.handleDown();
|
|
1228
937
|
return true;
|
|
1229
938
|
case 'home':
|
|
1230
|
-
|
|
1231
|
-
if (key.ctrl || this.isInScrollbackMode) {
|
|
1232
|
-
this.scrollToTop();
|
|
1233
|
-
}
|
|
1234
|
-
else {
|
|
1235
|
-
this.moveCursorToLineStart();
|
|
1236
|
-
}
|
|
939
|
+
this.moveCursorToLineStart();
|
|
1237
940
|
return true;
|
|
1238
941
|
case 'end':
|
|
1239
|
-
|
|
1240
|
-
if (key.ctrl || this.isInScrollbackMode) {
|
|
1241
|
-
this.scrollToBottom();
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
this.moveCursorToLineEnd();
|
|
1245
|
-
}
|
|
1246
|
-
return true;
|
|
1247
|
-
case 'pageup':
|
|
1248
|
-
this.scrollUp(20); // Scroll up by 20 lines
|
|
1249
|
-
return true;
|
|
1250
|
-
case 'pagedown':
|
|
1251
|
-
this.scrollDown(20); // Scroll down by 20 lines
|
|
942
|
+
this.moveCursorToLineEnd();
|
|
1252
943
|
return true;
|
|
1253
944
|
case 'tab':
|
|
1254
945
|
if (key.shift) {
|
|
1255
946
|
this.toggleEditMode();
|
|
1256
947
|
return true;
|
|
1257
948
|
}
|
|
1258
|
-
|
|
949
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
950
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
951
|
+
this.togglePasteExpansion();
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
this.toggleThinking();
|
|
955
|
+
}
|
|
956
|
+
return true;
|
|
957
|
+
case 'escape':
|
|
958
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
959
|
+
if (this.mode === 'streaming') {
|
|
960
|
+
this.emit('interrupt');
|
|
961
|
+
}
|
|
962
|
+
else if (this.buffer.length > 0) {
|
|
963
|
+
this.clear();
|
|
964
|
+
}
|
|
1259
965
|
return true;
|
|
1260
966
|
}
|
|
1261
967
|
return false;
|
|
@@ -1273,6 +979,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1273
979
|
this.insertPlainText(chunk, insertPos);
|
|
1274
980
|
this.cursor = insertPos + chunk.length;
|
|
1275
981
|
this.emit('change', this.buffer);
|
|
982
|
+
this.updateSuggestions();
|
|
1276
983
|
this.scheduleRender();
|
|
1277
984
|
}
|
|
1278
985
|
insertNewline() {
|
|
@@ -1297,6 +1004,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1297
1004
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1298
1005
|
}
|
|
1299
1006
|
this.emit('change', this.buffer);
|
|
1007
|
+
this.updateSuggestions();
|
|
1300
1008
|
this.scheduleRender();
|
|
1301
1009
|
}
|
|
1302
1010
|
deleteForward() {
|
|
@@ -1524,12 +1232,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1524
1232
|
timestamp: Date.now(),
|
|
1525
1233
|
});
|
|
1526
1234
|
this.emit('queue', text);
|
|
1527
|
-
this.clear(); // Clear immediately for queued input
|
|
1235
|
+
this.clear(); // Clear immediately for queued input, re-render to update queue display
|
|
1528
1236
|
}
|
|
1529
1237
|
else {
|
|
1530
|
-
// In idle mode, clear the input
|
|
1531
|
-
// The
|
|
1532
|
-
|
|
1238
|
+
// In idle mode, clear the input WITHOUT rendering.
|
|
1239
|
+
// The caller will display the user message and start streaming.
|
|
1240
|
+
// We'll render the input area again after streaming ends.
|
|
1241
|
+
this.clear(true); // Skip render - streaming will handle display
|
|
1533
1242
|
this.emit('submit', text);
|
|
1534
1243
|
}
|
|
1535
1244
|
}
|
|
@@ -1546,9 +1255,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1546
1255
|
if (available <= 0)
|
|
1547
1256
|
return;
|
|
1548
1257
|
const chunk = clean.slice(0, available);
|
|
1549
|
-
|
|
1550
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1551
|
-
if (isMultiline && !isShortMultiline) {
|
|
1258
|
+
if (isMultilinePaste(chunk)) {
|
|
1552
1259
|
this.insertPastePlaceholder(chunk);
|
|
1553
1260
|
}
|
|
1554
1261
|
else {
|
|
@@ -1612,236 +1319,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1612
1319
|
return { lines, cursorLine, cursorCol };
|
|
1613
1320
|
}
|
|
1614
1321
|
// ===========================================================================
|
|
1615
|
-
// SCROLLBACK BUFFER
|
|
1616
|
-
// ===========================================================================
|
|
1617
|
-
/**
|
|
1618
|
-
* Add content to the scrollback buffer for history retention
|
|
1619
|
-
*/
|
|
1620
|
-
addToScrollback(content) {
|
|
1621
|
-
if (!content)
|
|
1622
|
-
return;
|
|
1623
|
-
// Split content into lines and add to buffer
|
|
1624
|
-
const lines = content.split('\n');
|
|
1625
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1626
|
-
const line = lines[i];
|
|
1627
|
-
if (line !== undefined) {
|
|
1628
|
-
// Only add non-empty lines or preserve newlines between content
|
|
1629
|
-
if (i < lines.length - 1 || line.length > 0) {
|
|
1630
|
-
this.scrollbackBuffer.push(line);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
// Trim buffer if it exceeds max size
|
|
1635
|
-
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1636
|
-
this.scrollbackBuffer.shift();
|
|
1637
|
-
}
|
|
1638
|
-
// If we're in live mode (not scrolled up), keep offset at 0
|
|
1639
|
-
if (this.scrollbackOffset === 0) {
|
|
1640
|
-
this.isInScrollbackMode = false;
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
/**
|
|
1644
|
-
* Scroll up by a number of lines (PageUp)
|
|
1645
|
-
*/
|
|
1646
|
-
scrollUp(lines = 10) {
|
|
1647
|
-
const { rows } = this.getSize();
|
|
1648
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1649
|
-
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1650
|
-
// Calculate max scroll offset
|
|
1651
|
-
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1652
|
-
this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
|
|
1653
|
-
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1654
|
-
if (this.isInScrollbackMode) {
|
|
1655
|
-
this.renderScrollbackView();
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
/**
|
|
1659
|
-
* Scroll down by a number of lines (PageDown)
|
|
1660
|
-
*/
|
|
1661
|
-
scrollDown(lines = 10) {
|
|
1662
|
-
this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
|
|
1663
|
-
this.isInScrollbackMode = this.scrollbackOffset > 0;
|
|
1664
|
-
if (this.isInScrollbackMode) {
|
|
1665
|
-
this.renderScrollbackView();
|
|
1666
|
-
}
|
|
1667
|
-
else {
|
|
1668
|
-
// Returned to live mode - force re-render
|
|
1669
|
-
this.forceRender();
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
/**
|
|
1673
|
-
* Jump to the top of scrollback buffer
|
|
1674
|
-
*/
|
|
1675
|
-
scrollToTop() {
|
|
1676
|
-
const { rows } = this.getSize();
|
|
1677
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1678
|
-
const visibleLines = Math.max(1, rows - chatBoxHeight);
|
|
1679
|
-
const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
|
|
1680
|
-
this.scrollbackOffset = maxOffset;
|
|
1681
|
-
this.isInScrollbackMode = true;
|
|
1682
|
-
this.renderScrollbackView();
|
|
1683
|
-
}
|
|
1684
|
-
/**
|
|
1685
|
-
* Jump to the bottom (live mode)
|
|
1686
|
-
*/
|
|
1687
|
-
scrollToBottom() {
|
|
1688
|
-
this.scrollbackOffset = 0;
|
|
1689
|
-
this.isInScrollbackMode = false;
|
|
1690
|
-
this.forceRender();
|
|
1691
|
-
}
|
|
1692
|
-
/**
|
|
1693
|
-
* Toggle scrollback mode on/off (Alt+S hotkey)
|
|
1694
|
-
*/
|
|
1695
|
-
toggleScrollbackMode() {
|
|
1696
|
-
if (this.isInScrollbackMode) {
|
|
1697
|
-
this.scrollToBottom();
|
|
1698
|
-
}
|
|
1699
|
-
else if (this.scrollbackBuffer.length > 0) {
|
|
1700
|
-
this.scrollUp(20);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Render the scrollback buffer view with enhanced visuals
|
|
1705
|
-
* Features:
|
|
1706
|
-
* - Visual scroll position indicator
|
|
1707
|
-
* - Progress bar showing position in history
|
|
1708
|
-
* - Keyboard navigation hints
|
|
1709
|
-
* - Animated indicators
|
|
1710
|
-
*/
|
|
1711
|
-
renderScrollbackView() {
|
|
1712
|
-
const { rows, cols } = this.getSize();
|
|
1713
|
-
const chatBoxHeight = this.getChatBoxHeight();
|
|
1714
|
-
const contentHeight = Math.max(1, rows - chatBoxHeight);
|
|
1715
|
-
writeLock.lock('renderScrollback');
|
|
1716
|
-
try {
|
|
1717
|
-
this.write(ESC.SAVE);
|
|
1718
|
-
this.write(ESC.HIDE);
|
|
1719
|
-
// Clear content area
|
|
1720
|
-
for (let i = 1; i <= contentHeight; i++) {
|
|
1721
|
-
this.write(ESC.TO(i, 1));
|
|
1722
|
-
this.write(ESC.CLEAR_LINE);
|
|
1723
|
-
}
|
|
1724
|
-
// Calculate which lines to show
|
|
1725
|
-
const totalLines = this.scrollbackBuffer.length;
|
|
1726
|
-
const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
|
|
1727
|
-
const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
|
|
1728
|
-
const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
|
|
1729
|
-
// Build header bar with navigation hints
|
|
1730
|
-
const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
|
|
1731
|
-
this.write(ESC.TO(1, 1));
|
|
1732
|
-
this.write(headerInfo);
|
|
1733
|
-
// Render visible lines with line numbers and visual guides
|
|
1734
|
-
const lineNumWidth = String(totalLines).length + 1;
|
|
1735
|
-
const contentStart = 2; // Start after header
|
|
1736
|
-
for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
|
|
1737
|
-
const line = visibleLines[i] ?? '';
|
|
1738
|
-
const lineNum = startIdx + i + 1;
|
|
1739
|
-
this.write(ESC.TO(contentStart + i, 1));
|
|
1740
|
-
// Line number gutter
|
|
1741
|
-
const numStr = String(lineNum).padStart(lineNumWidth, ' ');
|
|
1742
|
-
this.write(theme.ui.muted(`${numStr} │ `));
|
|
1743
|
-
// Content with truncation
|
|
1744
|
-
const gutterWidth = lineNumWidth + 4;
|
|
1745
|
-
const maxLen = cols - gutterWidth - 2;
|
|
1746
|
-
const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
|
|
1747
|
-
this.write(displayLine);
|
|
1748
|
-
}
|
|
1749
|
-
// Add visual scroll track on the right edge
|
|
1750
|
-
this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
|
|
1751
|
-
this.write(ESC.RESTORE);
|
|
1752
|
-
this.write(ESC.SHOW);
|
|
1753
|
-
}
|
|
1754
|
-
finally {
|
|
1755
|
-
writeLock.unlock();
|
|
1756
|
-
}
|
|
1757
|
-
// Re-render chat box
|
|
1758
|
-
this.forceRender();
|
|
1759
|
-
}
|
|
1760
|
-
/**
|
|
1761
|
-
* Build scrollback header with navigation hints
|
|
1762
|
-
*/
|
|
1763
|
-
buildScrollbackHeader(cols, totalLines, startIdx, endIdx) {
|
|
1764
|
-
const percentage = Math.round((endIdx / totalLines) * 100);
|
|
1765
|
-
// Animated scroll indicator
|
|
1766
|
-
const scrollFrames = ['◆', '◇', '◆', '◈'];
|
|
1767
|
-
this.scrollIndicatorFrame = (this.scrollIndicatorFrame + 1) % scrollFrames.length;
|
|
1768
|
-
const indicator = scrollFrames[this.scrollIndicatorFrame];
|
|
1769
|
-
// Build header parts
|
|
1770
|
-
const leftPart = theme.info(`${indicator} SCROLLBACK`) +
|
|
1771
|
-
theme.ui.muted(` [${startIdx + 1}-${endIdx} of ${totalLines}]`);
|
|
1772
|
-
const progressBar = this.buildProgressBar(percentage, 15);
|
|
1773
|
-
const rightPart = progressBar +
|
|
1774
|
-
theme.ui.muted(` ${percentage}%`) +
|
|
1775
|
-
theme.ui.muted(' │ ') +
|
|
1776
|
-
theme.primary('PgUp') + theme.ui.muted('/') + theme.primary('PgDn') +
|
|
1777
|
-
theme.ui.muted(' scroll · ') +
|
|
1778
|
-
theme.primary('End') + theme.ui.muted(' exit');
|
|
1779
|
-
const leftLen = this.visibleLength(leftPart);
|
|
1780
|
-
const rightLen = this.visibleLength(rightPart);
|
|
1781
|
-
const padding = Math.max(1, cols - leftLen - rightLen - 2);
|
|
1782
|
-
return `${leftPart}${' '.repeat(padding)}${rightPart}`;
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Render visual scroll track on the right side
|
|
1786
|
-
*/
|
|
1787
|
-
renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx) {
|
|
1788
|
-
if (totalLines <= contentHeight || cols < 40)
|
|
1789
|
-
return;
|
|
1790
|
-
const trackHeight = contentHeight - 1; // Exclude header
|
|
1791
|
-
const viewportRatio = (endIdx - startIdx) / totalLines;
|
|
1792
|
-
const positionRatio = startIdx / Math.max(1, totalLines - (endIdx - startIdx));
|
|
1793
|
-
// Calculate thumb size and position
|
|
1794
|
-
const thumbSize = Math.max(1, Math.round(viewportRatio * trackHeight));
|
|
1795
|
-
const thumbStart = Math.round(positionRatio * (trackHeight - thumbSize));
|
|
1796
|
-
// Render track on right edge
|
|
1797
|
-
for (let i = 0; i < trackHeight; i++) {
|
|
1798
|
-
const row = 2 + i; // Start after header
|
|
1799
|
-
this.write(ESC.TO(row, cols));
|
|
1800
|
-
if (i >= thumbStart && i < thumbStart + thumbSize) {
|
|
1801
|
-
// Thumb (viewport indicator)
|
|
1802
|
-
this.write(theme.accent('█'));
|
|
1803
|
-
}
|
|
1804
|
-
else {
|
|
1805
|
-
// Track background
|
|
1806
|
-
this.write(theme.ui.muted('░'));
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Build a visual progress bar
|
|
1812
|
-
*/
|
|
1813
|
-
buildProgressBar(percentage, width = 10) {
|
|
1814
|
-
const filled = Math.round((percentage / 100) * width);
|
|
1815
|
-
const empty = width - filled;
|
|
1816
|
-
const bar = theme.accent('█'.repeat(filled)) +
|
|
1817
|
-
theme.ui.muted('░'.repeat(empty));
|
|
1818
|
-
return `${theme.ui.muted('[')}${bar}${theme.ui.muted(']')}`;
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* Get scrollback buffer content (for persistence)
|
|
1822
|
-
*/
|
|
1823
|
-
getScrollbackBuffer() {
|
|
1824
|
-
return [...this.scrollbackBuffer];
|
|
1825
|
-
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Load scrollback buffer (for restoration)
|
|
1828
|
-
*/
|
|
1829
|
-
loadScrollbackBuffer(lines) {
|
|
1830
|
-
this.scrollbackBuffer = [...lines];
|
|
1831
|
-
// Trim if necessary
|
|
1832
|
-
while (this.scrollbackBuffer.length > this.maxScrollbackLines) {
|
|
1833
|
-
this.scrollbackBuffer.shift();
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Clear scrollback buffer
|
|
1838
|
-
*/
|
|
1839
|
-
clearScrollbackBuffer() {
|
|
1840
|
-
this.scrollbackBuffer = [];
|
|
1841
|
-
this.scrollbackOffset = 0;
|
|
1842
|
-
this.isInScrollbackMode = false;
|
|
1843
|
-
}
|
|
1844
|
-
// ===========================================================================
|
|
1845
1322
|
// UTILITIES
|
|
1846
1323
|
// ===========================================================================
|
|
1847
1324
|
getComposedLength() {
|
|
@@ -1914,19 +1391,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1914
1391
|
this.shiftPlaceholders(position, text.length);
|
|
1915
1392
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1916
1393
|
}
|
|
1917
|
-
shouldInlineMultiline(content) {
|
|
1918
|
-
const lines = content.split('\n').length;
|
|
1919
|
-
const maxInlineLines = 4;
|
|
1920
|
-
const maxInlineChars = 240;
|
|
1921
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1922
|
-
}
|
|
1923
1394
|
findPlaceholderAt(position) {
|
|
1924
1395
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1925
1396
|
}
|
|
1926
|
-
buildPlaceholder(
|
|
1397
|
+
buildPlaceholder(summary) {
|
|
1927
1398
|
const id = ++this.pasteCounter;
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1399
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1400
|
+
// Show first line preview (truncated)
|
|
1401
|
+
const preview = summary.preview.length > 30
|
|
1402
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1403
|
+
: summary.preview;
|
|
1404
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1930
1405
|
return { id, placeholder };
|
|
1931
1406
|
}
|
|
1932
1407
|
insertPastePlaceholder(content) {
|
|
@@ -1934,21 +1409,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1934
1409
|
if (available <= 0)
|
|
1935
1410
|
return;
|
|
1936
1411
|
const cleanContent = content.slice(0, available);
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1412
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1413
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1414
|
+
if (summary.lineCount < 5) {
|
|
1415
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1416
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1417
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1418
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1939
1422
|
const insertPos = this.cursor;
|
|
1940
1423
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1941
1424
|
this.pastePlaceholders.push({
|
|
1942
1425
|
id,
|
|
1943
1426
|
content: cleanContent,
|
|
1944
|
-
lineCount,
|
|
1427
|
+
lineCount: summary.lineCount,
|
|
1945
1428
|
placeholder,
|
|
1946
1429
|
start: insertPos,
|
|
1947
1430
|
end: insertPos + placeholder.length,
|
|
1431
|
+
summary,
|
|
1432
|
+
expanded: false,
|
|
1948
1433
|
});
|
|
1949
1434
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1950
1435
|
this.cursor = insertPos + placeholder.length;
|
|
1951
1436
|
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1439
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1440
|
+
*/
|
|
1441
|
+
togglePasteExpansion() {
|
|
1442
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1443
|
+
if (!placeholder)
|
|
1444
|
+
return false;
|
|
1445
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1446
|
+
// Update the placeholder text in buffer
|
|
1447
|
+
const newPlaceholder = placeholder.expanded
|
|
1448
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1449
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1450
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1451
|
+
// Update buffer
|
|
1452
|
+
this.buffer =
|
|
1453
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1454
|
+
newPlaceholder +
|
|
1455
|
+
this.buffer.slice(placeholder.end);
|
|
1456
|
+
// Update placeholder tracking
|
|
1457
|
+
placeholder.placeholder = newPlaceholder;
|
|
1458
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1459
|
+
// Shift other placeholders
|
|
1460
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1461
|
+
this.scheduleRender();
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
buildExpandedPlaceholder(ph) {
|
|
1465
|
+
const lines = ph.content.split('\n');
|
|
1466
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1467
|
+
const lastLines = lines.length > 5
|
|
1468
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1469
|
+
: '';
|
|
1470
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1471
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1472
|
+
}
|
|
1952
1473
|
deletePlaceholder(placeholder) {
|
|
1953
1474
|
const length = placeholder.end - placeholder.start;
|
|
1954
1475
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1956,11 +1477,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1956
1477
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1957
1478
|
this.cursor = placeholder.start;
|
|
1958
1479
|
}
|
|
1959
|
-
updateContextUsage(value
|
|
1960
|
-
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1961
|
-
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1962
|
-
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1963
|
-
}
|
|
1480
|
+
updateContextUsage(value) {
|
|
1964
1481
|
if (value === null || !Number.isFinite(value)) {
|
|
1965
1482
|
this.contextUsage = null;
|
|
1966
1483
|
}
|
|
@@ -1987,28 +1504,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1987
1504
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1988
1505
|
this.setEditMode(next);
|
|
1989
1506
|
}
|
|
1990
|
-
scheduleStreamingRender(delayMs) {
|
|
1991
|
-
if (this.streamingRenderTimer)
|
|
1992
|
-
return;
|
|
1993
|
-
const wait = Math.max(16, delayMs);
|
|
1994
|
-
this.streamingRenderTimer = setTimeout(() => {
|
|
1995
|
-
this.streamingRenderTimer = null;
|
|
1996
|
-
// During streaming, only update chat box (not full render)
|
|
1997
|
-
if (this.scrollRegionActive) {
|
|
1998
|
-
this.renderChatBoxAtBottom();
|
|
1999
|
-
}
|
|
2000
|
-
else {
|
|
2001
|
-
this.render();
|
|
2002
|
-
}
|
|
2003
|
-
}, wait);
|
|
2004
|
-
}
|
|
2005
|
-
resetStreamingRenderThrottle() {
|
|
2006
|
-
if (this.streamingRenderTimer) {
|
|
2007
|
-
clearTimeout(this.streamingRenderTimer);
|
|
2008
|
-
this.streamingRenderTimer = null;
|
|
2009
|
-
}
|
|
2010
|
-
this.lastStreamingRender = 0;
|
|
2011
|
-
}
|
|
2012
1507
|
scheduleRender() {
|
|
2013
1508
|
if (!this.canRender())
|
|
2014
1509
|
return;
|