erosolar-cli 1.7.250 → 1.7.251
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/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 +183 -31
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +54 -123
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +358 -575
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +12 -15
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +8 -22
- package/dist/shell/terminalInputAdapter.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 +115 -2
- 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/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,22 +81,12 @@ 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;
|
|
@@ -97,22 +94,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
97
94
|
autoContinueHotkey = 'alt+c';
|
|
98
95
|
// Output interceptor cleanup
|
|
99
96
|
outputInterceptorCleanup;
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
thinkingEnabled = true;
|
|
104
|
-
// Streaming input area render timer (updates elapsed time display)
|
|
97
|
+
// Streaming render throttle
|
|
98
|
+
lastStreamingRender = 0;
|
|
99
|
+
streamingRenderInterval = 250; // ms between renders during streaming
|
|
105
100
|
streamingRenderTimer = null;
|
|
106
101
|
constructor(writeStream = process.stdout, config = {}) {
|
|
107
102
|
super();
|
|
108
103
|
this.out = writeStream;
|
|
109
|
-
// Use schema defaults for configuration consistency
|
|
110
104
|
this.config = {
|
|
111
|
-
maxLines: config.maxLines ??
|
|
112
|
-
maxLength: config.maxLength ??
|
|
105
|
+
maxLines: config.maxLines ?? 1000,
|
|
106
|
+
maxLength: config.maxLength ?? 10000,
|
|
113
107
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
114
|
-
promptChar: config.promptChar ??
|
|
115
|
-
continuationChar: config.continuationChar ??
|
|
108
|
+
promptChar: config.promptChar ?? '> ',
|
|
109
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
116
110
|
};
|
|
117
111
|
}
|
|
118
112
|
// ===========================================================================
|
|
@@ -191,11 +185,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
191
185
|
if (handled)
|
|
192
186
|
return;
|
|
193
187
|
}
|
|
194
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
195
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
196
|
-
this.emit('showHelp');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
188
|
// Insert printable characters
|
|
200
189
|
if (str && !key?.ctrl && !key?.meta) {
|
|
201
190
|
this.insertText(str);
|
|
@@ -204,226 +193,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
204
193
|
/**
|
|
205
194
|
* Set the input mode
|
|
206
195
|
*
|
|
207
|
-
* Streaming
|
|
208
|
-
*
|
|
209
|
-
* the cursor is (below the streamed content).
|
|
196
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
197
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
210
198
|
*/
|
|
211
199
|
setMode(mode) {
|
|
212
200
|
const prevMode = this.mode;
|
|
213
201
|
this.mode = mode;
|
|
214
202
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
215
|
-
//
|
|
216
|
-
this.
|
|
217
|
-
const { rows } = this.getSize();
|
|
218
|
-
// Banner takes ~7 lines (adjust if needed)
|
|
219
|
-
const bannerLines = 7;
|
|
220
|
-
this.pinnedTopRows = bannerLines;
|
|
221
|
-
// Full input area during streaming (status + divider + input + divider + controls)
|
|
222
|
-
const inputAreaLines = 5;
|
|
223
|
-
this.reservedLines = inputAreaLines;
|
|
224
|
-
// Clear everything between banner and input area (removes duplicates)
|
|
225
|
-
for (let i = bannerLines + 1; i <= rows - inputAreaLines; i++) {
|
|
226
|
-
this.write(ESC.TO(i, 1));
|
|
227
|
-
this.write(ESC.CLEAR_LINE);
|
|
228
|
-
}
|
|
229
|
-
// Enable scroll region: from below banner to above input area
|
|
203
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
204
|
+
this.resetStreamingRenderThrottle();
|
|
230
205
|
this.enableScrollRegion();
|
|
231
|
-
// Render full input area at absolute bottom
|
|
232
|
-
this.forceRender();
|
|
233
|
-
// Position cursor right after banner for streaming content
|
|
234
|
-
this.write(ESC.TO(bannerLines + 1, 1));
|
|
235
|
-
// Start timer to update elapsed time in status bar (every 1 second)
|
|
236
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
237
|
-
if (this.mode === 'streaming') {
|
|
238
|
-
this.renderStreamingStatusOnly();
|
|
239
|
-
}
|
|
240
|
-
}, 1000);
|
|
241
206
|
this.renderDirty = true;
|
|
207
|
+
this.render();
|
|
242
208
|
}
|
|
243
209
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.streamingRenderTimer = null;
|
|
248
|
-
}
|
|
249
|
-
// Reset streaming time and pinned rows
|
|
250
|
-
this.streamingStartTime = null;
|
|
251
|
-
this.pinnedTopRows = 0;
|
|
252
|
-
// Disable scroll region
|
|
253
|
-
this.disableScrollRegion();
|
|
254
|
-
// Show cursor again
|
|
255
|
-
this.write(ESC.SHOW);
|
|
256
|
-
// Position cursor at end of content area and add newline
|
|
257
|
-
const { rows } = this.getSize();
|
|
258
|
-
const contentEnd = Math.max(1, rows - this.reservedLines);
|
|
259
|
-
this.write(ESC.TO(contentEnd, 1));
|
|
260
|
-
this.write('\n');
|
|
261
|
-
// Reset flow mode tracking
|
|
262
|
-
this.flowModeRenderedLines = 0;
|
|
263
|
-
// Re-render the input area in normal mode
|
|
210
|
+
// Streaming ended - render the input area
|
|
211
|
+
this.resetStreamingRenderThrottle();
|
|
212
|
+
this.enableScrollRegion();
|
|
264
213
|
this.forceRender();
|
|
265
214
|
}
|
|
266
215
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Enable or disable flow mode.
|
|
269
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
270
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
271
|
-
*/
|
|
272
|
-
setFlowMode(enabled) {
|
|
273
|
-
if (this.flowMode === enabled)
|
|
274
|
-
return;
|
|
275
|
-
this.flowMode = enabled;
|
|
276
|
-
this.renderDirty = true;
|
|
277
|
-
this.scheduleRender();
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Check if flow mode is enabled.
|
|
281
|
-
*/
|
|
282
|
-
isFlowMode() {
|
|
283
|
-
return this.flowMode;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Set available slash commands for auto-complete suggestions.
|
|
287
|
-
*/
|
|
288
|
-
setCommands(commands) {
|
|
289
|
-
this.commandSuggestions = commands;
|
|
290
|
-
this.updateSuggestions();
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Update filtered suggestions based on current input.
|
|
294
|
-
*/
|
|
295
|
-
updateSuggestions() {
|
|
296
|
-
const input = this.buffer.trim();
|
|
297
|
-
// Only show suggestions when input starts with "/"
|
|
298
|
-
if (!input.startsWith('/')) {
|
|
299
|
-
this.showSuggestions = false;
|
|
300
|
-
this.filteredSuggestions = [];
|
|
301
|
-
this.selectedSuggestionIndex = 0;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const query = input.toLowerCase();
|
|
305
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
306
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
307
|
-
// Show suggestions if we have matches
|
|
308
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
309
|
-
// Keep selection in bounds
|
|
310
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
311
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Select next suggestion (arrow down / tab).
|
|
316
|
-
*/
|
|
317
|
-
selectNextSuggestion() {
|
|
318
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
319
|
-
return;
|
|
320
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
321
|
-
this.renderDirty = true;
|
|
322
|
-
this.scheduleRender();
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
326
|
-
*/
|
|
327
|
-
selectPrevSuggestion() {
|
|
328
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
329
|
-
return;
|
|
330
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
331
|
-
? this.filteredSuggestions.length - 1
|
|
332
|
-
: this.selectedSuggestionIndex - 1;
|
|
333
|
-
this.renderDirty = true;
|
|
334
|
-
this.scheduleRender();
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Accept current suggestion and insert into buffer.
|
|
338
|
-
*/
|
|
339
|
-
acceptSuggestion() {
|
|
340
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
341
|
-
return false;
|
|
342
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
343
|
-
if (!selected)
|
|
344
|
-
return false;
|
|
345
|
-
// Replace buffer with selected command
|
|
346
|
-
this.buffer = selected.command + ' ';
|
|
347
|
-
this.cursor = this.buffer.length;
|
|
348
|
-
this.showSuggestions = false;
|
|
349
|
-
this.renderDirty = true;
|
|
350
|
-
this.scheduleRender();
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Check if suggestions are visible.
|
|
355
|
-
*/
|
|
356
|
-
areSuggestionsVisible() {
|
|
357
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Update token count for metrics display
|
|
361
|
-
*/
|
|
362
|
-
setTokensUsed(tokens) {
|
|
363
|
-
this.tokensUsed = tokens;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Toggle thinking/reasoning mode
|
|
367
|
-
*/
|
|
368
|
-
toggleThinking() {
|
|
369
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
370
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
371
|
-
this.scheduleRender();
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Get thinking enabled state
|
|
375
|
-
*/
|
|
376
|
-
isThinkingEnabled() {
|
|
377
|
-
return this.thinkingEnabled;
|
|
378
|
-
}
|
|
379
216
|
/**
|
|
380
217
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
381
218
|
*/
|
|
382
219
|
setPinnedHeaderLines(count) {
|
|
383
|
-
//
|
|
384
|
-
if (this.pinnedTopRows !==
|
|
385
|
-
this.pinnedTopRows =
|
|
220
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
221
|
+
if (this.pinnedTopRows !== 0) {
|
|
222
|
+
this.pinnedTopRows = 0;
|
|
386
223
|
if (this.scrollRegionActive) {
|
|
387
224
|
this.applyScrollRegion();
|
|
388
225
|
}
|
|
389
226
|
}
|
|
390
227
|
}
|
|
391
|
-
/**
|
|
392
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
393
|
-
* restore the default bottom-aligned layout.
|
|
394
|
-
*/
|
|
395
|
-
setInlineAnchor(row) {
|
|
396
|
-
if (row === null || row === undefined) {
|
|
397
|
-
this.inlineAnchorRow = null;
|
|
398
|
-
this.inlineLayout = false;
|
|
399
|
-
this.renderDirty = true;
|
|
400
|
-
this.render();
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const { rows } = this.getSize();
|
|
404
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
405
|
-
this.inlineAnchorRow = clamped;
|
|
406
|
-
this.inlineLayout = true;
|
|
407
|
-
this.renderDirty = true;
|
|
408
|
-
this.render();
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
412
|
-
* output by re-evaluating the anchor before each render.
|
|
413
|
-
*/
|
|
414
|
-
setInlineAnchorProvider(provider) {
|
|
415
|
-
this.anchorProvider = provider;
|
|
416
|
-
if (!provider) {
|
|
417
|
-
this.inlineLayout = false;
|
|
418
|
-
this.inlineAnchorRow = null;
|
|
419
|
-
this.renderDirty = true;
|
|
420
|
-
this.render();
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
this.inlineLayout = true;
|
|
424
|
-
this.renderDirty = true;
|
|
425
|
-
this.render();
|
|
426
|
-
}
|
|
427
228
|
/**
|
|
428
229
|
* Get current mode
|
|
429
230
|
*/
|
|
@@ -533,6 +334,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
533
334
|
this.streamingLabel = next;
|
|
534
335
|
this.scheduleRender();
|
|
535
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
339
|
+
*/
|
|
340
|
+
setMetaStatus(meta) {
|
|
341
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
342
|
+
? Math.floor(meta.elapsedSeconds)
|
|
343
|
+
: null;
|
|
344
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
345
|
+
? Math.floor(meta.tokensUsed)
|
|
346
|
+
: null;
|
|
347
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
348
|
+
? Math.floor(meta.tokenLimit)
|
|
349
|
+
: null;
|
|
350
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
351
|
+
? Math.floor(meta.thinkingMs)
|
|
352
|
+
: null;
|
|
353
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
354
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
355
|
+
this.metaTokensUsed === nextTokens &&
|
|
356
|
+
this.metaTokenLimit === nextLimit &&
|
|
357
|
+
this.metaThinkingMs === nextThinking &&
|
|
358
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
362
|
+
this.metaTokensUsed = nextTokens;
|
|
363
|
+
this.metaTokenLimit = nextLimit;
|
|
364
|
+
this.metaThinkingMs = nextThinking;
|
|
365
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
366
|
+
this.scheduleRender();
|
|
367
|
+
}
|
|
536
368
|
/**
|
|
537
369
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
538
370
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -542,16 +374,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
542
374
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
543
375
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
544
376
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
377
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
545
378
|
if (this.verificationEnabled === nextVerification &&
|
|
546
379
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
547
380
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
548
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
381
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
382
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
549
383
|
return;
|
|
550
384
|
}
|
|
551
385
|
this.verificationEnabled = nextVerification;
|
|
552
386
|
this.autoContinueEnabled = nextAutoContinue;
|
|
553
387
|
this.verificationHotkey = nextVerifyHotkey;
|
|
554
388
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
389
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
555
390
|
this.scheduleRender();
|
|
556
391
|
}
|
|
557
392
|
/**
|
|
@@ -566,183 +401,91 @@ export class TerminalInput extends EventEmitter {
|
|
|
566
401
|
/**
|
|
567
402
|
* Render the input area - Claude Code style with mode controls
|
|
568
403
|
*
|
|
569
|
-
* During streaming
|
|
570
|
-
*
|
|
404
|
+
* During streaming we keep the scroll region active and repaint only the
|
|
405
|
+
* pinned status/input block (throttled) so streamed content can scroll
|
|
406
|
+
* naturally above while elapsed time and status stay fresh.
|
|
571
407
|
*/
|
|
572
408
|
render() {
|
|
573
409
|
if (!this.canRender())
|
|
574
410
|
return;
|
|
575
411
|
if (this.isRendering)
|
|
576
412
|
return;
|
|
413
|
+
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
414
|
+
// During streaming we still render the pinned input/status region, but throttle
|
|
415
|
+
// to avoid fighting with the streamed content flow.
|
|
416
|
+
if (streamingActive && this.lastStreamingRender > 0) {
|
|
417
|
+
const elapsed = Date.now() - this.lastStreamingRender;
|
|
418
|
+
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
419
|
+
if (waitMs > 0) {
|
|
420
|
+
this.renderDirty = true;
|
|
421
|
+
this.scheduleStreamingRender(waitMs);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
577
425
|
const shouldSkip = !this.renderDirty &&
|
|
578
426
|
this.buffer === this.lastRenderContent &&
|
|
579
427
|
this.cursor === this.lastRenderCursor;
|
|
580
428
|
this.renderDirty = false;
|
|
581
|
-
// Skip if nothing changed
|
|
429
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
582
430
|
if (shouldSkip) {
|
|
583
431
|
return;
|
|
584
432
|
}
|
|
585
|
-
// If write lock is held, defer render
|
|
433
|
+
// If write lock is held, defer render to avoid race conditions
|
|
586
434
|
if (writeLock.isLocked()) {
|
|
587
435
|
writeLock.safeWrite(() => this.render());
|
|
588
436
|
return;
|
|
589
437
|
}
|
|
590
438
|
this.isRendering = true;
|
|
439
|
+
// Use write lock during render to prevent interleaved output
|
|
591
440
|
writeLock.lock('terminalInput.render');
|
|
592
441
|
try {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
finally {
|
|
597
|
-
writeLock.unlock();
|
|
598
|
-
this.isRendering = false;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
603
|
-
*
|
|
604
|
-
* Flow mode attempted inline rendering but caused duplicate renders
|
|
605
|
-
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
606
|
-
*/
|
|
607
|
-
renderFlowMode() {
|
|
608
|
-
// Use stable bottom-pinned approach
|
|
609
|
-
this.renderBottomPinned();
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
613
|
-
*
|
|
614
|
-
* Layout when suggestions visible:
|
|
615
|
-
* - Top divider
|
|
616
|
-
* - Input line(s)
|
|
617
|
-
* - Bottom divider
|
|
618
|
-
* - Suggestions (command list)
|
|
619
|
-
*
|
|
620
|
-
* Layout when suggestions hidden:
|
|
621
|
-
* - Status bar (Ready/Streaming)
|
|
622
|
-
* - Top divider
|
|
623
|
-
* - Input line(s)
|
|
624
|
-
* - Bottom divider
|
|
625
|
-
* - Mode controls
|
|
626
|
-
*/
|
|
627
|
-
renderBottomPinned() {
|
|
628
|
-
const { rows, cols } = this.getSize();
|
|
629
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
630
|
-
// Wrap buffer into display lines
|
|
631
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
632
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
633
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
634
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
635
|
-
// Calculate display window (keep cursor visible)
|
|
636
|
-
let startLine = 0;
|
|
637
|
-
if (lines.length > displayLines) {
|
|
638
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
639
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
640
|
-
}
|
|
641
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
642
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
643
|
-
// Calculate suggestion display
|
|
644
|
-
const suggestionsToShow = this.showSuggestions
|
|
645
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
646
|
-
: [];
|
|
647
|
-
const suggestionLines = suggestionsToShow.length;
|
|
648
|
-
this.write(ESC.HIDE);
|
|
649
|
-
this.write(ESC.RESET);
|
|
650
|
-
const divider = renderDivider(cols - 2);
|
|
651
|
-
// Calculate positions from absolute bottom
|
|
652
|
-
let currentRow;
|
|
653
|
-
if (suggestionLines > 0) {
|
|
654
|
-
// With suggestions: input area + dividers + suggestions
|
|
655
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
656
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
657
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
658
|
-
this.updateReservedLines(totalHeight);
|
|
659
|
-
// Top divider
|
|
660
|
-
this.write(ESC.TO(currentRow, 1));
|
|
661
|
-
this.write(ESC.CLEAR_LINE);
|
|
662
|
-
this.write(divider);
|
|
663
|
-
currentRow++;
|
|
664
|
-
// Input lines
|
|
665
|
-
let finalRow = currentRow;
|
|
666
|
-
let finalCol = 3;
|
|
667
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
668
|
-
this.write(ESC.TO(currentRow, 1));
|
|
669
|
-
this.write(ESC.CLEAR_LINE);
|
|
670
|
-
const line = visibleLines[i] ?? '';
|
|
671
|
-
const absoluteLineIdx = startLine + i;
|
|
672
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
673
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
674
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
675
|
-
if (isCursorLine) {
|
|
676
|
-
const col = Math.min(cursorCol, line.length);
|
|
677
|
-
this.write(line.slice(0, col));
|
|
678
|
-
this.write(ESC.REVERSE);
|
|
679
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
680
|
-
this.write(ESC.RESET);
|
|
681
|
-
this.write(line.slice(col + 1));
|
|
682
|
-
finalRow = currentRow;
|
|
683
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
this.write(line);
|
|
687
|
-
}
|
|
688
|
-
currentRow++;
|
|
442
|
+
if (!this.scrollRegionActive) {
|
|
443
|
+
this.enableScrollRegion();
|
|
689
444
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
this.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
445
|
+
const { rows, cols } = this.getSize();
|
|
446
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
447
|
+
// Wrap buffer into display lines
|
|
448
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
449
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
450
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
451
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
452
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
453
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
454
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
455
|
+
// Calculate display window (keep cursor visible)
|
|
456
|
+
let startLine = 0;
|
|
457
|
+
if (lines.length > displayLines) {
|
|
458
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
459
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
460
|
+
}
|
|
461
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
462
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
463
|
+
// Render
|
|
464
|
+
this.write(ESC.HIDE);
|
|
465
|
+
this.write(ESC.RESET);
|
|
466
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
467
|
+
let currentRow = startRow;
|
|
468
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
469
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
470
|
+
// Meta/status header (elapsed, tokens/context)
|
|
471
|
+
for (const metaLine of metaLines) {
|
|
697
472
|
this.write(ESC.TO(currentRow, 1));
|
|
698
473
|
this.write(ESC.CLEAR_LINE);
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
// Indent and highlight selected
|
|
702
|
-
this.write(' ');
|
|
703
|
-
if (isSelected) {
|
|
704
|
-
this.write(ESC.REVERSE);
|
|
705
|
-
this.write(ESC.BOLD);
|
|
706
|
-
}
|
|
707
|
-
this.write(suggestion.command);
|
|
708
|
-
if (isSelected) {
|
|
709
|
-
this.write(ESC.RESET);
|
|
710
|
-
}
|
|
711
|
-
// Description (dimmed)
|
|
712
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
713
|
-
if (descSpace > 10 && suggestion.description) {
|
|
714
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
715
|
-
this.write(ESC.RESET);
|
|
716
|
-
this.write(ESC.DIM);
|
|
717
|
-
this.write(' ');
|
|
718
|
-
this.write(desc);
|
|
719
|
-
this.write(ESC.RESET);
|
|
720
|
-
}
|
|
721
|
-
currentRow++;
|
|
474
|
+
this.write(metaLine);
|
|
475
|
+
currentRow += 1;
|
|
722
476
|
}
|
|
723
|
-
//
|
|
724
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
// Without suggestions: normal layout with status bar and controls
|
|
728
|
-
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
729
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
730
|
-
this.updateReservedLines(totalHeight);
|
|
731
|
-
// Status bar
|
|
732
|
-
this.write(ESC.TO(currentRow, 1));
|
|
733
|
-
this.write(ESC.CLEAR_LINE);
|
|
734
|
-
this.write(this.buildStatusBar(cols));
|
|
735
|
-
currentRow++;
|
|
736
|
-
// Top divider
|
|
477
|
+
// Separator line
|
|
737
478
|
this.write(ESC.TO(currentRow, 1));
|
|
738
479
|
this.write(ESC.CLEAR_LINE);
|
|
480
|
+
const divider = renderDivider(cols - 2);
|
|
739
481
|
this.write(divider);
|
|
740
|
-
currentRow
|
|
741
|
-
//
|
|
482
|
+
currentRow += 1;
|
|
483
|
+
// Render input lines
|
|
742
484
|
let finalRow = currentRow;
|
|
743
485
|
let finalCol = 3;
|
|
744
486
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
745
|
-
|
|
487
|
+
const rowNum = currentRow + i;
|
|
488
|
+
this.write(ESC.TO(rowNum, 1));
|
|
746
489
|
this.write(ESC.CLEAR_LINE);
|
|
747
490
|
const line = visibleLines[i] ?? '';
|
|
748
491
|
const absoluteLineIdx = startLine + i;
|
|
@@ -756,6 +499,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
756
499
|
this.write(ESC.RESET);
|
|
757
500
|
this.write(ESC.BG_DARK);
|
|
758
501
|
if (isCursorLine) {
|
|
502
|
+
// Render with block cursor
|
|
759
503
|
const col = Math.min(cursorCol, line.length);
|
|
760
504
|
const before = line.slice(0, col);
|
|
761
505
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -765,149 +509,219 @@ export class TerminalInput extends EventEmitter {
|
|
|
765
509
|
this.write(at);
|
|
766
510
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
767
511
|
this.write(after);
|
|
768
|
-
finalRow =
|
|
512
|
+
finalRow = rowNum;
|
|
769
513
|
finalCol = this.config.promptChar.length + col + 1;
|
|
770
514
|
}
|
|
771
515
|
else {
|
|
772
516
|
this.write(line);
|
|
773
517
|
}
|
|
774
|
-
// Pad to edge
|
|
518
|
+
// Pad to edge for clean look
|
|
775
519
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
776
520
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
777
521
|
if (padding > 0)
|
|
778
522
|
this.write(' '.repeat(padding));
|
|
779
523
|
this.write(ESC.RESET);
|
|
780
|
-
currentRow++;
|
|
781
524
|
}
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
this.write(ESC.
|
|
785
|
-
this.write(divider);
|
|
786
|
-
currentRow++;
|
|
787
|
-
// Mode controls
|
|
788
|
-
this.write(ESC.TO(currentRow, 1));
|
|
525
|
+
// Mode controls line (Claude Code style)
|
|
526
|
+
const controlRow = currentRow + visibleLines.length;
|
|
527
|
+
this.write(ESC.TO(controlRow, 1));
|
|
789
528
|
this.write(ESC.CLEAR_LINE);
|
|
790
529
|
this.write(this.buildModeControls(cols));
|
|
791
|
-
// Position cursor
|
|
530
|
+
// Position cursor
|
|
792
531
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
532
|
+
this.write(ESC.SHOW);
|
|
533
|
+
// Update state
|
|
534
|
+
this.lastRenderContent = this.buffer;
|
|
535
|
+
this.lastRenderCursor = this.cursor;
|
|
536
|
+
if (streamingActive) {
|
|
537
|
+
this.lastStreamingRender = Date.now();
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
this.lastStreamingRender = 0;
|
|
541
|
+
}
|
|
542
|
+
if (this.streamingRenderTimer) {
|
|
543
|
+
clearTimeout(this.streamingRenderTimer);
|
|
544
|
+
this.streamingRenderTimer = null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
writeLock.unlock();
|
|
549
|
+
this.isRendering = false;
|
|
793
550
|
}
|
|
794
|
-
this.write(ESC.SHOW);
|
|
795
|
-
// Update state
|
|
796
|
-
this.lastRenderContent = this.buffer;
|
|
797
|
-
this.lastRenderCursor = this.cursor;
|
|
798
551
|
}
|
|
799
552
|
/**
|
|
800
|
-
*
|
|
801
|
-
* Uses cursor save/restore to update elapsed time without disrupting streaming.
|
|
553
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
802
554
|
*/
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
555
|
+
buildMetaLines(width) {
|
|
556
|
+
const lines = [];
|
|
557
|
+
if (this.metaThinkingMs !== null) {
|
|
558
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
559
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
560
|
+
}
|
|
561
|
+
const statusParts = [];
|
|
562
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
563
|
+
if (statusLabel) {
|
|
564
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
565
|
+
}
|
|
566
|
+
if (this.mode === 'streaming' || isStreamingMode()) {
|
|
567
|
+
statusParts.push({ text: 'esc to interrupt', tone: 'warn' });
|
|
568
|
+
}
|
|
569
|
+
if (this.metaElapsedSeconds !== null) {
|
|
570
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
571
|
+
}
|
|
572
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
573
|
+
if (tokensRemaining !== null) {
|
|
574
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
575
|
+
}
|
|
576
|
+
if (statusParts.length) {
|
|
577
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
578
|
+
}
|
|
579
|
+
const usageParts = [];
|
|
580
|
+
if (this.metaTokensUsed !== null) {
|
|
581
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
582
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
583
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
584
|
+
}
|
|
585
|
+
if (this.contextUsage !== null) {
|
|
586
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
587
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
588
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
589
|
+
}
|
|
590
|
+
if (this.queue.length > 0) {
|
|
591
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
592
|
+
}
|
|
593
|
+
if (usageParts.length) {
|
|
594
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
595
|
+
}
|
|
596
|
+
return lines;
|
|
816
597
|
}
|
|
817
598
|
/**
|
|
818
|
-
*
|
|
819
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
599
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
820
600
|
*/
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (this.streamingStartTime) {
|
|
828
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
829
|
-
const mins = Math.floor(elapsed / 60);
|
|
830
|
-
const secs = elapsed % 60;
|
|
831
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
832
|
-
}
|
|
833
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
601
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
602
|
+
const width = Math.max(1, cols);
|
|
603
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
604
|
+
const row = startRow + i;
|
|
605
|
+
this.write(ESC.TO(row, 1));
|
|
606
|
+
this.write(' '.repeat(width));
|
|
834
607
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Build Claude Code style mode controls line.
|
|
611
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
612
|
+
*/
|
|
613
|
+
buildModeControls(cols) {
|
|
614
|
+
const width = Math.max(8, cols - 2);
|
|
615
|
+
const leftParts = [];
|
|
616
|
+
const rightParts = [];
|
|
617
|
+
if (this.streamingLabel) {
|
|
618
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
843
619
|
}
|
|
844
|
-
// Override/warning status
|
|
845
620
|
if (this.overrideStatusMessage) {
|
|
846
|
-
|
|
621
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
847
622
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
623
|
+
if (this.statusMessage) {
|
|
624
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
625
|
+
}
|
|
626
|
+
const editLabel = this.editMode === 'display-edits' ? 'accept edits on' : 'ask before edits';
|
|
627
|
+
const editIcon = this.editMode === 'display-edits' ? '⏵⏵' : '🛡';
|
|
628
|
+
leftParts.push({
|
|
629
|
+
text: `${editIcon} ${editLabel} (shift+tab to cycle)`,
|
|
630
|
+
tone: this.editMode === 'display-edits' ? 'success' : 'muted',
|
|
631
|
+
});
|
|
632
|
+
const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
|
|
633
|
+
leftParts.push({
|
|
634
|
+
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
635
|
+
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
636
|
+
});
|
|
637
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
638
|
+
leftParts.push({
|
|
639
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
640
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
641
|
+
});
|
|
642
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
643
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
851
644
|
}
|
|
852
|
-
// Multi-line indicator
|
|
853
645
|
if (this.buffer.includes('\n')) {
|
|
854
|
-
|
|
646
|
+
const lineCount = this.buffer.split('\n').length;
|
|
647
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
855
648
|
}
|
|
856
|
-
if (
|
|
857
|
-
|
|
649
|
+
if (this.pastePlaceholders.length > 0) {
|
|
650
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
651
|
+
leftParts.push({
|
|
652
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
653
|
+
tone: 'info',
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const contextRemaining = this.computeContextRemaining();
|
|
657
|
+
if (this.thinkingModeLabel) {
|
|
658
|
+
rightParts.push({ text: `thinking ${this.thinkingModeLabel} (/thinking)`, tone: 'info' });
|
|
659
|
+
}
|
|
660
|
+
if (contextRemaining !== null) {
|
|
661
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
662
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
663
|
+
? 'Context auto-compact imminent'
|
|
664
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
665
|
+
rightParts.push({ text: label, tone });
|
|
666
|
+
}
|
|
667
|
+
if (!rightParts.length || width < 60) {
|
|
668
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
669
|
+
return renderStatusLine(merged, width);
|
|
670
|
+
}
|
|
671
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
672
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
673
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
674
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
675
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
676
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
677
|
+
}
|
|
678
|
+
computeContextRemaining() {
|
|
679
|
+
if (this.contextUsage === null) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
683
|
+
}
|
|
684
|
+
computeTokensRemaining() {
|
|
685
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
686
|
+
return null;
|
|
858
687
|
}
|
|
859
|
-
const
|
|
860
|
-
return
|
|
688
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
689
|
+
return this.formatTokenCount(remaining);
|
|
690
|
+
}
|
|
691
|
+
formatElapsedLabel(seconds) {
|
|
692
|
+
if (seconds < 60) {
|
|
693
|
+
return `${seconds}s`;
|
|
694
|
+
}
|
|
695
|
+
const mins = Math.floor(seconds / 60);
|
|
696
|
+
const secs = seconds % 60;
|
|
697
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
698
|
+
}
|
|
699
|
+
formatTokenCount(value) {
|
|
700
|
+
if (!Number.isFinite(value)) {
|
|
701
|
+
return `${value}`;
|
|
702
|
+
}
|
|
703
|
+
if (value >= 1_000_000) {
|
|
704
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
705
|
+
}
|
|
706
|
+
if (value >= 1_000) {
|
|
707
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
708
|
+
}
|
|
709
|
+
return `${Math.round(value)}`;
|
|
710
|
+
}
|
|
711
|
+
visibleLength(value) {
|
|
712
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
713
|
+
return value.replace(ansiPattern, '').length;
|
|
861
714
|
}
|
|
862
715
|
/**
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
716
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
717
|
+
* needing a TTY. Not used by production code.
|
|
867
718
|
*/
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
875
|
-
if (this.editMode === 'display-edits') {
|
|
876
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
880
|
-
}
|
|
881
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
882
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
883
|
-
// Verification (green when on) - per schema.verificationMode
|
|
884
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
885
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
886
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
887
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
888
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
889
|
-
let rightPart = '';
|
|
890
|
-
if (this.contextUsage !== null) {
|
|
891
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
892
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
893
|
-
if (rem < 10)
|
|
894
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
895
|
-
else if (rem < 25)
|
|
896
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
897
|
-
else
|
|
898
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
899
|
-
}
|
|
900
|
-
// Calculate visible lengths (strip ANSI)
|
|
901
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
902
|
-
const leftLen = strip(leftPart).length;
|
|
903
|
-
const rightLen = strip(rightPart).length;
|
|
904
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
905
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
906
|
-
}
|
|
907
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
908
|
-
return `${leftPart} ${rightPart}`;
|
|
909
|
-
}
|
|
910
|
-
return leftPart;
|
|
719
|
+
getDebugUiSnapshot(width) {
|
|
720
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
721
|
+
return {
|
|
722
|
+
meta: this.buildMetaLines(cols - 2),
|
|
723
|
+
controls: this.buildModeControls(cols),
|
|
724
|
+
};
|
|
911
725
|
}
|
|
912
726
|
/**
|
|
913
727
|
* Force a re-render
|
|
@@ -930,17 +744,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
930
744
|
handleResize() {
|
|
931
745
|
this.lastRenderContent = '';
|
|
932
746
|
this.lastRenderCursor = -1;
|
|
747
|
+
this.resetStreamingRenderThrottle();
|
|
933
748
|
// Re-clamp pinned header rows to the new terminal height
|
|
934
749
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
750
|
+
if (this.scrollRegionActive) {
|
|
751
|
+
this.disableScrollRegion();
|
|
752
|
+
this.enableScrollRegion();
|
|
753
|
+
}
|
|
935
754
|
this.scheduleRender();
|
|
936
755
|
}
|
|
937
756
|
/**
|
|
938
757
|
* Register with display's output interceptor to position cursor correctly.
|
|
939
758
|
* When scroll region is active, output needs to go to the scroll region,
|
|
940
759
|
* not the protected bottom area where the input is rendered.
|
|
941
|
-
*
|
|
942
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
943
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
944
760
|
*/
|
|
945
761
|
registerOutputInterceptor(display) {
|
|
946
762
|
if (this.outputInterceptorCleanup) {
|
|
@@ -948,11 +764,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
948
764
|
}
|
|
949
765
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
950
766
|
beforeWrite: () => {
|
|
951
|
-
//
|
|
952
|
-
//
|
|
767
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
768
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
769
|
+
if (this.scrollRegionActive) {
|
|
770
|
+
const { rows } = this.getSize();
|
|
771
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
772
|
+
this.write(ESC.SAVE);
|
|
773
|
+
this.write(ESC.TO(scrollBottom, 1));
|
|
774
|
+
}
|
|
953
775
|
},
|
|
954
776
|
afterWrite: () => {
|
|
955
|
-
//
|
|
777
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
778
|
+
if (this.scrollRegionActive) {
|
|
779
|
+
this.write(ESC.RESTORE);
|
|
780
|
+
}
|
|
956
781
|
},
|
|
957
782
|
});
|
|
958
783
|
}
|
|
@@ -962,11 +787,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
962
787
|
dispose() {
|
|
963
788
|
if (this.disposed)
|
|
964
789
|
return;
|
|
965
|
-
// Clean up streaming render timer
|
|
966
|
-
if (this.streamingRenderTimer) {
|
|
967
|
-
clearInterval(this.streamingRenderTimer);
|
|
968
|
-
this.streamingRenderTimer = null;
|
|
969
|
-
}
|
|
970
790
|
// Clean up output interceptor
|
|
971
791
|
if (this.outputInterceptorCleanup) {
|
|
972
792
|
this.outputInterceptorCleanup();
|
|
@@ -974,6 +794,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
974
794
|
}
|
|
975
795
|
this.disposed = true;
|
|
976
796
|
this.enabled = false;
|
|
797
|
+
this.resetStreamingRenderThrottle();
|
|
977
798
|
this.disableScrollRegion();
|
|
978
799
|
this.disableBracketedPaste();
|
|
979
800
|
this.buffer = '';
|
|
@@ -1079,22 +900,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1079
900
|
this.toggleEditMode();
|
|
1080
901
|
return true;
|
|
1081
902
|
}
|
|
1082
|
-
|
|
1083
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1084
|
-
this.togglePasteExpansion();
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
this.toggleThinking();
|
|
1088
|
-
}
|
|
1089
|
-
return true;
|
|
1090
|
-
case 'escape':
|
|
1091
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1092
|
-
if (this.mode === 'streaming') {
|
|
1093
|
-
this.emit('interrupt');
|
|
1094
|
-
}
|
|
1095
|
-
else if (this.buffer.length > 0) {
|
|
1096
|
-
this.clear();
|
|
1097
|
-
}
|
|
903
|
+
this.insertText(' ');
|
|
1098
904
|
return true;
|
|
1099
905
|
}
|
|
1100
906
|
return false;
|
|
@@ -1112,7 +918,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1112
918
|
this.insertPlainText(chunk, insertPos);
|
|
1113
919
|
this.cursor = insertPos + chunk.length;
|
|
1114
920
|
this.emit('change', this.buffer);
|
|
1115
|
-
this.updateSuggestions();
|
|
1116
921
|
this.scheduleRender();
|
|
1117
922
|
}
|
|
1118
923
|
insertNewline() {
|
|
@@ -1137,7 +942,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1137
942
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1138
943
|
}
|
|
1139
944
|
this.emit('change', this.buffer);
|
|
1140
|
-
this.updateSuggestions();
|
|
1141
945
|
this.scheduleRender();
|
|
1142
946
|
}
|
|
1143
947
|
deleteForward() {
|
|
@@ -1387,7 +1191,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1387
1191
|
if (available <= 0)
|
|
1388
1192
|
return;
|
|
1389
1193
|
const chunk = clean.slice(0, available);
|
|
1390
|
-
|
|
1194
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1195
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1196
|
+
if (isMultiline && !isShortMultiline) {
|
|
1391
1197
|
this.insertPastePlaceholder(chunk);
|
|
1392
1198
|
}
|
|
1393
1199
|
else {
|
|
@@ -1407,6 +1213,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1407
1213
|
return;
|
|
1408
1214
|
this.applyScrollRegion();
|
|
1409
1215
|
this.scrollRegionActive = true;
|
|
1216
|
+
this.forceRender();
|
|
1410
1217
|
}
|
|
1411
1218
|
disableScrollRegion() {
|
|
1412
1219
|
if (!this.scrollRegionActive)
|
|
@@ -1557,17 +1364,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1557
1364
|
this.shiftPlaceholders(position, text.length);
|
|
1558
1365
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1559
1366
|
}
|
|
1367
|
+
shouldInlineMultiline(content) {
|
|
1368
|
+
const lines = content.split('\n').length;
|
|
1369
|
+
const maxInlineLines = 4;
|
|
1370
|
+
const maxInlineChars = 240;
|
|
1371
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1372
|
+
}
|
|
1560
1373
|
findPlaceholderAt(position) {
|
|
1561
1374
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1562
1375
|
}
|
|
1563
|
-
buildPlaceholder(
|
|
1376
|
+
buildPlaceholder(lineCount) {
|
|
1564
1377
|
const id = ++this.pasteCounter;
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
const preview = summary.preview.length > 30
|
|
1568
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1569
|
-
: summary.preview;
|
|
1570
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1378
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1379
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1571
1380
|
return { id, placeholder };
|
|
1572
1381
|
}
|
|
1573
1382
|
insertPastePlaceholder(content) {
|
|
@@ -1575,67 +1384,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1575
1384
|
if (available <= 0)
|
|
1576
1385
|
return;
|
|
1577
1386
|
const cleanContent = content.slice(0, available);
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
if (summary.lineCount < 5) {
|
|
1581
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1582
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1583
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1584
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1387
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1388
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1588
1389
|
const insertPos = this.cursor;
|
|
1589
1390
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1590
1391
|
this.pastePlaceholders.push({
|
|
1591
1392
|
id,
|
|
1592
1393
|
content: cleanContent,
|
|
1593
|
-
lineCount
|
|
1394
|
+
lineCount,
|
|
1594
1395
|
placeholder,
|
|
1595
1396
|
start: insertPos,
|
|
1596
1397
|
end: insertPos + placeholder.length,
|
|
1597
|
-
summary,
|
|
1598
|
-
expanded: false,
|
|
1599
1398
|
});
|
|
1600
1399
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1601
1400
|
this.cursor = insertPos + placeholder.length;
|
|
1602
1401
|
}
|
|
1603
|
-
/**
|
|
1604
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1605
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1606
|
-
*/
|
|
1607
|
-
togglePasteExpansion() {
|
|
1608
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1609
|
-
if (!placeholder)
|
|
1610
|
-
return false;
|
|
1611
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1612
|
-
// Update the placeholder text in buffer
|
|
1613
|
-
const newPlaceholder = placeholder.expanded
|
|
1614
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1615
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1616
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1617
|
-
// Update buffer
|
|
1618
|
-
this.buffer =
|
|
1619
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1620
|
-
newPlaceholder +
|
|
1621
|
-
this.buffer.slice(placeholder.end);
|
|
1622
|
-
// Update placeholder tracking
|
|
1623
|
-
placeholder.placeholder = newPlaceholder;
|
|
1624
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1625
|
-
// Shift other placeholders
|
|
1626
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1627
|
-
this.scheduleRender();
|
|
1628
|
-
return true;
|
|
1629
|
-
}
|
|
1630
|
-
buildExpandedPlaceholder(ph) {
|
|
1631
|
-
const lines = ph.content.split('\n');
|
|
1632
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1633
|
-
const lastLines = lines.length > 5
|
|
1634
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1635
|
-
: '';
|
|
1636
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1637
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1638
|
-
}
|
|
1639
1402
|
deletePlaceholder(placeholder) {
|
|
1640
1403
|
const length = placeholder.end - placeholder.start;
|
|
1641
1404
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1643,7 +1406,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1643
1406
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1644
1407
|
this.cursor = placeholder.start;
|
|
1645
1408
|
}
|
|
1646
|
-
updateContextUsage(value) {
|
|
1409
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1410
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1411
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1412
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1413
|
+
}
|
|
1647
1414
|
if (value === null || !Number.isFinite(value)) {
|
|
1648
1415
|
this.contextUsage = null;
|
|
1649
1416
|
}
|
|
@@ -1670,6 +1437,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1670
1437
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1671
1438
|
this.setEditMode(next);
|
|
1672
1439
|
}
|
|
1440
|
+
scheduleStreamingRender(delayMs) {
|
|
1441
|
+
if (this.streamingRenderTimer)
|
|
1442
|
+
return;
|
|
1443
|
+
const wait = Math.max(16, delayMs);
|
|
1444
|
+
this.streamingRenderTimer = setTimeout(() => {
|
|
1445
|
+
this.streamingRenderTimer = null;
|
|
1446
|
+
this.render();
|
|
1447
|
+
}, wait);
|
|
1448
|
+
}
|
|
1449
|
+
resetStreamingRenderThrottle() {
|
|
1450
|
+
if (this.streamingRenderTimer) {
|
|
1451
|
+
clearTimeout(this.streamingRenderTimer);
|
|
1452
|
+
this.streamingRenderTimer = null;
|
|
1453
|
+
}
|
|
1454
|
+
this.lastStreamingRender = 0;
|
|
1455
|
+
}
|
|
1673
1456
|
scheduleRender() {
|
|
1674
1457
|
if (!this.canRender())
|
|
1675
1458
|
return;
|