erosolar-cli 1.7.228 → 1.7.230

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 +23 -45
  14. package/dist/shell/terminalInput.d.ts.map +1 -1
  15. package/dist/shell/terminalInput.js +145 -255
  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,28 @@ 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
+ // Clear the reserved block to avoid stale meta/status lines
447
+ this.clearReservedArea(startRow, this.reservedLines, cols);
448
+ // Meta/status header (elapsed, tokens/context)
449
+ if (metaLine) {
450
+ this.write(ESC.TO(currentRow, 1));
451
+ this.write(ESC.CLEAR_LINE);
452
+ this.write(metaLine);
453
+ currentRow += 1;
454
+ }
455
+ // Separator line
456
+ this.write(ESC.TO(currentRow, 1));
519
457
  this.write(ESC.CLEAR_LINE);
520
458
  const divider = renderDivider(cols - 2);
521
459
  this.write(divider);
460
+ currentRow += 1;
522
461
  // Render input lines
523
- let finalRow = inputStartRow;
462
+ let finalRow = currentRow;
524
463
  let finalCol = 3;
525
464
  for (let i = 0; i < visibleLines.length; i++) {
526
- const rowNum = inputStartRow + i;
465
+ const rowNum = currentRow + i;
527
466
  this.write(ESC.TO(rowNum, 1));
528
467
  this.write(ESC.CLEAR_LINE);
529
468
  const line = visibleLines[i] ?? '';
@@ -561,15 +500,12 @@ export class TerminalInput extends EventEmitter {
561
500
  this.write(' '.repeat(padding));
562
501
  this.write(ESC.RESET);
563
502
  }
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));
503
+ // Mode controls line (Claude Code style)
504
+ const controlRow = currentRow + visibleLines.length;
505
+ this.write(ESC.TO(controlRow, 1));
570
506
  this.write(ESC.CLEAR_LINE);
571
507
  this.write(this.buildModeControls(cols));
572
- // Position cursor in input area
508
+ // Position cursor
573
509
  this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
574
510
  this.write(ESC.SHOW);
575
511
  // Update state
@@ -582,79 +518,83 @@ export class TerminalInput extends EventEmitter {
582
518
  }
583
519
  }
584
520
  /**
585
- * Build status bar showing streaming/ready status, elapsed time, and token count.
586
- * This is the TOP line above the input area.
521
+ * Build a compact meta line above the divider (elapsed, context usage, queue size).
587
522
  */
