erosolar-cli 1.7.251 → 1.7.252
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 +124 -54
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +616 -356
- 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 -115
- 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 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
|
|
205
229
|
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);
|
|
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 we
|
|
405
|
-
*
|
|
406
|
-
* naturally above while elapsed time and status stay fresh.
|
|
568
|
+
* During streaming, we skip this and use renderStreamingFooter() instead
|
|
569
|
+
* to avoid cursor positioning conflicts with streaming content.
|
|
407
570
|
*/
|
|
408
571
|
render() {
|
|
409
572
|
if (!this.canRender())
|
|
410
573
|
return;
|
|
411
574
|
if (this.isRendering)
|
|
412
575
|
return;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
}
|
|
576
|
+
// During streaming, use compact footer rendered by timer - don't interfere
|
|
577
|
+
if (this.mode === 'streaming') {
|
|
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,189 @@ 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 compact streaming footer - 2 lines at absolute bottom.
|
|
804
|
+
* Line 1: Divider
|
|
805
|
+
* Line 2: Streaming stats (elapsed, tokens/sec, mode icons, context)
|
|
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
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
807
|
+
renderStreamingFooter() {
|
|
808
|
+
if (!this.isTTY())
|
|
809
|
+
return;
|
|
810
|
+
const { rows, cols } = this.getSize();
|
|
811
|
+
const { green: GREEN, cyan: CYAN, yellow: YELLOW, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
812
|
+
// Save cursor position (so streaming can continue)
|
|
813
|
+
this.write(ESC.SAVE);
|
|
814
|
+
// Row 1: Divider at rows - 1
|
|
815
|
+
const dividerRow = rows - 1;
|
|
816
|
+
this.write(ESC.TO(dividerRow, 1));
|
|
817
|
+
this.write(ESC.CLEAR_LINE);
|
|
818
|
+
this.write(renderDivider(cols - 2));
|
|
819
|
+
// Row 2: Streaming stats line at rows
|
|
820
|
+
this.write(ESC.TO(rows, 1));
|
|
821
|
+
this.write(ESC.CLEAR_LINE);
|
|
822
|
+
// Build streaming stats: elapsed | mode icons | context
|
|
823
|
+
let elapsed = '0s';
|
|
824
|
+
if (this.streamingStartTime) {
|
|
825
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
826
|
+
const mins = Math.floor(secs / 60);
|
|
827
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
828
|
+
}
|
|
829
|
+
const leftPart = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
830
|
+
// Queue indicator
|
|
831
|
+
const queuePart = this.queue.length > 0 ? ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}` : '';
|
|
832
|
+
// Mode icons (compact)
|
|
833
|
+
const modeIcons = [
|
|
834
|
+
this.editMode === 'display-edits' ? `${GREEN}⏵⏵${R}` : `${YELLOW}⏸⏸${R}`,
|
|
835
|
+
this.thinkingEnabled ? `${CYAN}💭${R}` : `${DIM}○${R}`,
|
|
836
|
+
this.verificationEnabled ? `${GREEN}✓${R}` : `${DIM}○${R}`,
|
|
837
|
+
this.autoContinueEnabled ? `${MAGENTA}↻${R}` : `${DIM}○${R}`,
|
|
838
|
+
].join(' ');
|
|
839
|
+
// Context usage
|
|
840
|
+
let ctxPart = '';
|
|
585
841
|
if (this.contextUsage !== null) {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
842
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
843
|
+
if (rem < 10)
|
|
844
|
+
ctxPart = `${RED}ctx:${rem}%${R}`;
|
|
845
|
+
else if (rem < 25)
|
|
846
|
+
ctxPart = `${YELLOW}ctx:${rem}%${R}`;
|
|
847
|
+
else
|
|
848
|
+
ctxPart = `${DIM}ctx:${rem}%${R}`;
|
|
849
|
+
}
|
|
850
|
+
const rightPart = modeIcons + (ctxPart ? ` ${DIM}·${R} ${ctxPart}` : '');
|
|
851
|
+
// Calculate spacing
|
|
852
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
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);
|
|
597
859
|
}
|
|
598
860
|
/**
|
|
599
|
-
*
|
|
861
|
+
* Build status bar showing streaming/ready status and key info.
|
|
862
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
600
863
|
*/
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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' });
|
|
619
|
-
}
|
|
620
|
-
if (this.overrideStatusMessage) {
|
|
621
|
-
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
622
|
-
}
|
|
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' });
|
|
864
|
+
buildStatusBar(cols) {
|
|
865
|
+
const maxWidth = cols - 2;
|
|
866
|
+
const parts = [];
|
|
867
|
+
// Streaming status with elapsed time (left side)
|
|
868
|
+
if (this.mode === 'streaming') {
|
|
869
|
+
let statusText = '● Streaming';
|
|
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
|
|
644
877
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
648
881
|
}
|
|
882
|
+
// Paste indicator
|
|
649
883
|
if (this.pastePlaceholders.length > 0) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
653
|
-
tone: 'info',
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
const contextRemaining = this.computeContextRemaining();
|
|
657
|
-
if (this.thinkingModeLabel) {
|
|
658
|
-
rightParts.push({ text: `thinking ${this.thinkingModeLabel} (/thinking)`, tone: 'info' });
|
|
659
|
-
}
|
|
660
|
-
if (contextRemaining !== null) {
|
|
661
|
-
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
662
|
-
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
663
|
-
? 'Context auto-compact imminent'
|
|
664
|
-
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
665
|
-
rightParts.push({ text: label, tone });
|
|
666
|
-
}
|
|
667
|
-
if (!rightParts.length || width < 60) {
|
|
668
|
-
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
669
|
-
return renderStatusLine(merged, width);
|
|
670
|
-
}
|
|
671
|
-
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
672
|
-
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
673
|
-
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
674
|
-
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
675
|
-
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
676
|
-
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
677
|
-
}
|
|
678
|
-
computeContextRemaining() {
|
|
679
|
-
if (this.contextUsage === null) {
|
|
680
|
-
return null;
|
|
884
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
885
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
681
886
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
686
|
-
return null;
|
|
887
|
+
// Override/warning status
|
|
888
|
+
if (this.overrideStatusMessage) {
|
|
889
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
687
890
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
formatElapsedLabel(seconds) {
|
|
692
|
-
if (seconds < 60) {
|
|
693
|
-
return `${seconds}s`;
|
|
891
|
+
// If idle with empty buffer, show quick shortcuts
|
|
892
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
893
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
694
894
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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`;
|
|
895
|
+
// Multi-line indicator
|
|
896
|
+
if (this.buffer.includes('\n')) {
|
|
897
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
705
898
|
}
|
|
706
|
-
if (
|
|
707
|
-
return
|
|
899
|
+
if (parts.length === 0) {
|
|
900
|
+
return ''; // Empty status bar when idle
|
|
708
901
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
visibleLength(value) {
|
|
712
|
-
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
713
|
-
return value.replace(ansiPattern, '').length;
|
|
902
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
903
|
+
return joined.slice(0, maxWidth);
|
|
714
904
|
}
|
|
715
905
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
906
|
+
* Build mode controls line showing toggles and context info.
|
|
907
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
908
|
+
*
|
|
909
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
718
910
|
*/
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
911
|
+
buildModeControls(cols) {
|
|
912
|
+
const maxWidth = cols - 2;
|
|
913
|
+
// Use schema-defined colors for consistency
|
|
914
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
915
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
916
|
+
const toggles = [];
|
|
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;
|
|
725
954
|
}
|
|
726
955
|
/**
|
|
727
956
|
* Force a re-render
|
|
@@ -744,19 +973,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
744
973
|
handleResize() {
|
|
745
974
|
this.lastRenderContent = '';
|
|
746
975
|
this.lastRenderCursor = -1;
|
|
747
|
-
this.resetStreamingRenderThrottle();
|
|
748
976
|
// Re-clamp pinned header rows to the new terminal height
|
|
749
977
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
750
|
-
if (this.scrollRegionActive) {
|
|
751
|
-
this.disableScrollRegion();
|
|
752
|
-
this.enableScrollRegion();
|
|
753
|
-
}
|
|
754
978
|
this.scheduleRender();
|
|
755
979
|
}
|
|
756
980
|
/**
|
|
757
981
|
* Register with display's output interceptor to position cursor correctly.
|
|
758
982
|
* When scroll region is active, output needs to go to the scroll region,
|
|
759
983
|
* 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.
|
|
760
987
|
*/
|
|
761
988
|
registerOutputInterceptor(display) {
|
|
762
989
|
if (this.outputInterceptorCleanup) {
|
|
@@ -764,20 +991,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
764
991
|
}
|
|
765
992
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
766
993
|
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
|
-
}
|
|
994
|
+
// Scroll region handles content containment automatically
|
|
995
|
+
// No per-write cursor manipulation needed
|
|
775
996
|
},
|
|
776
997
|
afterWrite: () => {
|
|
777
|
-
//
|
|
778
|
-
if (this.scrollRegionActive) {
|
|
779
|
-
this.write(ESC.RESTORE);
|
|
780
|
-
}
|
|
998
|
+
// No cursor manipulation needed
|
|
781
999
|
},
|
|
782
1000
|
});
|
|
783
1001
|
}
|
|
@@ -787,6 +1005,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
787
1005
|
dispose() {
|
|
788
1006
|
if (this.disposed)
|
|
789
1007
|
return;
|
|
1008
|
+
// Clean up streaming render timer
|
|
1009
|
+
if (this.streamingRenderTimer) {
|
|
1010
|
+
clearInterval(this.streamingRenderTimer);
|
|
1011
|
+
this.streamingRenderTimer = null;
|
|
1012
|
+
}
|
|
790
1013
|
// Clean up output interceptor
|
|
791
1014
|
if (this.outputInterceptorCleanup) {
|
|
792
1015
|
this.outputInterceptorCleanup();
|
|
@@ -794,7 +1017,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
794
1017
|
}
|
|
795
1018
|
this.disposed = true;
|
|
796
1019
|
this.enabled = false;
|
|
797
|
-
this.resetStreamingRenderThrottle();
|
|
798
1020
|
this.disableScrollRegion();
|
|
799
1021
|
this.disableBracketedPaste();
|
|
800
1022
|
this.buffer = '';
|
|
@@ -900,7 +1122,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
900
1122
|
this.toggleEditMode();
|
|
901
1123
|
return true;
|
|
902
1124
|
}
|
|
903
|
-
|
|
1125
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
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
|
+
}
|
|
904
1141
|
return true;
|
|
905
1142
|
}
|
|
906
1143
|
return false;
|
|
@@ -918,6 +1155,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
918
1155
|
this.insertPlainText(chunk, insertPos);
|
|
919
1156
|
this.cursor = insertPos + chunk.length;
|
|
920
1157
|
this.emit('change', this.buffer);
|
|
1158
|
+
this.updateSuggestions();
|
|
921
1159
|
this.scheduleRender();
|
|
922
1160
|
}
|
|
923
1161
|
insertNewline() {
|
|
@@ -942,6 +1180,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
942
1180
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
943
1181
|
}
|
|
944
1182
|
this.emit('change', this.buffer);
|
|
1183
|
+
this.updateSuggestions();
|
|
945
1184
|
this.scheduleRender();
|
|
946
1185
|
}
|
|
947
1186
|
deleteForward() {
|
|
@@ -1191,9 +1430,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1191
1430
|
if (available <= 0)
|
|
1192
1431
|
return;
|
|
1193
1432
|
const chunk = clean.slice(0, available);
|
|
1194
|
-
|
|
1195
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1196
|
-
if (isMultiline && !isShortMultiline) {
|
|
1433
|
+
if (isMultilinePaste(chunk)) {
|
|
1197
1434
|
this.insertPastePlaceholder(chunk);
|
|
1198
1435
|
}
|
|
1199
1436
|
else {
|
|
@@ -1213,7 +1450,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1213
1450
|
return;
|
|
1214
1451
|
this.applyScrollRegion();
|
|
1215
1452
|
this.scrollRegionActive = true;
|
|
1216
|
-
this.forceRender();
|
|
1217
1453
|
}
|
|
1218
1454
|
disableScrollRegion() {
|
|
1219
1455
|
if (!this.scrollRegionActive)
|
|
@@ -1364,19 +1600,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1364
1600
|
this.shiftPlaceholders(position, text.length);
|
|
1365
1601
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1366
1602
|
}
|
|
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
1603
|
findPlaceholderAt(position) {
|
|
1374
1604
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1375
1605
|
}
|
|
1376
|
-
buildPlaceholder(
|
|
1606
|
+
buildPlaceholder(summary) {
|
|
1377
1607
|
const id = ++this.pasteCounter;
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1608
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1609
|
+
// Show first line preview (truncated)
|
|
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}"`;
|
|
1380
1614
|
return { id, placeholder };
|
|
1381
1615
|
}
|
|
1382
1616
|
insertPastePlaceholder(content) {
|
|
@@ -1384,21 +1618,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1384
1618
|
if (available <= 0)
|
|
1385
1619
|
return;
|
|
1386
1620
|
const cleanContent = content.slice(0, available);
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1621
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1622
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
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);
|
|
1389
1631
|
const insertPos = this.cursor;
|
|
1390
1632
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1391
1633
|
this.pastePlaceholders.push({
|
|
1392
1634
|
id,
|
|
1393
1635
|
content: cleanContent,
|
|
1394
|
-
lineCount,
|
|
1636
|
+
lineCount: summary.lineCount,
|
|
1395
1637
|
placeholder,
|
|
1396
1638
|
start: insertPos,
|
|
1397
1639
|
end: insertPos + placeholder.length,
|
|
1640
|
+
summary,
|
|
1641
|
+
expanded: false,
|
|
1398
1642
|
});
|
|
1399
1643
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1400
1644
|
this.cursor = insertPos + placeholder.length;
|
|
1401
1645
|
}
|
|
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
|
+
}
|
|
1402
1682
|
deletePlaceholder(placeholder) {
|
|
1403
1683
|
const length = placeholder.end - placeholder.start;
|
|
1404
1684
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1406,11 +1686,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1406
1686
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1407
1687
|
this.cursor = placeholder.start;
|
|
1408
1688
|
}
|
|
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
|
-
}
|
|
1689
|
+
updateContextUsage(value) {
|
|
1414
1690
|
if (value === null || !Number.isFinite(value)) {
|
|
1415
1691
|
this.contextUsage = null;
|
|
1416
1692
|
}
|
|
@@ -1437,22 +1713,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1437
1713
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1438
1714
|
this.setEditMode(next);
|
|
1439
1715
|
}
|
|
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
1716
|
scheduleRender() {
|
|
1457
1717
|
if (!this.canRender())
|
|
1458
1718
|
return;
|