erosolar-cli 1.7.225 → 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.
Files changed (59) hide show
  1. package/README.md +22 -148
  2. package/dist/mcp/sseClient.d.ts.map +1 -1
  3. package/dist/mcp/sseClient.js +9 -18
  4. package/dist/mcp/sseClient.js.map +1 -1
  5. package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
  6. package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
  7. package/dist/plugins/tools/build/buildPlugin.js +4 -10
  8. package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
  9. package/dist/shell/interactiveShell.d.ts +2 -2
  10. package/dist/shell/interactiveShell.d.ts.map +1 -1
  11. package/dist/shell/interactiveShell.js +12 -12
  12. package/dist/shell/interactiveShell.js.map +1 -1
  13. package/dist/shell/terminalInput.d.ts +18 -46
  14. package/dist/shell/terminalInput.d.ts.map +1 -1
  15. package/dist/shell/terminalInput.js +106 -264
  16. package/dist/shell/terminalInput.js.map +1 -1
  17. package/dist/shell/terminalInputAdapter.d.ts +6 -8
  18. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  19. package/dist/shell/terminalInputAdapter.js +6 -12
  20. package/dist/shell/terminalInputAdapter.js.map +1 -1
  21. package/dist/ui/theme.d.ts.map +1 -1
  22. package/dist/ui/theme.js +6 -8
  23. package/dist/ui/theme.js.map +1 -1
  24. package/dist/ui/unified/layout.d.ts.map +1 -1
  25. package/dist/ui/unified/layout.js +13 -26
  26. package/dist/ui/unified/layout.js.map +1 -1
  27. package/package.json +1 -1
  28. package/dist/core/aiFlowOptimizer.d.ts +0 -26
  29. package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
  30. package/dist/core/aiFlowOptimizer.js +0 -31
  31. package/dist/core/aiFlowOptimizer.js.map +0 -1
  32. package/dist/core/aiOptimizationEngine.d.ts +0 -158
  33. package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
  34. package/dist/core/aiOptimizationEngine.js +0 -428
  35. package/dist/core/aiOptimizationEngine.js.map +0 -1
  36. package/dist/core/aiOptimizationIntegration.d.ts +0 -93
  37. package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
  38. package/dist/core/aiOptimizationIntegration.js +0 -250
  39. package/dist/core/aiOptimizationIntegration.js.map +0 -1
  40. package/dist/core/enhancedErrorRecovery.d.ts +0 -100
  41. package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
  42. package/dist/core/enhancedErrorRecovery.js +0 -345
  43. package/dist/core/enhancedErrorRecovery.js.map +0 -1
  44. package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
  45. package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
  46. package/dist/shell/claudeCodeStreamHandler.js +0 -322
  47. package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
  48. package/dist/shell/inputQueueManager.d.ts +0 -144
  49. package/dist/shell/inputQueueManager.d.ts.map +0 -1
  50. package/dist/shell/inputQueueManager.js +0 -290
  51. package/dist/shell/inputQueueManager.js.map +0 -1
  52. package/dist/shell/streamingOutputManager.d.ts +0 -115
  53. package/dist/shell/streamingOutputManager.d.ts.map +0 -1
  54. package/dist/shell/streamingOutputManager.js +0 -225
  55. package/dist/shell/streamingOutputManager.js.map +0 -1
  56. package/dist/ui/persistentPrompt.d.ts +0 -50
  57. package/dist/ui/persistentPrompt.d.ts.map +0 -1
  58. package/dist/ui/persistentPrompt.js +0 -92
  59. 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, generatePasteSummary } from '../core/multilinePasteHandler.js';
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,64 +187,24 @@ export class TerminalInput extends EventEmitter {
195
187
  /**
196
188
  * Set the input mode
197
189
  *
198
- * When entering streaming mode:
199
- * - Clear the input area (it will be re-rendered after streaming)
200
- * - Position cursor at where input was so content flows from there
201
- *
202
- * When exiting streaming mode:
203
- * - Re-render the input area below the new 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.
204
192
  */
205
193
  setMode(mode) {
206
194
  const prevMode = this.mode;
207
195
  this.mode = mode;
208
196
  if (mode === 'streaming' && prevMode !== 'streaming') {
209
- // Track streaming start time for elapsed display
210
- this.streamingStartTime = Date.now();
211
- // Disable scroll region - let content flow naturally
212
- // The input area will be re-rendered after streaming ends
213
- this.disableScrollRegion();
214
- // Clear the input area at the bottom so streaming content can use it
215
- const { rows, cols } = this.getSize();
216
- this.write(ESC.HIDE);
217
- // Clear from status bar row to bottom
218
- const statusBarRow = Math.max(1, rows - this.reservedLines - 1);
219
- for (let r = statusBarRow; r <= rows; r++) {
220
- this.write(ESC.TO(r, 1) + ESC.CLEAR_LINE);
221
- }
222
- // Position cursor where streaming content should start (where status bar was)
223
- this.write(ESC.TO(statusBarRow, 1));
224
- this.write(ESC.SHOW);
197
+ // Keep scroll region active so status/prompt stay pinned while streaming
198
+ this.enableScrollRegion();
225
199
  this.renderDirty = true;
200
+ this.render();
226
201
  }
227
202
  else if (mode !== 'streaming' && prevMode === 'streaming') {
228
- // Reset streaming time
229
- this.streamingStartTime = null;
230
- // Add newlines to ensure input area doesn't overlap last content
231
- this.write('\n');
232
- // Re-render the input area below the streaming content
203
+ // Streaming ended - render the input area
204
+ this.enableScrollRegion();
233
205
  this.forceRender();
234
206
  }
235
207
  }
236
- /**
237
- * Update token count for metrics display
238
- */
239
- setTokensUsed(tokens) {
240
- this.tokensUsed = tokens;
241
- }
242
- /**
243
- * Toggle thinking/reasoning mode
244
- */
245
- toggleThinking() {
246
- this.thinkingEnabled = !this.thinkingEnabled;
247
- this.emit('thinkingToggle', this.thinkingEnabled);
248
- this.scheduleRender();
249
- }
250
- /**
251
- * Get thinking enabled state
252
- */
253
- isThinkingEnabled() {
254
- return this.thinkingEnabled;
255
- }
256
208
  /**
257
209
  * Keep the top N rows pinned outside the scroll region (used for the launch banner).
258
210
  */
@@ -265,42 +217,6 @@ export class TerminalInput extends EventEmitter {
265
217
  }
266
218
  }
