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.
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 +19 -45
  14. package/dist/shell/terminalInput.d.ts.map +1 -1
  15. package/dist/shell/terminalInput.js +130 -256
  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,55 +187,24 @@ export class TerminalInput extends EventEmitter {
195
187
  /**
196
188
  * Set the input mode
197
189
  *
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).
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
- // 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);
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
- // 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
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
- // 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();
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
- // Reserved lines: separator(1) + controls(1) + input lines
485
- // Layout: [separator] [controls] [input...]
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
- // 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));
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 = inputStartRow;
460
+ let finalRow = currentRow;
524
461
  let finalCol = 3;
525
462
  for (let i = 0; i < visibleLines.length; i++) {
526
- const rowNum = inputStartRow + i;
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
- // 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));
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 in input area
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 status bar showing streaming/ready status, elapsed time, and token count.
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
- buildStatusBar(cols) {
521
+ buildMetaLine(width) {
589
522
  const parts = [];
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' });
523
+ if (this.metaElapsedSeconds !== null) {
524
+ parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
603
525
  }
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' });
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 pct = Math.max(0, 100 - this.contextUsage);
614
- parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
531
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
532
+ parts.push({ text: `used ${this.contextUsage}%`, tone });
615
533
  }
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' });
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, cols - 2);
537
+ return parts.length ? renderStatusLine(parts, width) : '';
622
538
  }
623
539
  /**
624
- * Build mode controls line showing toggles and shortcuts.
625
- * This is the BOTTOM line below the input area.
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
- // Thinking mode toggle
630
- parts.push({
631
- text: this.thinkingEnabled ? '💭 on (tab)' : '💭 off (tab)',
632
- tone: this.thinkingEnabled ? 'info' : 'muted',
633
- });
634
- // Verification toggle
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: this.verificationEnabled ? `✓ verify (${this.verificationHotkey})` : `✗ verify (${this.verificationHotkey})`,
556
+ text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
637
557
  tone: this.verificationEnabled ? 'success' : 'muted',
638
558
  });
639
- // Edit mode
559
+ const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
640
560
  parts.push({
641
- text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
642
- tone: 'muted',
561
+ text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
562
+ tone: this.autoContinueEnabled ? 'info' : 'muted',
643
563
  });
644
- // Override/warning status
645
- if (this.overrideStatusMessage) {
646
- parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
647
- }
648
- // Queue indicator during streaming
649
- if (this.mode === 'streaming' && this.queue.length > 0) {
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
- parts.push({ text: `${this.buffer.split('\n').length}L`, tone: 'muted' });
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
- // Scroll region handles content containment automatically
704
- // No per-write cursor manipulation needed
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
- // No cursor manipulation needed
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
- // 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
- }
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
- if (isMultilinePaste(chunk)) {
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(summary) {
1232
+ buildPlaceholder(lineCount) {
1309
1233
  const id = ++this.pasteCounter;
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}"`;
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 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);
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: summary.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);