erosolar-cli 1.7.252 → 1.7.253
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 -124
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +356 -616
- 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 +133 -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,225 +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, compact streaming footer takes 2 lines
|
|
219
|
-
const bannerLines = 7;
|
|
220
|
-
const streamingFooterLines = 2;
|
|
221
|
-
this.pinnedTopRows = bannerLines;
|
|
222
|
-
this.reservedLines = streamingFooterLines;
|
|
223
|
-
// Clear entire screen below banner
|
|
224
|
-
for (let i = bannerLines + 1; i <= rows; i++) {
|
|
225
|
-
this.write(ESC.TO(i, 1));
|
|
226
|
-
this.write(ESC.CLEAR_LINE);
|
|
227
|
-
}
|
|
228
|
-
// Enable scroll region: from below banner to above streaming footer
|
|
203
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
204
|
+
this.resetStreamingRenderThrottle();
|
|
229
205
|
this.enableScrollRegion();
|
|
230
|
-
// Render compact streaming footer at absolute bottom
|
|
231
|
-
this.renderStreamingFooter();
|
|
232
|
-
// Position cursor right after banner for streaming content
|
|
233
|
-
this.write(ESC.TO(bannerLines + 1, 1));
|
|
234
|
-
// Start timer to update streaming footer (every 1 second)
|
|
235
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
236
|
-
if (this.mode === 'streaming') {
|
|
237
|
-
this.renderStreamingFooter();
|
|
238
|
-
}
|
|
239
|
-
}, 1000);
|
|
240
206
|
this.renderDirty = true;
|
|
207
|
+
this.render();
|
|
241
208
|
}
|
|
242
209
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.streamingRenderTimer = null;
|
|
247
|
-
}
|
|
248
|
-
// Reset streaming time and pinned rows
|
|
249
|
-
this.streamingStartTime = null;
|
|
250
|
-
this.pinnedTopRows = 0;
|
|
251
|
-
// Disable scroll region
|
|
252
|
-
this.disableScrollRegion();
|
|
253
|
-
// Show cursor again
|
|
254
|
-
this.write(ESC.SHOW);
|
|
255
|
-
// Position cursor at end of content area and add newline
|
|
256
|
-
const { rows } = this.getSize();
|
|
257
|
-
const contentEnd = Math.max(1, rows - this.reservedLines);
|
|
258
|
-
this.write(ESC.TO(contentEnd, 1));
|
|
259
|
-
this.write('\n');
|
|
260
|
-
// Reset flow mode tracking
|
|
261
|
-
this.flowModeRenderedLines = 0;
|
|
262
|
-
// Re-render the input area in normal mode
|
|
210
|
+
// Streaming ended - render the input area
|
|
211
|
+
this.resetStreamingRenderThrottle();
|
|
212
|
+
this.enableScrollRegion();
|
|
263
213
|
this.forceRender();
|
|
264
214
|
}
|
|
265
215
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Enable or disable flow mode.
|
|
268
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
269
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
270
|
-
*/
|
|
271
|
-
setFlowMode(enabled) {
|
|
272
|
-
if (this.flowMode === enabled)
|
|
273
|
-
return;
|
|
274
|
-
this.flowMode = enabled;
|
|
275
|
-
this.renderDirty = true;
|
|
276
|
-
this.scheduleRender();
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Check if flow mode is enabled.
|
|
280
|
-
*/
|
|
281
|
-
isFlowMode() {
|
|
282
|
-
return this.flowMode;
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Set available slash commands for auto-complete suggestions.
|
|
286
|
-
*/
|
|
287
|
-
setCommands(commands) {
|
|
288
|
-
this.commandSuggestions = commands;
|
|
289
|
-
this.updateSuggestions();
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Update filtered suggestions based on current input.
|
|
293
|
-
*/
|
|
294
|
-
updateSuggestions() {
|
|
295
|
-
const input = this.buffer.trim();
|
|
296
|
-
// Only show suggestions when input starts with "/"
|
|
297
|
-
if (!input.startsWith('/')) {
|
|
298
|
-
this.showSuggestions = false;
|
|
299
|
-
this.filteredSuggestions = [];
|
|
300
|
-
this.selectedSuggestionIndex = 0;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const query = input.toLowerCase();
|
|
304
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
305
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
306
|
-
// Show suggestions if we have matches
|
|
307
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
308
|
-
// Keep selection in bounds
|
|
309
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
310
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Select next suggestion (arrow down / tab).
|
|
315
|
-
*/
|
|
316
|
-
selectNextSuggestion() {
|
|
317
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
318
|
-
return;
|
|
319
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
320
|
-
this.renderDirty = true;
|
|
321
|
-
this.scheduleRender();
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
325
|
-
*/
|
|
326
|
-
selectPrevSuggestion() {
|
|
327
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
328
|
-
return;
|
|
329
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
330
|
-
? this.filteredSuggestions.length - 1
|
|
331
|
-
: this.selectedSuggestionIndex - 1;
|
|
332
|
-
this.renderDirty = true;
|
|
333
|
-
this.scheduleRender();
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Accept current suggestion and insert into buffer.
|
|
337
|
-
*/
|
|
338
|
-
acceptSuggestion() {
|
|
339
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
340
|
-
return false;
|
|
341
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
342
|
-
if (!selected)
|
|
343
|
-
return false;
|
|
344
|
-
// Replace buffer with selected command
|
|
345
|
-
this.buffer = selected.command + ' ';
|
|
346
|
-
this.cursor = this.buffer.length;
|
|
347
|
-
this.showSuggestions = false;
|
|
348
|
-
this.renderDirty = true;
|
|
349
|
-
this.scheduleRender();
|
|
350
|
-
return true;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Check if suggestions are visible.
|
|
354
|
-
*/
|
|
355
|
-
areSuggestionsVisible() {
|
|
356
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Update token count for metrics display
|
|
360
|
-
*/
|
|
361
|
-
setTokensUsed(tokens) {
|
|
362
|
-
this.tokensUsed = tokens;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Toggle thinking/reasoning mode
|
|
366
|
-
*/
|
|
367
|
-
toggleThinking() {
|
|
368
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
369
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
370
|
-
this.scheduleRender();
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Get thinking enabled state
|
|
374
|
-
*/
|
|
375
|
-
isThinkingEnabled() {
|
|
376
|
-
return this.thinkingEnabled;
|
|
377
|
-
}
|
|
378
216
|
/**
|
|
379
217
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
380
218
|
*/
|
|
381
219
|
setPinnedHeaderLines(count) {
|
|
382
|
-
//
|
|
383
|
-
if (this.pinnedTopRows !==
|
|
384
|
-
this.pinnedTopRows =
|
|
220
|
+
// No pinned header rows anymore; keep everything in the scroll region.
|
|
221
|
+
if (this.pinnedTopRows !== 0) {
|
|
222
|
+
this.pinnedTopRows = 0;
|
|
385
223
|
if (this.scrollRegionActive) {
|
|
386
224
|
this.applyScrollRegion();
|
|
387
225
|
}
|
|
388
226
|
}
|
|
389
227
|
}
|
|
390
|
-
/**
|
|
391
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
392
|
-
* restore the default bottom-aligned layout.
|
|
393
|
-
*/
|
|
394
|
-
setInlineAnchor(row) {
|
|
395
|
-
if (row === null || row === undefined) {
|
|
396
|
-
this.inlineAnchorRow = null;
|
|
397
|
-
this.inlineLayout = false;
|
|
398
|
-
this.renderDirty = true;
|
|
399
|
-
this.render();
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
const { rows } = this.getSize();
|
|
403
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
404
|
-
this.inlineAnchorRow = clamped;
|
|
405
|
-
this.inlineLayout = true;
|
|
406
|
-
this.renderDirty = true;
|
|
407
|
-
this.render();
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
411
|
-
* output by re-evaluating the anchor before each render.
|
|
412
|
-
*/
|
|
413
|
-
setInlineAnchorProvider(provider) {
|
|
414
|
-
this.anchorProvider = provider;
|
|
415
|
-
if (!provider) {
|
|
416
|
-
this.inlineLayout = false;
|
|
417
|
-
this.inlineAnchorRow = null;
|
|
418
|
-
this.renderDirty = true;
|
|
419
|
-
this.render();
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
this.inlineLayout = true;
|
|
423
|
-
this.renderDirty = true;
|
|
424
|
-
this.render();
|
|
425
|
-
}
|
|
426
228
|
/**
|
|
427
229
|
* Get current mode
|
|
428
230
|
*/
|
|
@@ -532,6 +334,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
532
334
|
this.streamingLabel = next;
|
|
533
335
|
this.scheduleRender();
|
|
534
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
|
+
}
|
|
535
368
|
/**
|
|
536
369
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
537
370
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -541,16 +374,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
541
374
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
542
375
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
543
376
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
377
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
544
378
|
if (this.verificationEnabled === nextVerification &&
|
|
545
379
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
546
380
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
547
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
381
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
382
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
548
383
|
return;
|
|
549
384
|
}
|
|
550
385
|
this.verificationEnabled = nextVerification;
|
|
551
386
|
this.autoContinueEnabled = nextAutoContinue;
|
|
552
387
|
this.verificationHotkey = nextVerifyHotkey;
|
|
553
388
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
389
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
554
390
|
this.scheduleRender();
|
|
555
391
|
}
|
|
556
392
|
/**
|
|
@@ -565,187 +401,91 @@ export class TerminalInput extends EventEmitter {
|
|
|
565
401
|
/**
|
|
566
402
|
* Render the input area - Claude Code style with mode controls
|
|
567
403
|
*
|
|
568
|
-
* During streaming
|
|
569
|
-
*
|
|
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.
|
|
570
407
|
*/
|
|
571
408
|
render() {
|
|
572
409
|
if (!this.canRender())
|
|
573
410
|
return;
|
|
574
411
|
if (this.isRendering)
|
|
575
412
|
return;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}
|
|
579
424
|
}
|
|
580
425
|
const shouldSkip = !this.renderDirty &&
|
|
581
426
|
this.buffer === this.lastRenderContent &&
|
|
582
427
|
this.cursor === this.lastRenderCursor;
|
|
583
428
|
this.renderDirty = false;
|
|
584
|
-
// Skip if nothing changed
|
|
429
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
585
430
|
if (shouldSkip) {
|
|
586
431
|
return;
|
|
587
432
|
}
|
|
588
|
-
// If write lock is held, defer render
|
|
433
|
+
// If write lock is held, defer render to avoid race conditions
|
|
589
434
|
if (writeLock.isLocked()) {
|
|
590
435
|
writeLock.safeWrite(() => this.render());
|
|
591
436
|
return;
|
|
592
437
|
}
|
|
593
438
|
this.isRendering = true;
|
|
439
|
+
// Use write lock during render to prevent interleaved output
|
|
594
440
|
writeLock.lock('terminalInput.render');
|
|
595
441
|
try {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
finally {
|
|
600
|
-
writeLock.unlock();
|
|
601
|
-
this.isRendering = false;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
606
|
-
*
|
|
607
|
-
* Flow mode attempted inline rendering but caused duplicate renders
|
|
608
|
-
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
609
|
-
*/
|
|
610
|
-
renderFlowMode() {
|
|
611
|
-
// Use stable bottom-pinned approach
|
|
612
|
-
this.renderBottomPinned();
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
616
|
-
*
|
|
617
|
-
* Layout when suggestions visible:
|
|
618
|
-
* - Top divider
|
|
619
|
-
* - Input line(s)
|
|
620
|
-
* - Bottom divider
|
|
621
|
-
* - Suggestions (command list)
|
|
622
|
-
*
|
|
623
|
-
* Layout when suggestions hidden:
|
|
624
|
-
* - Status bar (Ready/Streaming)
|
|
625
|
-
* - Top divider
|
|
626
|
-
* - Input line(s)
|
|
627
|
-
* - Bottom divider
|
|
628
|
-
* - Mode controls
|
|
629
|
-
*/
|
|
630
|
-
renderBottomPinned() {
|
|
631
|
-
const { rows, cols } = this.getSize();
|
|
632
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
633
|
-
// Wrap buffer into display lines
|
|
634
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
635
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
636
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
637
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
638
|
-
// Calculate display window (keep cursor visible)
|
|
639
|
-
let startLine = 0;
|
|
640
|
-
if (lines.length > displayLines) {
|
|
641
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
642
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
643
|
-
}
|
|
644
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
645
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
646
|
-
// Calculate suggestion display
|
|
647
|
-
const suggestionsToShow = this.showSuggestions
|
|
648
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
649
|
-
: [];
|
|
650
|
-
const suggestionLines = suggestionsToShow.length;
|
|
651
|
-
this.write(ESC.HIDE);
|
|
652
|
-
this.write(ESC.RESET);
|
|
653
|
-
const divider = renderDivider(cols - 2);
|
|
654
|
-
// Calculate positions from absolute bottom
|
|
655
|
-
let currentRow;
|
|
656
|
-
if (suggestionLines > 0) {
|
|
657
|
-
// With suggestions: input area + dividers + suggestions
|
|
658
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
659
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
660
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
661
|
-
this.updateReservedLines(totalHeight);
|
|
662
|
-
// Top divider
|
|
663
|
-
this.write(ESC.TO(currentRow, 1));
|
|
664
|
-
this.write(ESC.CLEAR_LINE);
|
|
665
|
-
this.write(divider);
|
|
666
|
-
currentRow++;
|
|
667
|
-
// Input lines
|
|
668
|
-
let finalRow = currentRow;
|
|
669
|
-
let finalCol = 3;
|
|
670
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
671
|
-
this.write(ESC.TO(currentRow, 1));
|
|
672
|
-
this.write(ESC.CLEAR_LINE);
|
|
673
|
-
const line = visibleLines[i] ?? '';
|
|
674
|
-
const absoluteLineIdx = startLine + i;
|
|
675
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
676
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
677
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
678
|
-
if (isCursorLine) {
|
|
679
|
-
const col = Math.min(cursorCol, line.length);
|
|
680
|
-
this.write(line.slice(0, col));
|
|
681
|
-
this.write(ESC.REVERSE);
|
|
682
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
683
|
-
this.write(ESC.RESET);
|
|
684
|
-
this.write(line.slice(col + 1));
|
|
685
|
-
finalRow = currentRow;
|
|
686
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
this.write(line);
|
|
690
|
-
}
|
|
691
|
-
currentRow++;
|
|
442
|
+
if (!this.scrollRegionActive) {
|
|
443
|
+
this.enableScrollRegion();
|
|
692
444
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
this.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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) {
|
|
700
472
|
this.write(ESC.TO(currentRow, 1));
|
|
701
473
|
this.write(ESC.CLEAR_LINE);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// Indent and highlight selected
|
|
705
|
-
this.write(' ');
|
|
706
|
-
if (isSelected) {
|
|
707
|
-
this.write(ESC.REVERSE);
|
|
708
|
-
this.write(ESC.BOLD);
|
|
709
|
-
}
|
|
710
|
-
this.write(suggestion.command);
|
|
711
|
-
if (isSelected) {
|
|
712
|
-
this.write(ESC.RESET);
|
|
713
|
-
}
|
|
714
|
-
// Description (dimmed)
|
|
715
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
716
|
-
if (descSpace > 10 && suggestion.description) {
|
|
717
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
718
|
-
this.write(ESC.RESET);
|
|
719
|
-
this.write(ESC.DIM);
|
|
720
|
-
this.write(' ');
|
|
721
|
-
this.write(desc);
|
|
722
|
-
this.write(ESC.RESET);
|
|
723
|
-
}
|
|
724
|
-
currentRow++;
|
|
474
|
+
this.write(metaLine);
|
|
475
|
+
currentRow += 1;
|
|
725
476
|
}
|
|
726
|
-
//
|
|
727
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
// Without suggestions: normal layout with status bar and controls
|
|
731
|
-
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
732
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
733
|
-
this.updateReservedLines(totalHeight);
|
|
734
|
-
// Status bar
|
|
735
|
-
this.write(ESC.TO(currentRow, 1));
|
|
736
|
-
this.write(ESC.CLEAR_LINE);
|
|
737
|
-
this.write(this.buildStatusBar(cols));
|
|
738
|
-
currentRow++;
|
|
739
|
-
// Top divider
|
|
477
|
+
// Separator line
|
|
740
478
|
this.write(ESC.TO(currentRow, 1));
|
|
741
479
|
this.write(ESC.CLEAR_LINE);
|
|
480
|
+
const divider = renderDivider(cols - 2);
|
|
742
481
|
this.write(divider);
|
|
743
|
-
currentRow
|
|
744
|
-
//
|
|
482
|
+
currentRow += 1;
|
|
483
|
+
// Render input lines
|
|
745
484
|
let finalRow = currentRow;
|
|
746
485
|
let finalCol = 3;
|
|
747
486
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
748
|
-
|
|
487
|
+
const rowNum = currentRow + i;
|
|
488
|
+
this.write(ESC.TO(rowNum, 1));
|
|
749
489
|
this.write(ESC.CLEAR_LINE);
|
|
750
490
|
const line = visibleLines[i] ?? '';
|
|
751
491
|
const absoluteLineIdx = startLine + i;
|
|
@@ -759,6 +499,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
759
499
|
this.write(ESC.RESET);
|
|
760
500
|
this.write(ESC.BG_DARK);
|
|
761
501
|
if (isCursorLine) {
|
|
502
|
+
// Render with block cursor
|
|
762
503
|
const col = Math.min(cursorCol, line.length);
|
|
763
504
|
const before = line.slice(0, col);
|
|
764
505
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -768,189 +509,219 @@ export class TerminalInput extends EventEmitter {
|
|
|
768
509
|
this.write(at);
|
|
769
510
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
770
511
|
this.write(after);
|
|
771
|
-
finalRow =
|
|
512
|
+
finalRow = rowNum;
|
|
772
513
|
finalCol = this.config.promptChar.length + col + 1;
|
|
773
514
|
}
|
|
774
515
|
else {
|
|
775
516
|
this.write(line);
|
|
776
517
|
}
|
|
777
|
-
// Pad to edge
|
|
518
|
+
// Pad to edge for clean look
|
|
778
519
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
779
520
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
780
521
|
if (padding > 0)
|
|
781
522
|
this.write(' '.repeat(padding));
|
|
782
523
|
this.write(ESC.RESET);
|
|
783
|
-
currentRow++;
|
|
784
524
|
}
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
this.write(ESC.
|
|
788
|
-
this.write(divider);
|
|
789
|
-
currentRow++;
|
|
790
|
-
// Mode controls
|
|
791
|
-
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));
|
|
792
528
|
this.write(ESC.CLEAR_LINE);
|
|
793
529
|
this.write(this.buildModeControls(cols));
|
|
794
|
-
// Position cursor
|
|
530
|
+
// Position cursor
|
|
795
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;
|
|
796
550
|
}
|
|
797
|
-
this.write(ESC.SHOW);
|
|
798
|
-
// Update state
|
|
799
|
-
this.lastRenderContent = this.buffer;
|
|
800
|
-
this.lastRenderCursor = this.cursor;
|
|
801
551
|
}
|
|
802
552
|
/**
|
|
803
|
-
*
|
|
804
|
-
* Line 1: Divider
|
|
805
|
-
* Line 2: Streaming stats (elapsed, tokens/sec, mode icons, context)
|
|
553
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
806
554
|
*/
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
this.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
this.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
this.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
this.autoContinueEnabled ? `${MAGENTA}↻${R}` : `${DIM}○${R}`,
|
|
838
|
-
].join(' ');
|
|
839
|
-
// Context usage
|
|
840
|
-
let ctxPart = '';
|
|
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
|
+
}
|
|
841
585
|
if (this.contextUsage !== null) {
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const leftLen = strip(leftPart + queuePart).length;
|
|
854
|
-
const rightLen = strip(rightPart).length;
|
|
855
|
-
const space = Math.max(2, cols - leftLen - rightLen - 2);
|
|
856
|
-
this.write(leftPart + queuePart + ' '.repeat(space) + rightPart);
|
|
857
|
-
// Restore cursor position to continue streaming
|
|
858
|
-
this.write(ESC.RESTORE);
|
|
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;
|
|
859
597
|
}
|
|
860
598
|
/**
|
|
861
|
-
*
|
|
862
|
-
* 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.
|
|
863
600
|
*/
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if (this.streamingStartTime) {
|
|
871
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
872
|
-
const mins = Math.floor(elapsed / 60);
|
|
873
|
-
const secs = elapsed % 60;
|
|
874
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
875
|
-
}
|
|
876
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
877
|
-
}
|
|
878
|
-
// Queue indicator during streaming
|
|
879
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
880
|
-
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
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));
|
|
881
607
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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' });
|
|
886
619
|
}
|
|
887
|
-
// Override/warning status
|
|
888
620
|
if (this.overrideStatusMessage) {
|
|
889
|
-
|
|
621
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
890
622
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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' });
|
|
894
644
|
}
|
|
895
|
-
// Multi-line indicator
|
|
896
645
|
if (this.buffer.includes('\n')) {
|
|
897
|
-
|
|
646
|
+
const lineCount = this.buffer.split('\n').length;
|
|
647
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
648
|
+
}
|
|
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 });
|
|
898
666
|
}
|
|
899
|
-
if (
|
|
900
|
-
|
|
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;
|
|
901
687
|
}
|
|
902
|
-
const
|
|
903
|
-
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;
|
|
904
714
|
}
|
|
905
715
|
/**
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* 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.
|
|
910
718
|
*/
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
918
|
-
if (this.editMode === 'display-edits') {
|
|
919
|
-
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
920
|
-
}
|
|
921
|
-
else {
|
|
922
|
-
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
923
|
-
}
|
|
924
|
-
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
925
|
-
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
926
|
-
// Verification (green when on) - per schema.verificationMode
|
|
927
|
-
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
928
|
-
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
929
|
-
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
930
|
-
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
931
|
-
// Context usage with color - per schema.contextUsage thresholds
|
|
932
|
-
let rightPart = '';
|
|
933
|
-
if (this.contextUsage !== null) {
|
|
934
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
935
|
-
// Thresholds: critical < 10%, warning < 25%
|
|
936
|
-
if (rem < 10)
|
|
937
|
-
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
938
|
-
else if (rem < 25)
|
|
939
|
-
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
940
|
-
else
|
|
941
|
-
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
942
|
-
}
|
|
943
|
-
// Calculate visible lengths (strip ANSI)
|
|
944
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
945
|
-
const leftLen = strip(leftPart).length;
|
|
946
|
-
const rightLen = strip(rightPart).length;
|
|
947
|
-
if (leftLen + rightLen < maxWidth - 4) {
|
|
948
|
-
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
949
|
-
}
|
|
950
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
951
|
-
return `${leftPart} ${rightPart}`;
|
|
952
|
-
}
|
|
953
|
-
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
|
+
};
|
|
954
725
|
}
|
|
955
726
|
/**
|
|
956
727
|
* Force a re-render
|
|
@@ -973,17 +744,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
973
744
|
handleResize() {
|
|
974
745
|
this.lastRenderContent = '';
|
|
975
746
|
this.lastRenderCursor = -1;
|
|
747
|
+
this.resetStreamingRenderThrottle();
|
|
976
748
|
// Re-clamp pinned header rows to the new terminal height
|
|
977
749
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
750
|
+
if (this.scrollRegionActive) {
|
|
751
|
+
this.disableScrollRegion();
|
|
752
|
+
this.enableScrollRegion();
|
|
753
|
+
}
|
|
978
754
|
this.scheduleRender();
|
|
979
755
|
}
|
|
980
756
|
/**
|
|
981
757
|
* Register with display's output interceptor to position cursor correctly.
|
|
982
758
|
* When scroll region is active, output needs to go to the scroll region,
|
|
983
759
|
* not the protected bottom area where the input is rendered.
|
|
984
|
-
*
|
|
985
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
986
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
987
760
|
*/
|
|
988
761
|
registerOutputInterceptor(display) {
|
|
989
762
|
if (this.outputInterceptorCleanup) {
|
|
@@ -991,11 +764,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
991
764
|
}
|
|
992
765
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
993
766
|
beforeWrite: () => {
|
|
994
|
-
//
|
|
995
|
-
//
|
|
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
|
+
}
|
|
996
775
|
},
|
|
997
776
|
afterWrite: () => {
|
|
998
|
-
//
|
|
777
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
778
|
+
if (this.scrollRegionActive) {
|
|
779
|
+
this.write(ESC.RESTORE);
|
|
780
|
+
}
|
|
999
781
|
},
|
|
1000
782
|
});
|
|
1001
783
|
}
|
|
@@ -1005,11 +787,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1005
787
|
dispose() {
|
|
1006
788
|
if (this.disposed)
|
|
1007
789
|
return;
|
|
1008
|
-
// Clean up streaming render timer
|
|
1009
|
-
if (this.streamingRenderTimer) {
|
|
1010
|
-
clearInterval(this.streamingRenderTimer);
|
|
1011
|
-
this.streamingRenderTimer = null;
|
|
1012
|
-
}
|
|
1013
790
|
// Clean up output interceptor
|
|
1014
791
|
if (this.outputInterceptorCleanup) {
|
|
1015
792
|
this.outputInterceptorCleanup();
|
|
@@ -1017,6 +794,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1017
794
|
}
|
|
1018
795
|
this.disposed = true;
|
|
1019
796
|
this.enabled = false;
|
|
797
|
+
this.resetStreamingRenderThrottle();
|
|
1020
798
|
this.disableScrollRegion();
|
|
1021
799
|
this.disableBracketedPaste();
|
|
1022
800
|
this.buffer = '';
|
|
@@ -1122,22 +900,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1122
900
|
this.toggleEditMode();
|
|
1123
901
|
return true;
|
|
1124
902
|
}
|
|
1125
|
-
|
|
1126
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1127
|
-
this.togglePasteExpansion();
|
|
1128
|
-
}
|
|
1129
|
-
else {
|
|
1130
|
-
this.toggleThinking();
|
|
1131
|
-
}
|
|
1132
|
-
return true;
|
|
1133
|
-
case 'escape':
|
|
1134
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1135
|
-
if (this.mode === 'streaming') {
|
|
1136
|
-
this.emit('interrupt');
|
|
1137
|
-
}
|
|
1138
|
-
else if (this.buffer.length > 0) {
|
|
1139
|
-
this.clear();
|
|
1140
|
-
}
|
|
903
|
+
this.insertText(' ');
|
|
1141
904
|
return true;
|
|
1142
905
|
}
|
|
1143
906
|
return false;
|
|
@@ -1155,7 +918,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1155
918
|
this.insertPlainText(chunk, insertPos);
|
|
1156
919
|
this.cursor = insertPos + chunk.length;
|
|
1157
920
|
this.emit('change', this.buffer);
|
|
1158
|
-
this.updateSuggestions();
|
|
1159
921
|
this.scheduleRender();
|
|
1160
922
|
}
|
|
1161
923
|
insertNewline() {
|
|
@@ -1180,7 +942,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1180
942
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1181
943
|
}
|
|
1182
944
|
this.emit('change', this.buffer);
|
|
1183
|
-
this.updateSuggestions();
|
|
1184
945
|
this.scheduleRender();
|
|
1185
946
|
}
|
|
1186
947
|
deleteForward() {
|
|
@@ -1430,7 +1191,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1430
1191
|
if (available <= 0)
|
|
1431
1192
|
return;
|
|
1432
1193
|
const chunk = clean.slice(0, available);
|
|
1433
|
-
|
|
1194
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1195
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1196
|
+
if (isMultiline && !isShortMultiline) {
|
|
1434
1197
|
this.insertPastePlaceholder(chunk);
|
|
1435
1198
|
}
|
|
1436
1199
|
else {
|
|
@@ -1450,6 +1213,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1450
1213
|
return;
|
|
1451
1214
|
this.applyScrollRegion();
|
|
1452
1215
|
this.scrollRegionActive = true;
|
|
1216
|
+
this.forceRender();
|
|
1453
1217
|
}
|
|
1454
1218
|
disableScrollRegion() {
|
|
1455
1219
|
if (!this.scrollRegionActive)
|
|
@@ -1600,17 +1364,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1600
1364
|
this.shiftPlaceholders(position, text.length);
|
|
1601
1365
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1602
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
|
+
}
|
|
1603
1373
|
findPlaceholderAt(position) {
|
|
1604
1374
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1605
1375
|
}
|
|
1606
|
-
buildPlaceholder(
|
|
1376
|
+
buildPlaceholder(lineCount) {
|
|
1607
1377
|
const id = ++this.pasteCounter;
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
const preview = summary.preview.length > 30
|
|
1611
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1612
|
-
: summary.preview;
|
|
1613
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1378
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1379
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1614
1380
|
return { id, placeholder };
|
|
1615
1381
|
}
|
|
1616
1382
|
insertPastePlaceholder(content) {
|
|
@@ -1618,67 +1384,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1618
1384
|
if (available <= 0)
|
|
1619
1385
|
return;
|
|
1620
1386
|
const cleanContent = content.slice(0, available);
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
if (summary.lineCount < 5) {
|
|
1624
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1625
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1626
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1627
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1387
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1388
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1631
1389
|
const insertPos = this.cursor;
|
|
1632
1390
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1633
1391
|
this.pastePlaceholders.push({
|
|
1634
1392
|
id,
|
|
1635
1393
|
content: cleanContent,
|
|
1636
|
-
lineCount
|
|
1394
|
+
lineCount,
|
|
1637
1395
|
placeholder,
|
|
1638
1396
|
start: insertPos,
|
|
1639
1397
|
end: insertPos + placeholder.length,
|
|
1640
|
-
summary,
|
|
1641
|
-
expanded: false,
|
|
1642
1398
|
});
|
|
1643
1399
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1644
1400
|
this.cursor = insertPos + placeholder.length;
|
|
1645
1401
|
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1648
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1649
|
-
*/
|
|
1650
|
-
togglePasteExpansion() {
|
|
1651
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1652
|
-
if (!placeholder)
|
|
1653
|
-
return false;
|
|
1654
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1655
|
-
// Update the placeholder text in buffer
|
|
1656
|
-
const newPlaceholder = placeholder.expanded
|
|
1657
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1658
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1659
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1660
|
-
// Update buffer
|
|
1661
|
-
this.buffer =
|
|
1662
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1663
|
-
newPlaceholder +
|
|
1664
|
-
this.buffer.slice(placeholder.end);
|
|
1665
|
-
// Update placeholder tracking
|
|
1666
|
-
placeholder.placeholder = newPlaceholder;
|
|
1667
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1668
|
-
// Shift other placeholders
|
|
1669
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1670
|
-
this.scheduleRender();
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
buildExpandedPlaceholder(ph) {
|
|
1674
|
-
const lines = ph.content.split('\n');
|
|
1675
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1676
|
-
const lastLines = lines.length > 5
|
|
1677
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1678
|
-
: '';
|
|
1679
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1680
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1681
|
-
}
|
|
1682
1402
|
deletePlaceholder(placeholder) {
|
|
1683
1403
|
const length = placeholder.end - placeholder.start;
|
|
1684
1404
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1686,7 +1406,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1686
1406
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1687
1407
|
this.cursor = placeholder.start;
|
|
1688
1408
|
}
|
|
1689
|
-
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
|
+
}
|
|
1690
1414
|
if (value === null || !Number.isFinite(value)) {
|
|
1691
1415
|
this.contextUsage = null;
|
|
1692
1416
|
}
|
|
@@ -1713,6 +1437,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
1713
1437
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1714
1438
|
this.setEditMode(next);
|
|
1715
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
|
+
}
|
|
1716
1456
|
scheduleRender() {
|
|
1717
1457
|
if (!this.canRender())
|
|
1718
1458
|
return;
|