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.
Files changed (59) hide show
  1. package/README.md +148 -22
  2. package/dist/core/aiFlowOptimizer.d.ts +26 -0
  3. package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
  4. package/dist/core/aiFlowOptimizer.js +31 -0
  5. package/dist/core/aiFlowOptimizer.js.map +1 -0
  6. package/dist/core/aiOptimizationEngine.d.ts +158 -0
  7. package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
  8. package/dist/core/aiOptimizationEngine.js +428 -0
  9. package/dist/core/aiOptimizationEngine.js.map +1 -0
  10. package/dist/core/aiOptimizationIntegration.d.ts +93 -0
  11. package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
  12. package/dist/core/aiOptimizationIntegration.js +250 -0
  13. package/dist/core/aiOptimizationIntegration.js.map +1 -0
  14. package/dist/core/enhancedErrorRecovery.d.ts +100 -0
  15. package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
  16. package/dist/core/enhancedErrorRecovery.js +345 -0
  17. package/dist/core/enhancedErrorRecovery.js.map +1 -0
  18. package/dist/mcp/sseClient.d.ts.map +1 -1
  19. package/dist/mcp/sseClient.js +18 -9
  20. package/dist/mcp/sseClient.js.map +1 -1
  21. package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
  22. package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
  23. package/dist/plugins/tools/build/buildPlugin.js +10 -4
  24. package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
  25. package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
  26. package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
  27. package/dist/shell/claudeCodeStreamHandler.js +322 -0
  28. package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
  29. package/dist/shell/inputQueueManager.d.ts +144 -0
  30. package/dist/shell/inputQueueManager.d.ts.map +1 -0
  31. package/dist/shell/inputQueueManager.js +290 -0
  32. package/dist/shell/inputQueueManager.js.map +1 -0
  33. package/dist/shell/interactiveShell.d.ts +2 -2
  34. package/dist/shell/interactiveShell.d.ts.map +1 -1
  35. package/dist/shell/interactiveShell.js +12 -12
  36. package/dist/shell/interactiveShell.js.map +1 -1
  37. package/dist/shell/streamingOutputManager.d.ts +115 -0
  38. package/dist/shell/streamingOutputManager.d.ts.map +1 -0
  39. package/dist/shell/streamingOutputManager.js +225 -0
  40. package/dist/shell/streamingOutputManager.js.map +1 -0
  41. package/dist/shell/terminalInput.d.ts +45 -19
  42. package/dist/shell/terminalInput.d.ts.map +1 -1
  43. package/dist/shell/terminalInput.js +262 -120
  44. package/dist/shell/terminalInput.js.map +1 -1
  45. package/dist/shell/terminalInputAdapter.d.ts +8 -6
  46. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  47. package/dist/shell/terminalInputAdapter.js +12 -6
  48. package/dist/shell/terminalInputAdapter.js.map +1 -1
  49. package/dist/ui/persistentPrompt.d.ts +50 -0
  50. package/dist/ui/persistentPrompt.d.ts.map +1 -0
  51. package/dist/ui/persistentPrompt.js +92 -0
  52. package/dist/ui/persistentPrompt.js.map +1 -0
  53. package/dist/ui/theme.d.ts.map +1 -1
  54. package/dist/ui/theme.js +8 -6
  55. package/dist/ui/theme.js.map +1 -1
  56. package/dist/ui/unified/layout.d.ts.map +1 -1
  57. package/dist/ui/unified/layout.js +26 -13
  58. package/dist/ui/unified/layout.js.map +1 -1
  59. 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 keeps the scroll region active so the prompt/status stay pinned
191
- * below the streaming output. When streaming ends, we refresh the input area.
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
- // Keep scroll region active so status/prompt stay pinned while streaming
198
- this.enableScrollRegion();
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
- // Streaming ended - render the input area
204
- this.enableScrollRegion();
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
- if (!this.scrollRegionActive) {
421
- this.enableScrollRegion();
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
- 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));
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
- 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));
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 = currentRow;
523
+ let finalRow = inputStartRow;
461
524
  let finalCol = 3;
462
525
  for (let i = 0; i < visibleLines.length; i++) {
463
- const rowNum = currentRow + i;
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
- // Mode controls line (Claude Code style)
502
- const controlRow = currentRow + visibleLines.length;
503
- this.write(ESC.TO(controlRow, 1));
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 a compact meta line above the divider (elapsed, context usage, queue size).
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
- buildMetaLine(width) {
588
+ buildStatusBar(cols) {
522
589
  const parts = [];
523
- if (this.metaElapsedSeconds !== null) {
524
- parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
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
- if (this.metaTokensUsed !== null) {
527
- const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
528
- parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
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 tone = this.contextUsage >= 75 ? 'warn' : 'muted';
532
- parts.push({ text: `used ${this.contextUsage}%`, tone });
613
+ const pct = Math.max(0, 100 - this.contextUsage);
614
+ parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
533
615
  }
534
- if (this.mode === 'streaming' && this.queue.length > 0) {
535
- parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
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 parts.length ? renderStatusLine(parts, width) : '';
621
+ return renderStatusLine(parts, cols - 2);
538
622
  }
539
623
  /**
540
- * Build Claude Code style mode controls line.
541
- * Combines streaming label + override status + main status for simultaneous display.
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
- 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';
629
+ // Thinking mode toggle
555
630
  parts.push({
556
- text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
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
- const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
639
+ // Edit mode
560
640
  parts.push({
561
- text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
562
- tone: this.autoContinueEnabled ? 'info' : 'muted',
641
+ text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
642
+ tone: 'muted',
563
643
  });
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 });
644
+ // Override/warning status
645
+ if (this.overrideStatusMessage) {
646
+ parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
570
647
  }
571
- if (this.buffer.includes('\n')) {
572
- const lineCount = this.buffer.split('\n').length;
573
- parts.push({ text: `${lineCount} lines`, tone: 'muted' });
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
- 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
- });
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
- // Scroll region handles containment now; interceptor is a no-op placeholder to keep API stable.
623
- this.outputInterceptorCleanup = display.registerOutputInterceptor({});
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
- this.insertText(' ');
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
- const isMultiline = isMultilinePaste(chunk);
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(lineCount) {
1308
+ buildPlaceholder(summary) {
1217
1309
  const id = ++this.pasteCounter;
1218
- const plural = lineCount === 1 ? '' : 's';
1219
- const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
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 lineCount = cleanContent.split('\n').length;
1228
- const { id, placeholder } = this.buildPlaceholder(lineCount);
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);