erosolar-cli 1.7.285 → 1.7.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -148
- package/dist/bin/erosolar.js +1 -0
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/capabilities/agentSpawningCapability.d.ts.map +1 -1
- package/dist/capabilities/agentSpawningCapability.js +56 -31
- package/dist/capabilities/agentSpawningCapability.js.map +1 -1
- package/dist/contracts/agent-schemas.json +0 -15
- package/dist/contracts/tools.schema.json +0 -9
- package/dist/core/agent.d.ts +2 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js.map +1 -1
- package/dist/core/customCommands.d.ts +1 -0
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +3 -0
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/hooks.d.ts +113 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +267 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/metricsTracker.d.ts +122 -0
- package/dist/core/metricsTracker.d.ts.map +1 -0
- package/dist/{alpha-zero → core}/metricsTracker.js +2 -5
- package/dist/core/metricsTracker.js.map +1 -0
- package/dist/core/securityAssessment.d.ts +91 -0
- package/dist/core/securityAssessment.d.ts.map +1 -0
- package/dist/core/securityAssessment.js +580 -0
- package/dist/core/securityAssessment.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +0 -14
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +22 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +0 -5
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +14 -3
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/core/validationRunner.d.ts +1 -3
- package/dist/core/validationRunner.d.ts.map +1 -1
- package/dist/core/validationRunner.js.map +1 -1
- package/dist/core/verification.d.ts +137 -0
- package/dist/core/verification.d.ts.map +1 -0
- package/dist/core/verification.js +323 -0
- package/dist/core/verification.js.map +1 -0
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +21 -0
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +9 -18
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +4 -10
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +0 -2
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +2 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +22 -7
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +229 -159
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts +2 -0
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +40 -9
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +1 -4
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +78 -186
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +496 -927
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +28 -35
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +26 -50
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/agentConfig.d.ts +27 -0
- package/dist/subagents/agentConfig.d.ts.map +1 -0
- package/dist/subagents/agentConfig.js +89 -0
- package/dist/subagents/agentConfig.js.map +1 -0
- package/dist/subagents/agentRegistry.d.ts +33 -0
- package/dist/subagents/agentRegistry.d.ts.map +1 -0
- package/dist/subagents/agentRegistry.js +162 -0
- package/dist/subagents/agentRegistry.js.map +1 -0
- package/dist/subagents/taskRunner.d.ts +7 -1
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +180 -47
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +13 -12
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +23 -44
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +139 -286
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +6 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +0 -158
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +0 -348
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +1 -0
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +15 -25
- package/dist/ui/unified/layout.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +10 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +78 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/package.json +1 -1
- package/dist/alpha-zero/agentWrapper.d.ts +0 -84
- package/dist/alpha-zero/agentWrapper.d.ts.map +0 -1
- package/dist/alpha-zero/agentWrapper.js +0 -171
- package/dist/alpha-zero/agentWrapper.js.map +0 -1
- package/dist/alpha-zero/codeEvaluator.d.ts +0 -25
- package/dist/alpha-zero/codeEvaluator.d.ts.map +0 -1
- package/dist/alpha-zero/codeEvaluator.js +0 -273
- package/dist/alpha-zero/codeEvaluator.js.map +0 -1
- package/dist/alpha-zero/competitiveRunner.d.ts +0 -66
- package/dist/alpha-zero/competitiveRunner.d.ts.map +0 -1
- package/dist/alpha-zero/competitiveRunner.js +0 -224
- package/dist/alpha-zero/competitiveRunner.js.map +0 -1
- package/dist/alpha-zero/index.d.ts +0 -67
- package/dist/alpha-zero/index.d.ts.map +0 -1
- package/dist/alpha-zero/index.js +0 -99
- package/dist/alpha-zero/index.js.map +0 -1
- package/dist/alpha-zero/introspection.d.ts +0 -128
- package/dist/alpha-zero/introspection.d.ts.map +0 -1
- package/dist/alpha-zero/introspection.js +0 -300
- package/dist/alpha-zero/introspection.js.map +0 -1
- package/dist/alpha-zero/metricsTracker.d.ts +0 -71
- package/dist/alpha-zero/metricsTracker.d.ts.map +0 -1
- package/dist/alpha-zero/metricsTracker.js.map +0 -1
- package/dist/alpha-zero/security/core.d.ts +0 -125
- package/dist/alpha-zero/security/core.d.ts.map +0 -1
- package/dist/alpha-zero/security/core.js +0 -271
- package/dist/alpha-zero/security/core.js.map +0 -1
- package/dist/alpha-zero/security/google.d.ts +0 -125
- package/dist/alpha-zero/security/google.d.ts.map +0 -1
- package/dist/alpha-zero/security/google.js +0 -311
- package/dist/alpha-zero/security/google.js.map +0 -1
- package/dist/alpha-zero/security/googleLoader.d.ts +0 -17
- package/dist/alpha-zero/security/googleLoader.d.ts.map +0 -1
- package/dist/alpha-zero/security/googleLoader.js +0 -41
- package/dist/alpha-zero/security/googleLoader.js.map +0 -1
- package/dist/alpha-zero/security/index.d.ts +0 -29
- package/dist/alpha-zero/security/index.d.ts.map +0 -1
- package/dist/alpha-zero/security/index.js +0 -32
- package/dist/alpha-zero/security/index.js.map +0 -1
- package/dist/alpha-zero/security/simulation.d.ts +0 -124
- package/dist/alpha-zero/security/simulation.d.ts.map +0 -1
- package/dist/alpha-zero/security/simulation.js +0 -277
- package/dist/alpha-zero/security/simulation.js.map +0 -1
- package/dist/alpha-zero/selfModification.d.ts +0 -109
- package/dist/alpha-zero/selfModification.d.ts.map +0 -1
- package/dist/alpha-zero/selfModification.js +0 -233
- package/dist/alpha-zero/selfModification.js.map +0 -1
- package/dist/alpha-zero/types.d.ts +0 -170
- package/dist/alpha-zero/types.d.ts.map +0 -1
- package/dist/alpha-zero/types.js +0 -31
- package/dist/alpha-zero/types.js.map +0 -1
- package/dist/capabilities/securityTestingCapability.d.ts +0 -13
- package/dist/capabilities/securityTestingCapability.d.ts.map +0 -1
- package/dist/capabilities/securityTestingCapability.js +0 -25
- package/dist/capabilities/securityTestingCapability.js.map +0 -1
- package/dist/core/aiFlowOptimizer.d.ts +0 -26
- package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
- package/dist/core/aiFlowOptimizer.js +0 -31
- package/dist/core/aiFlowOptimizer.js.map +0 -1
- package/dist/core/aiOptimizationEngine.d.ts +0 -158
- package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
- package/dist/core/aiOptimizationEngine.js +0 -428
- package/dist/core/aiOptimizationEngine.js.map +0 -1
- package/dist/core/aiOptimizationIntegration.d.ts +0 -93
- package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
- package/dist/core/aiOptimizationIntegration.js +0 -250
- package/dist/core/aiOptimizationIntegration.js.map +0 -1
- package/dist/core/enhancedErrorRecovery.d.ts +0 -100
- package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
- package/dist/core/enhancedErrorRecovery.js +0 -345
- package/dist/core/enhancedErrorRecovery.js.map +0 -1
- package/dist/core/hooksSystem.d.ts +0 -65
- package/dist/core/hooksSystem.d.ts.map +0 -1
- package/dist/core/hooksSystem.js +0 -273
- package/dist/core/hooksSystem.js.map +0 -1
- package/dist/core/memorySystem.d.ts +0 -48
- package/dist/core/memorySystem.d.ts.map +0 -1
- package/dist/core/memorySystem.js +0 -271
- package/dist/core/memorySystem.js.map +0 -1
- package/dist/core/unified/errors.d.ts +0 -189
- package/dist/core/unified/errors.d.ts.map +0 -1
- package/dist/core/unified/errors.js +0 -497
- package/dist/core/unified/errors.js.map +0 -1
- package/dist/core/unified/index.d.ts +0 -19
- package/dist/core/unified/index.d.ts.map +0 -1
- package/dist/core/unified/index.js +0 -68
- package/dist/core/unified/index.js.map +0 -1
- package/dist/core/unified/schema.d.ts +0 -101
- package/dist/core/unified/schema.d.ts.map +0 -1
- package/dist/core/unified/schema.js +0 -350
- package/dist/core/unified/schema.js.map +0 -1
- package/dist/core/unified/toolRuntime.d.ts +0 -179
- package/dist/core/unified/toolRuntime.d.ts.map +0 -1
- package/dist/core/unified/toolRuntime.js +0 -517
- package/dist/core/unified/toolRuntime.js.map +0 -1
- package/dist/core/unified/tools.d.ts +0 -127
- package/dist/core/unified/tools.d.ts.map +0 -1
- package/dist/core/unified/tools.js +0 -1333
- package/dist/core/unified/tools.js.map +0 -1
- package/dist/core/unified/types.d.ts +0 -352
- package/dist/core/unified/types.d.ts.map +0 -1
- package/dist/core/unified/types.js +0 -12
- package/dist/core/unified/types.js.map +0 -1
- package/dist/core/unified/version.d.ts +0 -209
- package/dist/core/unified/version.d.ts.map +0 -1
- package/dist/core/unified/version.js +0 -454
- package/dist/core/unified/version.js.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.d.ts +0 -3
- package/dist/plugins/tools/security/securityPlugin.d.ts.map +0 -1
- package/dist/plugins/tools/security/securityPlugin.js +0 -12
- package/dist/plugins/tools/security/securityPlugin.js.map +0 -1
- package/dist/security/active-stack-security.d.ts +0 -112
- package/dist/security/active-stack-security.d.ts.map +0 -1
- package/dist/security/active-stack-security.js +0 -296
- package/dist/security/active-stack-security.js.map +0 -1
- package/dist/security/advanced-persistence-research.d.ts +0 -92
- package/dist/security/advanced-persistence-research.d.ts.map +0 -1
- package/dist/security/advanced-persistence-research.js +0 -195
- package/dist/security/advanced-persistence-research.js.map +0 -1
- package/dist/security/advanced-targeting.d.ts +0 -119
- package/dist/security/advanced-targeting.d.ts.map +0 -1
- package/dist/security/advanced-targeting.js +0 -233
- package/dist/security/advanced-targeting.js.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.d.ts +0 -104
- package/dist/security/assessment/vulnerabilityAssessment.d.ts.map +0 -1
- package/dist/security/assessment/vulnerabilityAssessment.js +0 -315
- package/dist/security/assessment/vulnerabilityAssessment.js.map +0 -1
- package/dist/security/authorization/securityAuthorization.d.ts +0 -88
- package/dist/security/authorization/securityAuthorization.d.ts.map +0 -1
- package/dist/security/authorization/securityAuthorization.js +0 -172
- package/dist/security/authorization/securityAuthorization.js.map +0 -1
- package/dist/security/comprehensive-targeting.d.ts +0 -85
- package/dist/security/comprehensive-targeting.d.ts.map +0 -1
- package/dist/security/comprehensive-targeting.js +0 -438
- package/dist/security/comprehensive-targeting.js.map +0 -1
- package/dist/security/global-security-integration.d.ts +0 -91
- package/dist/security/global-security-integration.d.ts.map +0 -1
- package/dist/security/global-security-integration.js +0 -218
- package/dist/security/global-security-integration.js.map +0 -1
- package/dist/security/index.d.ts +0 -38
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/index.js +0 -47
- package/dist/security/index.js.map +0 -1
- package/dist/security/persistence-analyzer.d.ts +0 -56
- package/dist/security/persistence-analyzer.d.ts.map +0 -1
- package/dist/security/persistence-analyzer.js +0 -187
- package/dist/security/persistence-analyzer.js.map +0 -1
- package/dist/security/persistence-cli.d.ts +0 -36
- package/dist/security/persistence-cli.d.ts.map +0 -1
- package/dist/security/persistence-cli.js +0 -160
- package/dist/security/persistence-cli.js.map +0 -1
- package/dist/security/persistence-research.d.ts +0 -92
- package/dist/security/persistence-research.d.ts.map +0 -1
- package/dist/security/persistence-research.js +0 -364
- package/dist/security/persistence-research.js.map +0 -1
- package/dist/security/research/persistenceResearch.d.ts +0 -97
- package/dist/security/research/persistenceResearch.d.ts.map +0 -1
- package/dist/security/research/persistenceResearch.js +0 -282
- package/dist/security/research/persistenceResearch.js.map +0 -1
- package/dist/security/security-integration.d.ts +0 -74
- package/dist/security/security-integration.d.ts.map +0 -1
- package/dist/security/security-integration.js +0 -137
- package/dist/security/security-integration.js.map +0 -1
- package/dist/security/security-testing-framework.d.ts +0 -112
- package/dist/security/security-testing-framework.d.ts.map +0 -1
- package/dist/security/security-testing-framework.js +0 -364
- package/dist/security/security-testing-framework.js.map +0 -1
- package/dist/security/simulation/attackSimulation.d.ts +0 -93
- package/dist/security/simulation/attackSimulation.d.ts.map +0 -1
- package/dist/security/simulation/attackSimulation.js +0 -341
- package/dist/security/simulation/attackSimulation.js.map +0 -1
- package/dist/security/strategic-operations.d.ts +0 -100
- package/dist/security/strategic-operations.d.ts.map +0 -1
- package/dist/security/strategic-operations.js +0 -276
- package/dist/security/strategic-operations.js.map +0 -1
- package/dist/security/tool-security-wrapper.d.ts +0 -58
- package/dist/security/tool-security-wrapper.d.ts.map +0 -1
- package/dist/security/tool-security-wrapper.js +0 -156
- package/dist/security/tool-security-wrapper.js.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.js +0 -322
- package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
- package/dist/shell/inputQueueManager.d.ts +0 -144
- package/dist/shell/inputQueueManager.d.ts.map +0 -1
- package/dist/shell/inputQueueManager.js +0 -290
- package/dist/shell/inputQueueManager.js.map +0 -1
- package/dist/shell/metricsTracker.d.ts +0 -60
- package/dist/shell/metricsTracker.d.ts.map +0 -1
- package/dist/shell/metricsTracker.js +0 -119
- package/dist/shell/metricsTracker.js.map +0 -1
- package/dist/shell/streamingOutputManager.d.ts +0 -115
- package/dist/shell/streamingOutputManager.d.ts.map +0 -1
- package/dist/shell/streamingOutputManager.js +0 -225
- package/dist/shell/streamingOutputManager.js.map +0 -1
- package/dist/tools/securityTools.d.ts +0 -22
- package/dist/tools/securityTools.d.ts.map +0 -1
- package/dist/tools/securityTools.js +0 -448
- package/dist/tools/securityTools.js.map +0 -1
- package/dist/ui/persistentPrompt.d.ts +0 -50
- package/dist/ui/persistentPrompt.d.ts.map +0 -1
- package/dist/ui/persistentPrompt.js +0 -92
- package/dist/ui/persistentPrompt.js.map +0 -1
- package/dist/ui/terminalUISchema.d.ts +0 -195
- package/dist/ui/terminalUISchema.d.ts.map +0 -1
- package/dist/ui/terminalUISchema.js +0 -113
- package/dist/ui/terminalUISchema.js.map +0 -1
- package/scripts/deploy-security-capabilities.js +0 -178
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - One bottom-pinned chat box for the entire session (no inline anchors)
|
|
6
7
|
* - Native bracketed paste support (no heuristics)
|
|
7
8
|
* - Clean cursor model with render-time wrapping
|
|
8
9
|
* - State machine for different input modes
|
|
9
10
|
* - No readline dependency for display
|
|
10
11
|
*/
|
|
11
12
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
13
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
14
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
|
-
import {
|
|
15
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
16
|
+
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
17
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
16
18
|
// ANSI escape codes
|
|
17
19
|
const ESC = {
|
|
18
20
|
// Cursor control
|
|
@@ -22,9 +24,6 @@ const ESC = {
|
|
|
22
24
|
SHOW: '\x1b[?25h',
|
|
23
25
|
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
24
26
|
TO_COL: (col) => `\x1b[${col}G`,
|
|
25
|
-
// Screen control
|
|
26
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
27
|
-
HOME: '\x1b[H',
|
|
28
27
|
// Line control
|
|
29
28
|
CLEAR_LINE: '\x1b[2K',
|
|
30
29
|
CLEAR_TO_END: '\x1b[0J',
|
|
@@ -70,6 +69,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
70
69
|
statusMessage = null;
|
|
71
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
72
71
|
streamingLabel = null; // Streaming progress indicator
|
|
72
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
73
|
+
metaTokensUsed = null; // Optional token usage
|
|
74
|
+
metaTokenLimit = null; // Optional token window
|
|
75
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
76
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
73
77
|
reservedLines = 2;
|
|
74
78
|
scrollRegionActive = false;
|
|
75
79
|
lastRenderContent = '';
|
|
@@ -77,49 +81,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
77
81
|
renderDirty = false;
|
|
78
82
|
isRendering = false;
|
|
79
83
|
pinnedTopRows = 0;
|
|
80
|
-
inlineAnchorRow = null;
|
|
81
|
-
inlineLayout = false;
|
|
82
|
-
anchorProvider = null;
|
|
83
|
-
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
84
|
-
flowMode = true;
|
|
85
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
86
|
-
contentEndRow = 0; // Row where content ends (for idle mode positioning)
|
|
87
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
88
|
-
commandSuggestions = [];
|
|
89
|
-
filteredSuggestions = [];
|
|
90
|
-
selectedSuggestionIndex = 0;
|
|
91
|
-
showSuggestions = false;
|
|
92
|
-
maxVisibleSuggestions = 10;
|
|
93
84
|
// Lifecycle
|
|
94
85
|
disposed = false;
|
|
95
86
|
enabled = true;
|
|
96
87
|
contextUsage = null;
|
|
88
|
+
contextAutoCompactThreshold = 90;
|
|
89
|
+
// Track current content row in scroll region (starts at top, moves down)
|
|
90
|
+
contentRow = 1;
|
|
91
|
+
thinkingModeLabel = null;
|
|
97
92
|
editMode = 'display-edits';
|
|
98
93
|
verificationEnabled = true;
|
|
99
94
|
autoContinueEnabled = false;
|
|
100
95
|
verificationHotkey = 'alt+v';
|
|
101
96
|
autoContinueHotkey = 'alt+c';
|
|
97
|
+
thinkingHotkey = '/thinking';
|
|
98
|
+
modelLabel = null;
|
|
99
|
+
providerLabel = null;
|
|
102
100
|
// Output interceptor cleanup
|
|
103
101
|
outputInterceptorCleanup;
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
thinkingEnabled = true;
|
|
108
|
-
modelInfo = null; // Provider · Model info
|
|
109
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
102
|
+
// Streaming render throttle
|
|
103
|
+
lastStreamingRender = 0;
|
|
104
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
110
105
|
streamingRenderTimer = null;
|
|
111
|
-
// Unified UI initialization flag
|
|
112
|
-
unifiedUIInitialized = false;
|
|
113
106
|
constructor(writeStream = process.stdout, config = {}) {
|
|
114
107
|
super();
|
|
115
108
|
this.out = writeStream;
|
|
116
|
-
// Use schema defaults for configuration consistency
|
|
117
109
|
this.config = {
|
|
118
|
-
maxLines: config.maxLines ??
|
|
119
|
-
maxLength: config.maxLength ??
|
|
110
|
+
maxLines: config.maxLines ?? 1000,
|
|
111
|
+
maxLength: config.maxLength ?? 10000,
|
|
120
112
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
121
|
-
promptChar: config.promptChar ??
|
|
122
|
-
continuationChar: config.continuationChar ??
|
|
113
|
+
promptChar: config.promptChar ?? '> ',
|
|
114
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
123
115
|
};
|
|
124
116
|
}
|
|
125
117
|
// ===========================================================================
|
|
@@ -198,594 +190,46 @@ export class TerminalInput extends EventEmitter {
|
|
|
198
190
|
if (handled)
|
|
199
191
|
return;
|
|
200
192
|
}
|
|
201
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
202
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
203
|
-
this.emit('showHelp');
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
193
|
// Insert printable characters
|
|
207
194
|
if (str && !key?.ctrl && !key?.meta) {
|
|
208
195
|
this.insertText(str);
|
|
209
196
|
}
|
|
210
197
|
}
|
|
211
|
-
// Banner content to write on init (set via setBannerContent before initializeUnifiedUI)
|
|
212
|
-
bannerContent = null;
|
|
213
|
-
/**
|
|
214
|
-
* Set banner content to be written when unified UI initializes.
|
|
215
|
-
*/
|
|
216
|
-
setBannerContent(content) {
|
|
217
|
-
this.bannerContent = content;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Initialize the unified UI system immediately.
|
|
221
|
-
* Clears screen, writes banner, renders input area immediately below.
|
|
222
|
-
* This creates a compact layout on launch (no empty space).
|
|
223
|
-
* Scroll region is set up later when streaming starts.
|
|
224
|
-
*/
|
|
225
|
-
initializeUnifiedUI() {
|
|
226
|
-
if (this.unifiedUIInitialized) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Reserve lines for input area (used later when scroll region is set up)
|
|
230
|
-
this.pinnedTopRows = 0;
|
|
231
|
-
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
232
|
-
// Hide cursor during setup
|
|
233
|
-
this.write(ESC.HIDE);
|
|
234
|
-
// Clear screen
|
|
235
|
-
this.write(ESC.HOME);
|
|
236
|
-
this.write(ESC.CLEAR_SCREEN);
|
|
237
|
-
// Position cursor at row 1
|
|
238
|
-
this.write(ESC.TO(1, 1));
|
|
239
|
-
// Write banner as first content
|
|
240
|
-
let currentRow = 1;
|
|
241
|
-
if (this.bannerContent) {
|
|
242
|
-
process.stdout.write(this.bannerContent + '\n');
|
|
243
|
-
// Count banner lines
|
|
244
|
-
currentRow += this.bannerContent.split('\n').length;
|
|
245
|
-
}
|
|
246
|
-
// Mark unified UI as initialized
|
|
247
|
-
this.unifiedUIInitialized = true;
|
|
248
|
-
// Render input area immediately after banner (not at bottom)
|
|
249
|
-
this.renderInlineInputArea(currentRow);
|
|
250
|
-
// Show cursor
|
|
251
|
-
this.write(ESC.SHOW);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Render input area at a specific row (inline, not pinned to bottom).
|
|
255
|
-
* Used on launch for compact layout.
|
|
256
|
-
*/
|
|
257
|
-
renderInlineInputArea(startRow) {
|
|
258
|
-
const { cols } = this.getSize();
|
|
259
|
-
const divider = '─'.repeat(cols - 1);
|
|
260
|
-
// Move to start row
|
|
261
|
-
this.write(ESC.TO(startRow, 1));
|
|
262
|
-
// Status bar
|
|
263
|
-
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
264
|
-
// Model info line (if set)
|
|
265
|
-
if (this.modelInfo) {
|
|
266
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
267
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
268
|
-
if (this.contextUsage !== null) {
|
|
269
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
270
|
-
if (rem < 10)
|
|
271
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
272
|
-
else if (rem < 25)
|
|
273
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
274
|
-
else
|
|
275
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
276
|
-
}
|
|
277
|
-
process.stdout.write(modelLine + '\n');
|
|
278
|
-
}
|
|
279
|
-
// Top divider
|
|
280
|
-
process.stdout.write(divider + '\n');
|
|
281
|
-
// Input line with prompt
|
|
282
|
-
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
283
|
-
process.stdout.write(ESC.BG_DARK + ' '.repeat(Math.max(0, cols - this.config.promptChar.length - 1)) + ESC.RESET + '\n');
|
|
284
|
-
// Bottom divider
|
|
285
|
-
process.stdout.write(divider + '\n');
|
|
286
|
-
// Mode controls
|
|
287
|
-
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
288
|
-
// Position cursor in input area
|
|
289
|
-
this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
|
|
290
|
-
}
|
|
291
198
|
/**
|
|
292
199
|
* Set the input mode
|
|
293
200
|
*
|
|
294
|
-
* Streaming
|
|
295
|
-
*
|
|
296
|
-
* the cursor is (below the streamed content).
|
|
201
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
202
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
297
203
|
*/
|
|
298
204
|
setMode(mode) {
|
|
299
205
|
const prevMode = this.mode;
|
|
300
206
|
this.mode = mode;
|
|
301
207
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
302
|
-
//
|
|
303
|
-
this.
|
|
304
|
-
|
|
305
|
-
// Ensure unified UI is initialized (if not already done on launch)
|
|
306
|
-
if (!this.unifiedUIInitialized) {
|
|
307
|
-
this.initializeUnifiedUI();
|
|
308
|
-
}
|
|
309
|
-
// Set up scroll region to reserve bottom for persistent input area
|
|
310
|
-
this.pinnedTopRows = 0;
|
|
311
|
-
this.reservedLines = 6; // status + model + divider + input + divider + controls
|
|
312
|
-
// Ensure scroll region is enabled (may have been initialized already)
|
|
313
|
-
if (!this.scrollRegionActive) {
|
|
314
|
-
const contentBottomRow = Math.max(1, rows - this.reservedLines);
|
|
315
|
-
this.write(ESC.TO(contentBottomRow, 1));
|
|
316
|
-
this.enableScrollRegion();
|
|
317
|
-
}
|
|
318
|
-
// Render bottom input area
|
|
319
|
-
this.renderBottomInputArea();
|
|
320
|
-
// Start timer to update bottom input area (updates elapsed time)
|
|
321
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
322
|
-
if (this.mode === 'streaming') {
|
|
323
|
-
this.updateStreamingStatus();
|
|
324
|
-
this.renderBottomInputArea();
|
|
325
|
-
}
|
|
326
|
-
}, 1000);
|
|
208
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
209
|
+
this.resetStreamingRenderThrottle();
|
|
210
|
+
this.enableScrollRegion();
|
|
327
211
|
this.renderDirty = true;
|
|
212
|
+
this.render();
|
|
328
213
|
}
|
|
329
214
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
clearInterval(this.streamingRenderTimer);
|
|
333
|
-
this.streamingRenderTimer = null;
|
|
334
|
-
}
|
|
335
|
-
// Reset streaming time
|
|
336
|
-
this.streamingStartTime = null;
|
|
337
|
-
// Keep scroll region active for consistent bottom-pinned UI
|
|
338
|
-
// (scroll region reserves bottom for input area in all modes)
|
|
339
|
-
// Reset flow mode tracking
|
|
340
|
-
this.flowModeRenderedLines = 0;
|
|
341
|
-
// Render using unified bottom input area (same layout as streaming)
|
|
342
|
-
writeLock.withLock(() => {
|
|
343
|
-
this.renderBottomInputArea();
|
|
344
|
-
}, 'terminalInput.streamingEnd');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Update streaming status label (called by timer)
|
|
349
|
-
*/
|
|
350
|
-
updateStreamingStatus() {
|
|
351
|
-
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
352
|
-
return;
|
|
353
|
-
// Calculate elapsed time
|
|
354
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
355
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
356
|
-
const minutes = Math.floor(seconds / 60);
|
|
357
|
-
const secs = seconds % 60;
|
|
358
|
-
// Format elapsed time
|
|
359
|
-
let elapsedStr;
|
|
360
|
-
if (minutes > 0) {
|
|
361
|
-
elapsedStr = `${minutes}m ${secs}s`;
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
elapsedStr = `${secs}s`;
|
|
365
|
-
}
|
|
366
|
-
// Update streaming label
|
|
367
|
-
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Render input area - unified for streaming and normal modes.
|
|
371
|
-
*
|
|
372
|
-
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
373
|
-
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
374
|
-
*/
|
|
375
|
-
renderPinnedInputArea() {
|
|
376
|
-
const { rows, cols } = this.getSize();
|
|
377
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
378
|
-
const divider = renderDivider(cols - 2);
|
|
379
|
-
const isStreaming = this.mode === 'streaming';
|
|
380
|
-
// Wrap buffer into display lines (multi-line support)
|
|
381
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
382
|
-
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
383
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
384
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
385
|
-
// Calculate display window (keep cursor visible)
|
|
386
|
-
let startLine = 0;
|
|
387
|
-
if (lines.length > displayLines) {
|
|
388
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
389
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
390
|
-
}
|
|
391
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
392
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
393
|
-
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
394
|
-
const hasModelInfo = !!this.modelInfo;
|
|
395
|
-
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
396
|
-
// Save cursor position during streaming (so content flow resumes correctly)
|
|
397
|
-
if (isStreaming) {
|
|
398
|
-
this.write(ESC.SAVE);
|
|
399
|
-
}
|
|
400
|
-
this.write(ESC.HIDE);
|
|
401
|
-
this.write(ESC.RESET);
|
|
402
|
-
// Calculate start row based on mode:
|
|
403
|
-
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
404
|
-
// - Normal: right after content (contentEndRow + 1)
|
|
405
|
-
let currentRow;
|
|
406
|
-
if (isStreaming) {
|
|
407
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
// In normal mode, render right after content
|
|
411
|
-
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
412
|
-
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
413
|
-
currentRow = Math.max(1, contentRow + 1);
|
|
414
|
-
}
|
|
415
|
-
let finalRow = currentRow;
|
|
416
|
-
let finalCol = 3;
|
|
417
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
418
|
-
this.write(ESC.TO(currentRow, 1));
|
|
419
|
-
this.write(ESC.CLEAR_TO_END);
|
|
420
|
-
// Status bar
|
|
421
|
-
this.write(ESC.TO(currentRow, 1));
|
|
422
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
423
|
-
currentRow++;
|
|
424
|
-
// Model info line (if set) - displayed below status, above input
|
|
425
|
-
if (hasModelInfo) {
|
|
426
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
427
|
-
this.write(ESC.TO(currentRow, 1));
|
|
428
|
-
// Build model info with context usage
|
|
429
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
430
|
-
if (this.contextUsage !== null) {
|
|
431
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
432
|
-
if (rem < 10)
|
|
433
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
434
|
-
else if (rem < 25)
|
|
435
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
436
|
-
else
|
|
437
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
438
|
-
}
|
|
439
|
-
this.write(modelLine);
|
|
440
|
-
currentRow++;
|
|
441
|
-
}
|
|
442
|
-
// Top divider
|
|
443
|
-
this.write(ESC.TO(currentRow, 1));
|
|
444
|
-
this.write(divider);
|
|
445
|
-
currentRow++;
|
|
446
|
-
// Input lines with background styling
|
|
447
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
448
|
-
this.write(ESC.TO(currentRow, 1));
|
|
449
|
-
const line = visibleLines[i] ?? '';
|
|
450
|
-
const absoluteLineIdx = startLine + i;
|
|
451
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
452
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
453
|
-
// Background
|
|
454
|
-
this.write(ESC.BG_DARK);
|
|
455
|
-
// Prompt prefix
|
|
456
|
-
this.write(ESC.DIM);
|
|
457
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
458
|
-
this.write(ESC.RESET);
|
|
459
|
-
this.write(ESC.BG_DARK);
|
|
460
|
-
if (isCursorLine) {
|
|
461
|
-
const col = Math.min(cursorCol, line.length);
|
|
462
|
-
const before = line.slice(0, col);
|
|
463
|
-
const at = col < line.length ? line[col] : ' ';
|
|
464
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
465
|
-
this.write(before);
|
|
466
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
467
|
-
this.write(at);
|
|
468
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
469
|
-
this.write(after);
|
|
470
|
-
finalRow = currentRow;
|
|
471
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
this.write(line);
|
|
475
|
-
}
|
|
476
|
-
// Pad to edge
|
|
477
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
478
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
479
|
-
if (padding > 0)
|
|
480
|
-
this.write(' '.repeat(padding));
|
|
481
|
-
this.write(ESC.RESET);
|
|
482
|
-
currentRow++;
|
|
483
|
-
}
|
|
484
|
-
// Bottom divider
|
|
485
|
-
this.write(ESC.TO(currentRow, 1));
|
|
486
|
-
this.write(divider);
|
|
487
|
-
currentRow++;
|
|
488
|
-
// Mode controls line
|
|
489
|
-
this.write(ESC.TO(currentRow, 1));
|
|
490
|
-
this.write(this.buildModeControls(cols));
|
|
491
|
-
// Restore cursor position during streaming, or show cursor in normal mode
|
|
492
|
-
if (isStreaming) {
|
|
493
|
-
this.write(ESC.RESTORE);
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
// Position cursor in input area
|
|
497
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
498
|
-
this.write(ESC.SHOW);
|
|
499
|
-
}
|
|
500
|
-
// Update reserved lines for scroll region calculations
|
|
501
|
-
this.updateReservedLines(totalHeight);
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Render input area during streaming (alias for unified method)
|
|
505
|
-
*/
|
|
506
|
-
renderStreamingInputArea() {
|
|
507
|
-
this.renderPinnedInputArea();
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Render bottom input area - UNIFIED for all modes.
|
|
511
|
-
* Uses cursor save/restore to update bottom without affecting content flow.
|
|
512
|
-
*
|
|
513
|
-
* Layout (same for idle/streaming/ready):
|
|
514
|
-
* - Status bar (streaming timer or "Type a message")
|
|
515
|
-
* - Model info line (provider · model · ctx)
|
|
516
|
-
* - Divider
|
|
517
|
-
* - Input area
|
|
518
|
-
* - Divider
|
|
519
|
-
* - Mode controls
|
|
520
|
-
*/
|
|
521
|
-
renderBottomInputArea() {
|
|
522
|
-
const { rows, cols } = this.getSize();
|
|
523
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
524
|
-
const divider = renderDivider(cols - 2);
|
|
525
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
526
|
-
const isStreaming = this.mode === 'streaming';
|
|
527
|
-
// Wrap buffer into display lines
|
|
528
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
529
|
-
// Allow multi-line in non-streaming, single line during streaming
|
|
530
|
-
const maxDisplayLines = isStreaming ? 1 : 3;
|
|
531
|
-
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
532
|
-
const visibleLines = lines.slice(0, displayLines);
|
|
533
|
-
// Calculate total height for bottom area
|
|
534
|
-
const hasModelInfo = !!this.modelInfo;
|
|
535
|
-
const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
|
|
536
|
-
// Ensure scroll region is always enabled (unified behavior)
|
|
537
|
-
if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
|
|
538
|
-
this.reservedLines = totalHeight;
|
|
215
|
+
// Streaming ended - render the input area
|
|
216
|
+
this.resetStreamingRenderThrottle();
|
|
539
217
|
this.enableScrollRegion();
|
|
218
|
+
this.forceRender();
|
|
540
219
|
}
|
|
541
|
-
const startRow = Math.max(1, rows - totalHeight + 1);
|
|
542
|
-
// Save cursor, hide it
|
|
543
|
-
this.write(ESC.SAVE);
|
|
544
|
-
this.write(ESC.HIDE);
|
|
545
|
-
let currentRow = startRow;
|
|
546
|
-
// Clear the bottom reserved area
|
|
547
|
-
for (let r = startRow; r <= rows; r++) {
|
|
548
|
-
this.write(ESC.TO(r, 1));
|
|
549
|
-
this.write(ESC.CLEAR_LINE);
|
|
550
|
-
}
|
|
551
|
-
// Status bar - UNIFIED: same format for all modes
|
|
552
|
-
this.write(ESC.TO(currentRow, 1));
|
|
553
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
554
|
-
currentRow++;
|
|
555
|
-
// Model info line (if set)
|
|
556
|
-
if (hasModelInfo) {
|
|
557
|
-
this.write(ESC.TO(currentRow, 1));
|
|
558
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
559
|
-
if (this.contextUsage !== null) {
|
|
560
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
561
|
-
if (rem < 10)
|
|
562
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
563
|
-
else if (rem < 25)
|
|
564
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
565
|
-
else
|
|
566
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
567
|
-
}
|
|
568
|
-
this.write(modelLine);
|
|
569
|
-
currentRow++;
|
|
570
|
-
}
|
|
571
|
-
// Top divider
|
|
572
|
-
this.write(ESC.TO(currentRow, 1));
|
|
573
|
-
this.write(divider);
|
|
574
|
-
currentRow++;
|
|
575
|
-
// Input lines with background styling
|
|
576
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
577
|
-
this.write(ESC.TO(currentRow, 1));
|
|
578
|
-
const line = visibleLines[i] ?? '';
|
|
579
|
-
const isFirstLine = i === 0;
|
|
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
|
-
this.write(line);
|
|
586
|
-
// Pad to edge
|
|
587
|
-
const lineLen = this.config.promptChar.length + line.length;
|
|
588
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
589
|
-
if (padding > 0)
|
|
590
|
-
this.write(' '.repeat(padding));
|
|
591
|
-
this.write(ESC.RESET);
|
|
592
|
-
currentRow++;
|
|
593
|
-
}
|
|
594
|
-
// Bottom divider
|
|
595
|
-
this.write(ESC.TO(currentRow, 1));
|
|
596
|
-
this.write(divider);
|
|
597
|
-
currentRow++;
|
|
598
|
-
// Mode controls
|
|
599
|
-
this.write(ESC.TO(currentRow, 1));
|
|
600
|
-
this.write(this.buildModeControls(cols));
|
|
601
|
-
// Cursor positioning depends on mode:
|
|
602
|
-
// - Streaming: restore to content area (where streaming output continues)
|
|
603
|
-
// - Normal: position in input area for typing
|
|
604
|
-
if (isStreaming) {
|
|
605
|
-
this.write(ESC.RESTORE);
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
// Position cursor in input area
|
|
609
|
-
// Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
|
|
610
|
-
const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
|
|
611
|
-
const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
|
|
612
|
-
const targetCol = this.config.promptChar.length + cursorCol + 1;
|
|
613
|
-
this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
|
|
614
|
-
}
|
|
615
|
-
this.write(ESC.SHOW);
|
|
616
|
-
// Track last render state
|
|
617
|
-
this.lastRenderContent = this.buffer;
|
|
618
|
-
this.lastRenderCursor = this.cursor;
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Enable or disable flow mode.
|
|
622
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
623
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
624
|
-
*/
|
|
625
|
-
setFlowMode(enabled) {
|
|
626
|
-
if (this.flowMode === enabled)
|
|
627
|
-
return;
|
|
628
|
-
this.flowMode = enabled;
|
|
629
|
-
this.renderDirty = true;
|
|
630
|
-
this.scheduleRender();
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Check if flow mode is enabled.
|
|
634
|
-
*/
|
|
635
|
-
isFlowMode() {
|
|
636
|
-
return this.flowMode;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Set the row where content ends (for idle mode positioning).
|
|
640
|
-
* Input area will render starting from this row + 1.
|
|
641
|
-
*/
|
|
642
|
-
setContentEndRow(row) {
|
|
643
|
-
this.contentEndRow = Math.max(0, row);
|
|
644
|
-
this.renderDirty = true;
|
|
645
|
-
this.scheduleRender();
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Set available slash commands for auto-complete suggestions.
|
|
649
|
-
*/
|
|
650
|
-
setCommands(commands) {
|
|
651
|
-
this.commandSuggestions = commands;
|
|
652
|
-
this.updateSuggestions();
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Update filtered suggestions based on current input.
|
|
656
|
-
*/
|
|
657
|
-
updateSuggestions() {
|
|
658
|
-
const input = this.buffer.trim();
|
|
659
|
-
// Only show suggestions when input starts with "/"
|
|
660
|
-
if (!input.startsWith('/')) {
|
|
661
|
-
this.showSuggestions = false;
|
|
662
|
-
this.filteredSuggestions = [];
|
|
663
|
-
this.selectedSuggestionIndex = 0;
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const query = input.toLowerCase();
|
|
667
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
668
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
669
|
-
// Show suggestions if we have matches
|
|
670
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
671
|
-
// Keep selection in bounds
|
|
672
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
673
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Select next suggestion (arrow down / tab).
|
|
678
|
-
*/
|
|
679
|
-
selectNextSuggestion() {
|
|
680
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
681
|
-
return;
|
|
682
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
683
|
-
this.renderDirty = true;
|
|
684
|
-
this.scheduleRender();
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
688
|
-
*/
|
|
689
|
-
selectPrevSuggestion() {
|
|
690
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
691
|
-
return;
|
|
692
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
693
|
-
? this.filteredSuggestions.length - 1
|
|
694
|
-
: this.selectedSuggestionIndex - 1;
|
|
695
|
-
this.renderDirty = true;
|
|
696
|
-
this.scheduleRender();
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Accept current suggestion and insert into buffer.
|
|
700
|
-
*/
|
|
701
|
-
acceptSuggestion() {
|
|
702
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
703
|
-
return false;
|
|
704
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
705
|
-
if (!selected)
|
|
706
|
-
return false;
|
|
707
|
-
// Replace buffer with selected command
|
|
708
|
-
this.buffer = selected.command + ' ';
|
|
709
|
-
this.cursor = this.buffer.length;
|
|
710
|
-
this.showSuggestions = false;
|
|
711
|
-
this.renderDirty = true;
|
|
712
|
-
this.scheduleRender();
|
|
713
|
-
return true;
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Check if suggestions are visible.
|
|
717
|
-
*/
|
|
718
|
-
areSuggestionsVisible() {
|
|
719
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Update token count for metrics display
|
|
723
|
-
*/
|
|
724
|
-
setTokensUsed(tokens) {
|
|
725
|
-
this.tokensUsed = tokens;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Toggle thinking/reasoning mode
|
|
729
|
-
*/
|
|
730
|
-
toggleThinking() {
|
|
731
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
732
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
733
|
-
this.scheduleRender();
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Get thinking enabled state
|
|
737
|
-
*/
|
|
738
|
-
isThinkingEnabled() {
|
|
739
|
-
return this.thinkingEnabled;
|
|
740
220
|
}
|
|
741
221
|
/**
|
|
742
222
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
743
223
|
*/
|
|
744
224
|
setPinnedHeaderLines(count) {
|
|
745
|
-
//
|
|
746
|
-
if (this.pinnedTopRows !==
|
|
747
|
-
this.pinnedTopRows =
|
|
225
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
226
|
+
if (this.pinnedTopRows !== 0) {
|
|
227
|
+
this.pinnedTopRows = 0;
|
|
748
228
|
if (this.scrollRegionActive) {
|
|
749
229
|
this.applyScrollRegion();
|
|
750
230
|
}
|
|
751
231
|
}
|
|
752
232
|
}
|
|
753
|
-
/**
|
|
754
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
755
|
-
* restore the default bottom-aligned layout.
|
|
756
|
-
*/
|
|
757
|
-
setInlineAnchor(row) {
|
|
758
|
-
if (row === null || row === undefined) {
|
|
759
|
-
this.inlineAnchorRow = null;
|
|
760
|
-
this.inlineLayout = false;
|
|
761
|
-
this.renderDirty = true;
|
|
762
|
-
this.render();
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
const { rows } = this.getSize();
|
|
766
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
767
|
-
this.inlineAnchorRow = clamped;
|
|
768
|
-
this.inlineLayout = true;
|
|
769
|
-
this.renderDirty = true;
|
|
770
|
-
this.render();
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
774
|
-
* output by re-evaluating the anchor before each render.
|
|
775
|
-
*/
|
|
776
|
-
setInlineAnchorProvider(provider) {
|
|
777
|
-
this.anchorProvider = provider;
|
|
778
|
-
if (!provider) {
|
|
779
|
-
this.inlineLayout = false;
|
|
780
|
-
this.inlineAnchorRow = null;
|
|
781
|
-
this.renderDirty = true;
|
|
782
|
-
this.render();
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
this.inlineLayout = true;
|
|
786
|
-
this.renderDirty = true;
|
|
787
|
-
this.render();
|
|
788
|
-
}
|
|
789
233
|
/**
|
|
790
234
|
* Get current mode
|
|
791
235
|
*/
|
|
@@ -895,6 +339,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
895
339
|
this.streamingLabel = next;
|
|
896
340
|
this.scheduleRender();
|
|
897
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
344
|
+
*/
|
|
345
|
+
setMetaStatus(meta) {
|
|
346
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
347
|
+
? Math.floor(meta.elapsedSeconds)
|
|
348
|
+
: null;
|
|
349
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
350
|
+
? Math.floor(meta.tokensUsed)
|
|
351
|
+
: null;
|
|
352
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
353
|
+
? Math.floor(meta.tokenLimit)
|
|
354
|
+
: null;
|
|
355
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
356
|
+
? Math.floor(meta.thinkingMs)
|
|
357
|
+
: null;
|
|
358
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
359
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
360
|
+
this.metaTokensUsed === nextTokens &&
|
|
361
|
+
this.metaTokenLimit === nextLimit &&
|
|
362
|
+
this.metaThinkingMs === nextThinking &&
|
|
363
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
367
|
+
this.metaTokensUsed = nextTokens;
|
|
368
|
+
this.metaTokenLimit = nextLimit;
|
|
369
|
+
this.metaThinkingMs = nextThinking;
|
|
370
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
371
|
+
this.scheduleRender();
|
|
372
|
+
}
|
|
898
373
|
/**
|
|
899
374
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
900
375
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -904,26 +379,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
904
379
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
905
380
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
906
381
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
382
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
383
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
907
384
|
if (this.verificationEnabled === nextVerification &&
|
|
908
385
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
909
386
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
910
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
387
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
388
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
389
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
911
390
|
return;
|
|
912
391
|
}
|
|
913
392
|
this.verificationEnabled = nextVerification;
|
|
914
393
|
this.autoContinueEnabled = nextAutoContinue;
|
|
915
394
|
this.verificationHotkey = nextVerifyHotkey;
|
|
916
395
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
917
|
-
this.
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Set the model info string (e.g., "OpenAI · gpt-4")
|
|
921
|
-
* This is displayed persistently above the input area.
|
|
922
|
-
*/
|
|
923
|
-
setModelInfo(info) {
|
|
924
|
-
if (this.modelInfo === info)
|
|
925
|
-
return;
|
|
926
|
-
this.modelInfo = info;
|
|
396
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
397
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
927
398
|
this.scheduleRender();
|
|
928
399
|
}
|
|
929
400
|
/**
|
|
@@ -936,298 +407,390 @@ export class TerminalInput extends EventEmitter {
|
|
|
936
407
|
this.scheduleRender();
|
|
937
408
|
}
|
|
938
409
|
/**
|
|
939
|
-
*
|
|
410
|
+
* Surface model/provider context in the controls bar.
|
|
411
|
+
*/
|
|
412
|
+
setModelContext(options) {
|
|
413
|
+
const nextModel = options.model?.trim() || null;
|
|
414
|
+
const nextProvider = options.provider?.trim() || null;
|
|
415
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.modelLabel = nextModel;
|
|
419
|
+
this.providerLabel = nextProvider;
|
|
420
|
+
this.scheduleRender();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Render the input area - Claude Code style with mode controls
|
|
940
424
|
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* - Ready mode: Shows status info
|
|
425
|
+
* During streaming we keep the scroll region active and repaint only the
|
|
426
|
+
* pinned status/input block (throttled) so streamed content can scroll
|
|
427
|
+
* naturally above while elapsed time and status stay fresh.
|
|
945
428
|
*/
|
|
946
429
|
render() {
|
|
947
430
|
if (!this.canRender())
|
|
948
431
|
return;
|
|
949
432
|
if (this.isRendering)
|
|
950
433
|
return;
|
|
434
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
435
|
+
// During streaming we still render the pinned input/status region, but throttle
|
|
436
|
+
// to avoid fighting with the streamed content flow.
|
|
437
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
438
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
439
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
440
|
+
if (waitMs > 0) {
|
|
441
|
+
this.renderDirty = true;
|
|
442
|
+
this.scheduleStreamingRender(waitMs);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
951
446
|
const shouldSkip = !this.renderDirty &&
|
|
952
447
|
this.buffer === this.lastRenderContent &&
|
|
953
448
|
this.cursor === this.lastRenderCursor;
|
|
954
449
|
this.renderDirty = false;
|
|
955
|
-
// Skip if nothing changed
|
|
450
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
956
451
|
if (shouldSkip) {
|
|
957
452
|
return;
|
|
958
453
|
}
|
|
959
|
-
// If write lock is held, defer render
|
|
454
|
+
// If write lock is held, defer render to avoid race conditions
|
|
960
455
|
if (writeLock.isLocked()) {
|
|
961
456
|
writeLock.safeWrite(() => this.render());
|
|
962
457
|
return;
|
|
963
458
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
this.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
* - Input line(s)
|
|
1002
|
-
* - Bottom divider
|
|
1003
|
-
* - Mode controls
|
|
1004
|
-
*/
|
|
1005
|
-
renderBottomPinned() {
|
|
1006
|
-
const { rows, cols } = this.getSize();
|
|
1007
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
1008
|
-
const isStreaming = this.mode === 'streaming';
|
|
1009
|
-
// Use unified pinned input area (works for both streaming and normal)
|
|
1010
|
-
// Only use complex rendering when suggestions are visible
|
|
1011
|
-
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
1012
|
-
if (!hasSuggestions) {
|
|
1013
|
-
this.renderPinnedInputArea();
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
// Wrap buffer into display lines
|
|
1017
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
1018
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
1019
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
1020
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
1021
|
-
// Calculate display window (keep cursor visible)
|
|
1022
|
-
let startLine = 0;
|
|
1023
|
-
if (lines.length > displayLines) {
|
|
1024
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
1025
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
1026
|
-
}
|
|
1027
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
1028
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
1029
|
-
// Calculate suggestion display (not during streaming)
|
|
1030
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
1031
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
1032
|
-
: [];
|
|
1033
|
-
const suggestionLines = suggestionsToShow.length;
|
|
1034
|
-
this.write(ESC.HIDE);
|
|
1035
|
-
this.write(ESC.RESET);
|
|
1036
|
-
const divider = renderDivider(cols - 2);
|
|
1037
|
-
// Calculate positions from absolute bottom
|
|
1038
|
-
let currentRow;
|
|
1039
|
-
if (suggestionLines > 0) {
|
|
1040
|
-
// With suggestions: input area + dividers + suggestions
|
|
1041
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
1042
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
1043
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
1044
|
-
this.updateReservedLines(totalHeight);
|
|
1045
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
1046
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1047
|
-
this.write(ESC.CLEAR_TO_END);
|
|
1048
|
-
// Top divider
|
|
459
|
+
const performRender = () => {
|
|
460
|
+
if (!this.scrollRegionActive) {
|
|
461
|
+
this.enableScrollRegion();
|
|
462
|
+
}
|
|
463
|
+
const { rows, cols } = this.getSize();
|
|
464
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
465
|
+
// Wrap buffer into display lines
|
|
466
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
467
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
468
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
469
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
470
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
471
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
472
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
473
|
+
// Calculate display window (keep cursor visible)
|
|
474
|
+
let startLine = 0;
|
|
475
|
+
if (lines.length > displayLines) {
|
|
476
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
477
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
478
|
+
}
|
|
479
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
480
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
481
|
+
// Hide cursor during render to prevent flicker
|
|
482
|
+
this.write(ESC.HIDE);
|
|
483
|
+
this.write(ESC.RESET);
|
|
484
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
485
|
+
let currentRow = startRow;
|
|
486
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
487
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
488
|
+
// Meta/status header (elapsed, tokens/context)
|
|
489
|
+
for (const metaLine of metaLines) {
|
|
490
|
+
this.write(ESC.TO(currentRow, 1));
|
|
491
|
+
this.write(ESC.CLEAR_LINE);
|
|
492
|
+
this.write(metaLine);
|
|
493
|
+
currentRow += 1;
|
|
494
|
+
}
|
|
495
|
+
// Separator line
|
|
1049
496
|
this.write(ESC.TO(currentRow, 1));
|
|
497
|
+
this.write(ESC.CLEAR_LINE);
|
|
498
|
+
const divider = renderDivider(cols - 2);
|
|
1050
499
|
this.write(divider);
|
|
1051
|
-
currentRow
|
|
1052
|
-
//
|
|
500
|
+
currentRow += 1;
|
|
501
|
+
// Render input lines
|
|
1053
502
|
let finalRow = currentRow;
|
|
1054
503
|
let finalCol = 3;
|
|
1055
504
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
1056
|
-
|
|
505
|
+
const rowNum = currentRow + i;
|
|
506
|
+
this.write(ESC.TO(rowNum, 1));
|
|
507
|
+
this.write(ESC.CLEAR_LINE);
|
|
1057
508
|
const line = visibleLines[i] ?? '';
|
|
1058
509
|
const absoluteLineIdx = startLine + i;
|
|
1059
510
|
const isFirstLine = absoluteLineIdx === 0;
|
|
1060
511
|
const isCursorLine = i === adjustedCursorLine;
|
|
512
|
+
// Background
|
|
513
|
+
this.write(ESC.BG_DARK);
|
|
514
|
+
// Prompt prefix
|
|
515
|
+
this.write(ESC.DIM);
|
|
1061
516
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
517
|
+
this.write(ESC.RESET);
|
|
518
|
+
this.write(ESC.BG_DARK);
|
|
1062
519
|
if (isCursorLine) {
|
|
520
|
+
// Render with block cursor
|
|
1063
521
|
const col = Math.min(cursorCol, line.length);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
this.write(
|
|
1068
|
-
this.write(
|
|
1069
|
-
|
|
522
|
+
const before = line.slice(0, col);
|
|
523
|
+
const at = col < line.length ? line[col] : ' ';
|
|
524
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
525
|
+
this.write(before);
|
|
526
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
527
|
+
this.write(at);
|
|
528
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
529
|
+
this.write(after);
|
|
530
|
+
finalRow = rowNum;
|
|
1070
531
|
finalCol = this.config.promptChar.length + col + 1;
|
|
1071
532
|
}
|
|
1072
533
|
else {
|
|
1073
534
|
this.write(line);
|
|
1074
535
|
}
|
|
1075
|
-
|
|
536
|
+
// Pad to edge for clean look
|
|
537
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
538
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
539
|
+
if (padding > 0)
|
|
540
|
+
this.write(' '.repeat(padding));
|
|
541
|
+
this.write(ESC.RESET);
|
|
1076
542
|
}
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
this.write(
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1084
|
-
const suggestion = suggestionsToShow[i];
|
|
1085
|
-
const isSelected = i === this.selectedSuggestionIndex;
|
|
1086
|
-
// Indent and highlight selected
|
|
1087
|
-
this.write(' ');
|
|
1088
|
-
if (isSelected) {
|
|
1089
|
-
this.write(ESC.REVERSE);
|
|
1090
|
-
this.write(ESC.BOLD);
|
|
1091
|
-
}
|
|
1092
|
-
this.write(suggestion.command);
|
|
1093
|
-
if (isSelected) {
|
|
1094
|
-
this.write(ESC.RESET);
|
|
1095
|
-
}
|
|
1096
|
-
// Description (dimmed)
|
|
1097
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
1098
|
-
if (descSpace > 10 && suggestion.description) {
|
|
1099
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
1100
|
-
this.write(ESC.RESET);
|
|
1101
|
-
this.write(ESC.DIM);
|
|
1102
|
-
this.write(' ');
|
|
1103
|
-
this.write(desc);
|
|
1104
|
-
this.write(ESC.RESET);
|
|
1105
|
-
}
|
|
1106
|
-
currentRow++;
|
|
1107
|
-
}
|
|
1108
|
-
// Position cursor in input area
|
|
543
|
+
// Mode controls line (Claude Code style)
|
|
544
|
+
const controlRow = currentRow + visibleLines.length;
|
|
545
|
+
this.write(ESC.TO(controlRow, 1));
|
|
546
|
+
this.write(ESC.CLEAR_LINE);
|
|
547
|
+
this.write(this.buildModeControls(cols));
|
|
548
|
+
// Position cursor in the input box for user editing
|
|
1109
549
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
550
|
+
this.write(ESC.SHOW);
|
|
551
|
+
// Update state
|
|
552
|
+
this.lastRenderContent = this.buffer;
|
|
553
|
+
this.lastRenderCursor = this.cursor;
|
|
554
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
555
|
+
if (this.streamingRenderTimer) {
|
|
556
|
+
clearTimeout(this.streamingRenderTimer);
|
|
557
|
+
this.streamingRenderTimer = null;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
// Use write lock during render to prevent interleaved output
|
|
561
|
+
writeLock.lock('terminalInput.render');
|
|
562
|
+
this.isRendering = true;
|
|
563
|
+
try {
|
|
564
|
+
performRender();
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
writeLock.unlock();
|
|
568
|
+
this.isRendering = false;
|
|
1110
569
|
}
|
|
1111
|
-
this.write(ESC.SHOW);
|
|
1112
|
-
// Update state
|
|
1113
|
-
this.lastRenderContent = this.buffer;
|
|
1114
|
-
this.lastRenderCursor = this.cursor;
|
|
1115
570
|
}
|
|
1116
571
|
/**
|
|
1117
|
-
* Build
|
|
572
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
573
|
+
* During streaming, shows model line pinned above streaming info.
|
|
1118
574
|
*/
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (this.
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
//
|
|
575
|
+
buildMetaLines(width) {
|
|
576
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
577
|
+
const lines = [];
|
|
578
|
+
// Model line should ALWAYS be shown (pinned above streaming content)
|
|
579
|
+
if (this.modelLabel) {
|
|
580
|
+
const modelText = this.providerLabel
|
|
581
|
+
? `model ${this.modelLabel} @ ${this.providerLabel}`
|
|
582
|
+
: `model ${this.modelLabel}`;
|
|
583
|
+
lines.push(renderStatusLine([{ text: modelText, tone: 'info' }], width));
|
|
584
|
+
}
|
|
585
|
+
// During streaming, add a compact status line with essential info
|
|
586
|
+
if (streamingActive) {
|
|
587
|
+
const parts = [];
|
|
588
|
+
// Essential streaming info
|
|
589
|
+
if (this.metaThinkingMs !== null) {
|
|
590
|
+
parts.push({ text: formatThinking(this.metaThinkingMs, this.metaThinkingHasContent), tone: 'info' });
|
|
591
|
+
}
|
|
592
|
+
if (this.metaElapsedSeconds !== null) {
|
|
593
|
+
parts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
594
|
+
}
|
|
595
|
+
parts.push({ text: 'esc to stop', tone: 'warn' });
|
|
596
|
+
if (parts.length) {
|
|
597
|
+
lines.push(renderStatusLine(parts, width));
|
|
598
|
+
}
|
|
599
|
+
return lines;
|
|
600
|
+
}
|
|
601
|
+
// Non-streaming: show full status info (model line already added above)
|
|
602
|
+
if (this.metaThinkingMs !== null) {
|
|
603
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
604
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
605
|
+
}
|
|
606
|
+
const statusParts = [];
|
|
607
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
608
|
+
if (statusLabel) {
|
|
609
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
610
|
+
}
|
|
611
|
+
if (this.metaElapsedSeconds !== null) {
|
|
612
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
613
|
+
}
|
|
614
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
615
|
+
if (tokensRemaining !== null) {
|
|
616
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
617
|
+
}
|
|
618
|
+
if (statusParts.length) {
|
|
619
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
620
|
+
}
|
|
621
|
+
const usageParts = [];
|
|
622
|
+
if (this.metaTokensUsed !== null) {
|
|
623
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
624
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
625
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
626
|
+
}
|
|
627
|
+
if (this.contextUsage !== null) {
|
|
628
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
629
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
630
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
631
|
+
}
|
|
1130
632
|
if (this.queue.length > 0) {
|
|
1131
|
-
|
|
633
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
1132
634
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
635
|
+
if (usageParts.length) {
|
|
636
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
637
|
+
}
|
|
638
|
+
return lines;
|
|
1136
639
|
}
|
|
1137
640
|
/**
|
|
1138
|
-
*
|
|
1139
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
641
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
1140
642
|
*/
|
|
1141
|
-
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (this.streamingStartTime) {
|
|
1148
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1149
|
-
const mins = Math.floor(elapsed / 60);
|
|
1150
|
-
const secs = elapsed % 60;
|
|
1151
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
1152
|
-
}
|
|
1153
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
643
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
644
|
+
const width = Math.max(1, cols);
|
|
645
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
646
|
+
const row = startRow + i;
|
|
647
|
+
this.write(ESC.TO(row, 1));
|
|
648
|
+
this.write(' '.repeat(width));
|
|
1154
649
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Build Claude Code style mode controls line.
|
|
653
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
654
|
+
*/
|
|
655
|
+
buildModeControls(cols) {
|
|
656
|
+
const width = Math.max(8, cols - 2);
|
|
657
|
+
const leftParts = [];
|
|
658
|
+
const rightParts = [];
|
|
659
|
+
if (this.streamingLabel) {
|
|
660
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
1163
661
|
}
|
|
1164
|
-
// Override/warning status
|
|
1165
662
|
if (this.overrideStatusMessage) {
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
663
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
664
|
+
}
|
|
665
|
+
if (this.statusMessage) {
|
|
666
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
667
|
+
}
|
|
668
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
669
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
670
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
671
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
672
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
673
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
674
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
675
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
676
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
677
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
678
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
679
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
1171
680
|
}
|
|
1172
|
-
// Multi-line indicator
|
|
1173
681
|
if (this.buffer.includes('\n')) {
|
|
1174
|
-
|
|
682
|
+
const lineCount = this.buffer.split('\n').length;
|
|
683
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
1175
684
|
}
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
685
|
+
if (this.pastePlaceholders.length > 0) {
|
|
686
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
687
|
+
leftParts.push({
|
|
688
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
689
|
+
tone: 'info',
|
|
690
|
+
});
|
|
1178
691
|
}
|
|
1179
|
-
const
|
|
1180
|
-
|
|
692
|
+
const contextRemaining = this.computeContextRemaining();
|
|
693
|
+
if (this.thinkingModeLabel) {
|
|
694
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
695
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
696
|
+
}
|
|
697
|
+
// Show model in controls only when NOT streaming (during streaming it's in meta lines)
|
|
698
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
699
|
+
if (this.modelLabel && !streamingActive) {
|
|
700
|
+
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
701
|
+
rightParts.push({ text: modelText, tone: 'muted' });
|
|
702
|
+
}
|
|
703
|
+
if (contextRemaining !== null) {
|
|
704
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
705
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
706
|
+
? 'Context auto-compact imminent'
|
|
707
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
708
|
+
rightParts.push({ text: label, tone });
|
|
709
|
+
}
|
|
710
|
+
if (!rightParts.length || width < 60) {
|
|
711
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
712
|
+
return renderStatusLine(merged, width);
|
|
713
|
+
}
|
|
714
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
715
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
716
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
717
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
718
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
719
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
720
|
+
}
|
|
721
|
+
formatHotkey(hotkey) {
|
|
722
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
723
|
+
if (!normalized)
|
|
724
|
+
return hotkey;
|
|
725
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
726
|
+
const map = {
|
|
727
|
+
shift: '⇧',
|
|
728
|
+
sh: '⇧',
|
|
729
|
+
alt: '⌥',
|
|
730
|
+
option: '⌥',
|
|
731
|
+
opt: '⌥',
|
|
732
|
+
ctrl: '⌃',
|
|
733
|
+
control: '⌃',
|
|
734
|
+
cmd: '⌘',
|
|
735
|
+
meta: '⌘',
|
|
736
|
+
};
|
|
737
|
+
const formatted = parts
|
|
738
|
+
.map((part) => {
|
|
739
|
+
const symbol = map[part];
|
|
740
|
+
if (symbol)
|
|
741
|
+
return symbol;
|
|
742
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
743
|
+
})
|
|
744
|
+
.join('');
|
|
745
|
+
return formatted || hotkey;
|
|
746
|
+
}
|
|
747
|
+
computeContextRemaining() {
|
|
748
|
+
if (this.contextUsage === null) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
752
|
+
}
|
|
753
|
+
computeTokensRemaining() {
|
|
754
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
758
|
+
return this.formatTokenCount(remaining);
|
|
759
|
+
}
|
|
760
|
+
formatElapsedLabel(seconds) {
|
|
761
|
+
if (seconds < 60) {
|
|
762
|
+
return `${seconds}s`;
|
|
763
|
+
}
|
|
764
|
+
const mins = Math.floor(seconds / 60);
|
|
765
|
+
const secs = seconds % 60;
|
|
766
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
767
|
+
}
|
|
768
|
+
formatTokenCount(value) {
|
|
769
|
+
if (!Number.isFinite(value)) {
|
|
770
|
+
return `${value}`;
|
|
771
|
+
}
|
|
772
|
+
if (value >= 1_000_000) {
|
|
773
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
774
|
+
}
|
|
775
|
+
if (value >= 1_000) {
|
|
776
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
777
|
+
}
|
|
778
|
+
return `${Math.round(value)}`;
|
|
779
|
+
}
|
|
780
|
+
visibleLength(value) {
|
|
781
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
782
|
+
return value.replace(ansiPattern, '').length;
|
|
1181
783
|
}
|
|
1182
784
|
/**
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
785
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
786
|
+
* needing a TTY. Not used by production code.
|
|
1187
787
|
*/
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
1195
|
-
if (this.editMode === 'display-edits') {
|
|
1196
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
1197
|
-
}
|
|
1198
|
-
else {
|
|
1199
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
1200
|
-
}
|
|
1201
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
1202
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
1203
|
-
// Verification (green when on) - per schema.verificationMode
|
|
1204
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
1205
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
1206
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
1207
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
1208
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
1209
|
-
let rightPart = '';
|
|
1210
|
-
if (this.contextUsage !== null) {
|
|
1211
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
1212
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
1213
|
-
if (rem < 10)
|
|
1214
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
1215
|
-
else if (rem < 25)
|
|
1216
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
1217
|
-
else
|
|
1218
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
1219
|
-
}
|
|
1220
|
-
// Calculate visible lengths (strip ANSI)
|
|
1221
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1222
|
-
const leftLen = strip(leftPart).length;
|
|
1223
|
-
const rightLen = strip(rightPart).length;
|
|
1224
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
1225
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
1226
|
-
}
|
|
1227
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
1228
|
-
return `${leftPart} ${rightPart}`;
|
|
1229
|
-
}
|
|
1230
|
-
return leftPart;
|
|
788
|
+
getDebugUiSnapshot(width) {
|
|
789
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
790
|
+
return {
|
|
791
|
+
meta: this.buildMetaLines(cols - 2),
|
|
792
|
+
controls: this.buildModeControls(cols),
|
|
793
|
+
};
|
|
1231
794
|
}
|
|
1232
795
|
/**
|
|
1233
796
|
* Force a re-render
|
|
@@ -1250,17 +813,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1250
813
|
handleResize() {
|
|
1251
814
|
this.lastRenderContent = '';
|
|
1252
815
|
this.lastRenderCursor = -1;
|
|
816
|
+
this.resetStreamingRenderThrottle();
|
|
1253
817
|
// Re-clamp pinned header rows to the new terminal height
|
|
1254
818
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
819
|
+
if (this.scrollRegionActive) {
|
|
820
|
+
this.disableScrollRegion();
|
|
821
|
+
this.enableScrollRegion();
|
|
822
|
+
}
|
|
1255
823
|
this.scheduleRender();
|
|
1256
824
|
}
|
|
1257
825
|
/**
|
|
1258
826
|
* Register with display's output interceptor to position cursor correctly.
|
|
1259
827
|
* When scroll region is active, output needs to go to the scroll region,
|
|
1260
828
|
* not the protected bottom area where the input is rendered.
|
|
1261
|
-
*
|
|
1262
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
1263
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
1264
829
|
*/
|
|
1265
830
|
registerOutputInterceptor(display) {
|
|
1266
831
|
if (this.outputInterceptorCleanup) {
|
|
@@ -1268,25 +833,66 @@ export class TerminalInput extends EventEmitter {
|
|
|
1268
833
|
}
|
|
1269
834
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
1270
835
|
beforeWrite: () => {
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
836
|
+
// Position cursor at current content row (starts at top, moves down).
|
|
837
|
+
// When contentRow reaches scrollBottom, terminal handles scrolling.
|
|
838
|
+
if (this.scrollRegionActive) {
|
|
839
|
+
const { rows } = this.getSize();
|
|
840
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
841
|
+
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
842
|
+
this.write(ESC.SAVE);
|
|
843
|
+
this.write(ESC.TO(targetRow, 1));
|
|
844
|
+
}
|
|
1273
845
|
},
|
|
1274
|
-
afterWrite: () => {
|
|
1275
|
-
//
|
|
846
|
+
afterWrite: (content) => {
|
|
847
|
+
// Advance content row by number of lines written and restore cursor.
|
|
848
|
+
if (this.scrollRegionActive) {
|
|
849
|
+
const { rows } = this.getSize();
|
|
850
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
851
|
+
// Count newlines in content to advance by correct amount
|
|
852
|
+
const lineCount = content ? (content.match(/\n/g) || []).length + 1 : 1;
|
|
853
|
+
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
854
|
+
this.write(ESC.RESTORE);
|
|
855
|
+
}
|
|
1276
856
|
},
|
|
1277
857
|
});
|
|
1278
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Write content directly into the scroll region (for banner, user prompts, etc.).
|
|
861
|
+
* Content starts at top and flows down, then scrolls when bottom is reached.
|
|
862
|
+
*/
|
|
863
|
+
writeToScrollRegion(content) {
|
|
864
|
+
if (!content)
|
|
865
|
+
return;
|
|
866
|
+
// Ensure scroll region is active
|
|
867
|
+
if (!this.scrollRegionActive) {
|
|
868
|
+
this.enableScrollRegion();
|
|
869
|
+
}
|
|
870
|
+
const { rows } = this.getSize();
|
|
871
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
872
|
+
const targetRow = Math.min(this.contentRow, scrollBottom);
|
|
873
|
+
// Write at current content position
|
|
874
|
+
this.write(ESC.SAVE);
|
|
875
|
+
this.write(ESC.TO(targetRow, 1));
|
|
876
|
+
this.write(content);
|
|
877
|
+
this.write(ESC.RESTORE);
|
|
878
|
+
// Advance contentRow by number of lines written
|
|
879
|
+
const lineCount = (content.match(/\n/g) || []).length + 1;
|
|
880
|
+
this.contentRow = Math.min(this.contentRow + lineCount, scrollBottom);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Reset content position to start of scroll region.
|
|
884
|
+
* Does NOT clear the terminal - content starts from current position.
|
|
885
|
+
*/
|
|
886
|
+
resetContentPosition() {
|
|
887
|
+
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
888
|
+
this.contentRow = scrollTop;
|
|
889
|
+
}
|
|
1279
890
|
/**
|
|
1280
891
|
* Dispose and clean up
|
|
1281
892
|
*/
|
|
1282
893
|
dispose() {
|
|
1283
894
|
if (this.disposed)
|
|
1284
895
|
return;
|
|
1285
|
-
// Clean up streaming render timer
|
|
1286
|
-
if (this.streamingRenderTimer) {
|
|
1287
|
-
clearInterval(this.streamingRenderTimer);
|
|
1288
|
-
this.streamingRenderTimer = null;
|
|
1289
|
-
}
|
|
1290
896
|
// Clean up output interceptor
|
|
1291
897
|
if (this.outputInterceptorCleanup) {
|
|
1292
898
|
this.outputInterceptorCleanup();
|
|
@@ -1294,6 +900,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1294
900
|
}
|
|
1295
901
|
this.disposed = true;
|
|
1296
902
|
this.enabled = false;
|
|
903
|
+
this.resetStreamingRenderThrottle();
|
|
1297
904
|
this.disableScrollRegion();
|
|
1298
905
|
this.disableBracketedPaste();
|
|
1299
906
|
this.buffer = '';
|
|
@@ -1399,22 +1006,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1399
1006
|
this.toggleEditMode();
|
|
1400
1007
|
return true;
|
|
1401
1008
|
}
|
|
1402
|
-
|
|
1403
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1404
|
-
this.togglePasteExpansion();
|
|
1405
|
-
}
|
|
1406
|
-
else {
|
|
1407
|
-
this.toggleThinking();
|
|
1408
|
-
}
|
|
1409
|
-
return true;
|
|
1410
|
-
case 'escape':
|
|
1411
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1412
|
-
if (this.mode === 'streaming') {
|
|
1413
|
-
this.emit('interrupt');
|
|
1414
|
-
}
|
|
1415
|
-
else if (this.buffer.length > 0) {
|
|
1416
|
-
this.clear();
|
|
1417
|
-
}
|
|
1009
|
+
this.insertText(' ');
|
|
1418
1010
|
return true;
|
|
1419
1011
|
}
|
|
1420
1012
|
return false;
|
|
@@ -1432,7 +1024,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1432
1024
|
this.insertPlainText(chunk, insertPos);
|
|
1433
1025
|
this.cursor = insertPos + chunk.length;
|
|
1434
1026
|
this.emit('change', this.buffer);
|
|
1435
|
-
this.updateSuggestions();
|
|
1436
1027
|
this.scheduleRender();
|
|
1437
1028
|
}
|
|
1438
1029
|
insertNewline() {
|
|
@@ -1457,7 +1048,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1457
1048
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1458
1049
|
}
|
|
1459
1050
|
this.emit('change', this.buffer);
|
|
1460
|
-
this.updateSuggestions();
|
|
1461
1051
|
this.scheduleRender();
|
|
1462
1052
|
}
|
|
1463
1053
|
deleteForward() {
|
|
@@ -1707,7 +1297,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1707
1297
|
if (available <= 0)
|
|
1708
1298
|
return;
|
|
1709
1299
|
const chunk = clean.slice(0, available);
|
|
1710
|
-
|
|
1300
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1301
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1302
|
+
if (isMultiline && !isShortMultiline) {
|
|
1711
1303
|
this.insertPastePlaceholder(chunk);
|
|
1712
1304
|
}
|
|
1713
1305
|
else {
|
|
@@ -1727,6 +1319,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1727
1319
|
return;
|
|
1728
1320
|
this.applyScrollRegion();
|
|
1729
1321
|
this.scrollRegionActive = true;
|
|
1322
|
+
this.forceRender();
|
|
1730
1323
|
}
|
|
1731
1324
|
disableScrollRegion() {
|
|
1732
1325
|
if (!this.scrollRegionActive)
|
|
@@ -1877,17 +1470,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1877
1470
|
this.shiftPlaceholders(position, text.length);
|
|
1878
1471
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1879
1472
|
}
|
|
1473
|
+
shouldInlineMultiline(content) {
|
|
1474
|
+
const lines = content.split('\n').length;
|
|
1475
|
+
const maxInlineLines = 4;
|
|
1476
|
+
const maxInlineChars = 240;
|
|
1477
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1478
|
+
}
|
|
1880
1479
|
findPlaceholderAt(position) {
|
|
1881
1480
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1882
1481
|
}
|
|
1883
|
-
buildPlaceholder(
|
|
1482
|
+
buildPlaceholder(lineCount) {
|
|
1884
1483
|
const id = ++this.pasteCounter;
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
const preview = summary.preview.length > 30
|
|
1888
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1889
|
-
: summary.preview;
|
|
1890
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1484
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1485
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1891
1486
|
return { id, placeholder };
|
|
1892
1487
|
}
|
|
1893
1488
|
insertPastePlaceholder(content) {
|
|
@@ -1895,67 +1490,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1895
1490
|
if (available <= 0)
|
|
1896
1491
|
return;
|
|
1897
1492
|
const cleanContent = content.slice(0, available);
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1900
|
-
if (summary.lineCount < 5) {
|
|
1901
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1902
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1903
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1904
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1493
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1494
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1908
1495
|
const insertPos = this.cursor;
|
|
1909
1496
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1910
1497
|
this.pastePlaceholders.push({
|
|
1911
1498
|
id,
|
|
1912
1499
|
content: cleanContent,
|
|
1913
|
-
lineCount
|
|
1500
|
+
lineCount,
|
|
1914
1501
|
placeholder,
|
|
1915
1502
|
start: insertPos,
|
|
1916
1503
|
end: insertPos + placeholder.length,
|
|
1917
|
-
summary,
|
|
1918
|
-
expanded: false,
|
|
1919
1504
|
});
|
|
1920
1505
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1921
1506
|
this.cursor = insertPos + placeholder.length;
|
|
1922
1507
|
}
|
|
1923
|
-
/**
|
|
1924
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1925
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1926
|
-
*/
|
|
1927
|
-
togglePasteExpansion() {
|
|
1928
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1929
|
-
if (!placeholder)
|
|
1930
|
-
return false;
|
|
1931
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1932
|
-
// Update the placeholder text in buffer
|
|
1933
|
-
const newPlaceholder = placeholder.expanded
|
|
1934
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1935
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1936
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1937
|
-
// Update buffer
|
|
1938
|
-
this.buffer =
|
|
1939
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1940
|
-
newPlaceholder +
|
|
1941
|
-
this.buffer.slice(placeholder.end);
|
|
1942
|
-
// Update placeholder tracking
|
|
1943
|
-
placeholder.placeholder = newPlaceholder;
|
|
1944
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1945
|
-
// Shift other placeholders
|
|
1946
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1947
|
-
this.scheduleRender();
|
|
1948
|
-
return true;
|
|
1949
|
-
}
|
|
1950
|
-
buildExpandedPlaceholder(ph) {
|
|
1951
|
-
const lines = ph.content.split('\n');
|
|
1952
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1953
|
-
const lastLines = lines.length > 5
|
|
1954
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1955
|
-
: '';
|
|
1956
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1957
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1958
|
-
}
|
|
1959
1508
|
deletePlaceholder(placeholder) {
|
|
1960
1509
|
const length = placeholder.end - placeholder.start;
|
|
1961
1510
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1963,7 +1512,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1963
1512
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1964
1513
|
this.cursor = placeholder.start;
|
|
1965
1514
|
}
|
|
1966
|
-
updateContextUsage(value) {
|
|
1515
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1516
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1517
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1518
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1519
|
+
}
|
|
1967
1520
|
if (value === null || !Number.isFinite(value)) {
|
|
1968
1521
|
this.contextUsage = null;
|
|
1969
1522
|
}
|
|
@@ -1990,6 +1543,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1990
1543
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1991
1544
|
this.setEditMode(next);
|
|
1992
1545
|
}
|
|
1546
|
+
scheduleStreamingRender(delayMs) {
|
|
1547
|
+
if (this.streamingRenderTimer)
|
|
1548
|
+
return;
|
|
1549
|
+
const wait = Math.max(16, delayMs);
|
|
1550
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1551
|
+
this.streamingRenderTimer = null;
|
|
1552
|
+
this.render();
|
|
1553
|
+
}, wait);
|
|
1554
|
+
}
|
|
1555
|
+
resetStreamingRenderThrottle() {
|
|
1556
|
+
if (this.streamingRenderTimer) {
|
|
1557
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1558
|
+
this.streamingRenderTimer = null;
|
|
1559
|
+
}
|
|
1560
|
+
this.lastStreamingRender = 0;
|
|
1561
|
+
}
|
|
1993
1562
|
scheduleRender() {
|
|
1994
1563
|
if (!this.canRender())
|
|
1995
1564
|
return;
|