erosolar-cli 1.7.257 → 1.7.258
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 +22 -148
- 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/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.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/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/shell/interactiveShell.d.ts +10 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +190 -35
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +66 -130
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +409 -606
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +20 -15
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +14 -22
- package/dist/shell/terminalInputAdapter.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 +19 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +135 -22
- 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/package.json +1 -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/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/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/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
|
@@ -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
|
|
@@ -67,6 +69,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
67
69
|
statusMessage = null;
|
|
68
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
69
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
|
|
70
77
|
reservedLines = 2;
|
|
71
78
|
scrollRegionActive = false;
|
|
72
79
|
lastRenderContent = '';
|
|
@@ -74,45 +81,35 @@ export class TerminalInput extends EventEmitter {
|
|
|
74
81
|
renderDirty = false;
|
|
75
82
|
isRendering = false;
|
|
76
83
|
pinnedTopRows = 0;
|
|
77
|
-
inlineAnchorRow = null;
|
|
78
|
-
inlineLayout = false;
|
|
79
|
-
anchorProvider = null;
|
|
80
|
-
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
81
|
-
flowMode = true;
|
|
82
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
83
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
84
|
-
commandSuggestions = [];
|
|
85
|
-
filteredSuggestions = [];
|
|
86
|
-
selectedSuggestionIndex = 0;
|
|
87
|
-
showSuggestions = false;
|
|
88
|
-
maxVisibleSuggestions = 10;
|
|
89
84
|
// Lifecycle
|
|
90
85
|
disposed = false;
|
|
91
86
|
enabled = true;
|
|
92
87
|
contextUsage = null;
|
|
88
|
+
contextAutoCompactThreshold = 90;
|
|
89
|
+
thinkingModeLabel = null;
|
|
93
90
|
editMode = 'display-edits';
|
|
94
91
|
verificationEnabled = true;
|
|
95
92
|
autoContinueEnabled = false;
|
|
96
93
|
verificationHotkey = 'alt+v';
|
|
97
94
|
autoContinueHotkey = 'alt+c';
|
|
95
|
+
thinkingHotkey = '/thinking';
|
|
96
|
+
modelLabel = null;
|
|
97
|
+
providerLabel = null;
|
|
98
98
|
// Output interceptor cleanup
|
|
99
99
|
outputInterceptorCleanup;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
thinkingEnabled = true;
|
|
104
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
100
|
+
// Streaming render throttle
|
|
101
|
+
lastStreamingRender = 0;
|
|
102
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
105
103
|
streamingRenderTimer = null;
|
|
106
104
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
105
|
super();
|
|
108
106
|
this.out = writeStream;
|
|
109
|
-
// Use schema defaults for configuration consistency
|
|
110
107
|
this.config = {
|
|
111
|
-
maxLines: config.maxLines ??
|
|
112
|
-
maxLength: config.maxLength ??
|
|
108
|
+
maxLines: config.maxLines ?? 1000,
|
|
109
|
+
maxLength: config.maxLength ?? 10000,
|
|
113
110
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
114
|
-
promptChar: config.promptChar ??
|
|
115
|
-
continuationChar: config.continuationChar ??
|
|
111
|
+
promptChar: config.promptChar ?? '> ',
|
|
112
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
116
113
|
};
|
|
117
114
|
}
|
|
118
115
|
// ===========================================================================
|
|
@@ -191,11 +188,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
191
188
|
if (handled)
|
|
192
189
|
return;
|
|
193
190
|
}
|
|
194
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
195
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
196
|
-
this.emit('showHelp');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
191
|
// Insert printable characters
|
|
200
192
|
if (str && !key?.ctrl && !key?.meta) {
|
|
201
193
|
this.insertText(str);
|
|
@@ -204,235 +196,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
204
196
|
/**
|
|
205
197
|
* Set the input mode
|
|
206
198
|
*
|
|
207
|
-
* Streaming
|
|
208
|
-
*
|
|
209
|
-
* the cursor is (below the streamed content).
|
|
199
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
200
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
210
201
|
*/
|
|
211
202
|
setMode(mode) {
|
|
212
203
|
const prevMode = this.mode;
|
|
213
204
|
this.mode = mode;
|
|
214
205
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
// This ensures ALL streaming output is visible
|
|
219
|
-
this.pinnedTopRows = 0;
|
|
220
|
-
this.reservedLines = 0;
|
|
221
|
-
// Disable any existing scroll region
|
|
222
|
-
this.disableScrollRegion();
|
|
223
|
-
// Hide cursor during streaming
|
|
224
|
-
this.write(ESC.HIDE);
|
|
225
|
-
// Start timer to update streaming status
|
|
226
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
227
|
-
if (this.mode === 'streaming') {
|
|
228
|
-
this.updateStreamingStatus();
|
|
229
|
-
}
|
|
230
|
-
}, 1000);
|
|
206
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
207
|
+
this.resetStreamingRenderThrottle();
|
|
208
|
+
this.enableScrollRegion();
|
|
231
209
|
this.renderDirty = true;
|
|
210
|
+
this.render();
|
|
232
211
|
}
|
|
233
212
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.streamingRenderTimer = null;
|
|
238
|
-
}
|
|
239
|
-
// Reset streaming time
|
|
240
|
-
this.streamingStartTime = null;
|
|
241
|
-
this.pinnedTopRows = 0;
|
|
242
|
-
// Ensure no scroll region is active
|
|
243
|
-
this.disableScrollRegion();
|
|
244
|
-
// Show cursor again
|
|
245
|
-
this.write(ESC.SHOW);
|
|
246
|
-
// Add newline after streaming content, then render input area
|
|
247
|
-
this.write('\n');
|
|
248
|
-
// Reset flow mode tracking
|
|
249
|
-
this.flowModeRenderedLines = 0;
|
|
250
|
-
// Re-render the input area in normal mode
|
|
213
|
+
// Streaming ended - render the input area
|
|
214
|
+
this.resetStreamingRenderThrottle();
|
|
215
|
+
this.enableScrollRegion();
|
|
251
216
|
this.forceRender();
|
|
252
217
|
}
|
|
253
218
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Update streaming status label (called by timer)
|
|
256
|
-
*/
|
|
257
|
-
updateStreamingStatus() {
|
|
258
|
-
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
259
|
-
return;
|
|
260
|
-
// Calculate elapsed time
|
|
261
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
262
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
263
|
-
const minutes = Math.floor(seconds / 60);
|
|
264
|
-
const secs = seconds % 60;
|
|
265
|
-
// Format elapsed time
|
|
266
|
-
let elapsedStr;
|
|
267
|
-
if (minutes > 0) {
|
|
268
|
-
elapsedStr = `${minutes}m ${secs}s`;
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
elapsedStr = `${secs}s`;
|
|
272
|
-
}
|
|
273
|
-
// Update streaming label
|
|
274
|
-
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Enable or disable flow mode.
|
|
278
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
279
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
280
|
-
*/
|
|
281
|
-
setFlowMode(enabled) {
|
|
282
|
-
if (this.flowMode === enabled)
|
|
283
|
-
return;
|
|
284
|
-
this.flowMode = enabled;
|
|
285
|
-
this.renderDirty = true;
|
|
286
|
-
this.scheduleRender();
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Check if flow mode is enabled.
|
|
290
|
-
*/
|
|
291
|
-
isFlowMode() {
|
|
292
|
-
return this.flowMode;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Set available slash commands for auto-complete suggestions.
|
|
296
|
-
*/
|
|
297
|
-
setCommands(commands) {
|
|
298
|
-
this.commandSuggestions = commands;
|
|
299
|
-
this.updateSuggestions();
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Update filtered suggestions based on current input.
|
|
303
|
-
*/
|
|
304
|
-
updateSuggestions() {
|
|
305
|
-
const input = this.buffer.trim();
|
|
306
|
-
// Only show suggestions when input starts with "/"
|
|
307
|
-
if (!input.startsWith('/')) {
|
|
308
|
-
this.showSuggestions = false;
|
|
309
|
-
this.filteredSuggestions = [];
|
|
310
|
-
this.selectedSuggestionIndex = 0;
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
const query = input.toLowerCase();
|
|
314
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
315
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
316
|
-
// Show suggestions if we have matches
|
|
317
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
318
|
-
// Keep selection in bounds
|
|
319
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
320
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Select next suggestion (arrow down / tab).
|
|
325
|
-
*/
|
|
326
|
-
selectNextSuggestion() {
|
|
327
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
328
|
-
return;
|
|
329
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
330
|
-
this.renderDirty = true;
|
|
331
|
-
this.scheduleRender();
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
335
|
-
*/
|
|
336
|
-
selectPrevSuggestion() {
|
|
337
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
338
|
-
return;
|
|
339
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
340
|
-
? this.filteredSuggestions.length - 1
|
|
341
|
-
: this.selectedSuggestionIndex - 1;
|
|
342
|
-
this.renderDirty = true;
|
|
343
|
-
this.scheduleRender();
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Accept current suggestion and insert into buffer.
|
|
347
|
-
*/
|
|
348
|
-
acceptSuggestion() {
|
|
349
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
350
|
-
return false;
|
|
351
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
352
|
-
if (!selected)
|
|
353
|
-
return false;
|
|
354
|
-
// Replace buffer with selected command
|
|
355
|
-
this.buffer = selected.command + ' ';
|
|
356
|
-
this.cursor = this.buffer.length;
|
|
357
|
-
this.showSuggestions = false;
|
|
358
|
-
this.renderDirty = true;
|
|
359
|
-
this.scheduleRender();
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Check if suggestions are visible.
|
|
364
|
-
*/
|
|
365
|
-
areSuggestionsVisible() {
|
|
366
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Update token count for metrics display
|
|
370
|
-
*/
|
|
371
|
-
setTokensUsed(tokens) {
|
|
372
|
-
this.tokensUsed = tokens;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Toggle thinking/reasoning mode
|
|
376
|
-
*/
|
|
377
|
-
toggleThinking() {
|
|
378
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
379
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
380
|
-
this.scheduleRender();
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Get thinking enabled state
|
|
384
|
-
*/
|
|
385
|
-
isThinkingEnabled() {
|
|
386
|
-
return this.thinkingEnabled;
|
|
387
|
-
}
|
|
388
219
|
/**
|
|
389
220
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
390
221
|
*/
|
|
391
222
|
setPinnedHeaderLines(count) {
|
|
392
|
-
//
|
|
393
|
-
if (this.pinnedTopRows !==
|
|
394
|
-
this.pinnedTopRows =
|
|
223
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
224
|
+
if (this.pinnedTopRows !== 0) {
|
|
225
|
+
this.pinnedTopRows = 0;
|
|
395
226
|
if (this.scrollRegionActive) {
|
|
396
227
|
this.applyScrollRegion();
|
|
397
228
|
}
|
|
398
229
|
}
|
|
399
230
|
}
|
|
400
|
-
/**
|
|
401
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
402
|
-
* restore the default bottom-aligned layout.
|
|
403
|
-
*/
|
|
404
|
-
setInlineAnchor(row) {
|
|
405
|
-
if (row === null || row === undefined) {
|
|
406
|
-
this.inlineAnchorRow = null;
|
|
407
|
-
this.inlineLayout = false;
|
|
408
|
-
this.renderDirty = true;
|
|
409
|
-
this.render();
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
const { rows } = this.getSize();
|
|
413
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
414
|
-
this.inlineAnchorRow = clamped;
|
|
415
|
-
this.inlineLayout = true;
|
|
416
|
-
this.renderDirty = true;
|
|
417
|
-
this.render();
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
421
|
-
* output by re-evaluating the anchor before each render.
|
|
422
|
-
*/
|
|
423
|
-
setInlineAnchorProvider(provider) {
|
|
424
|
-
this.anchorProvider = provider;
|
|
425
|
-
if (!provider) {
|
|
426
|
-
this.inlineLayout = false;
|
|
427
|
-
this.inlineAnchorRow = null;
|
|
428
|
-
this.renderDirty = true;
|
|
429
|
-
this.render();
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
this.inlineLayout = true;
|
|
433
|
-
this.renderDirty = true;
|
|
434
|
-
this.render();
|
|
435
|
-
}
|
|
436
231
|
/**
|
|
437
232
|
* Get current mode
|
|
438
233
|
*/
|
|
@@ -542,6 +337,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
542
337
|
this.streamingLabel = next;
|
|
543
338
|
this.scheduleRender();
|
|
544
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
342
|
+
*/
|
|
343
|
+
setMetaStatus(meta) {
|
|
344
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
345
|
+
? Math.floor(meta.elapsedSeconds)
|
|
346
|
+
: null;
|
|
347
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
348
|
+
? Math.floor(meta.tokensUsed)
|
|
349
|
+
: null;
|
|
350
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
351
|
+
? Math.floor(meta.tokenLimit)
|
|
352
|
+
: null;
|
|
353
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
354
|
+
? Math.floor(meta.thinkingMs)
|
|
355
|
+
: null;
|
|
356
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
357
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
358
|
+
this.metaTokensUsed === nextTokens &&
|
|
359
|
+
this.metaTokenLimit === nextLimit &&
|
|
360
|
+
this.metaThinkingMs === nextThinking &&
|
|
361
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
365
|
+
this.metaTokensUsed = nextTokens;
|
|
366
|
+
this.metaTokenLimit = nextLimit;
|
|
367
|
+
this.metaThinkingMs = nextThinking;
|
|
368
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
369
|
+
this.scheduleRender();
|
|
370
|
+
}
|
|
545
371
|
/**
|
|
546
372
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
547
373
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -551,16 +377,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
551
377
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
552
378
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
553
379
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
380
|
+
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
381
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
554
382
|
if (this.verificationEnabled === nextVerification &&
|
|
555
383
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
556
384
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
557
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
385
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
386
|
+
this.thinkingHotkey === nextThinkingHotkey &&
|
|
387
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
558
388
|
return;
|
|
559
389
|
}
|
|
560
390
|
this.verificationEnabled = nextVerification;
|
|
561
391
|
this.autoContinueEnabled = nextAutoContinue;
|
|
562
392
|
this.verificationHotkey = nextVerifyHotkey;
|
|
563
393
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
394
|
+
this.thinkingHotkey = nextThinkingHotkey;
|
|
395
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
564
396
|
this.scheduleRender();
|
|
565
397
|
}
|
|
566
398
|
/**
|
|
@@ -572,197 +404,104 @@ export class TerminalInput extends EventEmitter {
|
|
|
572
404
|
this.streamingLabel = null;
|
|
573
405
|
this.scheduleRender();
|
|
574
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Surface model/provider context in the controls bar.
|
|
409
|
+
*/
|
|
410
|
+
setModelContext(options) {
|
|
411
|
+
const nextModel = options.model?.trim() || null;
|
|
412
|
+
const nextProvider = options.provider?.trim() || null;
|
|
413
|
+
if (this.modelLabel === nextModel && this.providerLabel === nextProvider) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.modelLabel = nextModel;
|
|
417
|
+
this.providerLabel = nextProvider;
|
|
418
|
+
this.scheduleRender();
|
|
419
|
+
}
|
|
575
420
|
/**
|
|
576
421
|
* Render the input area - Claude Code style with mode controls
|
|
577
422
|
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
423
|
+
* During streaming we keep the scroll region active and repaint only the
|
|
424
|
+
* pinned status/input block (throttled) so streamed content can scroll
|
|
425
|
+
* naturally above while elapsed time and status stay fresh.
|
|
580
426
|
*/
|
|
581
427
|
render() {
|
|
582
428
|
if (!this.canRender())
|
|
583
429
|
return;
|
|
584
430
|
if (this.isRendering)
|
|
585
431
|
return;
|
|
432
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
433
|
+
// During streaming we still render the pinned input/status region, but throttle
|
|
434
|
+
// to avoid fighting with the streamed content flow.
|
|
435
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
436
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
437
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
438
|
+
if (waitMs > 0) {
|
|
439
|
+
this.renderDirty = true;
|
|
440
|
+
this.scheduleStreamingRender(waitMs);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
586
444
|
const shouldSkip = !this.renderDirty &&
|
|
587
445
|
this.buffer === this.lastRenderContent &&
|
|
588
446
|
this.cursor === this.lastRenderCursor;
|
|
589
447
|
this.renderDirty = false;
|
|
590
|
-
// Skip if nothing changed
|
|
448
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
591
449
|
if (shouldSkip) {
|
|
592
450
|
return;
|
|
593
451
|
}
|
|
594
|
-
// If write lock is held, defer render
|
|
452
|
+
// If write lock is held, defer render to avoid race conditions
|
|
595
453
|
if (writeLock.isLocked()) {
|
|
596
454
|
writeLock.safeWrite(() => this.render());
|
|
597
455
|
return;
|
|
598
456
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
// Render input area at bottom (outside scroll region)
|
|
603
|
-
this.renderBottomPinned();
|
|
604
|
-
}
|
|
605
|
-
finally {
|
|
606
|
-
writeLock.unlock();
|
|
607
|
-
this.isRendering = false;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
612
|
-
*
|
|
613
|
-
* Flow mode attempted inline rendering but caused duplicate renders
|
|
614
|
-
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
615
|
-
*/
|
|
616
|
-
renderFlowMode() {
|
|
617
|
-
// Use stable bottom-pinned approach
|
|
618
|
-
this.renderBottomPinned();
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
622
|
-
*
|
|
623
|
-
* Works for both normal and streaming modes:
|
|
624
|
-
* - During streaming: saves/restores cursor position
|
|
625
|
-
* - Status bar shows streaming info or "Type a message"
|
|
626
|
-
*
|
|
627
|
-
* Layout when suggestions visible:
|
|
628
|
-
* - Top divider
|
|
629
|
-
* - Input line(s)
|
|
630
|
-
* - Bottom divider
|
|
631
|
-
* - Suggestions (command list)
|
|
632
|
-
*
|
|
633
|
-
* Layout when suggestions hidden:
|
|
634
|
-
* - Status bar (Ready/Streaming)
|
|
635
|
-
* - Top divider
|
|
636
|
-
* - Input line(s)
|
|
637
|
-
* - Bottom divider
|
|
638
|
-
* - Mode controls
|
|
639
|
-
*/
|
|
640
|
-
renderBottomPinned() {
|
|
641
|
-
const { rows, cols } = this.getSize();
|
|
642
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
643
|
-
const isStreaming = this.mode === 'streaming';
|
|
644
|
-
// During streaming, skip rendering input area entirely
|
|
645
|
-
// Content flows naturally to terminal - no positioning disruption
|
|
646
|
-
// Terminal scrollback preserves full history
|
|
647
|
-
if (isStreaming) {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
// Wrap buffer into display lines
|
|
651
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
652
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
653
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
654
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
655
|
-
// Calculate display window (keep cursor visible)
|
|
656
|
-
let startLine = 0;
|
|
657
|
-
if (lines.length > displayLines) {
|
|
658
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
659
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
660
|
-
}
|
|
661
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
662
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
663
|
-
// Calculate suggestion display (not during streaming)
|
|
664
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
665
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
666
|
-
: [];
|
|
667
|
-
const suggestionLines = suggestionsToShow.length;
|
|
668
|
-
this.write(ESC.HIDE);
|
|
669
|
-
this.write(ESC.RESET);
|
|
670
|
-
const divider = renderDivider(cols - 2);
|
|
671
|
-
// Calculate positions from absolute bottom
|
|
672
|
-
let currentRow;
|
|
673
|
-
if (suggestionLines > 0) {
|
|
674
|
-
// With suggestions: input area + dividers + suggestions
|
|
675
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
676
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
677
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
678
|
-
this.updateReservedLines(totalHeight);
|
|
679
|
-
// Top divider
|
|
680
|
-
this.write(ESC.TO(currentRow, 1));
|
|
681
|
-
this.write(ESC.CLEAR_LINE);
|
|
682
|
-
this.write(divider);
|
|
683
|
-
currentRow++;
|
|
684
|
-
// Input lines
|
|
685
|
-
let finalRow = currentRow;
|
|
686
|
-
let finalCol = 3;
|
|
687
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
688
|
-
this.write(ESC.TO(currentRow, 1));
|
|
689
|
-
this.write(ESC.CLEAR_LINE);
|
|
690
|
-
const line = visibleLines[i] ?? '';
|
|
691
|
-
const absoluteLineIdx = startLine + i;
|
|
692
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
693
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
694
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
695
|
-
if (isCursorLine) {
|
|
696
|
-
const col = Math.min(cursorCol, line.length);
|
|
697
|
-
this.write(line.slice(0, col));
|
|
698
|
-
this.write(ESC.REVERSE);
|
|
699
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
700
|
-
this.write(ESC.RESET);
|
|
701
|
-
this.write(line.slice(col + 1));
|
|
702
|
-
finalRow = currentRow;
|
|
703
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
704
|
-
}
|
|
705
|
-
else {
|
|
706
|
-
this.write(line);
|
|
707
|
-
}
|
|
708
|
-
currentRow++;
|
|
457
|
+
const performRender = () => {
|
|
458
|
+
if (!this.scrollRegionActive) {
|
|
459
|
+
this.enableScrollRegion();
|
|
709
460
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
461
|
+
const { rows, cols } = this.getSize();
|
|
462
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
463
|
+
// Wrap buffer into display lines
|
|
464
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
465
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
466
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
467
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
468
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
469
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
470
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
471
|
+
// Calculate display window (keep cursor visible)
|
|
472
|
+
let startLine = 0;
|
|
473
|
+
if (lines.length > displayLines) {
|
|
474
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
475
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
476
|
+
}
|
|
477
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
478
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
479
|
+
// Render
|
|
480
|
+
this.write(ESC.HIDE);
|
|
481
|
+
this.write(ESC.RESET);
|
|
482
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
483
|
+
let currentRow = startRow;
|
|
484
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
485
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
486
|
+
// Meta/status header (elapsed, tokens/context)
|
|
487
|
+
for (const metaLine of metaLines) {
|
|
717
488
|
this.write(ESC.TO(currentRow, 1));
|
|
718
489
|
this.write(ESC.CLEAR_LINE);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
// Indent and highlight selected
|
|
722
|
-
this.write(' ');
|
|
723
|
-
if (isSelected) {
|
|
724
|
-
this.write(ESC.REVERSE);
|
|
725
|
-
this.write(ESC.BOLD);
|
|
726
|
-
}
|
|
727
|
-
this.write(suggestion.command);
|
|
728
|
-
if (isSelected) {
|
|
729
|
-
this.write(ESC.RESET);
|
|
730
|
-
}
|
|
731
|
-
// Description (dimmed)
|
|
732
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
733
|
-
if (descSpace > 10 && suggestion.description) {
|
|
734
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
735
|
-
this.write(ESC.RESET);
|
|
736
|
-
this.write(ESC.DIM);
|
|
737
|
-
this.write(' ');
|
|
738
|
-
this.write(desc);
|
|
739
|
-
this.write(ESC.RESET);
|
|
740
|
-
}
|
|
741
|
-
currentRow++;
|
|
490
|
+
this.write(metaLine);
|
|
491
|
+
currentRow += 1;
|
|
742
492
|
}
|
|
743
|
-
//
|
|
744
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
// Without suggestions: normal layout with status bar and controls
|
|
748
|
-
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
749
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
750
|
-
this.updateReservedLines(totalHeight);
|
|
751
|
-
// Status bar (streaming or normal)
|
|
752
|
-
this.write(ESC.TO(currentRow, 1));
|
|
753
|
-
this.write(ESC.CLEAR_LINE);
|
|
754
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
755
|
-
currentRow++;
|
|
756
|
-
// Top divider
|
|
493
|
+
// Separator line
|
|
757
494
|
this.write(ESC.TO(currentRow, 1));
|
|
758
495
|
this.write(ESC.CLEAR_LINE);
|
|
496
|
+
const divider = renderDivider(cols - 2);
|
|
759
497
|
this.write(divider);
|
|
760
|
-
currentRow
|
|
761
|
-
//
|
|
498
|
+
currentRow += 1;
|
|
499
|
+
// Render input lines
|
|
762
500
|
let finalRow = currentRow;
|
|
763
501
|
let finalCol = 3;
|
|
764
502
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
765
|
-
|
|
503
|
+
const rowNum = currentRow + i;
|
|
504
|
+
this.write(ESC.TO(rowNum, 1));
|
|
766
505
|
this.write(ESC.CLEAR_LINE);
|
|
767
506
|
const line = visibleLines[i] ?? '';
|
|
768
507
|
const absoluteLineIdx = startLine + i;
|
|
@@ -776,6 +515,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
776
515
|
this.write(ESC.RESET);
|
|
777
516
|
this.write(ESC.BG_DARK);
|
|
778
517
|
if (isCursorLine) {
|
|
518
|
+
// Render with block cursor
|
|
779
519
|
const col = Math.min(cursorCol, line.length);
|
|
780
520
|
const before = line.slice(0, col);
|
|
781
521
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -785,157 +525,251 @@ export class TerminalInput extends EventEmitter {
|
|
|
785
525
|
this.write(at);
|
|
786
526
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
787
527
|
this.write(after);
|
|
788
|
-
finalRow =
|
|
528
|
+
finalRow = rowNum;
|
|
789
529
|
finalCol = this.config.promptChar.length + col + 1;
|
|
790
530
|
}
|
|
791
531
|
else {
|
|
792
532
|
this.write(line);
|
|
793
533
|
}
|
|
794
|
-
// Pad to edge
|
|
534
|
+
// Pad to edge for clean look
|
|
795
535
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
796
536
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
797
537
|
if (padding > 0)
|
|
798
538
|
this.write(' '.repeat(padding));
|
|
799
539
|
this.write(ESC.RESET);
|
|
800
|
-
currentRow++;
|
|
801
540
|
}
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
this.write(ESC.
|
|
805
|
-
this.write(divider);
|
|
806
|
-
currentRow++;
|
|
807
|
-
// Mode controls
|
|
808
|
-
this.write(ESC.TO(currentRow, 1));
|
|
541
|
+
// Mode controls line (Claude Code style)
|
|
542
|
+
const controlRow = currentRow + visibleLines.length;
|
|
543
|
+
this.write(ESC.TO(controlRow, 1));
|
|
809
544
|
this.write(ESC.CLEAR_LINE);
|
|
810
545
|
this.write(this.buildModeControls(cols));
|
|
811
|
-
// Position cursor
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
546
|
+
// Position cursor
|
|
547
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
548
|
+
this.write(ESC.SHOW);
|
|
549
|
+
// Update state
|
|
550
|
+
this.lastRenderContent = this.buffer;
|
|
551
|
+
this.lastRenderCursor = this.cursor;
|
|
552
|
+
this.lastStreamingRender = streamingActive ? Date.now() : 0;
|
|
553
|
+
if (this.streamingRenderTimer) {
|
|
554
|
+
clearTimeout(this.streamingRenderTimer);
|
|
555
|
+
this.streamingRenderTimer = null;
|
|
817
556
|
}
|
|
557
|
+
};
|
|
558
|
+
// Use write lock during render to prevent interleaved output
|
|
559
|
+
writeLock.lock('terminalInput.render');
|
|
560
|
+
this.isRendering = true;
|
|
561
|
+
try {
|
|
562
|
+
performRender();
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
writeLock.unlock();
|
|
566
|
+
this.isRendering = false;
|
|
818
567
|
}
|
|
819
|
-
this.write(ESC.SHOW);
|
|
820
|
-
// Update state
|
|
821
|
-
this.lastRenderContent = this.buffer;
|
|
822
|
-
this.lastRenderCursor = this.cursor;
|
|
823
568
|
}
|
|
824
569
|
/**
|
|
825
|
-
* Build
|
|
570
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
826
571
|
*/
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
572
|
+
buildMetaLines(width) {
|
|
573
|
+
const lines = [];
|
|
574
|
+
if (this.metaThinkingMs !== null) {
|
|
575
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
576
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
577
|
+
}
|
|
578
|
+
if (this.modelLabel) {
|
|
579
|
+
const modelText = this.providerLabel
|
|
580
|
+
? `${this.modelLabel} @ ${this.providerLabel}`
|
|
581
|
+
: this.modelLabel;
|
|
582
|
+
lines.push(renderStatusLine([{ text: `model ${modelText}`, tone: 'muted' }], width));
|
|
583
|
+
}
|
|
584
|
+
const statusParts = [];
|
|
585
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
586
|
+
if (statusLabel) {
|
|
587
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
588
|
+
}
|
|
589
|
+
if (this.mode === 'streaming' || isStreamingMode()) {
|
|
590
|
+
statusParts.push({ text: 'esc to interrupt', tone: 'warn' });
|
|
591
|
+
}
|
|
592
|
+
if (this.metaElapsedSeconds !== null) {
|
|
593
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
594
|
+
}
|
|
595
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
596
|
+
if (tokensRemaining !== null) {
|
|
597
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
598
|
+
}
|
|
599
|
+
if (statusParts.length) {
|
|
600
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
601
|
+
}
|
|
602
|
+
const usageParts = [];
|
|
603
|
+
if (this.metaTokensUsed !== null) {
|
|
604
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
605
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
606
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
607
|
+
}
|
|
608
|
+
if (this.contextUsage !== null) {
|
|
609
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
610
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
611
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
612
|
+
}
|
|
838
613
|
if (this.queue.length > 0) {
|
|
839
|
-
|
|
614
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
840
615
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
616
|
+
if (usageParts.length) {
|
|
617
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
618
|
+
}
|
|
619
|
+
return lines;
|
|
844
620
|
}
|
|
845
621
|
/**
|
|
846
|
-
*
|
|
847
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
622
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
848
623
|
*/
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (this.streamingStartTime) {
|
|
856
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
857
|
-
const mins = Math.floor(elapsed / 60);
|
|
858
|
-
const secs = elapsed % 60;
|
|
859
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
860
|
-
}
|
|
861
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
862
|
-
}
|
|
863
|
-
// Queue indicator during streaming
|
|
864
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
865
|
-
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
624
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
625
|
+
const width = Math.max(1, cols);
|
|
626
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
627
|
+
const row = startRow + i;
|
|
628
|
+
this.write(ESC.TO(row, 1));
|
|
629
|
+
this.write(' '.repeat(width));
|
|
866
630
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Build Claude Code style mode controls line.
|
|
634
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
635
|
+
*/
|
|
636
|
+
buildModeControls(cols) {
|
|
637
|
+
const width = Math.max(8, cols - 2);
|
|
638
|
+
const leftParts = [];
|
|
639
|
+
const rightParts = [];
|
|
640
|
+
if (this.streamingLabel) {
|
|
641
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
871
642
|
}
|
|
872
|
-
// Override/warning status
|
|
873
643
|
if (this.overrideStatusMessage) {
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
644
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
645
|
+
}
|
|
646
|
+
if (this.statusMessage) {
|
|
647
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
648
|
+
}
|
|
649
|
+
const editHotkey = this.formatHotkey('shift+tab');
|
|
650
|
+
const editLabel = this.editMode === 'display-edits' ? 'edits: accept' : 'edits: ask';
|
|
651
|
+
const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
|
|
652
|
+
leftParts.push({ text: `${editHotkey} ${editLabel}`, tone: editTone });
|
|
653
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
654
|
+
const verifyLabel = this.verificationEnabled ? 'verify:on' : 'verify:off';
|
|
655
|
+
leftParts.push({ text: `${verifyHotkey} ${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
|
|
656
|
+
const continueHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
657
|
+
const continueLabel = this.autoContinueEnabled ? 'auto:on' : 'auto:off';
|
|
658
|
+
leftParts.push({ text: `${continueHotkey} ${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
|
|
659
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
660
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
879
661
|
}
|
|
880
|
-
// Multi-line indicator
|
|
881
662
|
if (this.buffer.includes('\n')) {
|
|
882
|
-
|
|
663
|
+
const lineCount = this.buffer.split('\n').length;
|
|
664
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
883
665
|
}
|
|
884
|
-
if (
|
|
885
|
-
|
|
666
|
+
if (this.pastePlaceholders.length > 0) {
|
|
667
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
668
|
+
leftParts.push({
|
|
669
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
670
|
+
tone: 'info',
|
|
671
|
+
});
|
|
886
672
|
}
|
|
887
|
-
const
|
|
888
|
-
|
|
673
|
+
const contextRemaining = this.computeContextRemaining();
|
|
674
|
+
if (this.thinkingModeLabel) {
|
|
675
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
676
|
+
rightParts.push({ text: `${thinkingHotkey} thinking:${this.thinkingModeLabel}`, tone: 'info' });
|
|
677
|
+
}
|
|
678
|
+
if (this.modelLabel) {
|
|
679
|
+
const modelText = this.providerLabel ? `${this.modelLabel} @ ${this.providerLabel}` : this.modelLabel;
|
|
680
|
+
rightParts.push({ text: modelText, tone: 'muted' });
|
|
681
|
+
}
|
|
682
|
+
if (contextRemaining !== null) {
|
|
683
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
684
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
685
|
+
? 'Context auto-compact imminent'
|
|
686
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
687
|
+
rightParts.push({ text: label, tone });
|
|
688
|
+
}
|
|
689
|
+
if (!rightParts.length || width < 60) {
|
|
690
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
691
|
+
return renderStatusLine(merged, width);
|
|
692
|
+
}
|
|
693
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
694
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
695
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
696
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
697
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
698
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
699
|
+
}
|
|
700
|
+
formatHotkey(hotkey) {
|
|
701
|
+
const normalized = hotkey.trim().toLowerCase();
|
|
702
|
+
if (!normalized)
|
|
703
|
+
return hotkey;
|
|
704
|
+
const parts = normalized.split('+').filter(Boolean);
|
|
705
|
+
const map = {
|
|
706
|
+
shift: '⇧',
|
|
707
|
+
sh: '⇧',
|
|
708
|
+
alt: '⌥',
|
|
709
|
+
option: '⌥',
|
|
710
|
+
opt: '⌥',
|
|
711
|
+
ctrl: '⌃',
|
|
712
|
+
control: '⌃',
|
|
713
|
+
cmd: '⌘',
|
|
714
|
+
meta: '⌘',
|
|
715
|
+
};
|
|
716
|
+
const formatted = parts
|
|
717
|
+
.map((part) => {
|
|
718
|
+
const symbol = map[part];
|
|
719
|
+
if (symbol)
|
|
720
|
+
return symbol;
|
|
721
|
+
return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
|
|
722
|
+
})
|
|
723
|
+
.join('');
|
|
724
|
+
return formatted || hotkey;
|
|
725
|
+
}
|
|
726
|
+
computeContextRemaining() {
|
|
727
|
+
if (this.contextUsage === null) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
731
|
+
}
|
|
732
|
+
computeTokensRemaining() {
|
|
733
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
737
|
+
return this.formatTokenCount(remaining);
|
|
738
|
+
}
|
|
739
|
+
formatElapsedLabel(seconds) {
|
|
740
|
+
if (seconds < 60) {
|
|
741
|
+
return `${seconds}s`;
|
|
742
|
+
}
|
|
743
|
+
const mins = Math.floor(seconds / 60);
|
|
744
|
+
const secs = seconds % 60;
|
|
745
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
746
|
+
}
|
|
747
|
+
formatTokenCount(value) {
|
|
748
|
+
if (!Number.isFinite(value)) {
|
|
749
|
+
return `${value}`;
|
|
750
|
+
}
|
|
751
|
+
if (value >= 1_000_000) {
|
|
752
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
753
|
+
}
|
|
754
|
+
if (value >= 1_000) {
|
|
755
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
756
|
+
}
|
|
757
|
+
return `${Math.round(value)}`;
|
|
758
|
+
}
|
|
759
|
+
visibleLength(value) {
|
|
760
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
761
|
+
return value.replace(ansiPattern, '').length;
|
|
889
762
|
}
|
|
890
763
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
764
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
765
|
+
* needing a TTY. Not used by production code.
|
|
895
766
|
*/
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
903
|
-
if (this.editMode === 'display-edits') {
|
|
904
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
905
|
-
}
|
|
906
|
-
else {
|
|
907
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
908
|
-
}
|
|
909
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
910
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
911
|
-
// Verification (green when on) - per schema.verificationMode
|
|
912
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
913
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
914
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
915
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
916
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
917
|
-
let rightPart = '';
|
|
918
|
-
if (this.contextUsage !== null) {
|
|
919
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
920
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
921
|
-
if (rem < 10)
|
|
922
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
923
|
-
else if (rem < 25)
|
|
924
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
925
|
-
else
|
|
926
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
927
|
-
}
|
|
928
|
-
// Calculate visible lengths (strip ANSI)
|
|
929
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
930
|
-
const leftLen = strip(leftPart).length;
|
|
931
|
-
const rightLen = strip(rightPart).length;
|
|
932
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
933
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
934
|
-
}
|
|
935
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
936
|
-
return `${leftPart} ${rightPart}`;
|
|
937
|
-
}
|
|
938
|
-
return leftPart;
|
|
767
|
+
getDebugUiSnapshot(width) {
|
|
768
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
769
|
+
return {
|
|
770
|
+
meta: this.buildMetaLines(cols - 2),
|
|
771
|
+
controls: this.buildModeControls(cols),
|
|
772
|
+
};
|
|
939
773
|
}
|
|
940
774
|
/**
|
|
941
775
|
* Force a re-render
|
|
@@ -958,17 +792,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
958
792
|
handleResize() {
|
|
959
793
|
this.lastRenderContent = '';
|
|
960
794
|
this.lastRenderCursor = -1;
|
|
795
|
+
this.resetStreamingRenderThrottle();
|
|
961
796
|
// Re-clamp pinned header rows to the new terminal height
|
|
962
797
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
798
|
+
if (this.scrollRegionActive) {
|
|
799
|
+
this.disableScrollRegion();
|
|
800
|
+
this.enableScrollRegion();
|
|
801
|
+
}
|
|
963
802
|
this.scheduleRender();
|
|
964
803
|
}
|
|
965
804
|
/**
|
|
966
805
|
* Register with display's output interceptor to position cursor correctly.
|
|
967
806
|
* When scroll region is active, output needs to go to the scroll region,
|
|
968
807
|
* not the protected bottom area where the input is rendered.
|
|
969
|
-
*
|
|
970
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
971
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
972
808
|
*/
|
|
973
809
|
registerOutputInterceptor(display) {
|
|
974
810
|
if (this.outputInterceptorCleanup) {
|
|
@@ -976,11 +812,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
976
812
|
}
|
|
977
813
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
978
814
|
beforeWrite: () => {
|
|
979
|
-
//
|
|
980
|
-
//
|
|
815
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
816
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
817
|
+
if (this.scrollRegionActive) {
|
|
818
|
+
const { rows } = this.getSize();
|
|
819
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
820
|
+
this.write(ESC.SAVE);
|
|
821
|
+
this.write(ESC.TO(scrollBottom, 1));
|
|
822
|
+
}
|
|
981
823
|
},
|
|
982
824
|
afterWrite: () => {
|
|
983
|
-
//
|
|
825
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
826
|
+
if (this.scrollRegionActive) {
|
|
827
|
+
this.write(ESC.RESTORE);
|
|
828
|
+
}
|
|
984
829
|
},
|
|
985
830
|
});
|
|
986
831
|
}
|
|
@@ -990,11 +835,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
990
835
|
dispose() {
|
|
991
836
|
if (this.disposed)
|
|
992
837
|
return;
|
|
993
|
-
// Clean up streaming render timer
|
|
994
|
-
if (this.streamingRenderTimer) {
|
|
995
|
-
clearInterval(this.streamingRenderTimer);
|
|
996
|
-
this.streamingRenderTimer = null;
|
|
997
|
-
}
|
|
998
838
|
// Clean up output interceptor
|
|
999
839
|
if (this.outputInterceptorCleanup) {
|
|
1000
840
|
this.outputInterceptorCleanup();
|
|
@@ -1002,6 +842,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1002
842
|
}
|
|
1003
843
|
this.disposed = true;
|
|
1004
844
|
this.enabled = false;
|
|
845
|
+
this.resetStreamingRenderThrottle();
|
|
1005
846
|
this.disableScrollRegion();
|
|
1006
847
|
this.disableBracketedPaste();
|
|
1007
848
|
this.buffer = '';
|
|
@@ -1107,22 +948,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1107
948
|
this.toggleEditMode();
|
|
1108
949
|
return true;
|
|
1109
950
|
}
|
|
1110
|
-
|
|
1111
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1112
|
-
this.togglePasteExpansion();
|
|
1113
|
-
}
|
|
1114
|
-
else {
|
|
1115
|
-
this.toggleThinking();
|
|
1116
|
-
}
|
|
1117
|
-
return true;
|
|
1118
|
-
case 'escape':
|
|
1119
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1120
|
-
if (this.mode === 'streaming') {
|
|
1121
|
-
this.emit('interrupt');
|
|
1122
|
-
}
|
|
1123
|
-
else if (this.buffer.length > 0) {
|
|
1124
|
-
this.clear();
|
|
1125
|
-
}
|
|
951
|
+
this.insertText(' ');
|
|
1126
952
|
return true;
|
|
1127
953
|
}
|
|
1128
954
|
return false;
|
|
@@ -1140,7 +966,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1140
966
|
this.insertPlainText(chunk, insertPos);
|
|
1141
967
|
this.cursor = insertPos + chunk.length;
|
|
1142
968
|
this.emit('change', this.buffer);
|
|
1143
|
-
this.updateSuggestions();
|
|
1144
969
|
this.scheduleRender();
|
|
1145
970
|
}
|
|
1146
971
|
insertNewline() {
|
|
@@ -1165,7 +990,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1165
990
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1166
991
|
}
|
|
1167
992
|
this.emit('change', this.buffer);
|
|
1168
|
-
this.updateSuggestions();
|
|
1169
993
|
this.scheduleRender();
|
|
1170
994
|
}
|
|
1171
995
|
deleteForward() {
|
|
@@ -1415,7 +1239,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1415
1239
|
if (available <= 0)
|
|
1416
1240
|
return;
|
|
1417
1241
|
const chunk = clean.slice(0, available);
|
|
1418
|
-
|
|
1242
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1243
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1244
|
+
if (isMultiline && !isShortMultiline) {
|
|
1419
1245
|
this.insertPastePlaceholder(chunk);
|
|
1420
1246
|
}
|
|
1421
1247
|
else {
|
|
@@ -1435,6 +1261,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1435
1261
|
return;
|
|
1436
1262
|
this.applyScrollRegion();
|
|
1437
1263
|
this.scrollRegionActive = true;
|
|
1264
|
+
this.forceRender();
|
|
1438
1265
|
}
|
|
1439
1266
|
disableScrollRegion() {
|
|
1440
1267
|
if (!this.scrollRegionActive)
|
|
@@ -1585,17 +1412,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1585
1412
|
this.shiftPlaceholders(position, text.length);
|
|
1586
1413
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1587
1414
|
}
|
|
1415
|
+
shouldInlineMultiline(content) {
|
|
1416
|
+
const lines = content.split('\n').length;
|
|
1417
|
+
const maxInlineLines = 4;
|
|
1418
|
+
const maxInlineChars = 240;
|
|
1419
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1420
|
+
}
|
|
1588
1421
|
findPlaceholderAt(position) {
|
|
1589
1422
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1590
1423
|
}
|
|
1591
|
-
buildPlaceholder(
|
|
1424
|
+
buildPlaceholder(lineCount) {
|
|
1592
1425
|
const id = ++this.pasteCounter;
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
const preview = summary.preview.length > 30
|
|
1596
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1597
|
-
: summary.preview;
|
|
1598
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1426
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1427
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1599
1428
|
return { id, placeholder };
|
|
1600
1429
|
}
|
|
1601
1430
|
insertPastePlaceholder(content) {
|
|
@@ -1603,67 +1432,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1603
1432
|
if (available <= 0)
|
|
1604
1433
|
return;
|
|
1605
1434
|
const cleanContent = content.slice(0, available);
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
if (summary.lineCount < 5) {
|
|
1609
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1610
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1611
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1612
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1435
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1436
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1616
1437
|
const insertPos = this.cursor;
|
|
1617
1438
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1618
1439
|
this.pastePlaceholders.push({
|
|
1619
1440
|
id,
|
|
1620
1441
|
content: cleanContent,
|
|
1621
|
-
lineCount
|
|
1442
|
+
lineCount,
|
|
1622
1443
|
placeholder,
|
|
1623
1444
|
start: insertPos,
|
|
1624
1445
|
end: insertPos + placeholder.length,
|
|
1625
|
-
summary,
|
|
1626
|
-
expanded: false,
|
|
1627
1446
|
});
|
|
1628
1447
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1629
1448
|
this.cursor = insertPos + placeholder.length;
|
|
1630
1449
|
}
|
|
1631
|
-
/**
|
|
1632
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1633
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1634
|
-
*/
|
|
1635
|
-
togglePasteExpansion() {
|
|
1636
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1637
|
-
if (!placeholder)
|
|
1638
|
-
return false;
|
|
1639
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1640
|
-
// Update the placeholder text in buffer
|
|
1641
|
-
const newPlaceholder = placeholder.expanded
|
|
1642
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1643
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1644
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1645
|
-
// Update buffer
|
|
1646
|
-
this.buffer =
|
|
1647
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1648
|
-
newPlaceholder +
|
|
1649
|
-
this.buffer.slice(placeholder.end);
|
|
1650
|
-
// Update placeholder tracking
|
|
1651
|
-
placeholder.placeholder = newPlaceholder;
|
|
1652
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1653
|
-
// Shift other placeholders
|
|
1654
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1655
|
-
this.scheduleRender();
|
|
1656
|
-
return true;
|
|
1657
|
-
}
|
|
1658
|
-
buildExpandedPlaceholder(ph) {
|
|
1659
|
-
const lines = ph.content.split('\n');
|
|
1660
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1661
|
-
const lastLines = lines.length > 5
|
|
1662
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1663
|
-
: '';
|
|
1664
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1665
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1666
|
-
}
|
|
1667
1450
|
deletePlaceholder(placeholder) {
|
|
1668
1451
|
const length = placeholder.end - placeholder.start;
|
|
1669
1452
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1671,7 +1454,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1671
1454
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1672
1455
|
this.cursor = placeholder.start;
|
|
1673
1456
|
}
|
|
1674
|
-
updateContextUsage(value) {
|
|
1457
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1458
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1459
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1460
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1461
|
+
}
|
|
1675
1462
|
if (value === null || !Number.isFinite(value)) {
|
|
1676
1463
|
this.contextUsage = null;
|
|
1677
1464
|
}
|
|
@@ -1698,6 +1485,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1698
1485
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1699
1486
|
this.setEditMode(next);
|
|
1700
1487
|
}
|
|
1488
|
+
scheduleStreamingRender(delayMs) {
|
|
1489
|
+
if (this.streamingRenderTimer)
|
|
1490
|
+
return;
|
|
1491
|
+
const wait = Math.max(16, delayMs);
|
|
1492
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1493
|
+
this.streamingRenderTimer = null;
|
|
1494
|
+
this.render();
|
|
1495
|
+
}, wait);
|
|
1496
|
+
}
|
|
1497
|
+
resetStreamingRenderThrottle() {
|
|
1498
|
+
if (this.streamingRenderTimer) {
|
|
1499
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1500
|
+
this.streamingRenderTimer = null;
|
|
1501
|
+
}
|
|
1502
|
+
this.lastStreamingRender = 0;
|
|
1503
|
+
}
|
|
1701
1504
|
scheduleRender() {
|
|
1702
1505
|
if (!this.canRender())
|
|
1703
1506
|
return;
|