erosolar-cli 1.7.228 → 1.7.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -148
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +9 -18
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +4 -10
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +2 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +12 -12
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +23 -45
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +145 -255
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +6 -8
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +6 -12
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +6 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +13 -26
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/aiFlowOptimizer.d.ts +0 -26
- package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
- package/dist/core/aiFlowOptimizer.js +0 -31
- package/dist/core/aiFlowOptimizer.js.map +0 -1
- package/dist/core/aiOptimizationEngine.d.ts +0 -158
- package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
- package/dist/core/aiOptimizationEngine.js +0 -428
- package/dist/core/aiOptimizationEngine.js.map +0 -1
- package/dist/core/aiOptimizationIntegration.d.ts +0 -93
- package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
- package/dist/core/aiOptimizationIntegration.js +0 -250
- package/dist/core/aiOptimizationIntegration.js.map +0 -1
- package/dist/core/enhancedErrorRecovery.d.ts +0 -100
- package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
- package/dist/core/enhancedErrorRecovery.js +0 -345
- package/dist/core/enhancedErrorRecovery.js.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.js +0 -322
- package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
- package/dist/shell/inputQueueManager.d.ts +0 -144
- package/dist/shell/inputQueueManager.d.ts.map +0 -1
- package/dist/shell/inputQueueManager.js +0 -290
- package/dist/shell/inputQueueManager.js.map +0 -1
- package/dist/shell/streamingOutputManager.d.ts +0 -115
- package/dist/shell/streamingOutputManager.d.ts.map +0 -1
- package/dist/shell/streamingOutputManager.js +0 -225
- package/dist/shell/streamingOutputManager.js.map +0 -1
- package/dist/ui/persistentPrompt.d.ts +0 -50
- package/dist/ui/persistentPrompt.d.ts.map +0 -1
- package/dist/ui/persistentPrompt.js +0 -92
- package/dist/ui/persistentPrompt.js.map +0 -1
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - One bottom-pinned chat box for the entire session (no inline anchors)
|
|
6
7
|
* - Native bracketed paste support (no heuristics)
|
|
7
8
|
* - Clean cursor model with render-time wrapping
|
|
8
9
|
* - State machine for different input modes
|
|
9
10
|
* - No readline dependency for display
|
|
10
11
|
*/
|
|
11
12
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
13
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
14
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
15
|
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
15
16
|
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
@@ -67,6 +68,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
67
68
|
statusMessage = null;
|
|
68
69
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
69
70
|
streamingLabel = null; // Streaming progress indicator
|
|
71
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
72
|
+
metaTokensUsed = null; // Optional token usage
|
|
73
|
+
metaTokenLimit = null; // Optional token window
|
|
70
74
|
reservedLines = 2;
|
|
71
75
|
scrollRegionActive = false;
|
|
72
76
|
lastRenderContent = '';
|
|
@@ -74,9 +78,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
74
78
|
renderDirty = false;
|
|
75
79
|
isRendering = false;
|
|
76
80
|
pinnedTopRows = 0;
|
|
77
|
-
inlineAnchorRow = null;
|
|
78
|
-
inlineLayout = false;
|
|
79
|
-
anchorProvider = null;
|
|
80
81
|
// Lifecycle
|
|
81
82
|
disposed = false;
|
|
82
83
|
enabled = true;
|
|
@@ -91,10 +92,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
91
92
|
// Streaming render throttle
|
|
92
93
|
lastStreamingRender = 0;
|
|
93
94
|
streamingRenderInterval = 250; // ms between renders during streaming
|
|
94
|
-
// Metrics tracking for status bar
|
|
95
|
-
streamingStartTime = null;
|
|
96
|
-
tokensUsed = 0;
|
|
97
|
-
thinkingEnabled = true;
|
|
98
95
|
constructor(writeStream = process.stdout, config = {}) {
|
|
99
96
|
super();
|
|
100
97
|
this.out = writeStream;
|
|
@@ -182,11 +179,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
182
179
|
if (handled)
|
|
183
180
|
return;
|
|
184
181
|
}
|
|
185
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
186
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
187
|
-
this.emit('showHelp');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
182
|
// Insert printable characters
|
|
191
183
|
if (str && !key?.ctrl && !key?.meta) {
|
|
192
184
|
this.insertText(str);
|
|
@@ -195,55 +187,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
195
187
|
/**
|
|
196
188
|
* Set the input mode
|
|
197
189
|
*
|
|
198
|
-
* Streaming
|
|
199
|
-
*
|
|
200
|
-
* the cursor is (below the streamed content).
|
|
190
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
191
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
201
192
|
*/
|
|
202
193
|
setMode(mode) {
|
|
203
194
|
const prevMode = this.mode;
|
|
204
195
|
this.mode = mode;
|
|
205
196
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
206
|
-
//
|
|
207
|
-
this.
|
|
208
|
-
// Disable scroll region - let content flow naturally from current position
|
|
209
|
-
// This means content appears right after where cursor currently is
|
|
210
|
-
// (which should be after the banner)
|
|
211
|
-
this.disableScrollRegion();
|
|
212
|
-
// Hide cursor during streaming to avoid racing chars
|
|
213
|
-
this.write(ESC.HIDE);
|
|
197
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
198
|
+
this.enableScrollRegion();
|
|
214
199
|
this.renderDirty = true;
|
|
200
|
+
this.render();
|
|
215
201
|
}
|
|
216
202
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
217
|
-
//
|
|
218
|
-
this.
|
|
219
|
-
// Show cursor again
|
|
220
|
-
this.write(ESC.SHOW);
|
|
221
|
-
// Add a newline to separate content from input area
|
|
222
|
-
this.write('\n');
|
|
223
|
-
// Re-render the input area below the content
|
|
203
|
+
// Streaming ended - render the input area
|
|
204
|
+
this.enableScrollRegion();
|
|
224
205
|
this.forceRender();
|
|
225
206
|
}
|
|
226
207
|
}
|
|
227
|
-
/**
|
|
228
|
-
* Update token count for metrics display
|
|
229
|
-
*/
|
|
230
|
-
setTokensUsed(tokens) {
|
|
231
|
-
this.tokensUsed = tokens;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Toggle thinking/reasoning mode
|
|
235
|
-
*/
|
|
236
|
-
toggleThinking() {
|
|
237
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
238
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
239
|
-
this.scheduleRender();
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Get thinking enabled state
|
|
243
|
-
*/
|
|
244
|
-
isThinkingEnabled() {
|
|
245
|
-
return this.thinkingEnabled;
|
|
246
|
-
}
|
|
247
208
|
/**
|
|
248
209
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
249
210
|
*/
|
|
@@ -256,42 +217,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
256
217
|
}
|
|
257
218
|
}
|
|
258
219
|
}
|
|
259
|
-
/**
|
|
260
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
261
|
-
* restore the default bottom-aligned layout.
|
|
262
|
-
*/
|
|
263
|
-
setInlineAnchor(row) {
|
|
264
|
-
if (row === null || row === undefined) {
|
|
265
|
-
this.inlineAnchorRow = null;
|
|
266
|
-
this.inlineLayout = false;
|
|
267
|
-
this.renderDirty = true;
|
|
268
|
-
this.render();
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const { rows } = this.getSize();
|
|
272
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
273
|
-
this.inlineAnchorRow = clamped;
|
|
274
|
-
this.inlineLayout = true;
|
|
275
|
-
this.renderDirty = true;
|
|
276
|
-
this.render();
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
280
|
-
* output by re-evaluating the anchor before each render.
|
|
281
|
-
*/
|
|
282
|
-
setInlineAnchorProvider(provider) {
|
|
283
|
-
this.anchorProvider = provider;
|
|
284
|
-
if (!provider) {
|
|
285
|
-
this.inlineLayout = false;
|
|
286
|
-
this.inlineAnchorRow = null;
|
|
287
|
-
this.renderDirty = true;
|
|
288
|
-
this.render();
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
this.inlineLayout = true;
|
|
292
|
-
this.renderDirty = true;
|
|
293
|
-
this.render();
|
|
294
|
-
}
|
|
295
220
|
/**
|
|
296
221
|
* Get current mode
|
|
297
222
|
*/
|
|
@@ -401,6 +326,29 @@ export class TerminalInput extends EventEmitter {
|
|
|
401
326
|
this.streamingLabel = next;
|
|
402
327
|
this.scheduleRender();
|
|
403
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
331
|
+
*/
|
|
332
|
+
setMetaStatus(meta) {
|
|
333
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
334
|
+
? Math.floor(meta.elapsedSeconds)
|
|
335
|
+
: null;
|
|
336
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
337
|
+
? Math.floor(meta.tokensUsed)
|
|
338
|
+
: null;
|
|
339
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
340
|
+
? Math.floor(meta.tokenLimit)
|
|
341
|
+
: null;
|
|
342
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
343
|
+
this.metaTokensUsed === nextTokens &&
|
|
344
|
+
this.metaTokenLimit === nextLimit) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
348
|
+
this.metaTokensUsed = nextTokens;
|
|
349
|
+
this.metaTokenLimit = nextLimit;
|
|
350
|
+
this.scheduleRender();
|
|
351
|
+
}
|
|
404
352
|
/**
|
|
405
353
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
406
354
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -469,10 +417,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
469
417
|
// Use write lock during render to prevent interleaved output
|
|
470
418
|
writeLock.lock('terminalInput.render');
|
|
471
419
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (this.scrollRegionActive) {
|
|
475
|
-
this.disableScrollRegion();
|
|
420
|
+
if (!this.scrollRegionActive) {
|
|
421
|
+
this.enableScrollRegion();
|
|
476
422
|
}
|
|
477
423
|
const { rows, cols } = this.getSize();
|
|
478
424
|
const maxWidth = Math.max(8, cols - 4);
|
|
@@ -481,9 +427,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
481
427
|
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
482
428
|
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
483
429
|
const displayLines = Math.min(lines.length, maxVisible);
|
|
484
|
-
|
|
485
|
-
//
|
|
486
|
-
this.updateReservedLines(displayLines + 2);
|
|
430
|
+
const metaLine = this.buildMetaLine(cols - 2);
|
|
431
|
+
// Reserved lines: optional meta(1) + separator(1) + input lines + controls(1)
|
|
432
|
+
this.updateReservedLines(displayLines + 2 + (metaLine ? 1 : 0));
|
|
487
433
|
// Calculate display window (keep cursor visible)
|
|
488
434
|
let startLine = 0;
|
|
489
435
|
if (lines.length > displayLines) {
|
|
@@ -495,35 +441,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
495
441
|
// Render
|
|
496
442
|
this.write(ESC.HIDE);
|
|
497
443
|
this.write(ESC.RESET);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
|
|
512
|
-
this.updateReservedLines(visibleLines.length + 4);
|
|
513
|
-
// Status bar (streaming status + metrics)
|
|
514
|
-
this.write(ESC.TO(statusBarRow, 1));
|
|
515
|
-
this.write(ESC.CLEAR_LINE);
|
|
516
|
-
this.write(this.buildStatusBar(cols));
|
|
517
|
-
// Top separator
|
|
518
|
-
this.write(ESC.TO(topSepRow, 1));
|
|
444
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
445
|
+
let currentRow = startRow;
|
|
446
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
447
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
448
|
+
// Meta/status header (elapsed, tokens/context)
|
|
449
|
+
if (metaLine) {
|
|
450
|
+
this.write(ESC.TO(currentRow, 1));
|
|
451
|
+
this.write(ESC.CLEAR_LINE);
|
|
452
|
+
this.write(metaLine);
|
|
453
|
+
currentRow += 1;
|
|
454
|
+
}
|
|
455
|
+
// Separator line
|
|
456
|
+
this.write(ESC.TO(currentRow, 1));
|
|
519
457
|
this.write(ESC.CLEAR_LINE);
|
|
520
458
|
const divider = renderDivider(cols - 2);
|
|
521
459
|
this.write(divider);
|
|
460
|
+
currentRow += 1;
|
|
522
461
|
// Render input lines
|
|
523
|
-
let finalRow =
|
|
462
|
+
let finalRow = currentRow;
|
|
524
463
|
let finalCol = 3;
|
|
525
464
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
526
|
-
const rowNum =
|
|
465
|
+
const rowNum = currentRow + i;
|
|
527
466
|
this.write(ESC.TO(rowNum, 1));
|
|
528
467
|
this.write(ESC.CLEAR_LINE);
|
|
529
468
|
const line = visibleLines[i] ?? '';
|
|
@@ -561,15 +500,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
561
500
|
this.write(' '.repeat(padding));
|
|
562
501
|
this.write(ESC.RESET);
|
|
563
502
|
}
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
this.write(ESC.
|
|
567
|
-
this.write(divider);
|
|
568
|
-
// Mode controls (shortcuts + thinking toggle)
|
|
569
|
-
this.write(ESC.TO(modeControlRow, 1));
|
|
503
|
+
// Mode controls line (Claude Code style)
|
|
504
|
+
const controlRow = currentRow + visibleLines.length;
|
|
505
|
+
this.write(ESC.TO(controlRow, 1));
|
|
570
506
|
this.write(ESC.CLEAR_LINE);
|
|
571
507
|
this.write(this.buildModeControls(cols));
|
|
572
|
-
// Position cursor
|
|
508
|
+
// Position cursor
|
|
573
509
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
574
510
|
this.write(ESC.SHOW);
|
|
575
511
|
// Update state
|
|
@@ -582,79 +518,83 @@ export class TerminalInput extends EventEmitter {
|
|
|
582
518
|
}
|
|
583
519
|
}
|
|
584
520
|
/**
|
|
585
|
-
* Build
|
|
586
|
-
* This is the TOP line above the input area.
|
|
521
|
+
* Build a compact meta line above the divider (elapsed, context usage, queue size).
|
|
587
522
|
*/
|
|
588
|
-
|
|
523
|
+
buildMetaLine(width) {
|
|
589
524
|
const parts = [];
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
let statusText = '● Streaming';
|
|
593
|
-
if (this.streamingStartTime) {
|
|
594
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
595
|
-
const mins = Math.floor(elapsed / 60);
|
|
596
|
-
const secs = elapsed % 60;
|
|
597
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
598
|
-
}
|
|
599
|
-
parts.push({ text: statusText, tone: 'success' });
|
|
525
|
+
if (this.metaElapsedSeconds !== null) {
|
|
526
|
+
parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
|
|
600
527
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
// Token count (context usage)
|
|
605
|
-
if (this.tokensUsed > 0) {
|
|
606
|
-
const tokenStr = this.tokensUsed >= 1000
|
|
607
|
-
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
608
|
-
: `${this.tokensUsed}`;
|
|
609
|
-
parts.push({ text: `${tokenStr} tokens`, tone: 'info' });
|
|
528
|
+
if (this.metaTokensUsed !== null) {
|
|
529
|
+
const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
|
|
530
|
+
parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
|
|
610
531
|
}
|
|
611
|
-
// Context window remaining
|
|
612
532
|
if (this.contextUsage !== null) {
|
|
613
|
-
const
|
|
614
|
-
parts.push({ text: `
|
|
533
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
534
|
+
parts.push({ text: `used ${this.contextUsage}%`, tone });
|
|
615
535
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
536
|
+
if (this.queue.length > 0) {
|
|
537
|
+
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
538
|
+
}
|
|
539
|
+
return parts.length ? renderStatusLine(parts, width) : '';
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
543
|
+
*/
|
|
544
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
545
|
+
const width = Math.max(1, cols);
|
|
546
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
547
|
+
const row = startRow + i;
|
|
548
|
+
this.write(ESC.TO(row, 1));
|
|
549
|
+
this.write(' '.repeat(width));
|
|
620
550
|
}
|
|
621
|
-
return renderStatusLine(parts, cols - 2);
|
|
622
551
|
}
|
|
623
552
|
/**
|
|
624
|
-
* Build mode controls line
|
|
625
|
-
*
|
|
553
|
+
* Build Claude Code style mode controls line.
|
|
554
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
626
555
|
*/
|
|
627
556
|
buildModeControls(cols) {
|
|
628
557
|
const parts = [];
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
558
|
+
if (this.streamingLabel) {
|
|
559
|
+
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
560
|
+
}
|
|
561
|
+
if (this.overrideStatusMessage) {
|
|
562
|
+
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
563
|
+
}
|
|
564
|
+
if (this.statusMessage) {
|
|
565
|
+
parts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
566
|
+
}
|
|
567
|
+
const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
|
|
635
568
|
parts.push({
|
|
636
|
-
text:
|
|
569
|
+
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
637
570
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
638
571
|
});
|
|
639
|
-
|
|
572
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
640
573
|
parts.push({
|
|
641
|
-
text: this.
|
|
642
|
-
tone: 'muted',
|
|
574
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
575
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
643
576
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
577
|
+
const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
|
|
578
|
+
parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
|
|
579
|
+
if (this.contextUsage !== null) {
|
|
580
|
+
const pct = Math.max(0, 100 - this.contextUsage);
|
|
581
|
+
const tone = pct < 25 ? 'warn' : 'muted';
|
|
582
|
+
parts.push({ text: `context ${pct}%`, tone });
|
|
647
583
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
|
|
584
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
585
|
+
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
651
586
|
}
|
|
652
|
-
// Multi-line indicator
|
|
653
587
|
if (this.buffer.includes('\n')) {
|
|
654
|
-
|
|
588
|
+
const lineCount = this.buffer.split('\n').length;
|
|
589
|
+
parts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
590
|
+
}
|
|
591
|
+
if (this.pastePlaceholders.length > 0) {
|
|
592
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
593
|
+
parts.push({
|
|
594
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
595
|
+
tone: 'info',
|
|
596
|
+
});
|
|
655
597
|
}
|
|
656
|
-
// Shortcuts hint (at the end)
|
|
657
|
-
parts.push({ text: '? · esc', tone: 'muted' });
|
|
658
598
|
return renderStatusLine(parts, cols - 2);
|
|
659
599
|
}
|
|
660
600
|
/**
|
|
@@ -690,9 +630,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
690
630
|
* Register with display's output interceptor to position cursor correctly.
|
|
691
631
|
* When scroll region is active, output needs to go to the scroll region,
|
|
692
632
|
* not the protected bottom area where the input is rendered.
|
|
693
|
-
*
|
|
694
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
695
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
696
633
|
*/
|
|
697
634
|
registerOutputInterceptor(display) {
|
|
698
635
|
if (this.outputInterceptorCleanup) {
|
|
@@ -700,11 +637,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
700
637
|
}
|
|
701
638
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
702
639
|
beforeWrite: () => {
|
|
703
|
-
//
|
|
704
|
-
//
|
|
640
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
641
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
642
|
+
if (this.scrollRegionActive) {
|
|
643
|
+
const { rows } = this.getSize();
|
|
644
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
645
|
+
this.write(ESC.SAVE);
|
|
646
|
+
this.write(ESC.TO(scrollBottom, 1));
|
|
647
|
+
}
|
|
705
648
|
},
|
|
706
649
|
afterWrite: () => {
|
|
707
|
-
//
|
|
650
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
651
|
+
if (this.scrollRegionActive) {
|
|
652
|
+
this.write(ESC.RESTORE);
|
|
653
|
+
}
|
|
708
654
|
},
|
|
709
655
|
});
|
|
710
656
|
}
|
|
@@ -826,22 +772,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
826
772
|
this.toggleEditMode();
|
|
827
773
|
return true;
|
|
828
774
|
}
|
|
829
|
-
|
|
830
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
831
|
-
this.togglePasteExpansion();
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
this.toggleThinking();
|
|
835
|
-
}
|
|
836
|
-
return true;
|
|
837
|
-
case 'escape':
|
|
838
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
839
|
-
if (this.mode === 'streaming') {
|
|
840
|
-
this.emit('interrupt');
|
|
841
|
-
}
|
|
842
|
-
else if (this.buffer.length > 0) {
|
|
843
|
-
this.clear();
|
|
844
|
-
}
|
|
775
|
+
this.insertText(' ');
|
|
845
776
|
return true;
|
|
846
777
|
}
|
|
847
778
|
return false;
|
|
@@ -1132,7 +1063,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1132
1063
|
if (available <= 0)
|
|
1133
1064
|
return;
|
|
1134
1065
|
const chunk = clean.slice(0, available);
|
|
1135
|
-
|
|
1066
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1067
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1068
|
+
if (isMultiline && !isShortMultiline) {
|
|
1136
1069
|
this.insertPastePlaceholder(chunk);
|
|
1137
1070
|
}
|
|
1138
1071
|
else {
|
|
@@ -1152,6 +1085,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1152
1085
|
return;
|
|
1153
1086
|
this.applyScrollRegion();
|
|
1154
1087
|
this.scrollRegionActive = true;
|
|
1088
|
+
this.forceRender();
|
|
1155
1089
|
}
|
|
1156
1090
|
disableScrollRegion() {
|
|
1157
1091
|
if (!this.scrollRegionActive)
|
|
@@ -1302,17 +1236,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1302
1236
|
this.shiftPlaceholders(position, text.length);
|
|
1303
1237
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1304
1238
|
}
|
|
1239
|
+
shouldInlineMultiline(content) {
|
|
1240
|
+
const lines = content.split('\n').length;
|
|
1241
|
+
const maxInlineLines = 4;
|
|
1242
|
+
const maxInlineChars = 240;
|
|
1243
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1244
|
+
}
|
|
1305
1245
|
findPlaceholderAt(position) {
|
|
1306
1246
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1307
1247
|
}
|
|
1308
|
-
buildPlaceholder(
|
|
1248
|
+
buildPlaceholder(lineCount) {
|
|
1309
1249
|
const id = ++this.pasteCounter;
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
const preview = summary.preview.length > 30
|
|
1313
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1314
|
-
: summary.preview;
|
|
1315
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1250
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1251
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1316
1252
|
return { id, placeholder };
|
|
1317
1253
|
}
|
|
1318
1254
|
insertPastePlaceholder(content) {
|
|
@@ -1320,67 +1256,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1320
1256
|
if (available <= 0)
|
|
1321
1257
|
return;
|
|
1322
1258
|
const cleanContent = content.slice(0, available);
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
if (summary.lineCount < 5) {
|
|
1326
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1327
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1328
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1329
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1259
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1260
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1333
1261
|
const insertPos = this.cursor;
|
|
1334
1262
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1335
1263
|
this.pastePlaceholders.push({
|
|
1336
1264
|
id,
|
|
1337
1265
|
content: cleanContent,
|
|
1338
|
-
lineCount
|
|
1266
|
+
lineCount,
|
|
1339
1267
|
placeholder,
|
|
1340
1268
|
start: insertPos,
|
|
1341
1269
|
end: insertPos + placeholder.length,
|
|
1342
|
-
summary,
|
|
1343
|
-
expanded: false,
|
|
1344
1270
|
});
|
|
1345
1271
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1346
1272
|
this.cursor = insertPos + placeholder.length;
|
|
1347
1273
|
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1350
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1351
|
-
*/
|
|
1352
|
-
togglePasteExpansion() {
|
|
1353
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1354
|
-
if (!placeholder)
|
|
1355
|
-
return false;
|
|
1356
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1357
|
-
// Update the placeholder text in buffer
|
|
1358
|
-
const newPlaceholder = placeholder.expanded
|
|
1359
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1360
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1361
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1362
|
-
// Update buffer
|
|
1363
|
-
this.buffer =
|
|
1364
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1365
|
-
newPlaceholder +
|
|
1366
|
-
this.buffer.slice(placeholder.end);
|
|
1367
|
-
// Update placeholder tracking
|
|
1368
|
-
placeholder.placeholder = newPlaceholder;
|
|
1369
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1370
|
-
// Shift other placeholders
|
|
1371
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1372
|
-
this.scheduleRender();
|
|
1373
|
-
return true;
|
|
1374
|
-
}
|
|
1375
|
-
buildExpandedPlaceholder(ph) {
|
|
1376
|
-
const lines = ph.content.split('\n');
|
|
1377
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1378
|
-
const lastLines = lines.length > 5
|
|
1379
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1380
|
-
: '';
|
|
1381
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1382
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1383
|
-
}
|
|
1384
1274
|
deletePlaceholder(placeholder) {
|
|
1385
1275
|
const length = placeholder.end - placeholder.start;
|
|
1386
1276
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|