erosolar-cli 1.7.253 → 1.7.254
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -22
- package/dist/core/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/customCommands.d.ts +0 -1
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +0 -3
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +14 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +5 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +3 -14
- package/dist/core/toolValidation.js.map +1 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +2 -10
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +31 -183
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/terminalInput.d.ts +131 -54
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +643 -353
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +15 -12
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +22 -8
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +0 -19
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +2 -133
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/terminalUISchema.d.ts +195 -0
- package/dist/ui/terminalUISchema.d.ts.map +1 -0
- package/dist/ui/terminalUISchema.js +113 -0
- package/dist/ui/terminalUISchema.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +0 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +25 -15
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,18 +3,16 @@
|
|
|
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)
|
|
7
6
|
* - Native bracketed paste support (no heuristics)
|
|
8
7
|
* - Clean cursor model with render-time wrapping
|
|
9
8
|
* - State machine for different input modes
|
|
10
9
|
* - No readline dependency for display
|
|
11
10
|
*/
|
|
12
11
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
14
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
15
|
-
import { renderDivider
|
|
16
|
-
import {
|
|
17
|
-
import { formatThinking } from '../ui/toolDisplay.js';
|
|
14
|
+
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
|
+
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
18
16
|
// ANSI escape codes
|
|
19
17
|
const ESC = {
|
|
20
18
|
// Cursor control
|
|
@@ -69,11 +67,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
67
|
statusMessage = null;
|
|
70
68
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
69
|
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
|
|
77
70
|
reservedLines = 2;
|
|
78
71
|
scrollRegionActive = false;
|
|
79
72
|
lastRenderContent = '';
|
|
@@ -81,12 +74,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
81
74
|
renderDirty = false;
|
|
82
75
|
isRendering = false;
|
|
83
76
|
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;
|
|
84
89
|
// Lifecycle
|
|
85
90
|
disposed = false;
|
|
86
91
|
enabled = true;
|
|
87
92
|
contextUsage = null;
|
|
88
|
-
contextAutoCompactThreshold = 90;
|
|
89
|
-
thinkingModeLabel = null;
|
|
90
93
|
editMode = 'display-edits';
|
|
91
94
|
verificationEnabled = true;
|
|
92
95
|
autoContinueEnabled = false;
|
|
@@ -94,19 +97,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
94
97
|
autoContinueHotkey = 'alt+c';
|
|
95
98
|
// Output interceptor cleanup
|
|
96
99
|
outputInterceptorCleanup;
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
// Metrics tracking for status bar
|
|
101
|
+
streamingStartTime = null;
|
|
102
|
+
tokensUsed = 0;
|
|
103
|
+
thinkingEnabled = true;
|
|
104
|
+
// Streaming input area render timer (updates elapsed time display)
|
|
100
105
|
streamingRenderTimer = null;
|
|
101
106
|
constructor(writeStream = process.stdout, config = {}) {
|
|
102
107
|
super();
|
|
103
108
|
this.out = writeStream;
|
|
109
|
+
// Use schema defaults for configuration consistency
|
|
104
110
|
this.config = {
|
|
105
|
-
maxLines: config.maxLines ??
|
|
106
|
-
maxLength: config.maxLength ??
|
|
111
|
+
maxLines: config.maxLines ?? DEFAULT_UI_CONFIG.inputArea.maxLines,
|
|
112
|
+
maxLength: config.maxLength ?? DEFAULT_UI_CONFIG.inputArea.maxLength,
|
|
107
113
|
maxQueueSize: config.maxQueueSize ?? 100,
|
|
108
|
-
promptChar: config.promptChar ??
|
|
109
|
-
continuationChar: config.continuationChar ??
|
|
114
|
+
promptChar: config.promptChar ?? DEFAULT_UI_CONFIG.inputArea.promptChar,
|
|
115
|
+
continuationChar: config.continuationChar ?? DEFAULT_UI_CONFIG.inputArea.continuationChar,
|
|
110
116
|
};
|
|
111
117
|
}
|
|
112
118
|
// ===========================================================================
|
|
@@ -185,6 +191,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
185
191
|
if (handled)
|
|
186
192
|
return;
|
|
187
193
|
}
|
|
194
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
195
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
196
|
+
this.emit('showHelp');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
188
199
|
// Insert printable characters
|
|
189
200
|
if (str && !key?.ctrl && !key?.meta) {
|
|
190
201
|
this.insertText(str);
|
|
@@ -193,38 +204,225 @@ export class TerminalInput extends EventEmitter {
|
|
|
193
204
|
/**
|
|
194
205
|
* Set the input mode
|
|
195
206
|
*
|
|
196
|
-
* Streaming
|
|
197
|
-
*
|
|
207
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
208
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
209
|
+
* the cursor is (below the streamed content).
|
|
198
210
|
*/
|
|
199
211
|
setMode(mode) {
|
|
200
212
|
const prevMode = this.mode;
|
|
201
213
|
this.mode = mode;
|
|
202
214
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
203
|
-
//
|
|
204
|
-
this.
|
|
215
|
+
// Track streaming start time for elapsed display
|
|
216
|
+
this.streamingStartTime = Date.now();
|
|
217
|
+
const { rows } = this.getSize();
|
|
218
|
+
// Banner ~7 lines, full input area 5 lines (status + div + input + div + controls)
|
|
219
|
+
const bannerLines = 7;
|
|
220
|
+
const inputAreaLines = 5;
|
|
221
|
+
this.pinnedTopRows = bannerLines;
|
|
222
|
+
this.reservedLines = inputAreaLines;
|
|
223
|
+
// Clear content area (between banner and input area)
|
|
224
|
+
for (let i = bannerLines + 1; i <= rows - inputAreaLines; i++) {
|
|
225
|
+
this.write(ESC.TO(i, 1));
|
|
226
|
+
this.write(ESC.CLEAR_LINE);
|
|
227
|
+
}
|
|
228
|
+
// Enable scroll region: from below banner to above input area
|
|
205
229
|
this.enableScrollRegion();
|
|
230
|
+
// Render full input area at bottom (user can type to queue)
|
|
231
|
+
this.renderInputAreaDuringStreaming();
|
|
232
|
+
// Position cursor in scroll region for streaming content
|
|
233
|
+
this.write(ESC.TO(bannerLines + 1, 1));
|
|
234
|
+
// Start timer to update status bar with elapsed time
|
|
235
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
236
|
+
if (this.mode === 'streaming') {
|
|
237
|
+
this.updateStreamingStatusBar();
|
|
238
|
+
}
|
|
239
|
+
}, 1000);
|
|
206
240
|
this.renderDirty = true;
|
|
207
|
-
this.render();
|
|
208
241
|
}
|
|
209
242
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
210
|
-
//
|
|
211
|
-
this.
|
|
212
|
-
|
|
243
|
+
// Stop streaming render timer
|
|
244
|
+
if (this.streamingRenderTimer) {
|
|
245
|
+
clearInterval(this.streamingRenderTimer);
|
|
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
|
|
213
263
|
this.forceRender();
|
|
214
264
|
}
|
|
215
265
|
}
|
|
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
|
+
}
|
|
216
378
|
/**
|
|
217
379
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
218
380
|
*/
|
|
219
381
|
setPinnedHeaderLines(count) {
|
|
220
|
-
//
|
|
221
|
-
if (this.pinnedTopRows !==
|
|
222
|
-
this.pinnedTopRows =
|
|
382
|
+
// Set pinned header rows (banner area that scroll region excludes)
|
|
383
|
+
if (this.pinnedTopRows !== count) {
|
|
384
|
+
this.pinnedTopRows = count;
|
|
223
385
|
if (this.scrollRegionActive) {
|
|
224
386
|
this.applyScrollRegion();
|
|
225
387
|
}
|
|
226
388
|
}
|
|
227
389
|
}
|
|
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
|
+
}
|
|
228
426
|
/**
|
|
229
427
|
* Get current mode
|
|
230
428
|
*/
|
|
@@ -334,37 +532,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
334
532
|
this.streamingLabel = next;
|
|
335
533
|
this.scheduleRender();
|
|
336
534
|
}
|
|
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
|
-
}
|
|
368
535
|
/**
|
|
369
536
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
370
537
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -374,19 +541,16 @@ export class TerminalInput extends EventEmitter {
|
|
|
374
541
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
375
542
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
376
543
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
377
|
-
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
378
544
|
if (this.verificationEnabled === nextVerification &&
|
|
379
545
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
380
546
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
381
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
382
|
-
this.thinkingModeLabel === nextThinkingLabel) {
|
|
547
|
+
this.autoContinueHotkey === nextAutoHotkey) {
|
|
383
548
|
return;
|
|
384
549
|
}
|
|
385
550
|
this.verificationEnabled = nextVerification;
|
|
386
551
|
this.autoContinueEnabled = nextAutoContinue;
|
|
387
552
|
this.verificationHotkey = nextVerifyHotkey;
|
|
388
553
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
389
|
-
this.thinkingModeLabel = nextThinkingLabel;
|
|
390
554
|
this.scheduleRender();
|
|
391
555
|
}
|
|
392
556
|
/**
|
|
@@ -401,91 +565,187 @@ export class TerminalInput extends EventEmitter {
|
|
|
401
565
|
/**
|
|
402
566
|
* Render the input area - Claude Code style with mode controls
|
|
403
567
|
*
|
|
404
|
-
* During streaming
|
|
405
|
-
* pinned status/input block (throttled) so streamed content can scroll
|
|
406
|
-
* naturally above while elapsed time and status stay fresh.
|
|
568
|
+
* During streaming, uses specialized streaming render to avoid cursor conflicts.
|
|
407
569
|
*/
|
|
408
570
|
render() {
|
|
409
571
|
if (!this.canRender())
|
|
410
572
|
return;
|
|
411
573
|
if (this.isRendering)
|
|
412
574
|
return;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
}
|
|
575
|
+
// During streaming, use streaming-specific render that preserves cursor
|
|
576
|
+
if (this.mode === 'streaming') {
|
|
577
|
+
this.renderInputAreaDuringStreaming();
|
|
578
|
+
return;
|
|
424
579
|
}
|
|
425
580
|
const shouldSkip = !this.renderDirty &&
|
|
426
581
|
this.buffer === this.lastRenderContent &&
|
|
427
582
|
this.cursor === this.lastRenderCursor;
|
|
428
583
|
this.renderDirty = false;
|
|
429
|
-
// Skip if nothing changed
|
|
584
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
430
585
|
if (shouldSkip) {
|
|
431
586
|
return;
|
|
432
587
|
}
|
|
433
|
-
// If write lock is held, defer render
|
|
588
|
+
// If write lock is held, defer render
|
|
434
589
|
if (writeLock.isLocked()) {
|
|
435
590
|
writeLock.safeWrite(() => this.render());
|
|
436
591
|
return;
|
|
437
592
|
}
|
|
438
593
|
this.isRendering = true;
|
|
439
|
-
// Use write lock during render to prevent interleaved output
|
|
440
594
|
writeLock.lock('terminalInput.render');
|
|
441
595
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
596
|
+
// Render input area at bottom (outside scroll region)
|
|
597
|
+
this.renderBottomPinned();
|
|
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++;
|
|
460
692
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
this.write(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
let
|
|
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) {
|
|
693
|
+
// Bottom divider
|
|
694
|
+
this.write(ESC.TO(currentRow, 1));
|
|
695
|
+
this.write(ESC.CLEAR_LINE);
|
|
696
|
+
this.write(divider);
|
|
697
|
+
currentRow++;
|
|
698
|
+
// Suggestions (Claude Code style)
|
|
699
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
472
700
|
this.write(ESC.TO(currentRow, 1));
|
|
473
701
|
this.write(ESC.CLEAR_LINE);
|
|
474
|
-
|
|
475
|
-
|
|
702
|
+
const suggestion = suggestionsToShow[i];
|
|
703
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
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++;
|
|
476
725
|
}
|
|
477
|
-
//
|
|
726
|
+
// Position cursor in input area
|
|
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
|
|
478
740
|
this.write(ESC.TO(currentRow, 1));
|
|
479
741
|
this.write(ESC.CLEAR_LINE);
|
|
480
|
-
const divider = renderDivider(cols - 2);
|
|
481
742
|
this.write(divider);
|
|
482
|
-
currentRow
|
|
483
|
-
//
|
|
743
|
+
currentRow++;
|
|
744
|
+
// Input lines
|
|
484
745
|
let finalRow = currentRow;
|
|
485
746
|
let finalCol = 3;
|
|
486
747
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
487
|
-
|
|
488
|
-
this.write(ESC.TO(rowNum, 1));
|
|
748
|
+
this.write(ESC.TO(currentRow, 1));
|
|
489
749
|
this.write(ESC.CLEAR_LINE);
|
|
490
750
|
const line = visibleLines[i] ?? '';
|
|
491
751
|
const absoluteLineIdx = startLine + i;
|
|
@@ -499,7 +759,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
499
759
|
this.write(ESC.RESET);
|
|
500
760
|
this.write(ESC.BG_DARK);
|
|
501
761
|
if (isCursorLine) {
|
|
502
|
-
// Render with block cursor
|
|
503
762
|
const col = Math.min(cursorCol, line.length);
|
|
504
763
|
const before = line.slice(0, col);
|
|
505
764
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -509,219 +768,219 @@ export class TerminalInput extends EventEmitter {
|
|
|
509
768
|
this.write(at);
|
|
510
769
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
511
770
|
this.write(after);
|
|
512
|
-
finalRow =
|
|
771
|
+
finalRow = currentRow;
|
|
513
772
|
finalCol = this.config.promptChar.length + col + 1;
|
|
514
773
|
}
|
|
515
774
|
else {
|
|
516
775
|
this.write(line);
|
|
517
776
|
}
|
|
518
|
-
// Pad to edge
|
|
777
|
+
// Pad to edge
|
|
519
778
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
520
779
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
521
780
|
if (padding > 0)
|
|
522
781
|
this.write(' '.repeat(padding));
|
|
523
782
|
this.write(ESC.RESET);
|
|
783
|
+
currentRow++;
|
|
524
784
|
}
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
this.write(ESC.
|
|
785
|
+
// Bottom divider
|
|
786
|
+
this.write(ESC.TO(currentRow, 1));
|
|
787
|
+
this.write(ESC.CLEAR_LINE);
|
|
788
|
+
this.write(divider);
|
|
789
|
+
currentRow++;
|
|
790
|
+
// Mode controls
|
|
791
|
+
this.write(ESC.TO(currentRow, 1));
|
|
528
792
|
this.write(ESC.CLEAR_LINE);
|
|
529
793
|
this.write(this.buildModeControls(cols));
|
|
530
|
-
// Position cursor
|
|
794
|
+
// Position cursor in input area
|
|
531
795
|
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;
|
|
550
796
|
}
|
|
797
|
+
this.write(ESC.SHOW);
|
|
798
|
+
// Update state
|
|
799
|
+
this.lastRenderContent = this.buffer;
|
|
800
|
+
this.lastRenderCursor = this.cursor;
|
|
551
801
|
}
|
|
552
802
|
/**
|
|
553
|
-
*
|
|
803
|
+
* Render full input area during streaming at absolute bottom.
|
|
804
|
+
* Same layout as normal but shows streaming status instead of "Type a message".
|
|
805
|
+
* User can type to queue messages.
|
|
554
806
|
*/
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
807
|
+
renderInputAreaDuringStreaming() {
|
|
808
|
+
if (!this.isTTY())
|
|
809
|
+
return;
|
|
810
|
+
const { rows, cols } = this.getSize();
|
|
811
|
+
const divider = renderDivider(cols - 2);
|
|
812
|
+
// Save cursor position
|
|
813
|
+
this.write(ESC.SAVE);
|
|
814
|
+
// Calculate positions (5 lines: status + div + input + div + controls)
|
|
815
|
+
let currentRow = rows - this.reservedLines + 1;
|
|
816
|
+
// Line 1: Status bar (streaming status)
|
|
817
|
+
this.write(ESC.TO(currentRow, 1));
|
|
818
|
+
this.write(ESC.CLEAR_LINE);
|
|
819
|
+
this.write(this.buildStreamingStatusBar(cols));
|
|
820
|
+
currentRow++;
|
|
821
|
+
// Line 2: Top divider
|
|
822
|
+
this.write(ESC.TO(currentRow, 1));
|
|
823
|
+
this.write(ESC.CLEAR_LINE);
|
|
824
|
+
this.write(divider);
|
|
825
|
+
currentRow++;
|
|
826
|
+
// Line 3: Input prompt (user can type to queue)
|
|
827
|
+
this.write(ESC.TO(currentRow, 1));
|
|
828
|
+
this.write(ESC.CLEAR_LINE);
|
|
829
|
+
this.write(ESC.BG_DARK);
|
|
830
|
+
this.write(ESC.DIM);
|
|
831
|
+
this.write(this.config.promptChar);
|
|
832
|
+
this.write(this.buffer);
|
|
833
|
+
// Pad to edge
|
|
834
|
+
const inputLen = this.config.promptChar.length + this.buffer.length;
|
|
835
|
+
const padding = Math.max(0, cols - inputLen - 1);
|
|
836
|
+
if (padding > 0)
|
|
837
|
+
this.write(' '.repeat(padding));
|
|
838
|
+
this.write(ESC.RESET);
|
|
839
|
+
currentRow++;
|
|
840
|
+
// Line 4: Bottom divider
|
|
841
|
+
this.write(ESC.TO(currentRow, 1));
|
|
842
|
+
this.write(ESC.CLEAR_LINE);
|
|
843
|
+
this.write(divider);
|
|
844
|
+
currentRow++;
|
|
845
|
+
// Line 5: Mode controls
|
|
846
|
+
this.write(ESC.TO(currentRow, 1));
|
|
847
|
+
this.write(ESC.CLEAR_LINE);
|
|
848
|
+
this.write(this.buildModeControls(cols));
|
|
849
|
+
// Restore cursor position
|
|
850
|
+
this.write(ESC.RESTORE);
|
|
597
851
|
}
|
|
598
852
|
/**
|
|
599
|
-
*
|
|
853
|
+
* Update only the status bar during streaming (with elapsed time).
|
|
600
854
|
*/
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
855
|
+
updateStreamingStatusBar() {
|
|
856
|
+
if (!this.isTTY())
|
|
857
|
+
return;
|
|
858
|
+
const { rows, cols } = this.getSize();
|
|
859
|
+
// Save cursor
|
|
860
|
+
this.write(ESC.SAVE);
|
|
861
|
+
// Status bar is first line of input area
|
|
862
|
+
const statusRow = rows - this.reservedLines + 1;
|
|
863
|
+
this.write(ESC.TO(statusRow, 1));
|
|
864
|
+
this.write(ESC.CLEAR_LINE);
|
|
865
|
+
this.write(this.buildStreamingStatusBar(cols));
|
|
866
|
+
// Restore cursor
|
|
867
|
+
this.write(ESC.RESTORE);
|
|
608
868
|
}
|
|
609
869
|
/**
|
|
610
|
-
* Build
|
|
611
|
-
* Combines streaming label + override status + main status for simultaneous display.
|
|
870
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
612
871
|
*/
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (this.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
872
|
+
buildStreamingStatusBar(cols) {
|
|
873
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
874
|
+
// Streaming status with elapsed time
|
|
875
|
+
let elapsed = '0s';
|
|
876
|
+
if (this.streamingStartTime) {
|
|
877
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
878
|
+
const mins = Math.floor(secs / 60);
|
|
879
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
880
|
+
}
|
|
881
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
882
|
+
// Queue indicator
|
|
883
|
+
if (this.queue.length > 0) {
|
|
884
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
625
885
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
886
|
+
// Hint for typing
|
|
887
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
888
|
+
return status;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Build status bar showing streaming/ready status and key info.
|
|
892
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
893
|
+
*/
|
|
894
|
+
buildStatusBar(cols) {
|
|
895
|
+
const maxWidth = cols - 2;
|
|
896
|
+
const parts = [];
|
|
897
|
+
// Streaming status with elapsed time (left side)
|
|
898
|
+
if (this.mode === 'streaming') {
|
|
899
|
+
let statusText = '● Streaming';
|
|
900
|
+
if (this.streamingStartTime) {
|
|
901
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
902
|
+
const mins = Math.floor(elapsed / 60);
|
|
903
|
+
const secs = elapsed % 60;
|
|
904
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
905
|
+
}
|
|
906
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
644
907
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
908
|
+
// Queue indicator during streaming
|
|
909
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
910
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
648
911
|
}
|
|
912
|
+
// Paste indicator
|
|
649
913
|
if (this.pastePlaceholders.length > 0) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
653
|
-
tone: 'info',
|
|
654
|
-
});
|
|
914
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
915
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
655
916
|
}
|
|
656
|
-
|
|
657
|
-
if (this.
|
|
658
|
-
|
|
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;
|
|
687
|
-
}
|
|
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`;
|
|
917
|
+
// Override/warning status
|
|
918
|
+
if (this.overrideStatusMessage) {
|
|
919
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
694
920
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
formatTokenCount(value) {
|
|
700
|
-
if (!Number.isFinite(value)) {
|
|
701
|
-
return `${value}`;
|
|
921
|
+
// If idle with empty buffer, show quick shortcuts
|
|
922
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
923
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
702
924
|
}
|
|
703
|
-
|
|
704
|
-
|
|
925
|
+
// Multi-line indicator
|
|
926
|
+
if (this.buffer.includes('\n')) {
|
|
927
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
705
928
|
}
|
|
706
|
-
if (
|
|
707
|
-
return
|
|
929
|
+
if (parts.length === 0) {
|
|
930
|
+
return ''; // Empty status bar when idle
|
|
708
931
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
visibleLength(value) {
|
|
712
|
-
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
713
|
-
return value.replace(ansiPattern, '').length;
|
|
932
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
933
|
+
return joined.slice(0, maxWidth);
|
|
714
934
|
}
|
|
715
935
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
936
|
+
* Build mode controls line showing toggles and context info.
|
|
937
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
938
|
+
*
|
|
939
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
718
940
|
*/
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
941
|
+
buildModeControls(cols) {
|
|
942
|
+
const maxWidth = cols - 2;
|
|
943
|
+
// Use schema-defined colors for consistency
|
|
944
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
945
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
946
|
+
const toggles = [];
|
|
947
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
948
|
+
if (this.editMode === 'display-edits') {
|
|
949
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
953
|
+
}
|
|
954
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
955
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
956
|
+
// Verification (green when on) - per schema.verificationMode
|
|
957
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
958
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
959
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
960
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
961
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
962
|
+
let rightPart = '';
|
|
963
|
+
if (this.contextUsage !== null) {
|
|
964
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
965
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
966
|
+
if (rem < 10)
|
|
967
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
968
|
+
else if (rem < 25)
|
|
969
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
970
|
+
else
|
|
971
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
972
|
+
}
|
|
973
|
+
// Calculate visible lengths (strip ANSI)
|
|
974
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
975
|
+
const leftLen = strip(leftPart).length;
|
|
976
|
+
const rightLen = strip(rightPart).length;
|
|
977
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
978
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
979
|
+
}
|
|
980
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
981
|
+
return `${leftPart} ${rightPart}`;
|
|
982
|
+
}
|
|
983
|
+
return leftPart;
|
|
725
984
|
}
|
|
726
985
|
/**
|
|
727
986
|
* Force a re-render
|
|
@@ -744,19 +1003,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
744
1003
|
handleResize() {
|
|
745
1004
|
this.lastRenderContent = '';
|
|
746
1005
|
this.lastRenderCursor = -1;
|
|
747
|
-
this.resetStreamingRenderThrottle();
|
|
748
1006
|
// Re-clamp pinned header rows to the new terminal height
|
|
749
1007
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
750
|
-
if (this.scrollRegionActive) {
|
|
751
|
-
this.disableScrollRegion();
|
|
752
|
-
this.enableScrollRegion();
|
|
753
|
-
}
|
|
754
1008
|
this.scheduleRender();
|
|
755
1009
|
}
|
|
756
1010
|
/**
|
|
757
1011
|
* Register with display's output interceptor to position cursor correctly.
|
|
758
1012
|
* When scroll region is active, output needs to go to the scroll region,
|
|
759
1013
|
* not the protected bottom area where the input is rendered.
|
|
1014
|
+
*
|
|
1015
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
1016
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
760
1017
|
*/
|
|
761
1018
|
registerOutputInterceptor(display) {
|
|
762
1019
|
if (this.outputInterceptorCleanup) {
|
|
@@ -764,20 +1021,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
764
1021
|
}
|
|
765
1022
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
766
1023
|
beforeWrite: () => {
|
|
767
|
-
//
|
|
768
|
-
//
|
|
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
|
-
}
|
|
1024
|
+
// Scroll region handles content containment automatically
|
|
1025
|
+
// No per-write cursor manipulation needed
|
|
775
1026
|
},
|
|
776
1027
|
afterWrite: () => {
|
|
777
|
-
//
|
|
778
|
-
if (this.scrollRegionActive) {
|
|
779
|
-
this.write(ESC.RESTORE);
|
|
780
|
-
}
|
|
1028
|
+
// No cursor manipulation needed
|
|
781
1029
|
},
|
|
782
1030
|
});
|
|
783
1031
|
}
|
|
@@ -787,6 +1035,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
787
1035
|
dispose() {
|
|
788
1036
|
if (this.disposed)
|
|
789
1037
|
return;
|
|
1038
|
+
// Clean up streaming render timer
|
|
1039
|
+
if (this.streamingRenderTimer) {
|
|
1040
|
+
clearInterval(this.streamingRenderTimer);
|
|
1041
|
+
this.streamingRenderTimer = null;
|
|
1042
|
+
}
|
|
790
1043
|
// Clean up output interceptor
|
|
791
1044
|
if (this.outputInterceptorCleanup) {
|
|
792
1045
|
this.outputInterceptorCleanup();
|
|
@@ -794,7 +1047,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
794
1047
|
}
|
|
795
1048
|
this.disposed = true;
|
|
796
1049
|
this.enabled = false;
|
|
797
|
-
this.resetStreamingRenderThrottle();
|
|
798
1050
|
this.disableScrollRegion();
|
|
799
1051
|
this.disableBracketedPaste();
|
|
800
1052
|
this.buffer = '';
|
|
@@ -900,7 +1152,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
900
1152
|
this.toggleEditMode();
|
|
901
1153
|
return true;
|
|
902
1154
|
}
|
|
903
|
-
|
|
1155
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1156
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1157
|
+
this.togglePasteExpansion();
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
this.toggleThinking();
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
case 'escape':
|
|
1164
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1165
|
+
if (this.mode === 'streaming') {
|
|
1166
|
+
this.emit('interrupt');
|
|
1167
|
+
}
|
|
1168
|
+
else if (this.buffer.length > 0) {
|
|
1169
|
+
this.clear();
|
|
1170
|
+
}
|
|
904
1171
|
return true;
|
|
905
1172
|
}
|
|
906
1173
|
return false;
|
|
@@ -918,6 +1185,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
918
1185
|
this.insertPlainText(chunk, insertPos);
|
|
919
1186
|
this.cursor = insertPos + chunk.length;
|
|
920
1187
|
this.emit('change', this.buffer);
|
|
1188
|
+
this.updateSuggestions();
|
|
921
1189
|
this.scheduleRender();
|
|
922
1190
|
}
|
|
923
1191
|
insertNewline() {
|
|
@@ -942,6 +1210,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
942
1210
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
943
1211
|
}
|
|
944
1212
|
this.emit('change', this.buffer);
|
|
1213
|
+
this.updateSuggestions();
|
|
945
1214
|
this.scheduleRender();
|
|
946
1215
|
}
|
|
947
1216
|
deleteForward() {
|
|
@@ -1191,9 +1460,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1191
1460
|
if (available <= 0)
|
|
1192
1461
|
return;
|
|
1193
1462
|
const chunk = clean.slice(0, available);
|
|
1194
|
-
|
|
1195
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1196
|
-
if (isMultiline && !isShortMultiline) {
|
|
1463
|
+
if (isMultilinePaste(chunk)) {
|
|
1197
1464
|
this.insertPastePlaceholder(chunk);
|
|
1198
1465
|
}
|
|
1199
1466
|
else {
|
|
@@ -1213,7 +1480,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1213
1480
|
return;
|
|
1214
1481
|
this.applyScrollRegion();
|
|
1215
1482
|
this.scrollRegionActive = true;
|
|
1216
|
-
this.forceRender();
|
|
1217
1483
|
}
|
|
1218
1484
|
disableScrollRegion() {
|
|
1219
1485
|
if (!this.scrollRegionActive)
|
|
@@ -1364,19 +1630,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1364
1630
|
this.shiftPlaceholders(position, text.length);
|
|
1365
1631
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1366
1632
|
}
|
|
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
|
-
}
|
|
1373
1633
|
findPlaceholderAt(position) {
|
|
1374
1634
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1375
1635
|
}
|
|
1376
|
-
buildPlaceholder(
|
|
1636
|
+
buildPlaceholder(summary) {
|
|
1377
1637
|
const id = ++this.pasteCounter;
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1638
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1639
|
+
// Show first line preview (truncated)
|
|
1640
|
+
const preview = summary.preview.length > 30
|
|
1641
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1642
|
+
: summary.preview;
|
|
1643
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1380
1644
|
return { id, placeholder };
|
|
1381
1645
|
}
|
|
1382
1646
|
insertPastePlaceholder(content) {
|
|
@@ -1384,21 +1648,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1384
1648
|
if (available <= 0)
|
|
1385
1649
|
return;
|
|
1386
1650
|
const cleanContent = content.slice(0, available);
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1651
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1652
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1653
|
+
if (summary.lineCount < 5) {
|
|
1654
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1655
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1656
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1657
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1389
1661
|
const insertPos = this.cursor;
|
|
1390
1662
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1391
1663
|
this.pastePlaceholders.push({
|
|
1392
1664
|
id,
|
|
1393
1665
|
content: cleanContent,
|
|
1394
|
-
lineCount,
|
|
1666
|
+
lineCount: summary.lineCount,
|
|
1395
1667
|
placeholder,
|
|
1396
1668
|
start: insertPos,
|
|
1397
1669
|
end: insertPos + placeholder.length,
|
|
1670
|
+
summary,
|
|
1671
|
+
expanded: false,
|
|
1398
1672
|
});
|
|
1399
1673
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1400
1674
|
this.cursor = insertPos + placeholder.length;
|
|
1401
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1678
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1679
|
+
*/
|
|
1680
|
+
togglePasteExpansion() {
|
|
1681
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1682
|
+
if (!placeholder)
|
|
1683
|
+
return false;
|
|
1684
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1685
|
+
// Update the placeholder text in buffer
|
|
1686
|
+
const newPlaceholder = placeholder.expanded
|
|
1687
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1688
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1689
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1690
|
+
// Update buffer
|
|
1691
|
+
this.buffer =
|
|
1692
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1693
|
+
newPlaceholder +
|
|
1694
|
+
this.buffer.slice(placeholder.end);
|
|
1695
|
+
// Update placeholder tracking
|
|
1696
|
+
placeholder.placeholder = newPlaceholder;
|
|
1697
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1698
|
+
// Shift other placeholders
|
|
1699
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1700
|
+
this.scheduleRender();
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1703
|
+
buildExpandedPlaceholder(ph) {
|
|
1704
|
+
const lines = ph.content.split('\n');
|
|
1705
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1706
|
+
const lastLines = lines.length > 5
|
|
1707
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1708
|
+
: '';
|
|
1709
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1710
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1711
|
+
}
|
|
1402
1712
|
deletePlaceholder(placeholder) {
|
|
1403
1713
|
const length = placeholder.end - placeholder.start;
|
|
1404
1714
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1406,11 +1716,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1406
1716
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1407
1717
|
this.cursor = placeholder.start;
|
|
1408
1718
|
}
|
|
1409
|
-
updateContextUsage(value
|
|
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
|
-
}
|
|
1719
|
+
updateContextUsage(value) {
|
|
1414
1720
|
if (value === null || !Number.isFinite(value)) {
|
|
1415
1721
|
this.contextUsage = null;
|
|
1416
1722
|
}
|
|
@@ -1437,22 +1743,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1437
1743
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1438
1744
|
this.setEditMode(next);
|
|
1439
1745
|
}
|
|
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
|
-
}
|
|
1456
1746
|
scheduleRender() {
|
|
1457
1747
|
if (!this.canRender())
|
|
1458
1748
|
return;
|