267
219
  }
268
- /**
269
- * Anchor prompt rendering near a specific row (inline layout). Pass null to
270
- * restore the default bottom-aligned layout.
271
- */
272
- setInlineAnchor(row) {
273
- if (row === null || row === undefined) {
274
- this.inlineAnchorRow = null;
275
- this.inlineLayout = false;
276
- this.renderDirty = true;
277
- this.render();
278
- return;
279
- }
280
- const { rows } = this.getSize();
281
- const clamped = Math.max(1, Math.min(Math.floor(row), rows));
282
- this.inlineAnchorRow = clamped;
283
- this.inlineLayout = true;
284
- this.renderDirty = true;
285
- this.render();
286
- }
287
- /**
288
- * Provide a dynamic anchor callback. When set, the prompt will follow the
289
- * output by re-evaluating the anchor before each render.
290
- */
291
- setInlineAnchorProvider(provider) {
292
- this.anchorProvider = provider;
293
- if (!provider) {
294
- this.inlineLayout = false;
295
- this.inlineAnchorRow = null;
296
- this.renderDirty = true;
297
- this.render();
298
- return;
299
- }
300
- this.inlineLayout = true;
301
- this.renderDirty = true;
302
- this.render();
303
- }
304
220
  /**
305
221
  * Get current mode
306
222
  */
