erosolar-cli 1.7.155 → 1.7.157

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 (90) hide show
  1. package/README.md +7 -17
  2. package/agents/erosolar-code.rules.json +1 -111
  3. package/agents/general.rules.json +0 -6
  4. package/dist/bin/erosolar.js +0 -22
  5. package/dist/bin/erosolar.js.map +1 -1
  6. package/dist/bin/selfTest.js +2 -190
  7. package/dist/bin/selfTest.js.map +1 -1
  8. package/dist/core/agent.d.ts +0 -12
  9. package/dist/core/agent.d.ts.map +1 -1
  10. package/dist/core/agent.js +12 -75
  11. package/dist/core/agent.js.map +1 -1
  12. package/dist/core/contextManager.d.ts +1 -2
  13. package/dist/core/contextManager.d.ts.map +1 -1
  14. package/dist/core/contextManager.js +2 -11
  15. package/dist/core/contextManager.js.map +1 -1
  16. package/dist/core/multilinePasteHandler.d.ts +4 -8
  17. package/dist/core/multilinePasteHandler.d.ts.map +1 -1
  18. package/dist/core/multilinePasteHandler.js +17 -67
  19. package/dist/core/multilinePasteHandler.js.map +1 -1
  20. package/dist/core/preferences.js +2 -8
  21. package/dist/core/preferences.js.map +1 -1
  22. package/dist/core/resultVerification.d.ts +1 -1
  23. package/dist/core/resultVerification.d.ts.map +1 -1
  24. package/dist/core/resultVerification.js +10 -11
  25. package/dist/core/resultVerification.js.map +1 -1
  26. package/dist/core/schemaValidator.d.ts.map +1 -1
  27. package/dist/core/schemaValidator.js +1 -36
  28. package/dist/core/schemaValidator.js.map +1 -1
  29. package/dist/core/toolRuntime.d.ts +0 -61
  30. package/dist/core/toolRuntime.d.ts.map +1 -1
  31. package/dist/core/toolRuntime.js +1 -303
  32. package/dist/core/toolRuntime.js.map +1 -1
  33. package/dist/core/unified/schema.d.ts.map +1 -1
  34. package/dist/core/unified/schema.js +1 -34
  35. package/dist/core/unified/schema.js.map +1 -1
  36. package/dist/headless/headlessApp.d.ts.map +1 -1
  37. package/dist/headless/headlessApp.js +0 -3
  38. package/dist/headless/headlessApp.js.map +1 -1
  39. package/dist/providers/anthropicProvider.d.ts.map +1 -1
  40. package/dist/providers/anthropicProvider.js +1 -4
  41. package/dist/providers/anthropicProvider.js.map +1 -1
  42. package/dist/shell/bracketedPasteManager.d.ts.map +1 -1
  43. package/dist/shell/bracketedPasteManager.js +3 -5
  44. package/dist/shell/bracketedPasteManager.js.map +1 -1
  45. package/dist/shell/composableMessage.js +2 -2
  46. package/dist/shell/composableMessage.js.map +1 -1
  47. package/dist/shell/interactiveShell.d.ts +1 -6
  48. package/dist/shell/interactiveShell.d.ts.map +1 -1
  49. package/dist/shell/interactiveShell.js +95 -421
  50. package/dist/shell/interactiveShell.js.map +1 -1
  51. package/dist/shell/shellApp.d.ts.map +1 -1
  52. package/dist/shell/shellApp.js +0 -7
  53. package/dist/shell/shellApp.js.map +1 -1
  54. package/dist/shell/systemPrompt.js +5 -5
  55. package/dist/shell/systemPrompt.js.map +1 -1
  56. package/dist/tools/bashTools.d.ts +0 -8
  57. package/dist/tools/bashTools.d.ts.map +1 -1
  58. package/dist/tools/bashTools.js +5 -80
  59. package/dist/tools/bashTools.js.map +1 -1
  60. package/dist/tools/diffUtils.d.ts.map +1 -1
  61. package/dist/tools/diffUtils.js +8 -12
  62. package/dist/tools/diffUtils.js.map +1 -1
  63. package/dist/tools/editTools.d.ts.map +1 -1
  64. package/dist/tools/editTools.js +36 -386
  65. package/dist/tools/editTools.js.map +1 -1
  66. package/dist/tools/fileTools.d.ts.map +1 -1
  67. package/dist/tools/fileTools.js +130 -25
  68. package/dist/tools/fileTools.js.map +1 -1
  69. package/dist/ui/ShellUIAdapter.d.ts +0 -5
  70. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  71. package/dist/ui/ShellUIAdapter.js +0 -21
  72. package/dist/ui/ShellUIAdapter.js.map +1 -1
  73. package/dist/ui/persistentPrompt.d.ts +10 -83
  74. package/dist/ui/persistentPrompt.d.ts.map +1 -1
  75. package/dist/ui/persistentPrompt.js +119 -521
  76. package/dist/ui/persistentPrompt.js.map +1 -1
  77. package/dist/ui/richText.js +4 -4
  78. package/dist/ui/richText.js.map +1 -1
  79. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  80. package/dist/ui/shortcutsHelp.js +13 -31
  81. package/dist/ui/shortcutsHelp.js.map +1 -1
  82. package/package.json +1 -4
  83. package/dist/ui/EnhancedPinnedChatBox.d.ts +0 -93
  84. package/dist/ui/EnhancedPinnedChatBox.d.ts.map +0 -1
  85. package/dist/ui/EnhancedPinnedChatBox.js +0 -309
  86. package/dist/ui/EnhancedPinnedChatBox.js.map +0 -1
  87. package/dist/ui/PinnedChatBoxEnhancer.d.ts +0 -88
  88. package/dist/ui/PinnedChatBoxEnhancer.d.ts.map +0 -1
  89. package/dist/ui/PinnedChatBoxEnhancer.js +0 -205
  90. package/dist/ui/PinnedChatBoxEnhancer.js.map +0 -1
