erosolar-cli 1.7.228 → 1.7.229
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 +19 -45
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +130 -256
- 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,26 @@ 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
|
-
const topSepRow = inputStartRow - 1;
|
|
510
|
-
const statusBarRow = topSepRow - 1;
|
|
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
|
+
// Meta/status header (elapsed, tokens/context)
|
|
447
|
+
if (metaLine) {
|
|
448
|
+
this.write(ESC.TO(currentRow, 1));
|
|
449
|
+
this.write(ESC.CLEAR_LINE);
|
|
450
|
+
this.write(metaLine);
|
|
451
|
+
currentRow += 1;
|
|
452
|
+
}
|
|
453
|
+
// Separator line
|
|
454
|
+
this.write(ESC.TO(currentRow, 1));
|
|
519
455
|
this.write(ESC.CLEAR_LINE);
|
|
520
456
|
const divider = renderDivider(cols - 2);
|
|
521
457
|
this.write(divider);
|
|
458
|
+
currentRow += 1;
|
|
522
459
|
// Render input lines
|
|
523
|
-
let finalRow =
|
|
460
|
+
let finalRow = currentRow;
|
|
524
461
|
let finalCol = 3;
|
|
525
462
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
526
|
-
const rowNum =
|
|
463
|
+
const rowNum = currentRow + i;
|
|
527
464
|
this.write(ESC.TO(rowNum, 1));
|
|
528
465
|
this.write(ESC.CLEAR_LINE);
|
|
529
466
|
const line = visibleLines[i] ?? '';
|
|
@@ -561,15 +498,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
561
498
|
this.write(' '.repeat(padding));
|
|
562
499
|
this.write(ESC.RESET);
|
|
563
500
|
}
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
this.write(ESC.
|
|
567
|
-
this.write(divider);
|
|
568
|
-
// Mode controls (shortcuts + thinking toggle)
|
|
569
|
-
this.write(ESC.TO(modeControlRow, 1));
|
|
501
|
+
// Mode controls line (Claude Code style)
|
|
502
|
+
const controlRow = currentRow + visibleLines.length;
|
|
503
|
+
this.write(ESC.TO(controlRow, 1));
|
|
570
504
|
this.write(ESC.CLEAR_LINE);
|
|
571
505
|
this.write(this.buildModeControls(cols));
|
|
572
|
-
// Position cursor
|
|
506
|
+
// Position cursor
|
|
573
507
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
574
508
|
this.write(ESC.SHOW);
|
|
575
509
|
// Update state
|
|
@@ -582,79 +516,69 @@ export class TerminalInput extends EventEmitter {
|
|
|
582
516
|
}
|
|
583
517
|
}
|
|
584
518
|
/**
|
|
585
|
-
* Build
|
|
586
|
-
* This is the TOP line above the input area.
|
|
519
|
+
* Build a compact meta line above the divider (elapsed, context usage, queue size).
|
|
587
520
|
*/
|
|
588
|
-
|
|
521
|
+
buildMetaLine(width) {
|
|
589
522
|
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' });
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
parts.push({ text: '○ Ready', tone: 'muted' });
|
|
523
|
+
if (this.metaElapsedSeconds !== null) {
|
|
524
|
+
parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
|
|
603
525
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
608
|
-
: `${this.tokensUsed}`;
|
|
609
|
-
parts.push({ text: `${tokenStr} tokens`, tone: 'info' });
|
|
526
|
+
if (this.metaTokensUsed !== null) {
|
|
527
|
+
const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
|
|
528
|
+
parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
|
|
610
529
|
}
|
|
611
|
-
// Context window remaining
|
|
612
530
|
if (this.contextUsage !== null) {
|
|
613
|
-
const
|
|
614
|
-
parts.push({ text: `
|
|
531
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
532
|
+
parts.push({ text: `used ${this.contextUsage}%`, tone });
|
|
615
533
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
619
|
-
parts.push({ text: `📋 ${totalLines}L`, tone: 'info' });
|
|
534
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
535
|
+
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
620
536
|
}
|
|
621
|
-
return renderStatusLine(parts,
|
|
537
|
+
return parts.length ? renderStatusLine(parts, width) : '';
|
|
622
538
|
}
|
|
623
539
|
/**
|
|
624
|
-
* Build mode controls line
|
|
625
|
-
*
|
|
540
|
+
* Build Claude Code style mode controls line.
|
|
541
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
626
542
|
*/
|
|
627
543
|
buildModeControls(cols) {
|
|
628
544
|
const parts = [];
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
545
|
+
if (this.streamingLabel) {
|
|
546
|
+
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
547
|
+
}
|
|
548
|
+
if (this.overrideStatusMessage) {
|
|
549
|
+
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
550
|
+
}
|
|
551
|
+
if (this.statusMessage) {
|
|
552
|
+
parts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
553
|
+
}
|
|
554
|
+
const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
|
|
635
555
|
parts.push({
|
|
636
|
-
text:
|
|
556
|
+
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
637
557
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
638
558
|
});
|
|
639
|
-
|
|
559
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
640
560
|
parts.push({
|
|
641
|
-
text: this.
|
|
642
|
-
tone: 'muted',
|
|
561
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
562
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
643
563
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
|
|
564
|
+
const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
|
|
565
|
+
parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
|
|
566
|
+
if (this.contextUsage !== null) {
|
|
567
|
+
const pct = Math.max(0, 100 - this.contextUsage);
|
|
568
|
+
const tone = pct < 25 ? 'warn' : 'muted';
|
|
569
|
+
parts.push({ text: `context ${pct}%`, tone });
|
|
651
570
|
}
|
|
652
|
-
// Multi-line indicator
|
|
653
571
|
if (this.buffer.includes('\n')) {
|
|
654
|
-
|
|
572
|
+
const lineCount = this.buffer.split('\n').length;
|
|
573
|
+
parts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
574
|
+
}
|
|
575
|
+
if (this.pastePlaceholders.length > 0) {
|
|
576
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
577
|
+
parts.push({
|
|
578
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
579
|
+
tone: 'info',
|
|
580
|
+
});
|
|
655
581
|
}
|
|
656
|
-
// Shortcuts hint (at the end)
|
|
657
|
-
parts.push({ text: '? · esc', tone: 'muted' });
|
|
658
582
|
return renderStatusLine(parts, cols - 2);
|
|
659
583
|
}
|
|
660
584
|
/**
|
|
@@ -690,9 +614,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
690
614
|
* Register with display's output interceptor to position cursor correctly.
|
|
691
615
|
* When scroll region is active, output needs to go to the scroll region,
|
|
692
616
|
* 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
617
|
*/
|
|
697
618
|
registerOutputInterceptor(display) {
|
|
698
619
|
if (this.outputInterceptorCleanup) {
|
|
@@ -700,11 +621,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
700
621
|
}
|
|
701
622
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
702
623
|
beforeWrite: () => {
|
|
703
|
-
//
|
|
704
|
-
//
|
|
624
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
625
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
626
|
+
if (this.scrollRegionActive) {
|
|
627
|
+
const { rows } = this.getSize();
|
|
628
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
629
|
+
this.write(ESC.SAVE);
|
|
630
|
+
this.write(ESC.TO(scrollBottom, 1));
|
|
631
|
+
}
|
|
705
632
|
},
|
|
706
633
|
afterWrite: () => {
|
|
707
|
-
//
|
|
634
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
635
|
+
if (this.scrollRegionActive) {
|
|
636
|
+
this.write(ESC.RESTORE);
|
|
637
|
+
}
|
|
708
638
|
},
|
|
709
639
|
});
|
|
710
640
|
}
|
|
@@ -826,22 +756,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
826
756
|
this.toggleEditMode();
|
|
827
757
|
return true;
|
|
828
758
|
}
|
|
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
|
-
}
|
|
759
|
+
this.insertText(' ');
|
|
845
760
|
return true;
|
|
846
761
|
}
|
|
847
762
|
return false;
|
|
@@ -1132,7 +1047,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1132
1047
|
if (available <= 0)
|
|
1133
1048
|
return;
|
|
1134
1049
|
const chunk = clean.slice(0, available);
|
|
1135
|
-
|
|
1050
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1051
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1052
|
+
if (isMultiline && !isShortMultiline) {
|
|
1136
1053
|
this.insertPastePlaceholder(chunk);
|
|
1137
1054
|
}
|
|
1138
1055
|
else {
|
|
@@ -1152,6 +1069,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1152
1069
|
return;
|
|
1153
1070
|
this.applyScrollRegion();
|
|
1154
1071
|
this.scrollRegionActive = true;
|
|
1072
|
+
this.forceRender();
|
|
1155
1073
|
}
|
|
1156
1074
|
disableScrollRegion() {
|
|
1157
1075
|
if (!this.scrollRegionActive)
|
|
@@ -1302,17 +1220,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1302
1220
|
this.shiftPlaceholders(position, text.length);
|
|
1303
1221
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1304
1222
|
}
|
|
1223
|
+
shouldInlineMultiline(content) {
|
|
1224
|
+
const lines = content.split('\n').length;
|
|
1225
|
+
const maxInlineLines = 4;
|
|
1226
|
+
const maxInlineChars = 240;
|
|
1227
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1228
|
+
}
|
|
1305
1229
|
findPlaceholderAt(position) {
|
|
1306
1230
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1307
1231
|
}
|
|
1308
|
-
buildPlaceholder(
|
|
1232
|
+
buildPlaceholder(lineCount) {
|
|
1309
1233
|
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}"`;
|
|
1234
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1235
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1316
1236
|
return { id, placeholder };
|
|
1317
1237
|
}
|
|
1318
1238
|
insertPastePlaceholder(content) {
|
|
@@ -1320,67 +1240,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1320
1240
|
if (available <= 0)
|
|
1321
1241
|
return;
|
|
1322
1242
|
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);
|
|
1243
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1244
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1333
1245
|
const insertPos = this.cursor;
|
|
1334
1246
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1335
1247
|
this.pastePlaceholders.push({
|
|
1336
1248
|
id,
|
|
1337
1249
|
content: cleanContent,
|
|
1338
|
-
lineCount
|
|
1250
|
+
lineCount,
|
|
1339
1251
|
placeholder,
|
|
1340
1252
|
start: insertPos,
|
|
1341
1253
|
end: insertPos + placeholder.length,
|
|
1342
|
-
summary,
|
|
1343
|
-
expanded: false,
|
|
1344
1254
|
});
|
|
1345
1255
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1346
1256
|
this.cursor = insertPos + placeholder.length;
|
|
1347
1257
|
}
|
|
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
1258
|
deletePlaceholder(placeholder) {
|
|
1385
1259
|
const length = placeholder.end - placeholder.start;
|
|
1386
1260
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|