588
- buildStatusBar(cols) {
523
+ buildMetaLine(width) {
589
524
  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' });
525
+ if (this.metaElapsedSeconds !== null) {
526
+ parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
600
527
  }
601
- else {
602
- parts.push({ text: '○ Ready', tone: 'muted' });
603
- }
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' });
528
+ if (this.metaTokensUsed !== null) {
529
+ const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
530
+ parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
610
531
  }
611
- // Context window remaining
612
532
  if (this.contextUsage !== null) {
613
- const pct = Math.max(0, 100 - this.contextUsage);
614
- parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
533
+ const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
534
+ parts.push({ text: `used ${this.contextUsage}%`, tone });
615
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
+ if (this.queue.length > 0) {
537
+ parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
538
+ }
539
+ return parts.length ? renderStatusLine(parts, width) : '';
540
+ }
541
+ /**
542
+ * Clear the reserved bottom block (meta + divider + input + controls) before repainting.
543
+ */
544
+ clearReservedArea(startRow, reservedLines, cols) {
545
+ const width = Math.max(1, cols);
546
+ for (let i = 0; i < reservedLines; i++) {
547
+ const row = startRow + i;
548
+ this.write(ESC.TO(row, 1));
549
+ this.write(' '.repeat(width));
620
550
  }
621
- return renderStatusLine(parts, cols - 2);
622
551
  }
623
552
  /**
624
- * Build mode controls line showing toggles and shortcuts.
625
- * This is the BOTTOM line below the input area.
553
+ * Build Claude Code style mode controls line.
554
+ * Combines streaming label + override status + main status for simultaneous display.
626
555
  */
627
556
  buildModeControls(cols) {
628
557
  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
558
+ if (this.streamingLabel) {
559
+ parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
560
+ }
561
+ if (this.overrideStatusMessage) {
562
+ parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
563
+ }
564
+ if (this.statusMessage) {
565
+ parts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
566
+ }
567
+ const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
635
568
  parts.push({
636
- text: this.verificationEnabled ? `✓ verify (${this.verificationHotkey})` : `✗ verify (${this.verificationHotkey})`,
569
+ text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
637
570
  tone: this.verificationEnabled ? 'success' : 'muted',
638
571
  });
639
- // Edit mode
572
+ const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
640
573
  parts.push({
641
- text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
642
- tone: 'muted',
574
+ text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
575
+ tone: this.autoContinueEnabled ? 'info' : 'muted',
643
576
  });
644
- // Override/warning status
645
- if (this.overrideStatusMessage) {
646
- parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
577
+ const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
578
+ parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
579
+ if (this.contextUsage !== null) {
580
+ const pct = Math.max(0, 100 - this.contextUsage);
581
+ const tone = pct < 25 ? 'warn' : 'muted';
582
+ parts.push({ text: `context ${pct}%`, tone });
647
583
  }
648
- // Queue indicator during streaming
649
- if (this.mode === 'streaming' && this.queue.length > 0) {
650
- parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
584
+ if (this.queue.length > 0 && this.mode !== 'streaming') {
585
+ parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
651
586
  }
652
- // Multi-line indicator
653
587
  if (this.buffer.includes('\n')) {
654
- parts.push({ text: `${this.buffer.split('\n').length}L`, tone: 'muted' });
588
+ const lineCount = this.buffer.split('\n').length;
589
+ parts.push({ text: `${lineCount} lines`, tone: 'muted' });
590
+ }
591
+ if (this.pastePlaceholders.length > 0) {
592
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
593
+ parts.push({
594
+ text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
595
+ tone: 'info',
596
+ });
655
597
  }
656
- // Shortcuts hint (at the end)
657
- parts.push({ text: '? · esc', tone: 'muted' });
658
598
  return renderStatusLine(parts, cols - 2);
659
599
  }
660
600
  /**
@@ -690,9 +630,6 @@ export class TerminalInput extends EventEmitter {
690
630
  * Register with display's output interceptor to position cursor correctly.
691
631
  * When scroll region is active, output needs to go to the scroll region,
692
632
  * 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
633
  */
697
634
  registerOutputInterceptor(display) {
698
635
  if (this.outputInterceptorCleanup) {
@@ -700,11 +637,20 @@ export class TerminalInput extends EventEmitter {
700
637
  }
701
638
  this.outputInterceptorCleanup = display.registerOutputInterceptor({
702
639
  beforeWrite: () => {
703
- // Scroll region handles content containment automatically
704
- // No per-write cursor manipulation needed
640
+ // When the scroll region is active, temporarily move the cursor into
641
+ // the scrollable area so streamed output lands above the pinned prompt.
642
+ if (this.scrollRegionActive) {
643
+ const { rows } = this.getSize();
644
+ const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
645
+ this.write(ESC.SAVE);
646
+ this.write(ESC.TO(scrollBottom, 1));
647
+ }
705
648
  },
706
649
  afterWrite: () => {
707
- // No cursor manipulation needed
650
+ // Restore cursor back to the pinned prompt after output completes.
651
+ if (this.scrollRegionActive) {
652
+ this.write(ESC.RESTORE);
653
+ }
708
654
  },
709
655
  });
710
656
  }
@@ -826,22 +772,7 @@ export class TerminalInput extends EventEmitter {
826
772
  this.toggleEditMode();
827
773
  return true;
828
774
  }
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
- }
775
+ this.insertText(' ');
845
776
  return true;
846
777
  }
847
778
  return false;
@@ -1132,7 +1063,9 @@ export class TerminalInput extends EventEmitter {
1132
1063
  if (available <= 0)
1133
1064
  return;
1134
1065
  const chunk = clean.slice(0, available);
1135
- if (isMultilinePaste(chunk)) {
1066
+ const isMultiline = isMultilinePaste(chunk);
1067
+ const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
1068
+ if (isMultiline && !isShortMultiline) {
1136
1069
  this.insertPastePlaceholder(chunk);
1137
1070
  }
1138
1071
  else {
@@ -1152,6 +1085,7 @@ export class TerminalInput extends EventEmitter {
1152
1085
  return;
1153
1086
  this.applyScrollRegion();
1154
1087
  this.scrollRegionActive = true;
1088
+ this.forceRender();
1155
1089
  }
1156
1090
  disableScrollRegion() {
1157
1091
  if (!this.scrollRegionActive)
@@ -1302,17 +1236,19 @@ export class TerminalInput extends EventEmitter {
1302
1236
  this.shiftPlaceholders(position, text.length);
1303
1237
  this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
1304
1238
  }
1239
+ shouldInlineMultiline(content) {
1240
+ const lines = content.split('\n').length;
1241
+ const maxInlineLines = 4;
1242
+ const maxInlineChars = 240;
1243
+ return lines <= maxInlineLines && content.length <= maxInlineChars;
1244
+ }
1305
1245
  findPlaceholderAt(position) {
1306
1246
  return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
1307
1247
  }
1308
- buildPlaceholder(summary) {
1248
+ buildPlaceholder(lineCount) {
1309
1249
  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}"`;
1250
+ const plural = lineCount === 1 ? '' : 's';
1251
+ const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
1316
1252
  return { id, placeholder };
1317
1253
  }
1318
1254
  insertPastePlaceholder(content) {
@@ -1320,67 +1256,21 @@ export class TerminalInput extends EventEmitter {
1320
1256
  if (available <= 0)
1321
1257
  return;
1322
1258
  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);
1259
+ const lineCount = cleanContent.split('\n').length;
1260
+ const { id, placeholder } = this.buildPlaceholder(lineCount);
1333
1261
  const insertPos = this.cursor;
1334
1262
  this.shiftPlaceholders(insertPos, placeholder.length);
1335
1263
  this.pastePlaceholders.push({
1336
1264
  id,
1337
1265
  content: cleanContent,
1338
- lineCount: summary.lineCount,
1266
+ lineCount,
1339
1267
  placeholder,
1340
1268
  start: insertPos,
1341
1269
  end: insertPos + placeholder.length,
1342
- summary,
1343
- expanded: false,
1344
1270
  });
1345
1271
  this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
1346
1272
  this.cursor = insertPos + placeholder.length;
1347
1273
  }
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
1274
  deletePlaceholder(placeholder) {
1385
1275
  const length = placeholder.end - placeholder.start;
1386
1276
  this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);