erosolar-cli 1.7.226 → 1.7.227
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 +18 -44
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +106 -254
- 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,56 +187,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
195
187
|
/**
|
|
196
188
|
* Set the input mode
|
|
197
189
|
*
|
|
198
|
-
* Streaming
|
|
199
|
-
*
|
|
200
|
-
* - Input area protected at bottom (outside scroll region)
|
|
201
|
-
* - Cursor positioned inside scroll region so content scrolls there
|
|
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.
|
|
202
192
|
*/
|
|
203
193
|
setMode(mode) {
|
|
204
194
|
const prevMode = this.mode;
|
|
205
195
|
this.mode = mode;
|
|
206
196
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
207
|
-
//
|
|
208
|
-
this.
|
|
209
|
-
const { rows } = this.getSize();
|
|
210
|
-
// Ensure scroll region is active to protect input area
|
|
211
|
-
if (!this.scrollRegionActive) {
|
|
212
|
-
this.applyScrollRegion();
|
|
213
|
-
this.scrollRegionActive = true;
|
|
214
|
-
}
|
|
215
|
-
// Position cursor at BOTTOM of scroll region (row = rows - reservedLines)
|
|
216
|
-
// This is where new streaming content should appear and scroll up from
|
|
217
|
-
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
218
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
197
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
198
|
+
this.enableScrollRegion();
|
|
219
199
|
this.renderDirty = true;
|
|
200
|
+
this.render();
|
|
220
201
|
}
|
|
221
202
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
222
|
-
//
|
|
223
|
-
this.
|
|
224
|
-
// Re-render the input area (scroll region keeps it protected)
|
|
203
|
+
// Streaming ended - render the input area
|
|
204
|
+
this.enableScrollRegion();
|
|
225
205
|
this.forceRender();
|
|
226
206
|
}
|
|
227
207
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Update token count for metrics display
|
|
230
|
-
*/
|
|
231
|
-
setTokensUsed(tokens) {
|
|
232
|
-
this.tokensUsed = tokens;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Toggle thinking/reasoning mode
|
|
236
|
-
*/
|
|
237
|
-
toggleThinking() {
|
|
238
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
239
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
240
|
-
this.scheduleRender();
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Get thinking enabled state
|
|
244
|
-
*/
|
|
245
|
-
isThinkingEnabled() {
|
|
246
|
-
return this.thinkingEnabled;
|
|
247
|
-
}
|
|
248
208
|
/**
|
|
249
209
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
250
210
|
*/
|
|
@@ -257,42 +217,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
257
217
|
}
|
|
258
218
|
}
|
|
259
219
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
262
|
-
* restore the default bottom-aligned layout.
|
|
263
|
-
*/
|
|
264
|
-
setInlineAnchor(row) {
|
|
265
|
-
if (row === null || row === undefined) {
|
|
266
|
-
this.inlineAnchorRow = null;
|
|
267
|
-
this.inlineLayout = false;
|
|
268
|
-
this.renderDirty = true;
|
|
269
|
-
this.render();
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const { rows } = this.getSize();
|
|
273
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
274
|
-
this.inlineAnchorRow = clamped;
|
|
275
|
-
this.inlineLayout = true;
|
|
276
|
-
this.renderDirty = true;
|
|
277
|
-
this.render();
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
281
|
-
* output by re-evaluating the anchor before each render.
|
|
282
|
-
*/
|
|
283
|
-
setInlineAnchorProvider(provider) {
|
|
284
|
-
this.anchorProvider = provider;
|
|
285
|
-
if (!provider) {
|
|
286
|
-
this.inlineLayout = false;
|
|
287
|
-
this.inlineAnchorRow = null;
|
|
288
|
-
this.renderDirty = true;
|
|
289
|
-
this.render();
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
this.inlineLayout = true;
|
|
293
|
-
this.renderDirty = true;
|
|
294
|
-
this.render();
|
|
295
|
-
}
|
|
296
220
|
/**
|
|
297
221
|
* Get current mode
|
|
298
222
|
*/
|
|
@@ -402,6 +326,29 @@ export class TerminalInput extends EventEmitter {
|
|
|
402
326
|
this.streamingLabel = next;
|
|
403
327
|
this.scheduleRender();
|
|
404
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
|
+
}
|
|
405
352
|
/**
|
|
406
353
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
407
354
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -480,9 +427,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
480
427
|
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
481
428
|
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
482
429
|
const displayLines = Math.min(lines.length, maxVisible);
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
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));
|
|
486
433
|
// Calculate display window (keep cursor visible)
|
|
487
434
|
let startLine = 0;
|
|
488
435
|
if (lines.length > displayLines) {
|
|
@@ -494,35 +441,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
494
441
|
// Render
|
|
495
442
|
this.write(ESC.HIDE);
|
|
496
443
|
this.write(ESC.RESET);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const topSepRow = inputStartRow - 1;
|
|
509
|
-
const statusBarRow = topSepRow - 1;
|
|
510
|
-
// Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
|
|
511
|
-
this.updateReservedLines(visibleLines.length + 4);
|
|
512
|
-
// Status bar (streaming status + metrics)
|
|
513
|
-
this.write(ESC.TO(statusBarRow, 1));
|
|
514
|
-
this.write(ESC.CLEAR_LINE);
|
|
515
|
-
this.write(this.buildStatusBar(cols));
|
|
516
|
-
// Top separator
|
|
517
|
-
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));
|
|
518
455
|
this.write(ESC.CLEAR_LINE);
|
|
519
456
|
const divider = renderDivider(cols - 2);
|
|
520
457
|
this.write(divider);
|
|
458
|
+
currentRow += 1;
|
|
521
459
|
// Render input lines
|
|
522
|
-
let finalRow =
|
|
460
|
+
let finalRow = currentRow;
|
|
523
461
|
let finalCol = 3;
|
|
524
462
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
525
|
-
const rowNum =
|
|
463
|
+
const rowNum = currentRow + i;
|
|
526
464
|
this.write(ESC.TO(rowNum, 1));
|
|
527
465
|
this.write(ESC.CLEAR_LINE);
|
|
528
466
|
const line = visibleLines[i] ?? '';
|
|
@@ -560,15 +498,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
560
498
|
this.write(' '.repeat(padding));
|
|
561
499
|
this.write(ESC.RESET);
|
|
562
500
|
}
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
this.write(ESC.
|
|
566
|
-
this.write(divider);
|
|
567
|
-
// Mode controls (shortcuts + thinking toggle)
|
|
568
|
-
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));
|
|
569
504
|
this.write(ESC.CLEAR_LINE);
|
|
570
505
|
this.write(this.buildModeControls(cols));
|
|
571
|
-
// Position cursor
|
|
506
|
+
// Position cursor
|
|
572
507
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
573
508
|
this.write(ESC.SHOW);
|
|
574
509
|
// Update state
|
|
@@ -581,86 +516,69 @@ export class TerminalInput extends EventEmitter {
|
|
|
581
516
|
}
|
|
582
517
|
}
|
|
583
518
|
/**
|
|
584
|
-
* Build
|
|
519
|
+
* Build a compact meta line above the divider (elapsed, context usage, queue size).
|
|
585
520
|
*/
|
|
586
|
-
|
|
521
|
+
buildMetaLine(width) {
|
|
587
522
|
const parts = [];
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
parts.push({ text: '● Streaming', tone: 'success' });
|
|
591
|
-
// Elapsed time
|
|
592
|
-
if (this.streamingStartTime) {
|
|
593
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
594
|
-
const mins = Math.floor(elapsed / 60);
|
|
595
|
-
const secs = elapsed % 60;
|
|
596
|
-
const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
597
|
-
parts.push({ text: `⏱ ${timeStr}`, tone: 'muted' });
|
|
598
|
-
}
|
|
523
|
+
if (this.metaElapsedSeconds !== null) {
|
|
524
|
+
parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
|
|
599
525
|
}
|
|
600
|
-
|
|
601
|
-
|
|
526
|
+
if (this.metaTokensUsed !== null) {
|
|
527
|
+
const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
|
|
528
|
+
parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
|
|
602
529
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
607
|
-
: `${this.tokensUsed}`;
|
|
608
|
-
parts.push({ text: `◈ ${tokenStr} tokens`, tone: 'info' });
|
|
530
|
+
if (this.contextUsage !== null) {
|
|
531
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
532
|
+
parts.push({ text: `used ${this.contextUsage}%`, tone });
|
|
609
533
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
text: this.thinkingEnabled ? '💭 Thinking on' : '💭 Thinking off',
|
|
613
|
-
tone: this.thinkingEnabled ? 'info' : 'muted',
|
|
614
|
-
});
|
|
615
|
-
// Paste block count if any
|
|
616
|
-
if (this.pastePlaceholders.length > 0) {
|
|
617
|
-
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
618
|
-
parts.push({
|
|
619
|
-
text: `📋 ${this.pastePlaceholders.length} paste(s), ${totalLines} lines`,
|
|
620
|
-
tone: 'info',
|
|
621
|
-
});
|
|
534
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
535
|
+
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
622
536
|
}
|
|
623
|
-
return renderStatusLine(parts,
|
|
537
|
+
return parts.length ? renderStatusLine(parts, width) : '';
|
|
624
538
|
}
|
|
625
539
|
/**
|
|
626
540
|
* Build Claude Code style mode controls line.
|
|
627
|
-
*
|
|
541
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
628
542
|
*/
|
|
629
543
|
buildModeControls(cols) {
|
|
630
544
|
const parts = [];
|
|
631
|
-
// Keyboard shortcuts hint
|
|
632
|
-
parts.push({ text: '? help', tone: 'muted' });
|
|
633
|
-
parts.push({ text: 'esc interrupt', tone: 'muted' });
|
|
634
|
-
// Streaming status messages
|
|
635
545
|
if (this.streamingLabel) {
|
|
636
546
|
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
637
547
|
}
|
|
638
548
|
if (this.overrideStatusMessage) {
|
|
639
549
|
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
640
550
|
}
|
|
641
|
-
|
|
642
|
-
|
|
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';
|
|
643
555
|
parts.push({
|
|
644
556
|
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
645
557
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
646
558
|
});
|
|
559
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
560
|
+
parts.push({
|
|
561
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
562
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
563
|
+
});
|
|
647
564
|
const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
|
|
648
565
|
parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
|
|
649
|
-
// Context usage
|
|
650
566
|
if (this.contextUsage !== null) {
|
|
651
567
|
const pct = Math.max(0, 100 - this.contextUsage);
|
|
652
568
|
const tone = pct < 25 ? 'warn' : 'muted';
|
|
653
|
-
parts.push({ text: `
|
|
569
|
+
parts.push({ text: `context ${pct}%`, tone });
|
|
654
570
|
}
|
|
655
|
-
// Queue during streaming
|
|
656
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
657
|
-
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
658
|
-
}
|
|
659
|
-
// Multi-line indicator
|
|
660
571
|
if (this.buffer.includes('\n')) {
|
|
661
572
|
const lineCount = this.buffer.split('\n').length;
|
|
662
573
|
parts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
663
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
|
+
});
|
|
581
|
+
}
|
|
664
582
|
return renderStatusLine(parts, cols - 2);
|
|
665
583
|
}
|
|
666
584
|
/**
|
|
@@ -696,23 +614,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
696
614
|
* Register with display's output interceptor to position cursor correctly.
|
|
697
615
|
* When scroll region is active, output needs to go to the scroll region,
|
|
698
616
|
* not the protected bottom area where the input is rendered.
|
|
699
|
-
*
|
|
700
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
701
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
702
617
|
*/
|
|
703
618
|
registerOutputInterceptor(display) {
|
|
704
619
|
if (this.outputInterceptorCleanup) {
|
|
705
620
|
this.outputInterceptorCleanup();
|
|
706
621
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
// Scroll region handles content containment automatically
|
|
710
|
-
// No per-write cursor manipulation needed
|
|
711
|
-
},
|
|
712
|
-
afterWrite: () => {
|
|
713
|
-
// No cursor manipulation needed
|
|
714
|
-
},
|
|
715
|
-
});
|
|
622
|
+
// Scroll region handles containment now; interceptor is a no-op placeholder to keep API stable.
|
|
623
|
+
this.outputInterceptorCleanup = display.registerOutputInterceptor({});
|
|
716
624
|
}
|
|
717
625
|
/**
|
|
718
626
|
* Dispose and clean up
|
|
@@ -832,22 +740,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
832
740
|
this.toggleEditMode();
|
|
833
741
|
return true;
|
|
834
742
|
}
|
|
835
|
-
|
|
836
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
837
|
-
this.togglePasteExpansion();
|
|
838
|
-
}
|
|
839
|
-
else {
|
|
840
|
-
this.toggleThinking();
|
|
841
|
-
}
|
|
842
|
-
return true;
|
|
843
|
-
case 'escape':
|
|
844
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
845
|
-
if (this.mode === 'streaming') {
|
|
846
|
-
this.emit('interrupt');
|
|
847
|
-
}
|
|
848
|
-
else if (this.buffer.length > 0) {
|
|
849
|
-
this.clear();
|
|
850
|
-
}
|
|
743
|
+
this.insertText(' ');
|
|
851
744
|
return true;
|
|
852
745
|
}
|
|
853
746
|
return false;
|
|
@@ -1138,7 +1031,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1138
1031
|
if (available <= 0)
|
|
1139
1032
|
return;
|
|
1140
1033
|
const chunk = clean.slice(0, available);
|
|
1141
|
-
|
|
1034
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1035
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1036
|
+
if (isMultiline && !isShortMultiline) {
|
|
1142
1037
|
this.insertPastePlaceholder(chunk);
|
|
1143
1038
|
}
|
|
1144
1039
|
else {
|
|
@@ -1158,6 +1053,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1158
1053
|
return;
|
|
1159
1054
|
this.applyScrollRegion();
|
|
1160
1055
|
this.scrollRegionActive = true;
|
|
1056
|
+
this.forceRender();
|
|
1161
1057
|
}
|
|
1162
1058
|
disableScrollRegion() {
|
|
1163
1059
|
if (!this.scrollRegionActive)
|
|
@@ -1308,17 +1204,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1308
1204
|
this.shiftPlaceholders(position, text.length);
|
|
1309
1205
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1310
1206
|
}
|
|
1207
|
+
shouldInlineMultiline(content) {
|
|
1208
|
+
const lines = content.split('\n').length;
|
|
1209
|
+
const maxInlineLines = 4;
|
|
1210
|
+
const maxInlineChars = 240;
|
|
1211
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1212
|
+
}
|
|
1311
1213
|
findPlaceholderAt(position) {
|
|
1312
1214
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1313
1215
|
}
|
|
1314
|
-
buildPlaceholder(
|
|
1216
|
+
buildPlaceholder(lineCount) {
|
|
1315
1217
|
const id = ++this.pasteCounter;
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
const preview = summary.preview.length > 30
|
|
1319
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1320
|
-
: summary.preview;
|
|
1321
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1218
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1219
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1322
1220
|
return { id, placeholder };
|
|
1323
1221
|
}
|
|
1324
1222
|
insertPastePlaceholder(content) {
|
|
@@ -1326,67 +1224,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1326
1224
|
if (available <= 0)
|
|
1327
1225
|
return;
|
|
1328
1226
|
const cleanContent = content.slice(0, available);
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
if (summary.lineCount < 5) {
|
|
1332
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1333
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1334
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1335
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1227
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1228
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1339
1229
|
const insertPos = this.cursor;
|
|
1340
1230
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1341
1231
|
this.pastePlaceholders.push({
|
|
1342
1232
|
id,
|
|
1343
1233
|
content: cleanContent,
|
|
1344
|
-
lineCount
|
|
1234
|
+
lineCount,
|
|
1345
1235
|
placeholder,
|
|
1346
1236
|
start: insertPos,
|
|
1347
1237
|
end: insertPos + placeholder.length,
|
|
1348
|
-
summary,
|
|
1349
|
-
expanded: false,
|
|
1350
1238
|
});
|
|
1351
1239
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1352
1240
|
this.cursor = insertPos + placeholder.length;
|
|
1353
1241
|
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1356
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1357
|
-
*/
|
|
1358
|
-
togglePasteExpansion() {
|
|
1359
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1360
|
-
if (!placeholder)
|
|
1361
|
-
return false;
|
|
1362
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1363
|
-
// Update the placeholder text in buffer
|
|
1364
|
-
const newPlaceholder = placeholder.expanded
|
|
1365
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1366
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1367
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1368
|
-
// Update buffer
|
|
1369
|
-
this.buffer =
|
|
1370
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1371
|
-
newPlaceholder +
|
|
1372
|
-
this.buffer.slice(placeholder.end);
|
|
1373
|
-
// Update placeholder tracking
|
|
1374
|
-
placeholder.placeholder = newPlaceholder;
|
|
1375
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1376
|
-
// Shift other placeholders
|
|
1377
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1378
|
-
this.scheduleRender();
|
|
1379
|
-
return true;
|
|
1380
|
-
}
|
|
1381
|
-
buildExpandedPlaceholder(ph) {
|
|
1382
|
-
const lines = ph.content.split('\n');
|
|
1383
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1384
|
-
const lastLines = lines.length > 5
|
|
1385
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1386
|
-
: '';
|
|
1387
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1388
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1389
|
-
}
|
|
1390
1242
|
deletePlaceholder(placeholder) {
|
|
1391
1243
|
const length = placeholder.end - placeholder.start;
|
|
1392
1244
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|