erosolar-cli 1.7.227 → 1.7.228
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/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/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 -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/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 +45 -19
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +262 -120
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +8 -6
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +12 -6
- package/dist/shell/terminalInputAdapter.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/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.map +1 -1
- package/dist/ui/unified/layout.js +26 -13
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,14 +3,13 @@
|
|
|
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
14
|
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
16
15
|
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
@@ -68,9 +67,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
68
67
|
statusMessage = null;
|
|
69
68
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
70
69
|
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
|
|
74
70
|
reservedLines = 2;
|
|
75
71
|
scrollRegionActive = false;
|
|
76
72
|
lastRenderContent = '';
|
|
@@ -78,6 +74,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
78
74
|
renderDirty = false;
|
|
79
75
|
isRendering = false;
|
|
80
76
|
pinnedTopRows = 0;
|
|
77
|
+
inlineAnchorRow = null;
|
|
78
|
+
inlineLayout = false;
|
|
79
|
+
anchorProvider = null;
|
|
81
80
|
// Lifecycle
|
|
82
81
|
disposed = false;
|
|
83
82
|
enabled = true;
|
|
@@ -92,6 +91,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
92
91
|
// Streaming render throttle
|
|
93
92
|
lastStreamingRender = 0;
|
|
94
93
|
streamingRenderInterval = 250; // ms between renders during streaming
|
|
94
|
+
// Metrics tracking for status bar
|
|
95
|
+
streamingStartTime = null;
|
|
96
|
+
tokensUsed = 0;
|
|
97
|
+
thinkingEnabled = true;
|
|
95
98
|
constructor(writeStream = process.stdout, config = {}) {
|
|
96
99
|
super();
|
|
97
100
|
this.out = writeStream;
|
|
@@ -179,6 +182,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
179
182
|
if (handled)
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
185
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
186
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
187
|
+
this.emit('showHelp');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
182
190
|
// Insert printable characters
|
|
183
191
|
if (str && !key?.ctrl && !key?.meta) {
|
|
184
192
|
this.insertText(str);
|
|
@@ -187,24 +195,55 @@ export class TerminalInput extends EventEmitter {
|
|
|
187
195
|
/**
|
|
188
196
|
* Set the input mode
|
|
189
197
|
*
|
|
190
|
-
* Streaming
|
|
191
|
-
*
|
|
198
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
199
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
200
|
+
* the cursor is (below the streamed content).
|
|
192
201
|
*/
|
|
193
202
|
setMode(mode) {
|
|
194
203
|
const prevMode = this.mode;
|
|
195
204
|
this.mode = mode;
|
|
196
205
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
197
|
-
//
|
|
198
|
-
this.
|
|
206
|
+
// Track streaming start time for elapsed display
|
|
207
|
+
this.streamingStartTime = Date.now();
|
|
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);
|
|
199
214
|
this.renderDirty = true;
|
|
200
|
-
this.render();
|
|
201
215
|
}
|
|
202
216
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
203
|
-
//
|
|
204
|
-
this.
|
|
217
|
+
// Reset streaming time
|
|
218
|
+
this.streamingStartTime = null;
|
|
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
|
|
205
224
|
this.forceRender();
|
|
206
225
|
}
|
|
207
226
|
}
|
|
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
|
+
}
|
|
208
247
|
/**
|
|
209
248
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
210
249
|
*/
|
|
@@ -217,6 +256,42 @@ export class TerminalInput extends EventEmitter {
|
|
|
217
256
|
}
|
|
218
257
|
}
|
|
219
258
|
}
|
|
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
|
+
}
|
|
220
295
|
/**
|
|
221
296
|
* Get current mode
|
|
222
297
|
*/
|
|
@@ -326,29 +401,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
326
401
|
this.streamingLabel = next;
|
|
327
402
|
this.scheduleRender();
|
|
328
403
|
}
|
|
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
|
-
}
|
|
352
404
|
/**
|
|
353
405
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
354
406
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -417,8 +469,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
417
469
|
// Use write lock during render to prevent interleaved output
|
|
418
470
|
writeLock.lock('terminalInput.render');
|
|
419
471
|
try {
|
|
420
|
-
|
|
421
|
-
|
|
472
|
+
// No scroll regions - we render the input area directly at the bottom
|
|
473
|
+
// Content flows naturally above it
|
|
474
|
+
if (this.scrollRegionActive) {
|
|
475
|
+
this.disableScrollRegion();
|
|
422
476
|
}
|
|
423
477
|
const { rows, cols } = this.getSize();
|
|
424
478
|
const maxWidth = Math.max(8, cols - 4);
|
|
@@ -427,9 +481,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
427
481
|
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
428
482
|
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
429
483
|
const displayLines = Math.min(lines.length, maxVisible);
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
this.updateReservedLines(displayLines + 2
|
|
484
|
+
// Reserved lines: separator(1) + controls(1) + input lines
|
|
485
|
+
// Layout: [separator] [controls] [input...]
|
|
486
|
+
this.updateReservedLines(displayLines + 2);
|
|
433
487
|
// Calculate display window (keep cursor visible)
|
|
434
488
|
let startLine = 0;
|
|
435
489
|
if (lines.length > displayLines) {
|
|
@@ -441,26 +495,35 @@ export class TerminalInput extends EventEmitter {
|
|
|
441
495
|
// Render
|
|
442
496
|
this.write(ESC.HIDE);
|
|
443
497
|
this.write(ESC.RESET);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
498
|
+
// Enhanced layout from bottom to top:
|
|
499
|
+
// Row N: Mode controls (shortcuts, thinking toggle)
|
|
500
|
+
// Row N-1: Bottom separator
|
|
501
|
+
// Row N-2: Input area
|
|
502
|
+
// Row N-3: Top separator
|
|
503
|
+
// Row N-4: Status bar (streaming status, elapsed time, tokens)
|
|
504
|
+
// Calculate positions from absolute bottom
|
|
505
|
+
const modeControlRow = rows;
|
|
506
|
+
const bottomSepRow = rows - 1;
|
|
507
|
+
const inputEndRow = rows - 2;
|
|
508
|
+
const inputStartRow = inputEndRow - visibleLines.length + 1;
|
|
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));
|
|
455
519
|
this.write(ESC.CLEAR_LINE);
|
|
456
520
|
const divider = renderDivider(cols - 2);
|
|
457
521
|
this.write(divider);
|
|
458
|
-
currentRow += 1;
|
|
459
522
|
// Render input lines
|
|
460
|
-
let finalRow =
|
|
523
|
+
let finalRow = inputStartRow;
|
|
461
524
|
let finalCol = 3;
|
|
462
525
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
463
|
-
const rowNum =
|
|
526
|
+
const rowNum = inputStartRow + i;
|
|
464
527
|
this.write(ESC.TO(rowNum, 1));
|
|
465
528
|
this.write(ESC.CLEAR_LINE);
|
|
466
529
|
const line = visibleLines[i] ?? '';
|
|
@@ -498,12 +561,15 @@ export class TerminalInput extends EventEmitter {
|
|
|
498
561
|
this.write(' '.repeat(padding));
|
|
499
562
|
this.write(ESC.RESET);
|
|
500
563
|
}
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
this.write(ESC.
|
|
564
|
+
// Bottom separator
|
|
565
|
+
this.write(ESC.TO(bottomSepRow, 1));
|
|
566
|
+
this.write(ESC.CLEAR_LINE);
|
|
567
|
+
this.write(divider);
|
|
568
|
+
// Mode controls (shortcuts + thinking toggle)
|
|
569
|
+
this.write(ESC.TO(modeControlRow, 1));
|
|
504
570
|
this.write(ESC.CLEAR_LINE);
|
|
505
571
|
this.write(this.buildModeControls(cols));
|
|
506
|
-
// Position cursor
|
|
572
|
+
// Position cursor in input area
|
|
507
573
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
508
574
|
this.write(ESC.SHOW);
|
|
509
575
|
// Update state
|
|
@@ -516,69 +582,79 @@ export class TerminalInput extends EventEmitter {
|
|
|
516
582
|
}
|
|
517
583
|
}
|
|
518
584
|
/**
|
|
519
|
-
* Build
|
|
585
|
+
* Build status bar showing streaming/ready status, elapsed time, and token count.
|
|
586
|
+
* This is the TOP line above the input area.
|
|
520
587
|
*/
|
|
521
|
-
|
|
588
|
+
buildStatusBar(cols) {
|
|
522
589
|
const parts = [];
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
// Streaming/Ready status with elapsed time
|
|
591
|
+
if (this.mode === 'streaming') {
|
|
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' });
|
|
525
603
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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' });
|
|
529
610
|
}
|
|
611
|
+
// Context window remaining
|
|
530
612
|
if (this.contextUsage !== null) {
|
|
531
|
-
const
|
|
532
|
-
parts.push({ text: `
|
|
613
|
+
const pct = Math.max(0, 100 - this.contextUsage);
|
|
614
|
+
parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
|
|
533
615
|
}
|
|
534
|
-
|
|
535
|
-
|
|
616
|
+
// Paste indicator
|
|
617
|
+
if (this.pastePlaceholders.length > 0) {
|
|
618
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
619
|
+
parts.push({ text: `📋 ${totalLines}L`, tone: 'info' });
|
|
536
620
|
}
|
|
537
|
-
return
|
|
621
|
+
return renderStatusLine(parts, cols - 2);
|
|
538
622
|
}
|
|
539
623
|
/**
|
|
540
|
-
* Build
|
|
541
|
-
*
|
|
624
|
+
* Build mode controls line showing toggles and shortcuts.
|
|
625
|
+
* This is the BOTTOM line below the input area.
|
|
542
626
|
*/
|
|
543
627
|
buildModeControls(cols) {
|
|
544
628
|
const parts = [];
|
|
545
|
-
|
|
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';
|
|
629
|
+
// Thinking mode toggle
|
|
555
630
|
parts.push({
|
|
556
|
-
text:
|
|
631
|
+
text: this.thinkingEnabled ? '💭 on (tab)' : '💭 off (tab)',
|
|
632
|
+
tone: this.thinkingEnabled ? 'info' : 'muted',
|
|
633
|
+
});
|
|
634
|
+
// Verification toggle
|
|
635
|
+
parts.push({
|
|
636
|
+
text: this.verificationEnabled ? `✓ verify (${this.verificationHotkey})` : `✗ verify (${this.verificationHotkey})`,
|
|
557
637
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
558
638
|
});
|
|
559
|
-
|
|
639
|
+
// Edit mode
|
|
560
640
|
parts.push({
|
|
561
|
-
text:
|
|
562
|
-
tone:
|
|
641
|
+
text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
|
|
642
|
+
tone: 'muted',
|
|
563
643
|
});
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const pct = Math.max(0, 100 - this.contextUsage);
|
|
568
|
-
const tone = pct < 25 ? 'warn' : 'muted';
|
|
569
|
-
parts.push({ text: `context ${pct}%`, tone });
|
|
644
|
+
// Override/warning status
|
|
645
|
+
if (this.overrideStatusMessage) {
|
|
646
|
+
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
570
647
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
parts.push({ text:
|
|
648
|
+
// Queue indicator during streaming
|
|
649
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
650
|
+
parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
|
|
574
651
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
parts.push({
|
|
578
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
579
|
-
tone: 'info',
|
|
580
|
-
});
|
|
652
|
+
// Multi-line indicator
|
|
653
|
+
if (this.buffer.includes('\n')) {
|
|
654
|
+
parts.push({ text: `${this.buffer.split('\n').length}L`, tone: 'muted' });
|
|
581
655
|
}
|
|
656
|
+
// Shortcuts hint (at the end)
|
|
657
|
+
parts.push({ text: '? · esc', tone: 'muted' });
|
|
582
658
|
return renderStatusLine(parts, cols - 2);
|
|
583
659
|
}
|
|
584
660
|
/**
|
|
@@ -614,13 +690,23 @@ export class TerminalInput extends EventEmitter {
|
|
|
614
690
|
* Register with display's output interceptor to position cursor correctly.
|
|
615
691
|
* When scroll region is active, output needs to go to the scroll region,
|
|
616
692
|
* 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.
|
|
617
696
|
*/
|
|
618
697
|
registerOutputInterceptor(display) {
|
|
619
698
|
if (this.outputInterceptorCleanup) {
|
|
620
699
|
this.outputInterceptorCleanup();
|
|
621
700
|
}
|
|
622
|
-
|
|
623
|
-
|
|
701
|
+
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
702
|
+
beforeWrite: () => {
|
|
703
|
+
// Scroll region handles content containment automatically
|
|
704
|
+
// No per-write cursor manipulation needed
|
|
705
|
+
},
|
|
706
|
+
afterWrite: () => {
|
|
707
|
+
// No cursor manipulation needed
|
|
708
|
+
},
|
|
709
|
+
});
|
|
624
710
|
}
|
|
625
711
|
/**
|
|
626
712
|
* Dispose and clean up
|
|
@@ -740,7 +826,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
740
826
|
this.toggleEditMode();
|
|
741
827
|
return true;
|
|
742
828
|
}
|
|
743
|
-
|
|
829
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
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
|
+
}
|
|
744
845
|
return true;
|
|
745
846
|
}
|
|
746
847
|
return false;
|
|
@@ -1031,9 +1132,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1031
1132
|
if (available <= 0)
|
|
1032
1133
|
return;
|
|
1033
1134
|
const chunk = clean.slice(0, available);
|
|
1034
|
-
|
|
1035
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1036
|
-
if (isMultiline && !isShortMultiline) {
|
|
1135
|
+
if (isMultilinePaste(chunk)) {
|
|
1037
1136
|
this.insertPastePlaceholder(chunk);
|
|
1038
1137
|
}
|
|
1039
1138
|
else {
|
|
@@ -1053,7 +1152,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1053
1152
|
return;
|
|
1054
1153
|
this.applyScrollRegion();
|
|
1055
1154
|
this.scrollRegionActive = true;
|
|
1056
|
-
this.forceRender();
|
|
1057
1155
|
}
|
|
1058
1156
|
disableScrollRegion() {
|
|
1059
1157
|
if (!this.scrollRegionActive)
|
|
@@ -1204,19 +1302,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1204
1302
|
this.shiftPlaceholders(position, text.length);
|
|
1205
1303
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1206
1304
|
}
|
|
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
|
-
}
|
|
1213
1305
|
findPlaceholderAt(position) {
|
|
1214
1306
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1215
1307
|
}
|
|
1216
|
-
buildPlaceholder(
|
|
1308
|
+
buildPlaceholder(summary) {
|
|
1217
1309
|
const id = ++this.pasteCounter;
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1310
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1311
|
+
// Show first line preview (truncated)
|
|
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}"`;
|
|
1220
1316
|
return { id, placeholder };
|
|
1221
1317
|
}
|
|
1222
1318
|
insertPastePlaceholder(content) {
|
|
@@ -1224,21 +1320,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1224
1320
|
if (available <= 0)
|
|
1225
1321
|
return;
|
|
1226
1322
|
const cleanContent = content.slice(0, available);
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1323
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1324
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
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);
|
|
1229
1333
|
const insertPos = this.cursor;
|
|
1230
1334
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1231
1335
|
this.pastePlaceholders.push({
|
|
1232
1336
|
id,
|
|
1233
1337
|
content: cleanContent,
|
|
1234
|
-
lineCount,
|
|
1338
|
+
lineCount: summary.lineCount,
|
|
1235
1339
|
placeholder,
|
|
1236
1340
|
start: insertPos,
|
|
1237
1341
|
end: insertPos + placeholder.length,
|
|
1342
|
+
summary,
|
|
1343
|
+
expanded: false,
|
|
1238
1344
|
});
|
|
1239
1345
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1240
1346
|
this.cursor = insertPos + placeholder.length;
|
|
1241
1347
|
}
|
|
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
|
+
}
|
|
1242
1384
|
deletePlaceholder(placeholder) {
|
|
1243
1385
|
const length = placeholder.end - placeholder.start;
|
|
1244
1386
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|