erosolar-cli 1.7.253 → 1.7.255
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 +126 -54
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +587 -354
- 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 (same as normal, user can type to queue)
|
|
231
|
+
this.forceRender();
|
|
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 (just re-renders input area)
|
|
235
|
+
this.streamingRenderTimer = setInterval(() => {
|
|
236
|
+
if (this.mode === 'streaming') {
|
|
237
|
+
this.forceRender();
|
|
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,192 @@ export class TerminalInput extends EventEmitter {
|
|
|
401
565
|
/**
|
|
402
566
|
* Render the input area - Claude Code style with mode controls
|
|
403
567
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* naturally above while elapsed time and status stay fresh.
|
|
568
|
+
* Same rendering for both normal and streaming modes - just different status bar.
|
|
569
|
+
* During streaming, uses cursor save/restore to preserve streaming position.
|
|
407
570
|
*/
|
|
408
571
|
render() {
|
|
409
572
|
if (!this.canRender())
|
|
410
573
|
return;
|
|
411
574
|
if (this.isRendering)
|
|
412
575
|
return;
|
|
413
|
-
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
414
|
-
// During streaming we still render the pinned input/status region, but throttle
|
|
415
|
-
// to avoid fighting with the streamed content flow.
|
|
416
|
-
if (streamingActive && this.lastStreamingRender > 0) {
|
|
417
|
-
const elapsed = Date.now() - this.lastStreamingRender;
|
|
418
|
-
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
419
|
-
if (waitMs > 0) {
|
|
420
|
-
this.renderDirty = true;
|
|
421
|
-
this.scheduleStreamingRender(waitMs);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
576
|
const shouldSkip = !this.renderDirty &&
|
|
426
577
|
this.buffer === this.lastRenderContent &&
|
|
427
578
|
this.cursor === this.lastRenderCursor;
|
|
428
579
|
this.renderDirty = false;
|
|
429
|
-
// Skip if nothing changed
|
|
580
|
+
// Skip if nothing changed (unless explicitly forced)
|
|
430
581
|
if (shouldSkip) {
|
|
431
582
|
return;
|
|
432
583
|
}
|
|
433
|
-
// If write lock is held, defer render
|
|
584
|
+
// If write lock is held, defer render
|
|
434
585
|
if (writeLock.isLocked()) {
|
|
435
586
|
writeLock.safeWrite(() => this.render());
|
|
436
587
|
return;
|
|
437
588
|
}
|
|
438
589
|
this.isRendering = true;
|
|
439
|
-
// Use write lock during render to prevent interleaved output
|
|
440
590
|
writeLock.lock('terminalInput.render');
|
|
441
591
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
592
|
+
// Render input area at bottom (outside scroll region)
|
|
593
|
+
this.renderBottomPinned();
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
writeLock.unlock();
|
|
597
|
+
this.isRendering = false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
602
|
+
*
|
|
603
|
+
* Flow mode attempted inline rendering but caused duplicate renders
|
|
604
|
+
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
605
|
+
*/
|
|
606
|
+
renderFlowMode() {
|
|
607
|
+
// Use stable bottom-pinned approach
|
|
608
|
+
this.renderBottomPinned();
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
612
|
+
*
|
|
613
|
+
* Works for both normal and streaming modes:
|
|
614
|
+
* - During streaming: saves/restores cursor position
|
|
615
|
+
* - Status bar shows streaming info or "Type a message"
|
|
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
|
+
const isStreaming = this.mode === 'streaming';
|
|
634
|
+
// During streaming, save cursor position to restore after render
|
|
635
|
+
if (isStreaming) {
|
|
636
|
+
this.write(ESC.SAVE);
|
|
637
|
+
}
|
|
638
|
+
// Wrap buffer into display lines
|
|
639
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
640
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
641
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
642
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
643
|
+
// Calculate display window (keep cursor visible)
|
|
644
|
+
let startLine = 0;
|
|
645
|
+
if (lines.length > displayLines) {
|
|
646
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
647
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
648
|
+
}
|
|
649
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
650
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
651
|
+
// Calculate suggestion display (not during streaming)
|
|
652
|
+
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
653
|
+
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
654
|
+
: [];
|
|
655
|
+
const suggestionLines = suggestionsToShow.length;
|
|
656
|
+
this.write(ESC.HIDE);
|
|
657
|
+
this.write(ESC.RESET);
|
|
658
|
+
const divider = renderDivider(cols - 2);
|
|
659
|
+
// Calculate positions from absolute bottom
|
|
660
|
+
let currentRow;
|
|
661
|
+
if (suggestionLines > 0) {
|
|
662
|
+
// With suggestions: input area + dividers + suggestions
|
|
663
|
+
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
664
|
+
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
665
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
666
|
+
this.updateReservedLines(totalHeight);
|
|
667
|
+
// Top divider
|
|
668
|
+
this.write(ESC.TO(currentRow, 1));
|
|
669
|
+
this.write(ESC.CLEAR_LINE);
|
|
670
|
+
this.write(divider);
|
|
671
|
+
currentRow++;
|
|
672
|
+
// Input lines
|
|
673
|
+
let finalRow = currentRow;
|
|
674
|
+
let finalCol = 3;
|
|
675
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
676
|
+
this.write(ESC.TO(currentRow, 1));
|
|
677
|
+
this.write(ESC.CLEAR_LINE);
|
|
678
|
+
const line = visibleLines[i] ?? '';
|
|
679
|
+
const absoluteLineIdx = startLine + i;
|
|
680
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
681
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
682
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
683
|
+
if (isCursorLine) {
|
|
684
|
+
const col = Math.min(cursorCol, line.length);
|
|
685
|
+
this.write(line.slice(0, col));
|
|
686
|
+
this.write(ESC.REVERSE);
|
|
687
|
+
this.write(col < line.length ? line[col] : ' ');
|
|
688
|
+
this.write(ESC.RESET);
|
|
689
|
+
this.write(line.slice(col + 1));
|
|
690
|
+
finalRow = currentRow;
|
|
691
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
this.write(line);
|
|
695
|
+
}
|
|
696
|
+
currentRow++;
|
|
460
697
|
}
|
|
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) {
|
|
698
|
+
// Bottom divider
|
|
699
|
+
this.write(ESC.TO(currentRow, 1));
|
|
700
|
+
this.write(ESC.CLEAR_LINE);
|
|
701
|
+
this.write(divider);
|
|
702
|
+
currentRow++;
|
|
703
|
+
// Suggestions (Claude Code style)
|
|
704
|
+
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
472
705
|
this.write(ESC.TO(currentRow, 1));
|
|
473
706
|
this.write(ESC.CLEAR_LINE);
|
|
474
|
-
|
|
475
|
-
|
|
707
|
+
const suggestion = suggestionsToShow[i];
|
|
708
|
+
const isSelected = i === this.selectedSuggestionIndex;
|
|
709
|
+
// Indent and highlight selected
|
|
710
|
+
this.write(' ');
|
|
711
|
+
if (isSelected) {
|
|
712
|
+
this.write(ESC.REVERSE);
|
|
713
|
+
this.write(ESC.BOLD);
|
|
714
|
+
}
|
|
715
|
+
this.write(suggestion.command);
|
|
716
|
+
if (isSelected) {
|
|
717
|
+
this.write(ESC.RESET);
|
|
718
|
+
}
|
|
719
|
+
// Description (dimmed)
|
|
720
|
+
const descSpace = cols - suggestion.command.length - 8;
|
|
721
|
+
if (descSpace > 10 && suggestion.description) {
|
|
722
|
+
const desc = suggestion.description.slice(0, descSpace);
|
|
723
|
+
this.write(ESC.RESET);
|
|
724
|
+
this.write(ESC.DIM);
|
|
725
|
+
this.write(' ');
|
|
726
|
+
this.write(desc);
|
|
727
|
+
this.write(ESC.RESET);
|
|
728
|
+
}
|
|
729
|
+
currentRow++;
|
|
476
730
|
}
|
|
477
|
-
//
|
|
731
|
+
// Position cursor in input area
|
|
732
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// Without suggestions: normal layout with status bar and controls
|
|
736
|
+
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
737
|
+
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
738
|
+
this.updateReservedLines(totalHeight);
|
|
739
|
+
// Status bar (streaming or normal)
|
|
740
|
+
this.write(ESC.TO(currentRow, 1));
|
|
741
|
+
this.write(ESC.CLEAR_LINE);
|
|
742
|
+
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
743
|
+
currentRow++;
|
|
744
|
+
// Top divider
|
|
478
745
|
this.write(ESC.TO(currentRow, 1));
|
|
479
746
|
this.write(ESC.CLEAR_LINE);
|
|
480
|
-
const divider = renderDivider(cols - 2);
|
|
481
747
|
this.write(divider);
|
|
482
|
-
currentRow
|
|
483
|
-
//
|
|
748
|
+
currentRow++;
|
|
749
|
+
// Input lines
|
|
484
750
|
let finalRow = currentRow;
|
|
485
751
|
let finalCol = 3;
|
|
486
752
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
487
|
-
|
|
488
|
-
this.write(ESC.TO(rowNum, 1));
|
|
753
|
+
this.write(ESC.TO(currentRow, 1));
|
|
489
754
|
this.write(ESC.CLEAR_LINE);
|
|
490
755
|
const line = visibleLines[i] ?? '';
|
|
491
756
|
const absoluteLineIdx = startLine + i;
|
|
@@ -499,7 +764,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
499
764
|
this.write(ESC.RESET);
|
|
500
765
|
this.write(ESC.BG_DARK);
|
|
501
766
|
if (isCursorLine) {
|
|
502
|
-
// Render with block cursor
|
|
503
767
|
const col = Math.min(cursorCol, line.length);
|
|
504
768
|
const before = line.slice(0, col);
|
|
505
769
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -509,219 +773,157 @@ export class TerminalInput extends EventEmitter {
|
|
|
509
773
|
this.write(at);
|
|
510
774
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
511
775
|
this.write(after);
|
|
512
|
-
finalRow =
|
|
776
|
+
finalRow = currentRow;
|
|
513
777
|
finalCol = this.config.promptChar.length + col + 1;
|
|
514
778
|
}
|
|
515
779
|
else {
|
|
516
780
|
this.write(line);
|
|
517
781
|
}
|
|
518
|
-
// Pad to edge
|
|
782
|
+
// Pad to edge
|
|
519
783
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
520
784
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
521
785
|
if (padding > 0)
|
|
522
786
|
this.write(' '.repeat(padding));
|
|
523
787
|
this.write(ESC.RESET);
|
|
788
|
+
currentRow++;
|
|
524
789
|
}
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
this.write(ESC.
|
|
790
|
+
// Bottom divider
|
|
791
|
+
this.write(ESC.TO(currentRow, 1));
|
|
792
|
+
this.write(ESC.CLEAR_LINE);
|
|
793
|
+
this.write(divider);
|
|
794
|
+
currentRow++;
|
|
795
|
+
// Mode controls
|
|
796
|
+
this.write(ESC.TO(currentRow, 1));
|
|
528
797
|
this.write(ESC.CLEAR_LINE);
|
|
529
798
|
this.write(this.buildModeControls(cols));
|
|
530
|
-
// Position cursor
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
// Update state
|
|
534
|
-
this.lastRenderContent = this.buffer;
|
|
535
|
-
this.lastRenderCursor = this.cursor;
|
|
536
|
-
if (streamingActive) {
|
|
537
|
-
this.lastStreamingRender = Date.now();
|
|
799
|
+
// Position cursor: restore for streaming, or position in input for normal
|
|
800
|
+
if (isStreaming) {
|
|
801
|
+
this.write(ESC.RESTORE);
|
|
538
802
|
}
|
|
539
803
|
else {
|
|
540
|
-
this.
|
|
541
|
-
}
|
|
542
|
-
if (this.streamingRenderTimer) {
|
|
543
|
-
clearTimeout(this.streamingRenderTimer);
|
|
544
|
-
this.streamingRenderTimer = null;
|
|
804
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
545
805
|
}
|
|
546
806
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
807
|
+
this.write(ESC.SHOW);
|
|
808
|
+
// Update state
|
|
809
|
+
this.lastRenderContent = this.buffer;
|
|
810
|
+
this.lastRenderCursor = this.cursor;
|
|
551
811
|
}
|
|
552
812
|
/**
|
|
553
|
-
* Build
|
|
813
|
+
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
554
814
|
*/
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (this.mode === 'streaming' || isStreamingMode()) {
|
|
567
|
-
statusParts.push({ text: 'esc to interrupt', tone: 'warn' });
|
|
568
|
-
}
|
|
569
|
-
if (this.metaElapsedSeconds !== null) {
|
|
570
|
-
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
571
|
-
}
|
|
572
|
-
const tokensRemaining = this.computeTokensRemaining();
|
|
573
|
-
if (tokensRemaining !== null) {
|
|
574
|
-
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
575
|
-
}
|
|
576
|
-
if (statusParts.length) {
|
|
577
|
-
lines.push(renderStatusLine(statusParts, width));
|
|
578
|
-
}
|
|
579
|
-
const usageParts = [];
|
|
580
|
-
if (this.metaTokensUsed !== null) {
|
|
581
|
-
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
582
|
-
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
583
|
-
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
584
|
-
}
|
|
585
|
-
if (this.contextUsage !== null) {
|
|
586
|
-
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
587
|
-
const left = Math.max(0, 100 - this.contextUsage);
|
|
588
|
-
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
589
|
-
}
|
|
815
|
+
buildStreamingStatusBar(cols) {
|
|
816
|
+
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
817
|
+
// Streaming status with elapsed time
|
|
818
|
+
let elapsed = '0s';
|
|
819
|
+
if (this.streamingStartTime) {
|
|
820
|
+
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
821
|
+
const mins = Math.floor(secs / 60);
|
|
822
|
+
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
823
|
+
}
|
|
824
|
+
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
825
|
+
// Queue indicator
|
|
590
826
|
if (this.queue.length > 0) {
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
if (usageParts.length) {
|
|
594
|
-
lines.push(renderStatusLine(usageParts, width));
|
|
827
|
+
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
595
828
|
}
|
|
596
|
-
|
|
829
|
+
// Hint for typing
|
|
830
|
+
status += ` ${DIM}· type to queue message${R}`;
|
|
831
|
+
return status;
|
|
597
832
|
}
|
|
598
833
|
/**
|
|
599
|
-
*
|
|
834
|
+
* Build status bar showing streaming/ready status and key info.
|
|
835
|
+
* This is the TOP line above the input area - minimal Claude Code style.
|
|
600
836
|
*/
|
|
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' });
|
|
837
|
+
buildStatusBar(cols) {
|
|
838
|
+
const maxWidth = cols - 2;
|
|
839
|
+
const parts = [];
|
|
840
|
+
// Streaming status with elapsed time (left side)
|
|
841
|
+
if (this.mode === 'streaming') {
|
|
842
|
+
let statusText = '● Streaming';
|
|
843
|
+
if (this.streamingStartTime) {
|
|
844
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
845
|
+
const mins = Math.floor(elapsed / 60);
|
|
846
|
+
const secs = elapsed % 60;
|
|
847
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
848
|
+
}
|
|
849
|
+
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
622
850
|
}
|
|
623
|
-
|
|
624
|
-
|
|
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' });
|
|
644
|
-
}
|
|
645
|
-
if (this.buffer.includes('\n')) {
|
|
646
|
-
const lineCount = this.buffer.split('\n').length;
|
|
647
|
-
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
851
|
+
// Queue indicator during streaming
|
|
852
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
853
|
+
parts.push(`\x1b[36mqueued: ${this.queue.length}\x1b[0m`); // Cyan
|
|
648
854
|
}
|
|
855
|
+
// Paste indicator
|
|
649
856
|
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 });
|
|
857
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
858
|
+
parts.push(`\x1b[36m📋 ${totalLines}L\x1b[0m`);
|
|
666
859
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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`;
|
|
860
|
+
// Override/warning status
|
|
861
|
+
if (this.overrideStatusMessage) {
|
|
862
|
+
parts.push(`\x1b[33m⚠ ${this.overrideStatusMessage}\x1b[0m`); // Yellow
|
|
694
863
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
formatTokenCount(value) {
|
|
700
|
-
if (!Number.isFinite(value)) {
|
|
701
|
-
return `${value}`;
|
|
864
|
+
// If idle with empty buffer, show quick shortcuts
|
|
865
|
+
if (this.mode !== 'streaming' && this.buffer.length === 0 && parts.length === 0) {
|
|
866
|
+
return `${ESC.DIM}Type a message or / for commands${ESC.RESET}`;
|
|
702
867
|
}
|
|
703
|
-
|
|
704
|
-
|
|
868
|
+
// Multi-line indicator
|
|
869
|
+
if (this.buffer.includes('\n')) {
|
|
870
|
+
parts.push(`${ESC.DIM}${this.buffer.split('\n').length}L${ESC.RESET}`);
|
|
705
871
|
}
|
|
706
|
-
if (
|
|
707
|
-
return
|
|
872
|
+
if (parts.length === 0) {
|
|
873
|
+
return ''; // Empty status bar when idle
|
|
708
874
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
visibleLength(value) {
|
|
712
|
-
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
713
|
-
return value.replace(ansiPattern, '').length;
|
|
875
|
+
const joined = parts.join(`${ESC.DIM} · ${ESC.RESET}`);
|
|
876
|
+
return joined.slice(0, maxWidth);
|
|
714
877
|
}
|
|
715
878
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
879
|
+
* Build mode controls line showing toggles and context info.
|
|
880
|
+
* This is the BOTTOM line below the input area - Claude Code style layout with erosolar features.
|
|
881
|
+
*
|
|
882
|
+
* Layout: [toggles on left] ... [context info on right]
|
|
718
883
|
*/
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
884
|
+
buildModeControls(cols) {
|
|
885
|
+
const maxWidth = cols - 2;
|
|
886
|
+
// Use schema-defined colors for consistency
|
|
887
|
+
const { green: GREEN, yellow: YELLOW, cyan: CYAN, magenta: MAGENTA, red: RED, dim: DIM, reset: R } = UI_COLORS;
|
|
888
|
+
// Mode toggles with colors (following ModeControlsSchema)
|
|
889
|
+
const toggles = [];
|
|
890
|
+
// Edit mode (green=auto, yellow=ask) - per schema.editMode
|
|
891
|
+
if (this.editMode === 'display-edits') {
|
|
892
|
+
toggles.push(`${GREEN}⏵⏵ auto-edit${R}`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
toggles.push(`${YELLOW}⏸⏸ ask-first${R}`);
|
|
896
|
+
}
|
|
897
|
+
// Thinking mode (cyan when on) - per schema.thinkingMode
|
|
898
|
+
toggles.push(this.thinkingEnabled ? `${CYAN}💭 think${R}` : `${DIM}○ think${R}`);
|
|
899
|
+
// Verification (green when on) - per schema.verificationMode
|
|
900
|
+
toggles.push(this.verificationEnabled ? `${GREEN}✓ verify${R}` : `${DIM}○ verify${R}`);
|
|
901
|
+
// Auto-continue (magenta when on) - per schema.autoContinueMode
|
|
902
|
+
toggles.push(this.autoContinueEnabled ? `${MAGENTA}↻ auto${R}` : `${DIM}○ auto${R}`);
|
|
903
|
+
const leftPart = toggles.join(`${DIM} · ${R}`) + `${DIM} (⇧⇥)${R}`;
|
|
904
|
+
// Context usage with color - per schema.contextUsage thresholds
|
|
905
|
+
let rightPart = '';
|
|
906
|
+
if (this.contextUsage !== null) {
|
|
907
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
908
|
+
// Thresholds: critical < 10%, warning < 25%
|
|
909
|
+
if (rem < 10)
|
|
910
|
+
rightPart = `${RED}⚠ ctx: ${rem}%${R}`;
|
|
911
|
+
else if (rem < 25)
|
|
912
|
+
rightPart = `${YELLOW}! ctx: ${rem}%${R}`;
|
|
913
|
+
else
|
|
914
|
+
rightPart = `${DIM}ctx: ${rem}%${R}`;
|
|
915
|
+
}
|
|
916
|
+
// Calculate visible lengths (strip ANSI)
|
|
917
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
918
|
+
const leftLen = strip(leftPart).length;
|
|
919
|
+
const rightLen = strip(rightPart).length;
|
|
920
|
+
if (leftLen + rightLen < maxWidth - 4) {
|
|
921
|
+
return `${leftPart}${' '.repeat(maxWidth - leftLen - rightLen)}${rightPart}`;
|
|
922
|
+
}
|
|
923
|
+
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
924
|
+
return `${leftPart} ${rightPart}`;
|
|
925
|
+
}
|
|
926
|
+
return leftPart;
|
|
725
927
|
}
|
|
726
928
|
/**
|
|
727
929
|
* Force a re-render
|
|
@@ -744,19 +946,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
744
946
|
handleResize() {
|
|
745
947
|
this.lastRenderContent = '';
|
|
746
948
|
this.lastRenderCursor = -1;
|
|
747
|
-
this.resetStreamingRenderThrottle();
|
|
748
949
|
// Re-clamp pinned header rows to the new terminal height
|
|
749
950
|
this.setPinnedHeaderLines(this.pinnedTopRows);
|
|
750
|
-
if (this.scrollRegionActive) {
|
|
751
|
-
this.disableScrollRegion();
|
|
752
|
-
this.enableScrollRegion();
|
|
753
|
-
}
|
|
754
951
|
this.scheduleRender();
|
|
755
952
|
}
|
|
756
953
|
/**
|
|
757
954
|
* Register with display's output interceptor to position cursor correctly.
|
|
758
955
|
* When scroll region is active, output needs to go to the scroll region,
|
|
759
956
|
* not the protected bottom area where the input is rendered.
|
|
957
|
+
*
|
|
958
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
959
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
760
960
|
*/
|
|
761
961
|
registerOutputInterceptor(display) {
|
|
762
962
|
if (this.outputInterceptorCleanup) {
|
|
@@ -764,20 +964,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
764
964
|
}
|
|
765
965
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
766
966
|
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
|
-
}
|
|
967
|
+
// Scroll region handles content containment automatically
|
|
968
|
+
// No per-write cursor manipulation needed
|
|
775
969
|
},
|
|
776
970
|
afterWrite: () => {
|
|
777
|
-
//
|
|
778
|
-
if (this.scrollRegionActive) {
|
|
779
|
-
this.write(ESC.RESTORE);
|
|
780
|
-
}
|
|
971
|
+
// No cursor manipulation needed
|
|
781
972
|
},
|
|
782
973
|
});
|
|
783
974
|
}
|
|
@@ -787,6 +978,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
787
978
|
dispose() {
|
|
788
979
|
if (this.disposed)
|
|
789
980
|
return;
|
|
981
|
+
// Clean up streaming render timer
|
|
982
|
+
if (this.streamingRenderTimer) {
|
|
983
|
+
clearInterval(this.streamingRenderTimer);
|
|
984
|
+
this.streamingRenderTimer = null;
|
|
985
|
+
}
|
|
790
986
|
// Clean up output interceptor
|
|
791
987
|
if (this.outputInterceptorCleanup) {
|
|
792
988
|
this.outputInterceptorCleanup();
|
|
@@ -794,7 +990,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
794
990
|
}
|
|
795
991
|
this.disposed = true;
|
|
796
992
|
this.enabled = false;
|
|
797
|
-
this.resetStreamingRenderThrottle();
|
|
798
993
|
this.disableScrollRegion();
|
|
799
994
|
this.disableBracketedPaste();
|
|
800
995
|
this.buffer = '';
|
|
@@ -900,7 +1095,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
900
1095
|
this.toggleEditMode();
|
|
901
1096
|
return true;
|
|
902
1097
|
}
|
|
903
|
-
|
|
1098
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
1099
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
1100
|
+
this.togglePasteExpansion();
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
this.toggleThinking();
|
|
1104
|
+
}
|
|
1105
|
+
return true;
|
|
1106
|
+
case 'escape':
|
|
1107
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1108
|
+
if (this.mode === 'streaming') {
|
|
1109
|
+
this.emit('interrupt');
|
|
1110
|
+
}
|
|
1111
|
+
else if (this.buffer.length > 0) {
|
|
1112
|
+
this.clear();
|
|
1113
|
+
}
|
|
904
1114
|
return true;
|
|
905
1115
|
}
|
|
906
1116
|
return false;
|
|
@@ -918,6 +1128,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
918
1128
|
this.insertPlainText(chunk, insertPos);
|
|
919
1129
|
this.cursor = insertPos + chunk.length;
|
|
920
1130
|
this.emit('change', this.buffer);
|
|
1131
|
+
this.updateSuggestions();
|
|
921
1132
|
this.scheduleRender();
|
|
922
1133
|
}
|
|
923
1134
|
insertNewline() {
|
|
@@ -942,6 +1153,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
942
1153
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
943
1154
|
}
|
|
944
1155
|
this.emit('change', this.buffer);
|
|
1156
|
+
this.updateSuggestions();
|
|
945
1157
|
this.scheduleRender();
|
|
946
1158
|
}
|
|
947
1159
|
deleteForward() {
|
|
@@ -1191,9 +1403,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1191
1403
|
if (available <= 0)
|
|
1192
1404
|
return;
|
|
1193
1405
|
const chunk = clean.slice(0, available);
|
|
1194
|
-
|
|
1195
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1196
|
-
if (isMultiline && !isShortMultiline) {
|
|
1406
|
+
if (isMultilinePaste(chunk)) {
|
|
1197
1407
|
this.insertPastePlaceholder(chunk);
|
|
1198
1408
|
}
|
|
1199
1409
|
else {
|
|
@@ -1213,7 +1423,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1213
1423
|
return;
|
|
1214
1424
|
this.applyScrollRegion();
|
|
1215
1425
|
this.scrollRegionActive = true;
|
|
1216
|
-
this.forceRender();
|
|
1217
1426
|
}
|
|
1218
1427
|
disableScrollRegion() {
|
|
1219
1428
|
if (!this.scrollRegionActive)
|
|
@@ -1364,19 +1573,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1364
1573
|
this.shiftPlaceholders(position, text.length);
|
|
1365
1574
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1366
1575
|
}
|
|
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
1576
|
findPlaceholderAt(position) {
|
|
1374
1577
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1375
1578
|
}
|
|
1376
|
-
buildPlaceholder(
|
|
1579
|
+
buildPlaceholder(summary) {
|
|
1377
1580
|
const id = ++this.pasteCounter;
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1581
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1582
|
+
// Show first line preview (truncated)
|
|
1583
|
+
const preview = summary.preview.length > 30
|
|
1584
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1585
|
+
: summary.preview;
|
|
1586
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1380
1587
|
return { id, placeholder };
|
|
1381
1588
|
}
|
|
1382
1589
|
insertPastePlaceholder(content) {
|
|
@@ -1384,21 +1591,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1384
1591
|
if (available <= 0)
|
|
1385
1592
|
return;
|
|
1386
1593
|
const cleanContent = content.slice(0, available);
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1594
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1595
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1596
|
+
if (summary.lineCount < 5) {
|
|
1597
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1598
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1599
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1600
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1389
1604
|
const insertPos = this.cursor;
|
|
1390
1605
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1391
1606
|
this.pastePlaceholders.push({
|
|
1392
1607
|
id,
|
|
1393
1608
|
content: cleanContent,
|
|
1394
|
-
lineCount,
|
|
1609
|
+
lineCount: summary.lineCount,
|
|
1395
1610
|
placeholder,
|
|
1396
1611
|
start: insertPos,
|
|
1397
1612
|
end: insertPos + placeholder.length,
|
|
1613
|
+
summary,
|
|
1614
|
+
expanded: false,
|
|
1398
1615
|
});
|
|
1399
1616
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1400
1617
|
this.cursor = insertPos + placeholder.length;
|
|
1401
1618
|
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1621
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1622
|
+
*/
|
|
1623
|
+
togglePasteExpansion() {
|
|
1624
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1625
|
+
if (!placeholder)
|
|
1626
|
+
return false;
|
|
1627
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1628
|
+
// Update the placeholder text in buffer
|
|
1629
|
+
const newPlaceholder = placeholder.expanded
|
|
1630
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1631
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1632
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1633
|
+
// Update buffer
|
|
1634
|
+
this.buffer =
|
|
1635
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1636
|
+
newPlaceholder +
|
|
1637
|
+
this.buffer.slice(placeholder.end);
|
|
1638
|
+
// Update placeholder tracking
|
|
1639
|
+
placeholder.placeholder = newPlaceholder;
|
|
1640
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1641
|
+
// Shift other placeholders
|
|
1642
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1643
|
+
this.scheduleRender();
|
|
1644
|
+
return true;
|
|
1645
|
+
}
|
|
1646
|
+
buildExpandedPlaceholder(ph) {
|
|
1647
|
+
const lines = ph.content.split('\n');
|
|
1648
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1649
|
+
const lastLines = lines.length > 5
|
|
1650
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1651
|
+
: '';
|
|
1652
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1653
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1654
|
+
}
|
|
1402
1655
|
deletePlaceholder(placeholder) {
|
|
1403
1656
|
const length = placeholder.end - placeholder.start;
|
|
1404
1657
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1406,11 +1659,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1406
1659
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1407
1660
|
this.cursor = placeholder.start;
|
|
1408
1661
|
}
|
|
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
|
-
}
|
|
1662
|
+
updateContextUsage(value) {
|
|
1414
1663
|
if (value === null || !Number.isFinite(value)) {
|
|
1415
1664
|
this.contextUsage = null;
|
|
1416
1665
|
}
|
|
@@ -1437,22 +1686,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1437
1686
|
const next = this.editMode === 'display-edits' ? 'ask-permission' : 'display-edits';
|
|
1438
1687
|
this.setEditMode(next);
|
|
1439
1688
|
}
|
|
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
1689
|
scheduleRender() {
|
|
1457
1690
|
if (!this.canRender())
|
|
1458
1691
|
return;
|