@@ -410,6 +326,29 @@ export class TerminalInput extends EventEmitter {
410
326
  this.streamingLabel = next;
411
327
  this.scheduleRender();
412
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
+ }
413
352
  /**
414
353
  * Keep mode toggles (verification/auto-continue) visible in the control bar.
415
354
  * Hotkey labels remain stable so the bar looks the same before/during streaming.
@@ -488,9 +427,9 @@ export class TerminalInput extends EventEmitter {
488
427
  const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
489
428
  const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
490
429
  const displayLines = Math.min(lines.length, maxVisible);
491
- // Reserved lines: separator(1) + controls(1) + input lines
492
- // Layout: [separator] [controls] [input...]
493
- 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));
494
433
  // Calculate display window (keep cursor visible)
495
434
  let startLine = 0;
496
435
  if (lines.length > displayLines) {
@@ -502,35 +441,26 @@ export class TerminalInput extends EventEmitter {
502
441
  // Render
503
442
  this.write(ESC.HIDE);
504
443
  this.write(ESC.RESET);
505
- // Enhanced layout from bottom to top:
506
- // Row N: Mode controls (shortcuts, thinking toggle)
507
- // Row N-1: Bottom separator
508
- // Row N-2: Input area
509
- // Row N-3: Top separator
510
- // Row N-4: Status bar (streaming status, elapsed time, tokens)
511
- // Calculate positions from absolute bottom
512
- const modeControlRow = rows;
513
- const bottomSepRow = rows - 1;
514
- const inputEndRow = rows - 2;
515
- const inputStartRow = inputEndRow - visibleLines.length + 1;
516
- const topSepRow = inputStartRow - 1;
517
- const statusBarRow = topSepRow - 1;
518
- // Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
519
- this.updateReservedLines(visibleLines.length + 4);
520
- // Status bar (streaming status + metrics)
521
- this.write(ESC.TO(statusBarRow, 1));
522
- this.write(ESC.CLEAR_LINE);
523
- this.write(this.buildStatusBar(cols));
524
- // Top separator
525
- 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));
526
455
  this.write(ESC.CLEAR_LINE);
527
456
  const divider = renderDivider(cols - 2);
528
457
  this.write(divider);
458
+ currentRow += 1;
529
459
  // Render input lines
530
- let finalRow = inputStartRow;
460
+ let finalRow = currentRow;
531
461
  let finalCol = 3;
532
462
  for (let i = 0; i < visibleLines.length; i++) {
533
- const rowNum = inputStartRow + i;
463
+ const rowNum = currentRow + i;
534
464
  this.write(ESC.TO(rowNum, 1));
535
465
  this.write(ESC.CLEAR_LINE);
536
466
  const line = visibleLines[i] ?? '';
@@ -568,15 +498,12 @@ export class TerminalInput extends EventEmitter {
568
498
  this.write(' '.repeat(padding));
569
499
  this.write(ESC.RESET);
570
500
  }
571
- // Bottom separator
572
- this.write(ESC.TO(bottomSepRow, 1));
573
- this.write(ESC.CLEAR_LINE);
574
- this.write(divider);
575
- // Mode controls (shortcuts + thinking toggle)
576
- 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));
577
504
  this.write(ESC.CLEAR_LINE);
578
505
  this.write(this.buildModeControls(cols));
579
- // Position cursor in input area
506
+ // Position cursor
580
507
  this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
581
508
  this.write(ESC.SHOW);
582
509
  // Update state
@@ -589,86 +516,69 @@ export class TerminalInput extends EventEmitter {
589
516
  }
590
517
  }
591
518
  /**
592
- * Build status bar showing streaming status, elapsed time, and token count.
519
+ * Build a compact meta line above the divider (elapsed, context usage, queue size).
593
520
  */