@@ -84,7 +84,6 @@ export class InteractiveShell {
84
84
  cleanupInProgress = false;
85
85
  slashPreviewVisible = false;
86
86
  keypressHandler = null;
87
- sigintHandler = null;
88
87
  rawDataHandler = null;
89
88
  skillRepository;
90
89
  skillToolHandlers = new Map();
@@ -114,6 +113,7 @@ export class InteractiveShell {
114
113
  _enabledPlugins;
115
114
  pinnedChatBox;
116
115
  readlineOutputSuppressed = false;
116
+ originalStdoutWrite = null;
117
117
  constructor(config) {
118
118
  this.profile = config.profile;
119
119
  this.profileLabel = config.profileLabel;
@@ -416,30 +416,6 @@ export class InteractiveShell {
416
416
  display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
417
417
  }
418
418
  setupHandlers() {
419
- // Set up SIGINT handler to clear input instead of exiting when there's content
420
- // This is a backup in case the keypress handler doesn't catch Ctrl+C
421
- this.sigintHandler = () => {
422
- const currentLine = this.rl.line || '';
423
- const hasComposedContent = this.composableMessage.hasContent();
424
- const hasAnyContent = currentLine.length > 0 || hasComposedContent;
425
- if (hasAnyContent) {
426
- // Clear all input buffers instead of exiting
427
- this.rl.line = '';
428
- this.rl.cursor = 0;
429
- this.composableMessage.clear();
430
- this.bracketedPaste.reset();
431
- this.persistentPrompt.updateInput('', 0);
432
- this.pinnedChatBox.setInput('');
433
- this.pinnedChatBox.clearPastedBlockState();
434
- output.write('\r\x1b[K^C\n');
435
- this.rl.prompt();
436
- return;
437
- }
438
- // No content - allow exit
439
- output.write('\n👋 Goodbye!\n');
440
- process.exit(0);
441
- };
442
- process.on('SIGINT', this.sigintHandler);
443
419
  // Set up raw data interception for bracketed paste
444
420
  this.setupRawPasteHandler();
445
421
  this.rl.on('line', (line) => {
@@ -448,10 +424,7 @@ export class InteractiveShell {
448
424
  if (this.bracketedPaste.isCapturingRaw()) {
449
425
  this.resetBufferedInputLines();
450
426
  // Clear the line that readline just echoed - move up and clear
451
- // Only write to output when not processing (during streaming, output is suppressed)
452
- if (!this.isProcessing) {
453
- output.write('\x1b[A\r\x1b[K');
454
- }
427
+ output.write('\x1b[A\r\x1b[K');
455
428
  // Show paste progress (this will update in place)
456
429
  this.showMultiLinePastePreview(this.bracketedPaste.getRawBufferLineCount(), this.bracketedPaste.getRawBufferPreview());
457
430
  return;
@@ -461,10 +434,7 @@ export class InteractiveShell {
461
434
  if (this.bracketedPaste.shouldIgnoreLineEvent()) {
462
435
  this.resetBufferedInputLines();
463
436
  // Clear the echoed line that readline wrote
464
- // Only write to output when not processing (during streaming, output is suppressed)
465
- if (!this.isProcessing) {
466
- output.write('\x1b[A\r\x1b[K');
467
- }
437
+ output.write('\x1b[A\r\x1b[K');
468
438
  return;
469
439
  }
470
440
  const normalized = this.bracketedPaste.process(line);
@@ -474,21 +444,15 @@ export class InteractiveShell {
474
444
  // If still accumulating multi-line paste, show preview
475
445
  if (normalized.isPending) {
476
446
  // Clear the line that readline just echoed - move up and clear
477
- // Only write to output when not processing (during streaming, output is suppressed)
478
- if (!this.isProcessing) {
479
- output.write('\x1b[A\r\x1b[K');
480
- }
447
+ output.write('\x1b[A\r\x1b[K');
481
448
  this.showMultiLinePastePreview(normalized.lineCount || 0, normalized.preview);
482
449
  return;
483
450
  }
484
- // Paste complete - ALWAYS capture for confirmation, never auto-submit
485
- // This is critical during streaming - pastes should be queued, not submitted
451
+ // Paste complete, store or submit the full content
486
452
  if (typeof normalized.result === 'string') {
487
453
  this.clearMultiLinePastePreview();
488
454
  const lineCount = normalized.lineCount ?? normalized.result.split('\n').length;
489
- // During streaming: capture paste and queue as follow-up when user presses Enter
490
- // Not streaming: capture paste and wait for user to press Enter to submit
491
- // Either way, we NEVER auto-submit pasted content
455
+ // All pastes (single or multi-line) are captured for confirmation before submit
492
456
  this.capturePaste(normalized.result, lineCount);
493
457
  return;
494
458
  }
@@ -514,11 +478,6 @@ export class InteractiveShell {
514
478
  this.rawDataHandler(); // This restores the original emit function
515
479
  this.rawDataHandler = null;
516
480
  }
517
- // Remove SIGINT handler
518
- if (this.sigintHandler) {
519
- process.off('SIGINT', this.sigintHandler);
520
- this.sigintHandler = null;
521
- }
522
481
  // Clear any pending cleanup to prevent hanging
523
482
  this.pendingCleanup = null;
524
483
  // Dispose persistent prompt
@@ -605,114 +564,15 @@ export class InteractiveShell {
605
564
  if (inputStream.setRawMode && !inputStream.isRaw) {
606
565
  inputStream.setRawMode(true);
607
566
  }
608
- // Track last escape time for double-escape detection
609
- let lastEscapeTime = 0;
610
567
  this.keypressHandler = (_str, key) => {
611
568
  // Handle special keys
612
569
  if (key) {
613
- // Shift+Enter or Option+Enter: Insert newline for multi-line input
614
- if ((key.name === 'enter' || key.name === 'return') && (key.shift || key.meta)) {
615
- const currentLine = this.rl.line || '';
616
- const cursorPos = this.rl.cursor || 0;
617
- // Insert newline at cursor position
618
- const newLine = currentLine.slice(0, cursorPos) + '\n' + currentLine.slice(cursorPos);
619
- this.rl.line = newLine;
620
- this.rl.cursor = cursorPos + 1;
621
- this.persistentPrompt.updateInput(newLine, cursorPos + 1);
622
- this.pinnedChatBox.setInput(newLine, cursorPos + 1);
623
- if (this.isProcessing) {
624
- this.pinnedChatBox.updatePersistentInput();
625
- }
626
- return; // Prevent readline from submitting
627
- }
628
- // Backslash+Enter: Insert newline (escape sequence for multi-line)
629
- if (key.name === 'enter' || key.name === 'return') {
630
- const currentLine = this.rl.line || '';
631
- const cursorPos = this.rl.cursor || 0;
632
- // Check if last char before cursor is backslash
633
- if (cursorPos > 0 && currentLine[cursorPos - 1] === '\\') {
634
- // Replace backslash with newline
635
- const newLine = currentLine.slice(0, cursorPos - 1) + '\n' + currentLine.slice(cursorPos);
636
- this.rl.line = newLine;
637
- this.rl.cursor = cursorPos; // Cursor stays after newline
638
- this.persistentPrompt.updateInput(newLine, cursorPos);
639
- this.pinnedChatBox.setInput(newLine, cursorPos);
640
- if (this.isProcessing) {
641
- this.pinnedChatBox.updatePersistentInput();
642
- }
643
- return; // Prevent readline from submitting
644
- }
645
- // During AI streaming, use pinned chat box for input submission
646
- if (this.isProcessing) {
647
- // Check if there's content BEFORE calling handleSubmit
648
- const hadContent = this.pinnedChatBox.getInput().length > 0 || this.pinnedChatBox.hasPastedBlock();
649
- const hadComposedContent = this.composableMessage.hasContent();
650
- const submittedInput = this.pinnedChatBox.handleSubmit();
651
- // If there was content (now queued), clear readline buffer and return
652
- // This prevents double-queueing via readline's 'line' event
653
- if (hadContent || hadComposedContent || submittedInput !== null) {
654
- // Clear readline's buffer to prevent 'line' event from re-processing
655
- this.rl.line = '';
656
- this.rl.cursor = 0;
657
- // Also clear composableMessage to prevent duplication since
658
- // pinnedChatBox already queued the paste content
659
- if (hadComposedContent) {
660
- this.composableMessage.clear();
661
- }
662
- // Input was handled by pinned chat box (queued)
663
- return;
664
- }
665
- }
666
- // Otherwise, let readline handle the enter key normally
667
- }
668
- // Ctrl+L: Clear screen (like Claude Code)
669
- if (key.ctrl && key.name === 'l') {
670
- display.clear();
671
- this.rl.prompt();
672
- return;
673
- }
674
- // ? alone: Show keyboard shortcuts help
675
- if (_str === '?' && !key.ctrl && !key.meta && !key.shift) {
676
- const currentLine = this.rl.line || '';
677
- // Only show help if input is empty (typing ? normally otherwise)
678
- if (currentLine.length === 0 && !this.isProcessing) {
679
- this.handleShortcutsCommand();
680
- this.rl.prompt();
681
- return;
682
- }
683
- }
684
570
  // Shift+Tab for profile switching
685
571
  if (key.name === 'tab' && key.shift && this.agentMenu) {
686
572
  this.showProfileSwitcher();
687
573
  }
688
- // Escape: Cancel current operation or double-tap to clear
574
+ // Escape: Cancel current operation if agent is running
689
575
  if (key.name === 'escape') {
690
- const now = Date.now();
691
- const timeSinceLastEscape = now - lastEscapeTime;
692
- lastEscapeTime = now;
693
- // Double Escape (within 500ms): Clear input and queue
694
- if (timeSinceLastEscape < 500 && !this.isProcessing) {
695
- const currentLine = this.rl.line || '';
696
- const hasQueue = this.followUpQueue.length > 0;
697
- if (currentLine.length > 0 || hasQueue) {
698
- // Clear everything
699
- this.rl.line = '';
700
- this.rl.cursor = 0;
701
- this.composableMessage.clear();
702
- this.bracketedPaste.reset();
703
- this.persistentPrompt.updateInput('', 0);
704
- this.pinnedChatBox.setInput('');
705
- this.pinnedChatBox.clearPastedBlockState();
706
- if (hasQueue) {
707
- this.followUpQueue.length = 0;
708
- this.refreshQueueIndicators();
709
- }
710
- output.write('\r\x1b[K');
711
- display.showInfo('Input and queue cleared.');
712
- this.rl.prompt();
713
- return;
714
- }
715
- }
716
576
  if (this.isProcessing && this.agent) {
717
577
  this.agent.requestCancellation();
718
578
  output.write('\n');
@@ -740,15 +600,11 @@ export class InteractiveShell {
740
600
  return;
741
601
  }
742
602
  const currentLine = this.rl.line || '';
743
- const hasComposedContent = this.composableMessage.hasContent();
744
- const hasChatBoxContent = this.pinnedChatBox.getInput().length > 0 || this.pinnedChatBox.hasPastedBlock();
745
- const hasAnyContent = currentLine.length > 0 || hasComposedContent || hasChatBoxContent;
746
- if (hasAnyContent) {
747
- // Clear all input buffers before exiting
603
+ if (currentLine.length > 0) {
604
+ // Clear the input buffer instead of exiting
605
+ // Write Ctrl+U to clear the line in readline
748
606
  this.rl.line = '';
749
607
  this.rl.cursor = 0;
750
- this.composableMessage.clear();
751
- this.bracketedPaste.reset();
752
608
  this.persistentPrompt.updateInput('', 0);
753
609
  this.pinnedChatBox.setInput('');
754
610
  this.pinnedChatBox.clearPastedBlockState();
@@ -760,79 +616,6 @@ export class InteractiveShell {
760
616
  }
761
617
  // If no text in buffer, let default Ctrl+C behavior exit
762
618
  }
763
- // Backspace: Handle chip-aware deletion
764
- if (key.name === 'backspace') {
765
- const currentLine = this.rl.line || '';
766
- const cursorPos = this.rl.cursor || 0;
767
- // Check if we're at the end of a paste chip (character before cursor is ']')
768
- if (cursorPos > 0 && currentLine[cursorPos - 1] === ']') {
769
- const beforeCursor = currentLine.slice(0, cursorPos);
770
- const chipStart = beforeCursor.lastIndexOf('[');
771
- if (chipStart !== -1) {
772
- // Verify this looks like a paste chip
773
- const potentialChip = beforeCursor.slice(chipStart);
774
- if (potentialChip.match(/^\[(?:📋|📝|📊|📄)[^\]]*\]$/)) {
775
- // Delete the entire chip by updating readline directly
776
- const newLine = currentLine.slice(0, chipStart) + currentLine.slice(cursorPos);
777
- this.rl.line = newLine;
778
- this.rl.cursor = chipStart;
779
- // Sync to display components
780
- this.persistentPrompt.updateInput(newLine, chipStart);
781
- this.pinnedChatBox.setInput(newLine, chipStart);
782
- // Check if this was the last chip - clear composable message
783
- const hasChipRemaining = /\[(?:📋|📝|📊|📄)[^\]]*\]/.test(newLine);
784
- if (!hasChipRemaining && this.composableMessage.hasContent()) {
785
- this.composableMessage.clear();
786
- this.updateComposeStatusSummary();
787
- }
788
- // During processing, update the persistent display
789
- if (this.isProcessing) {
790
- this.pinnedChatBox.updatePersistentInput();
791
- }
792
- else {
793
- // Refresh readline display
794
- this.rl.prompt(true);
795
- }
796
- // Prevent readline from processing this backspace
797
- return;
798
- }
799
- }
800
- }
801
- }
802
- // Delete key: Handle chip-aware deletion
803
- if (key.name === 'delete') {
804
- const currentLine = this.rl.line || '';
805
- const cursorPos = this.rl.cursor || 0;
806
- // Check if we're at the start of a paste chip
807
- if (cursorPos < currentLine.length && currentLine[cursorPos] === '[') {
808
- const afterCursor = currentLine.slice(cursorPos);
809
- const chipMatch = afterCursor.match(/^\[(?:📋|📝|📊|📄)[^\]]*\]/);
810
- if (chipMatch) {
811
- // Delete the entire chip
812
- const newLine = currentLine.slice(0, cursorPos) + currentLine.slice(cursorPos + chipMatch[0].length);
813
- this.rl.line = newLine;
814
- // Cursor stays in same position
815
- // Sync to display components
816
- this.persistentPrompt.updateInput(newLine, cursorPos);
817
- this.pinnedChatBox.setInput(newLine, cursorPos);
818
- // Check if this was the last chip
819
- const hasChipRemaining = /\[(?:📋|📝|📊|📄)[^\]]*\]/.test(newLine);
820
- if (!hasChipRemaining && this.composableMessage.hasContent()) {
821
- this.composableMessage.clear();
822
- this.updateComposeStatusSummary();
823
- }
824
- // During processing, update the persistent display
825
- if (this.isProcessing) {
826
- this.pinnedChatBox.updatePersistentInput();
827
- }
828
- else {
829
- this.rl.prompt(true);
830
- }
831
- // Prevent readline from processing this delete
832
- return;
833
- }
834
- }
835
- }
836
619
  }
837
620
  // Readline handles all keyboard input natively (history, shortcuts, etc.)
838
621
  // We just sync the current state to our display components
@@ -841,25 +624,6 @@ export class InteractiveShell {
841
624
  const currentLine = this.rl.line || '';
842
625
  const cursorPos = this.rl.cursor || 0;
843
626
  this.persistentPrompt.updateInput(currentLine, cursorPos);
844
- // During AI streaming, use handleInput for character-by-character input
845
- // to enable the persistent chat box functionality
846
- if (this.isProcessing && _str && key && key.name && !key.ctrl && !key.meta) {
847
- // Handle regular character input during streaming
848
- if (_str.length === 1 && !['enter', 'return', 'backspace', 'delete', 'escape', 'tab'].includes(key.name)) {
849
- this.pinnedChatBox.handleInput(_str);
850
- return; // Skip the normal sync since handleInput already updated the display
851
- }
852
- // Handle backspace during streaming
853
- if (key.name === 'backspace') {
854
- this.pinnedChatBox.handleBackspace();
855
- return; // Skip the normal sync since handleBackspace already updated the display
856
- }
857
- // Handle delete during streaming
858
- if (key.name === 'delete') {
859
- this.pinnedChatBox.handleDelete();
860
- return; // Skip the normal sync since handleDelete already updated the display
861
- }
862
- }
863
627
  // Sync to pinned chat box for display only
864
628
  this.pinnedChatBox.setInput(currentLine, cursorPos);
865
629
  // During processing, update the persistent input display so user can see what they're typing
@@ -867,18 +631,8 @@ export class InteractiveShell {
867
631
  this.pinnedChatBox.updatePersistentInput();
868
632
  }
869
633
  if (this.composableMessage.hasContent()) {
870
- // Check if all paste chips have been deleted from the input
871
- // Chips look like: [📋 Pasted: ...] or [📝 Code: ...] etc.
872
- const hasChipInInput = /\[(?:📋|📝|📊|📄)[^\]]*\]/.test(currentLine);
873
- if (!hasChipInInput) {
874
- // User deleted all chips - clear the composable message
875
- this.composableMessage.clear();
876
- this.updateComposeStatusSummary();
877
- }
878
- else {
879
- this.composableMessage.setDraft(currentLine);
880
- this.updateComposeStatusSummary();
881
- }
634
+ this.composableMessage.setDraft(currentLine);
635
+ this.updateComposeStatusSummary();
882
636
  }
883
637
  this.handleSlashCommandPreviewChange();
884
638
  });
@@ -906,11 +660,7 @@ export class InteractiveShell {
906
660
  });
907
661
  }
908
662
  setProcessingStatus(detail) {
909
- // Reasoning models (o1, deepseek-reasoner) don't stream - show distinct status
910
- const model = this.sessionState.model?.toLowerCase() || '';
911
- const isReasoningModel = model.includes('reasoner') || model.startsWith('o1') || model.includes('-reasoning');
912
- const baseMessage = isReasoningModel ? '🧠 Reasoning...' : 'Working on your request';
913
- this.statusTracker.setBase(baseMessage, {
663
+ this.statusTracker.setBase('Working on your request', {
914
664
  detail: this.describeStatusDetail(detail),
915
665
  tone: 'info',
916
666
  });
@@ -1042,47 +792,71 @@ export class InteractiveShell {
1042
792
  * Suppress readline's character echo during streaming.
1043
793
  * Characters typed will be captured but not echoed to the main output.
1044
794
  * Instead, they appear only in the persistent input box at the bottom.
1045
- *
1046
- * Strategy: Completely redirect readline's output to a null stream.
1047
- * AI streaming writes directly to process.stdout, bypassing readline,
1048
- * so it will still appear. Only readline's echo is suppressed.
1049
795
  */
1050
796
  suppressReadlineOutput() {
1051
797
  if (this.readlineOutputSuppressed || !output.isTTY) {
1052
798
  return;
1053
799
  }
1054
- // Save readline's original output stream
1055
- this.originalReadlineOutput = this.rl.output;
1056
- // Create a null stream that discards all writes
1057
- const nullStream = {
1058
- write: () => true,
1059
- end: () => { },
1060
- on: () => nullStream,
1061
- once: () => nullStream,
1062
- emit: () => false,
1063
- removeListener: () => nullStream,
1064
- isTTY: true,
1065
- columns: output.columns,
1066
- rows: output.rows,
800
+ this.originalStdoutWrite = output.write.bind(output);
801
+ const self = this;
802
+ // Replace stdout.write to filter readline echo
803
+ // Readline writes single characters for echo - we filter those out
804
+ // but allow multi-character writes (actual output) through
805
+ output.write = function (chunk, encodingOrCallback, callback) {
806
+ if (!self.originalStdoutWrite) {
807
+ return true;
808
+ }
809
+ const str = typeof chunk === 'string' ? chunk : chunk.toString();
810
+ // Filter out readline echo patterns:
811
+ // - Single printable characters (user typing)
812
+ // - Cursor movement sequences for single chars
813
+ // - Backspace sequences
814
+ // - Prompt redraws
815
+ // But allow through:
816
+ // - Multi-character content (actual AI output)
817
+ // - Newlines and control sequences for formatting
818
+ // If it's a single printable char, suppress it (user typing)
819
+ if (str.length === 1 && str.charCodeAt(0) >= 32 && str.charCodeAt(0) < 127) {
820
+ return true;
821
+ }
822
+ // Suppress backspace sequences (readline's delete char)
823
+ if (str === '\b \b' || str === '\x1b[D \x1b[D' || str === '\b' || str === '\x7f') {
824
+ return true;
825
+ }
826
+ // Suppress cursor movement sequences (readline cursor positioning)
827
+ if (/^\x1b\[\d*[ABCD]$/.test(str)) {
828
+ return true;
829
+ }
830
+ // Suppress readline prompt redraw patterns (starts with \r or cursor home)
831
+ if (/^\r/.test(str) && str.length < 20 && /^[\r\x1b\[\dGK> ]+$/.test(str)) {
832
+ return true;
833
+ }
834
+ // Suppress clear line + prompt patterns from readline
835
+ if (/^\x1b\[\d*[GK]/.test(str) && str.length < 15) {
836
+ return true;
837
+ }
838
+ // Suppress short sequences that look like readline control (not AI content)
839
+ // AI content is typically longer or contains actual text
840
+ if (str.length <= 3 && /^[\x1b\[\]\d;GKJ]+$/.test(str)) {
841
+ return true;
842
+ }
843
+ // Allow everything else through (actual AI output)
844
+ if (typeof encodingOrCallback === 'function') {
845
+ return self.originalStdoutWrite(chunk, encodingOrCallback);
846
+ }
847
+ return self.originalStdoutWrite(chunk, encodingOrCallback, callback);
1067
848
  };
1068
- // Redirect readline's output to null - this completely stops echo
1069
- this.rl.output = nullStream;
1070
849
  this.readlineOutputSuppressed = true;
1071
850
  }
1072
- // Store original readline output for restoration
1073
- originalReadlineOutput = null;
1074
851
  /**
1075
852
  * Restore normal readline output after streaming completes.
1076
853
  */
1077
854
  restoreReadlineOutput() {
1078
- if (!this.readlineOutputSuppressed) {
855
+ if (!this.readlineOutputSuppressed || !this.originalStdoutWrite) {
1079
856
  return;
1080
857
  }
1081
- // Restore readline's original output stream
1082
- if (this.originalReadlineOutput) {
1083
- this.rl.output = this.originalReadlineOutput;
1084
- this.originalReadlineOutput = null;
1085
- }
858
+ output.write = this.originalStdoutWrite;
859
+ this.originalStdoutWrite = null;
1086
860
  this.readlineOutputSuppressed = false;
1087
861
  }
1088
862
  enqueueUserInput(line, flushImmediately = false) {
@@ -1135,20 +909,12 @@ export class InteractiveShell {
1135
909
  return;
1136
910
  }
1137
911
  this.lastPastePreviewLineCount = lineCount;
912
+ // Clear current line and write preview (the line handler already moved cursor up)
913
+ output.write('\r\x1b[K');
1138
914
  const statusText = preview
1139
- ? `📋 Pasting: ${preview.slice(0, 50)}${preview.length > 50 ? '...' : ''}`
1140
- : `📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`;
1141
- // During processing, show preview in the persistent chat box at the bottom
1142
- // This prevents it from appearing in the streaming output area
1143
- if (this.isProcessing) {
1144
- this.pinnedChatBox.setInput(statusText, statusText.length);
1145
- this.pinnedChatBox.updatePersistentInput();
1146
- }
1147
- else {
1148
- // Not processing - write to output normally
1149
- output.write('\r\x1b[K');
1150
- output.write(theme.ui.muted(statusText));
1151
- }
915
+ ? `${theme.ui.muted('📋 Pasting:')} ${theme.ui.muted(preview.slice(0, 50))}${preview.length > 50 ? '...' : ''}`
916
+ : `${theme.ui.muted(`📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`)}`;
917
+ output.write(statusText);
1152
918
  }
1153
919
  /**
1154
920
  * Clear the multi-line paste preview
@@ -1157,18 +923,11 @@ export class InteractiveShell {
1157
923
  if (!this.inPasteCapture) {
1158
924
  return;
1159
925
  }
926
+ // Clear current line
927
+ output.write('\r\x1b[K');
1160
928
  // Reset tracking state
1161
929
  this.lastPastePreviewLineCount = 0;
1162
930
  this.inPasteCapture = false;
1163
- // During processing, clear the persistent chat box preview
1164
- if (this.isProcessing) {
1165
- this.pinnedChatBox.setInput('', 0);
1166
- this.pinnedChatBox.updatePersistentInput();
1167
- }
1168
- else {
1169
- // Not processing - clear output normally
1170
- output.write('\r\x1b[K');
1171
- }
1172
931
  }
1173
932
  /**
1174
933
  * Capture any paste (single or multi-line) without immediately submitting it.
@@ -1187,15 +946,7 @@ export class InteractiveShell {
1187
946
  const displayContent = lineCount === 1
1188
947
  ? content
1189
948
  : content.replace(/\n/g, ' ↵ '); // Visual newline indicator for 2-line pastes
1190
- // During processing, ONLY update pinnedChatBox - never write to output
1191
- // This prevents paste from leaking into the streaming area
1192
- if (this.isProcessing) {
1193
- // handleInput triggers handleMultilinePaste which handles rendering
1194
- // NO redundant updatePersistentInput call - it causes double renders
1195
- this.pinnedChatBox.handleInput(content);
1196
- return;
1197
- }
1198
- // Clear any echoed content first (only when not processing)
949
+ // Clear any echoed content first
1199
950
  output.write('\r\x1b[K');
1200
951
  // Get current readline content and append paste
1201
952
  const currentLine = this.rl.line || '';
@@ -1205,55 +956,42 @@ export class InteractiveShell {
1205
956
  const after = currentLine.slice(cursorPos);
1206
957
  const newLine = before + displayContent + after;
1207
958
  const newCursor = cursorPos + displayContent.length;
1208
- // Update readline buffer silently (without echoing to output)
1209
- this.rl.line = newLine;
1210
- this.rl.cursor = newCursor;
959
+ // Update readline buffer - write directly without storing in composableMessage
960
+ // This allows short pastes to flow through as normal typed text
961
+ this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line
962
+ this.rl.write(newLine); // Write new content
1211
963
  // Update persistent prompt display
1212
964
  this.persistentPrompt.updateInput(newLine, newCursor);
1213
- // CRITICAL: Update pinnedChatBox to show paste in the persistent bottom area
1214
- // Always use handleInput to trigger consistent paste detection and summarization
1215
- // This ensures multi-line paste is handled the same way on load and during processing
1216
- this.pinnedChatBox.handleInput(content);
1217
- // Not processing - render pinned chat box and sync readline
1218
- this.pinnedChatBox.forceRender();
965
+ // NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
966
+ // will decrement for each readline line event and auto-clear when all are processed.
967
+ // Clearing prematurely causes the remaining readline-echoed lines to pass through.
968
+ // Re-prompt to show the inline content
1219
969
  this.rl.prompt(true);
1220
970
  return;
1221
971
  }
1222
972
  // For longer pastes (3+ lines), store as a composable block
1223
973
  this.composableMessage.addPaste(content);
1224
- // During processing, ONLY update pinnedChatBox - never write to output
1225
- // This prevents paste from leaking into the streaming area
1226
- if (this.isProcessing) {
1227
- // handleInput triggers handleMultilinePaste which handles rendering
1228
- // NO redundant updatePersistentInput call - it causes double renders
1229
- this.pinnedChatBox.handleInput(content);
1230
- return;
1231
- }
1232
- // Clear remaining echoed lines from terminal (only when not processing)
974
+ // Clear remaining echoed lines from terminal
1233
975
  output.write('\r\x1b[K');
1234
976
  // Build the paste chips to show inline with prompt
1235
977
  // Format: [Pasted text #1 +104 lines] [Pasted text #2 +50 lines]
1236
978
  const pasteChips = this.composableMessage.formatPasteChips();
1237
- const displayText = `${pasteChips} `;
1238
- const cursorPos = displayText.length;
1239
979
  // Update status bar - minimal hint, no confirmation required
1240
980
  this.persistentPrompt.updateStatusBar({
1241
981
  message: 'Press Enter to send',
1242
982
  });
1243
- // Update both prompt systems with the paste chips
1244
- this.persistentPrompt.updateInput(displayText, cursorPos);
983
+ // Set the prompt to show paste chips, then position cursor after them
984
+ // The user can type additional text after the chips
985
+ this.persistentPrompt.updateInput(pasteChips + ' ', pasteChips.length + 1);
1245
986
  // Update readline's line buffer to include the chips as prefix
1246
987
  // This ensures typed text appears after the chips
1247
988
  if (this.rl.line !== undefined) {
1248
- this.rl.line = displayText;
1249
- this.rl.cursor = cursorPos;
1250
- }
1251
- // CRITICAL: Update pinnedChatBox to show paste chips in the persistent bottom area
1252
- // This ensures the paste indicator appears in the chat box, NOT in the streaming area
1253
- // Always use handleInput to trigger consistent paste detection and summarization
1254
- this.pinnedChatBox.handleInput(content);
1255
- // Not processing - render pinned chat box and sync readline
1256
- this.pinnedChatBox.forceRender();
989
+ this.rl.line = pasteChips + ' ';
990
+ this.rl.cursor = pasteChips.length + 1;
991
+ }
992
+ // NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
993
+ // will decrement for each readline line event (one per pasted line) and auto-clear when done.
994
+ // Clearing prematurely causes remaining readline-echoed lines to pass through and get displayed.
1257
995
  this.rl.prompt(true); // preserveCursor=true to keep position after chips
1258
996
  }
1259
997
  /**
@@ -1279,13 +1017,6 @@ export class InteractiveShell {
1279
1017
  const combined = this.bufferedInputLines.join('\n');
1280
1018
  this.bufferedInputLines = [];
1281
1019
  this.bufferedInputTimer = null;
1282
- // CRITICAL: Long multi-line content (3+ lines) should NEVER be auto-submitted
1283
- // This catches rapid input that bypasses bracketed paste detection
1284
- // Instead, capture it like a paste and require explicit Enter to submit
1285
- if (lineCount >= 3) {
1286
- this.capturePaste(combined, lineCount);
1287
- return;
1288
- }
1289
1020
  try {
1290
1021
  await this.processInputBlock(combined, lineCount > 1);
1291
1022
  }
@@ -1379,14 +1110,6 @@ export class InteractiveShell {
1379
1110
  this.slashPreviewVisible = false;
1380
1111
  this.uiAdapter.hideSlashCommandPreview();
1381
1112
  const trimmed = line.trim();
1382
- // CRITICAL SAFETY: Long multi-line content (3+ lines) should NEVER be auto-submitted
1383
- // Capture it for explicit user confirmation, regardless of how it arrived
1384
- const lineCount = line.split('\n').length;
1385
- if (lineCount >= 3 && !this.composableMessage.hasContent()) {
1386
- // Don't re-capture if we already have composed content (user is submitting a paste)
1387
- this.capturePaste(line, lineCount);
1388
- return;
1389
- }
1390
1113
  if (await this.handlePendingInteraction(trimmed)) {
1391
1114
  return;
1392
1115
  }
@@ -1443,14 +1166,6 @@ export class InteractiveShell {
1443
1166
  this.rl.prompt();
1444
1167
  return;
1445
1168
  }
1446
- // CRITICAL: If we're processing, queue the assembled content as a follow-up
1447
- // This ensures multi-line pastes submitted during streaming don't interrupt
1448
- if (this.isProcessing) {
1449
- this.enqueueFollowUpAction({ type: 'request', text: assembled });
1450
- // Clear input display after queueing
1451
- this.pinnedChatBox.clearInput();
1452
- return;
1453
- }
1454
1169
  // Check if assembled content is a continuous command
1455
1170
  if (this.isContinuousCommand(assembled)) {
1456
1171
  await this.processContinuousRequest(assembled);
@@ -1654,7 +1369,6 @@ export class InteractiveShell {
1654
1369
  this.buildSlashCommandList('Available Commands:'),
1655
1370
  '',
1656
1371
  'Type your request in natural language and press Enter.',
1657
- 'Press ? or type /shortcuts for keyboard shortcuts.',
1658
1372
  ];
1659
1373
  display.showSystemMessage(info.join('\n'));
1660
1374
  }
@@ -1937,7 +1651,7 @@ export class InteractiveShell {
1937
1651
  lines.push(theme.bold('Session File Changes'));
1938
1652
  lines.push('');
1939
1653
  lines.push(`${theme.info('•')} ${summary.files} file${summary.files === 1 ? '' : 's'} modified`);
1940
- lines.push(`${theme.info('•')} ${theme.success(`+${summary.additions}`)} ${theme.error(`-${summary.removals}`)} lines`);
1654
+ lines.push(`${theme.info('•')} ${theme.success('+' + summary.additions)} ${theme.error('-' + summary.removals)} lines`);
1941
1655
  lines.push('');
1942
1656
  // Group changes by file
1943
1657
  const fileMap = new Map();
@@ -1961,7 +1675,7 @@ export class InteractiveShell {
1961
1675
  if (stats.writes > 0)
1962
1676
  operations.push(`${stats.writes} write${stats.writes === 1 ? '' : 's'}`);
1963
1677
  const opsText = operations.join(', ');
1964
- const diffText = `${theme.success(`+${stats.additions}`)} ${theme.error(`-${stats.removals}`)}`;
1678
+ const diffText = `${theme.success('+' + stats.additions)} ${theme.error('-' + stats.removals)}`;
1965
1679
  lines.push(` ${theme.dim(path)}`);
1966
1680
  lines.push(` ${opsText} • ${diffText}`);
1967
1681
  }
@@ -2687,14 +2401,6 @@ export class InteractiveShell {
2687
2401
  this.rl.prompt();
2688
2402
  }
2689
2403
  async processRequest(request) {
2690
- // CRITICAL SAFETY: Long multi-line content should never be auto-queued
2691
- // This is a last-line defense - content should have been captured earlier
2692
- const lineCount = request.split('\n').length;
2693
- if (lineCount >= 3 && !this.composableMessage.hasContent()) {
2694
- // Capture instead of processing/queueing - user must explicitly confirm
2695
- this.capturePaste(request, lineCount);
2696
- return;
2697
- }
2698
2404
  if (this.isProcessing) {
2699
2405
  this.enqueueFollowUpAction({ type: 'request', text: request });
2700
2406
  return;
@@ -2709,17 +2415,10 @@ export class InteractiveShell {
2709
2415
  }
2710
2416
  this.isProcessing = true;
2711
2417
  const requestStartTime = Date.now(); // Alpha Zero 2 timing
2712
- // Detect reasoning models (o1, deepseek-reasoner) that don't stream text
2713
- const model = this.sessionState.model?.toLowerCase() || '';
2714
- const isReasoningModel = model.includes('reasoner') || model.startsWith('o1') || model.includes('-reasoning');
2715
2418
  // Keep persistent prompt visible during processing so users can type follow-up requests
2716
2419
  // The prompt will show a "processing" indicator but remain interactive
2717
- const statusMessage = isReasoningModel
2718
- ? '🧠 Reasoning... (type to queue follow-up)'
2719
- : '⏳ Processing... (type to queue follow-up)';
2720
- this.persistentPrompt.updateStatusBar({ message: statusMessage });
2420
+ this.persistentPrompt.updateStatusBar({ message: '⏳ Processing... (type to queue follow-up)' });
2721
2421
  // Update pinned chat box to show processing state
2722
- this.pinnedChatBox.setReasoningModel(isReasoningModel);
2723
2422
  // Clear the input display since the request was already submitted
2724
2423
  // Note: Don't set statusMessage here - the isProcessing flag already shows "⏳ Processing..."
2725
2424
  this.pinnedChatBox.setProcessing(true);
@@ -2727,7 +2426,6 @@ export class InteractiveShell {
2727
2426
  this.pinnedChatBox.clearInput();
2728
2427
  this.uiAdapter.startProcessing('Working on your request');
2729
2428
  this.setProcessingStatus();
2730
- let renderInterval = null;
2731
2429
  try {
2732
2430
  // Add visual separator between user prompt and AI response
2733
2431
  display.newLine();
@@ -2736,12 +2434,6 @@ export class InteractiveShell {
2736
2434
  this.pinnedChatBox.enableScrollRegion();
2737
2435
  // Suppress readline echo so typed characters only appear in persistent input box
2738
2436
  this.suppressReadlineOutput();
2739
- // Set up periodic render timer to ensure pinned chat box stays visible during streaming
2740
- renderInterval = setInterval(() => {
2741
- if (this.isProcessing && this.pinnedChatBox.isActive()) {
2742
- this.pinnedChatBox.forceRender();
2743
- }
2744
- }, 2000); // Render every 2 seconds during streaming
2745
2437
  // Enable streaming for real-time text output (Claude Code style)
2746
2438
  await agent.send(request, true);
2747
2439
  await this.awaitPendingCleanup();
@@ -2781,11 +2473,6 @@ export class InteractiveShell {
2781
2473
  this.rl.prompt();
2782
2474
  this.scheduleQueueProcessing();
2783
2475
  this.refreshQueueIndicators();
2784
- // Ensure any periodic render timer is cleaned up
2785
- if (renderInterval) {
2786
- clearInterval(renderInterval);
2787
- renderInterval = null;
2788
- }
2789
2476
  }
2790
2477
  }
2791
2478
  /**
@@ -2834,8 +2521,6 @@ export class InteractiveShell {
2834
2521
  this.pinnedChatBox.enableScrollRegion();
2835
2522
  // Suppress readline echo so typed characters only appear in persistent input box
2836
2523
  this.suppressReadlineOutput();
2837
- // Periodically refresh the pinned chat box while streaming
2838
- let renderInterval = null;
2839
2524
  let iteration = 0;
2840
2525
  let lastResponse = '';
2841
2526
  let consecutiveNoProgress = 0;
@@ -2858,11 +2543,6 @@ IMPORTANT: You have full git access. After making improvements:
2858
2543
  Commit frequently with descriptive messages. Push when ready.
2859
2544
  When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
2860
2545
  }
2861
- renderInterval = setInterval(() => {
2862
- if (this.isProcessing && this.pinnedChatBox.isActive()) {
2863
- this.pinnedChatBox.forceRender();
2864
- }
2865
- }, 2000);
2866
2546
  while (iteration < MAX_ITERATIONS) {
2867
2547
  iteration++;
2868
2548
  display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
@@ -3000,10 +2680,6 @@ What's the next action?`;
3000
2680
  const minutes = Math.floor(totalElapsed / 60000);
3001
2681
  const seconds = Math.floor((totalElapsed % 60000) / 1000);
3002
2682
  display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
3003
- if (renderInterval) {
3004
- clearInterval(renderInterval);
3005
- renderInterval = null;
3006
- }
3007
2683
  // Reset completion detector for next task
3008
2684
  resetTaskCompletionDetector();
3009
2685
  // Restore readline echo before other cleanup
@@ -3248,9 +2924,7 @@ What's the next action?`;
3248
2924
  display.showNarrative(content.trim());
3249
2925
  }
3250
2926
  // The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
3251
- // Note: Don't call forceRender() here - the output interceptor's afterWrite handles
3252
- // re-rendering the persistent input. Calling forceRender() immediately can cause
3253
- // cursor position issues when RESTORE_CURSOR doesn't work correctly in some terminals.
2927
+ this.pinnedChatBox.forceRender();
3254
2928
  return;
3255
2929
  }
3256
2930
  const cleanup = this.handleContextTelemetry(metadata, enriched);
@@ -3719,7 +3393,7 @@ What's the next action?`;
3719
3393
  // Truncate to reasonable length
3720
3394
  const maxLength = 50;
3721
3395
  return cleaned.length > maxLength
3722
- ? `${cleaned.slice(0, maxLength - 3)}...`
3396
+ ? cleaned.slice(0, maxLength - 3) + '...'
3723
3397
  : cleaned;
3724
3398
  }
3725
3399
  splitThinkingResponse(content) {