594
- buildStatusBar(cols) {
521
+ buildMetaLine(width) {
595
522
  const parts = [];
596
- // Streaming status
597
- if (this.mode === 'streaming') {
598
- parts.push({ text: '● Streaming', tone: 'success' });
599
- // Elapsed time
600
- if (this.streamingStartTime) {
601
- const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
602
- const mins = Math.floor(elapsed / 60);
603
- const secs = elapsed % 60;
604
- const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
605
- parts.push({ text: `⏱ ${timeStr}`, tone: 'muted' });
606
- }
523
+ if (this.metaElapsedSeconds !== null) {
524
+ parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
607
525
  }
608
- else {
609
- parts.push({ text: '○ Ready', tone: 'muted' });
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
- // Token count
612
- if (this.tokensUsed > 0) {
613
- const tokenStr = this.tokensUsed >= 1000
614
- ? `${(this.tokensUsed / 1000).toFixed(1)}k`
615
- : `${this.tokensUsed}`;
616
- 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 });
617
533
  }
618
- // Thinking mode
619
- parts.push({
620
- text: this.thinkingEnabled ? '💭 Thinking on' : '💭 Thinking off',
621
- tone: this.thinkingEnabled ? 'info' : 'muted',
622
- });
623
- // Paste block count if any
624
- if (this.pastePlaceholders.length > 0) {
625
- const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
626
- parts.push({
627
- text: `📋 ${this.pastePlaceholders.length} paste(s), ${totalLines} lines`,
628
- tone: 'info',
629
- });
534
+ if (this.mode === 'streaming' && this.queue.length > 0) {
535
+ parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
630
536
  }
631
- return renderStatusLine(parts, cols - 2);
537
+ return parts.length ? renderStatusLine(parts, width) : '';
632
538
  }
633
539
  /**
634
540
  * Build Claude Code style mode controls line.
635
- * Shows keyboard shortcuts and toggle states.
541
+ * Combines streaming label + override status + main status for simultaneous display.
636
542
  */
637
543
  buildModeControls(cols) {
638
544
  const parts = [];
639
- // Keyboard shortcuts hint
640
- parts.push({ text: '? help', tone: 'muted' });
641
- parts.push({ text: 'esc interrupt', tone: 'muted' });
642
- // Streaming status messages
643
545
  if (this.streamingLabel) {
644
546
  parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
645
547
  }
646
548
  if (this.overrideStatusMessage) {
647
549
  parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
648
550
  }
649
- // Mode toggles
650
- const verifyLabel = this.verificationEnabled ? 'verify on' : 'verify off';
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';
651
555
  parts.push({
652
556
  text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
653
557
  tone: this.verificationEnabled ? 'success' : 'muted',
654
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
+ });
655
564
  const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
656
565
  parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
657
- // Context usage
658
566
  if (this.contextUsage !== null) {
659
567
  const pct = Math.max(0, 100 - this.contextUsage);
660
568
  const tone = pct < 25 ? 'warn' : 'muted';
661
- parts.push({ text: `ctx ${pct}%`, tone });
662
- }
663
- // Queue during streaming
664
- if (this.mode === 'streaming' && this.queue.length > 0) {
665
- parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
569
+ parts.push({ text: `context ${pct}%`, tone });
666
570
  }
667
- // Multi-line indicator
668
571
  if (this.buffer.includes('\n')) {
669
572
  const lineCount = this.buffer.split('\n').length;
670
573
  parts.push({ text: `${lineCount} lines`, tone: 'muted' });
671
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
+ }
672
582
  return renderStatusLine(parts, cols - 2);
673
583
  }
674
584
  /**
@@ -704,23 +614,13 @@ export class TerminalInput extends EventEmitter {
704
614
  * Register with display's output interceptor to position cursor correctly.
705
615
  * When scroll region is active, output needs to go to the scroll region,
706
616
  * not the protected bottom area where the input is rendered.
707
- *
708
- * NOTE: With scroll region properly set, content naturally stays within
709
- * the region boundaries - no cursor manipulation needed per-write.
710
617
  */
711
618
  registerOutputInterceptor(display) {
712
619
  if (this.outputInterceptorCleanup) {
713
620
  this.outputInterceptorCleanup();
714
621
  }
715
- this.outputInterceptorCleanup = display.registerOutputInterceptor({
716
- beforeWrite: () => {
717
- // Scroll region handles content containment automatically
718
- // No per-write cursor manipulation needed
719
- },
720
- afterWrite: () => {
721
- // No cursor manipulation needed
722
- },
723
- });
622
+ // Scroll region handles containment now; interceptor is a no-op placeholder to keep API stable.
623
+ this.outputInterceptorCleanup = display.registerOutputInterceptor({});
724
624
  }
725
625
  /**
726
626
  * Dispose and clean up
@@ -840,22 +740,7 @@ export class TerminalInput extends EventEmitter {
840
740
  this.toggleEditMode();
841
741
  return true;
842
742
  }
843
- // Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
844
- if (this.findPlaceholderAt(this.cursor)) {
845
- this.togglePasteExpansion();
846
- }
847
- else {
848
- this.toggleThinking();
849
- }
850
- return true;
851
- case 'escape':
852
- // Esc: interrupt if streaming, otherwise clear buffer
853
- if (this.mode === 'streaming') {
854
- this.emit('interrupt');
855
- }
856
- else if (this.buffer.length > 0) {
857
- this.clear();
858
- }
743
+ this.insertText(' ');
859
744
  return true;
860
745
  }
861
746
  return false;
@@ -1146,7 +1031,9 @@ export class TerminalInput extends EventEmitter {
1146
1031
  if (available <= 0)
1147
1032
  return;
1148
1033
  const chunk = clean.slice(0, available);
1149
- if (isMultilinePaste(chunk)) {
1034
+ const isMultiline = isMultilinePaste(chunk);
1035
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1036
+ if (isMultiline && !isShortMultiline) {
1150
1037
  this.insertPastePlaceholder(chunk);
1151
1038
  }
1152
1039
  else {
@@ -1166,8 +1053,7 @@ export class TerminalInput extends EventEmitter {
1166
1053
  return;
1167
1054
  this.applyScrollRegion();
1168
1055
  this.scrollRegionActive = true;
1169
- // Don't force render here - let caller decide when to render
1170
- // This prevents cursor repositioning issues when entering streaming mode
1056
+ this.forceRender();
1171
1057
  }
1172
1058
  disableScrollRegion() {
1173
1059
  if (!this.scrollRegionActive)
@@ -1318,17 +1204,19 @@ export class TerminalInput extends EventEmitter {
1318
1204
  this.shiftPlaceholders(position, text.length);
1319
1205
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1320
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
+ }
1321
1213
  findPlaceholderAt(position) {
1322
1214
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1323
1215
  }
1324
- buildPlaceholder(summary) {
1216
+ buildPlaceholder(lineCount) {
1325
1217
  const id = ++this.pasteCounter;
1326
- const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
1327
- // Show first line preview (truncated)
1328
- const preview = summary.preview.length > 30
1329
- ? `${summary.preview.slice(0, 30)}...`
1330
- : summary.preview;
1331
- const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
1218
+ const plural = lineCount === 1 ? '' : 's';
1219
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1332
1220
  return { id, placeholder };
1333
1221
  }
1334
1222
  insertPastePlaceholder(content) {
@@ -1336,67 +1224,21 @@ export class TerminalInput extends EventEmitter {
1336
1224
  if (available <= 0)
1337
1225
  return;
1338
1226
  const cleanContent = content.slice(0, available);
1339
- const summary = generatePasteSummary(cleanContent);
1340
- // For short pastes (< 5 lines), show full content instead of placeholder
1341
- if (summary.lineCount < 5) {
1342
- const placeholder = this.findPlaceholderAt(this.cursor);
1343
- const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
1344
- this.insertPlainText(cleanContent, insertPos);
1345
- this.cursor = insertPos + cleanContent.length;
1346
- return;
1347
- }
1348
- const { id, placeholder } = this.buildPlaceholder(summary);
1227
+ const lineCount = cleanContent.split('\n').length;
1228
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1349
1229
  const insertPos = this.cursor;
1350
1230
  this.shiftPlaceholders(insertPos, placeholder.length);
1351
1231
  this.pastePlaceholders.push({
1352
1232
  id,
1353
1233
  content: cleanContent,
1354
- lineCount: summary.lineCount,
1234
+ lineCount,
1355
1235
  placeholder,
1356
1236
  start: insertPos,
1357
1237
  end: insertPos + placeholder.length,
1358
- summary,
1359
- expanded: false,
1360
1238
  });
1361
1239
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1362
1240
  this.cursor = insertPos + placeholder.length;
1363
1241
  }
1364
- /**
1365
- * Toggle expansion of a paste placeholder at the current cursor position.
1366
- * When expanded, shows first 3 and last 2 lines of the content.
1367
- */
1368
- togglePasteExpansion() {
1369
- const placeholder = this.findPlaceholderAt(this.cursor);
1370
- if (!placeholder)
1371
- return false;
1372
- placeholder.expanded = !placeholder.expanded;
1373
- // Update the placeholder text in buffer
1374
- const newPlaceholder = placeholder.expanded
1375
- ? this.buildExpandedPlaceholder(placeholder)
1376
- : this.buildPlaceholder(placeholder.summary).placeholder;
1377
- const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
1378
- // Update buffer
1379
- this.buffer =
1380
- this.buffer.slice(0, placeholder.start) +
1381
- newPlaceholder +
1382
- this.buffer.slice(placeholder.end);
1383
- // Update placeholder tracking
1384
- placeholder.placeholder = newPlaceholder;
1385
- placeholder.end = placeholder.start + newPlaceholder.length;
1386
- // Shift other placeholders
1387
- this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
1388
- this.scheduleRender();
1389
- return true;
1390
- }
1391
- buildExpandedPlaceholder(ph) {
1392
- const lines = ph.content.split('\n');
1393
- const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
1394
- const lastLines = lines.length > 5
1395
- ? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
1396
- : '';
1397
- const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
1398
- return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
1399
- }
1400
1242
  deletePlaceholder(placeholder) {
1401
1243
  const length = placeholder.end - placeholder.start;
1402
1244
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);