aider-ce 0.87.13__py3-none-any.whl → 0.88.0__py3-none-any.whl

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.

Potentially problematic release.


This version of aider-ce might be problematic. Click here for more details.

Files changed (60) hide show
  1. aider/__init__.py +1 -1
  2. aider/_version.py +2 -2
  3. aider/args.py +6 -0
  4. aider/coders/architect_coder.py +3 -3
  5. aider/coders/base_coder.py +505 -184
  6. aider/coders/context_coder.py +1 -1
  7. aider/coders/editblock_func_coder.py +2 -2
  8. aider/coders/navigator_coder.py +451 -649
  9. aider/coders/navigator_legacy_prompts.py +49 -284
  10. aider/coders/navigator_prompts.py +46 -473
  11. aider/coders/search_replace.py +0 -0
  12. aider/coders/wholefile_func_coder.py +2 -2
  13. aider/commands.py +56 -44
  14. aider/history.py +14 -12
  15. aider/io.py +354 -117
  16. aider/llm.py +12 -4
  17. aider/main.py +22 -19
  18. aider/mcp/__init__.py +65 -2
  19. aider/mcp/server.py +37 -11
  20. aider/models.py +45 -20
  21. aider/onboarding.py +4 -4
  22. aider/repo.py +7 -7
  23. aider/resources/model-metadata.json +8 -8
  24. aider/scrape.py +2 -2
  25. aider/sendchat.py +185 -15
  26. aider/tools/__init__.py +44 -23
  27. aider/tools/command.py +18 -0
  28. aider/tools/command_interactive.py +18 -0
  29. aider/tools/delete_block.py +23 -0
  30. aider/tools/delete_line.py +19 -1
  31. aider/tools/delete_lines.py +20 -1
  32. aider/tools/extract_lines.py +25 -2
  33. aider/tools/git.py +142 -0
  34. aider/tools/grep.py +47 -2
  35. aider/tools/indent_lines.py +25 -0
  36. aider/tools/insert_block.py +26 -0
  37. aider/tools/list_changes.py +15 -0
  38. aider/tools/ls.py +24 -1
  39. aider/tools/make_editable.py +18 -0
  40. aider/tools/make_readonly.py +19 -0
  41. aider/tools/remove.py +22 -0
  42. aider/tools/replace_all.py +21 -0
  43. aider/tools/replace_line.py +20 -1
  44. aider/tools/replace_lines.py +21 -1
  45. aider/tools/replace_text.py +22 -0
  46. aider/tools/show_numbered_context.py +18 -0
  47. aider/tools/undo_change.py +15 -0
  48. aider/tools/update_todo_list.py +131 -0
  49. aider/tools/view.py +23 -0
  50. aider/tools/view_files_at_glob.py +32 -27
  51. aider/tools/view_files_matching.py +51 -37
  52. aider/tools/view_files_with_symbol.py +41 -54
  53. aider/tools/view_todo_list.py +57 -0
  54. aider/waiting.py +20 -203
  55. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
  56. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
  57. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
  58. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
  59. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
  60. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
aider/io.py CHANGED
@@ -1,9 +1,11 @@
1
+ import asyncio
1
2
  import base64
2
3
  import functools
3
4
  import os
4
5
  import shutil
5
6
  import signal
6
7
  import subprocess
8
+ import sys
7
9
  import time
8
10
  import webbrowser
9
11
  from collections import defaultdict
@@ -30,14 +32,14 @@ from rich.color import ColorParseError
30
32
  from rich.columns import Columns
31
33
  from rich.console import Console
32
34
  from rich.markdown import Markdown
35
+ from rich.spinner import SPINNERS
33
36
  from rich.style import Style as RichStyle
34
37
  from rich.text import Text
35
38
 
36
- from aider.mdstream import MarkdownStream
37
-
38
39
  from .dump import dump # noqa: F401
39
40
  from .editor import pipe_editor
40
41
  from .utils import is_image_file, run_fzf
42
+ from .waiting import Spinner
41
43
 
42
44
  # Constants
43
45
  NOTIFICATION_MESSAGE = "Aider is waiting for your input"
@@ -71,6 +73,23 @@ def restore_multiline(func):
71
73
  return wrapper
72
74
 
73
75
 
76
+ def restore_multiline_async(func):
77
+ """Decorator to restore multiline mode after async function execution"""
78
+
79
+ @functools.wraps(func)
80
+ async def wrapper(self, *args, **kwargs):
81
+ orig_multiline = self.multiline_mode
82
+ self.multiline_mode = False
83
+ try:
84
+ return await func(self, *args, **kwargs)
85
+ except Exception:
86
+ raise
87
+ finally:
88
+ self.multiline_mode = orig_multiline
89
+
90
+ return wrapper
91
+
92
+
74
93
  def without_input_history(func):
75
94
  """Decorator to temporarily disable history saving for the prompt session buffer."""
76
95
 
@@ -310,6 +329,8 @@ class InputOutput:
310
329
  self.chat_history_file = None
311
330
 
312
331
  self.placeholder = None
332
+ self.fallback_spinner = None
333
+ self.prompt_session = None
313
334
  self.interrupted = False
314
335
  self.never_prompts = set()
315
336
  self.editingmode = editingmode
@@ -350,6 +371,9 @@ class InputOutput:
350
371
 
351
372
  self.code_theme = code_theme
352
373
 
374
+ self._stream_buffer = ""
375
+ self._stream_line_count = 0
376
+
353
377
  self.input = input
354
378
  self.output = output
355
379
 
@@ -387,20 +411,35 @@ class InputOutput:
387
411
  current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
388
412
  self.append_chat_history(f"\n# aider chat started at {current_time}\n\n")
389
413
 
390
- self.prompt_session = None
391
414
  self.is_dumb_terminal = is_dumb_terminal()
415
+ self.is_tty = sys.stdout.isatty()
392
416
 
393
417
  if self.is_dumb_terminal:
394
418
  self.pretty = False
395
419
  fancy_input = False
396
420
 
397
421
  if fancy_input:
422
+ # Spinner state
423
+ self.spinner_running = False
424
+ self.spinner_text = ""
425
+ self.spinner_frame_index = 0
426
+ self.spinner_last_frame_index = 0
427
+ self.unicode_palette = "░█"
428
+ # If unicode is supported, use the rich 'dots2' spinner, otherwise an ascii fallback
429
+ if self._spinner_supports_unicode():
430
+ self.spinner_frames = SPINNERS["dots2"]["frames"]
431
+ else:
432
+ # A simple ascii spinner
433
+ self.spinner_frames = SPINNERS["line"]["frames"]
434
+
398
435
  # Initialize PromptSession only if we have a capable terminal
399
436
  session_kwargs = {
400
437
  "input": self.input,
401
438
  "output": self.output,
402
439
  "lexer": PygmentsLexer(MarkdownLexer),
403
440
  "editing_mode": self.editingmode,
441
+ "bottom_toolbar": self.get_bottom_toolbar,
442
+ "refresh_interval": 0.1,
404
443
  }
405
444
  if self.editingmode == EditingMode.VI:
406
445
  session_kwargs["cursor"] = ModalCursorShapeConfig()
@@ -419,10 +458,60 @@ class InputOutput:
419
458
 
420
459
  self.file_watcher = file_watcher
421
460
  self.root = root
461
+ self.outstanding_confirmations = []
462
+ self.coder = None
422
463
 
423
464
  # Validate color settings after console is initialized
424
465
  self._validate_color_settings()
425
466
 
467
+ def _spinner_supports_unicode(self) -> bool:
468
+ if not self.is_tty:
469
+ return False
470
+ try:
471
+ out = self.unicode_palette
472
+ out += "\b" * len(self.unicode_palette)
473
+ out += " " * len(self.unicode_palette)
474
+ out += "\b" * len(self.unicode_palette)
475
+ sys.stdout.write(out)
476
+ sys.stdout.flush()
477
+ return True
478
+ except UnicodeEncodeError:
479
+ return False
480
+ except Exception:
481
+ return False
482
+
483
+ def start_spinner(self, text):
484
+ """Start the spinner."""
485
+ self.stop_spinner()
486
+
487
+ if self.prompt_session:
488
+ self.spinner_running = True
489
+ self.spinner_text = text
490
+ self.spinner_frame_index = self.spinner_last_frame_index
491
+ else:
492
+ self.fallback_spinner = Spinner(text)
493
+ self.fallback_spinner.step()
494
+
495
+ def stop_spinner(self):
496
+ """Stop the spinner."""
497
+ self.spinner_running = False
498
+ self.spinner_text = ""
499
+ # Keep last frame index to avoid spinner "jumping" on restart
500
+ self.spinner_last_frame_index = self.spinner_frame_index
501
+ if self.fallback_spinner:
502
+ self.fallback_spinner.end()
503
+ self.fallback_spinner = None
504
+
505
+ def get_bottom_toolbar(self):
506
+ """Get the current spinner frame and text for the bottom toolbar."""
507
+ if not self.spinner_running or not self.spinner_frames:
508
+ return None
509
+
510
+ frame = self.spinner_frames[self.spinner_frame_index]
511
+ self.spinner_frame_index = (self.spinner_frame_index + 1) % len(self.spinner_frames)
512
+
513
+ return f"{frame} {self.spinner_text}"
514
+
426
515
  def _validate_color_settings(self):
427
516
  """Validate configured color strings and reset invalid ones."""
428
517
  color_attributes = [
@@ -461,6 +550,7 @@ class InputOutput:
461
550
  "pygments.literal.string": f"bold italic {self.user_input_color}",
462
551
  }
463
552
  )
553
+ style_dict["bottom-toolbar"] = f"{self.user_input_color} noreverse"
464
554
 
465
555
  # Conditionally add 'completion-menu' style
466
556
  completion_menu_style = []
@@ -566,13 +656,35 @@ class InputOutput:
566
656
  print()
567
657
 
568
658
  def interrupt_input(self):
659
+ coder = self.coder() if self.coder else None
660
+ # interrupted_for_confirmation = False
661
+
662
+ if (
663
+ coder
664
+ and hasattr(coder, "input_task")
665
+ and coder.input_task
666
+ and not coder.input_task.done()
667
+ ):
668
+ coder.input_task.cancel()
669
+
569
670
  if self.prompt_session and self.prompt_session.app:
570
671
  # Store any partial input before interrupting
571
672
  self.placeholder = self.prompt_session.app.current_buffer.text
572
673
  self.interrupted = True
573
- self.prompt_session.app.exit()
574
674
 
575
- def get_input(
675
+ try:
676
+ self.prompt_session.app.exit()
677
+ finally:
678
+ pass
679
+
680
+ def reject_outstanding_confirmations(self):
681
+ """Reject all outstanding confirmation dialogs."""
682
+ for future in self.outstanding_confirmations:
683
+ if not future.done():
684
+ future.set_result(False)
685
+ self.outstanding_confirmations = []
686
+
687
+ async def get_input(
576
688
  self,
577
689
  root,
578
690
  rel_fnames,
@@ -582,6 +694,7 @@ class InputOutput:
582
694
  abs_read_only_stubs_fnames=None,
583
695
  edit_format=None,
584
696
  ):
697
+ self.reject_outstanding_confirmations()
585
698
  self.rule()
586
699
 
587
700
  # Ring the bell if needed
@@ -735,7 +848,7 @@ class InputOutput:
735
848
  def get_continuation(width, line_number, is_soft_wrap):
736
849
  return self.prompt_prefix
737
850
 
738
- line = self.prompt_session.prompt(
851
+ line = await self.prompt_session.prompt_async(
739
852
  show,
740
853
  default=default,
741
854
  completer=completer_instance,
@@ -747,7 +860,7 @@ class InputOutput:
747
860
  prompt_continuation=get_continuation,
748
861
  )
749
862
  else:
750
- line = input(show)
863
+ line = await asyncio.get_event_loop().run_in_executor(None, input, show)
751
864
 
752
865
  # Check if we were interrupted by a file change
753
866
  if self.interrupted:
@@ -758,15 +871,18 @@ class InputOutput:
758
871
 
759
872
  except EOFError:
760
873
  raise
874
+ except KeyboardInterrupt:
875
+ self.console.print()
876
+ return ""
877
+ except UnicodeEncodeError as err:
878
+ self.tool_error(str(err))
879
+ return ""
761
880
  except Exception as err:
762
881
  import traceback
763
882
 
764
883
  self.tool_error(str(err))
765
884
  self.tool_error(traceback.format_exc())
766
885
  return ""
767
- except UnicodeEncodeError as err:
768
- self.tool_error(str(err))
769
- return ""
770
886
  finally:
771
887
  if self.file_watcher:
772
888
  self.file_watcher.stop()
@@ -811,7 +927,6 @@ class InputOutput:
811
927
  inp = line
812
928
  break
813
929
 
814
- print()
815
930
  self.user_input(inp)
816
931
  return inp
817
932
 
@@ -876,18 +991,43 @@ class InputOutput:
876
991
  hist = "\n" + content.strip() + "\n\n"
877
992
  self.append_chat_history(hist)
878
993
 
879
- def offer_url(self, url, prompt="Open URL for more info?", allow_never=True):
994
+ async def offer_url(self, url, prompt="Open URL for more info?", allow_never=True):
880
995
  """Offer to open a URL in the browser, returns True if opened."""
881
996
  if url in self.never_prompts:
882
997
  return False
883
- if self.confirm_ask(prompt, subject=url, allow_never=allow_never):
998
+ if await self.confirm_ask(prompt, subject=url, allow_never=allow_never):
884
999
  webbrowser.open(url)
885
1000
  return True
886
1001
  return False
887
1002
 
888
- @restore_multiline
889
- @without_input_history
890
- def confirm_ask(
1003
+ @restore_multiline_async
1004
+ async def confirm_ask(
1005
+ self,
1006
+ *args,
1007
+ **kwargs,
1008
+ ):
1009
+ coder = self.coder() if self.coder else None
1010
+ interrupted_for_confirmation = False
1011
+ if (
1012
+ coder
1013
+ and hasattr(coder, "input_task")
1014
+ and coder.input_task
1015
+ and not coder.input_task.done()
1016
+ ):
1017
+ coder.confirmation_in_progress = True
1018
+ interrupted_for_confirmation = True
1019
+ # self.interrupt_input()
1020
+
1021
+ try:
1022
+ return await asyncio.create_task(self._confirm_ask(*args, **kwargs))
1023
+ except KeyboardInterrupt:
1024
+ # Re-raise KeyboardInterrupt to allow it to propagate
1025
+ raise
1026
+ finally:
1027
+ if interrupted_for_confirmation:
1028
+ coder.confirmation_in_progress = False
1029
+
1030
+ async def _confirm_ask(
891
1031
  self,
892
1032
  question,
893
1033
  default="y",
@@ -903,109 +1043,152 @@ class InputOutput:
903
1043
 
904
1044
  question_id = (question, subject)
905
1045
 
906
- if question_id in self.never_prompts:
907
- return False
1046
+ confirmation_future = asyncio.get_running_loop().create_future()
1047
+ self.outstanding_confirmations.append(confirmation_future)
908
1048
 
909
- if group and not group.show_group:
910
- group = None
911
- if group:
912
- allow_never = True
913
-
914
- valid_responses = ["yes", "no", "skip", "all"]
915
- options = " (Y)es/(N)o"
916
- if group:
917
- if not explicit_yes_required:
918
- options += "/(A)ll"
919
- options += "/(S)kip all"
920
- if allow_never:
921
- options += "/(D)on't ask again"
922
- valid_responses.append("don't")
923
-
924
- if default.lower().startswith("y"):
925
- question += options + " [Yes]: "
926
- elif default.lower().startswith("n"):
927
- question += options + " [No]: "
928
- else:
929
- question += options + f" [{default}]: "
1049
+ try:
1050
+ if question_id in self.never_prompts:
1051
+ if not confirmation_future.done():
1052
+ confirmation_future.set_result(False)
1053
+ return await confirmation_future
1054
+
1055
+ if group and not group.show_group:
1056
+ group = None
1057
+ if group:
1058
+ allow_never = True
1059
+
1060
+ valid_responses = ["yes", "no", "skip", "all"]
1061
+ options = " (Y)es/(N)o"
1062
+ if group:
1063
+ if not explicit_yes_required:
1064
+ options += "/(A)ll"
1065
+ options += "/(S)kip all"
1066
+ if allow_never:
1067
+ options += "/(D)on't ask again"
1068
+ valid_responses.append("don't")
1069
+
1070
+ if default.lower().startswith("y"):
1071
+ question += options + " [Yes]: "
1072
+ elif default.lower().startswith("n"):
1073
+ question += options + " [No]: "
1074
+ else:
1075
+ question += options + f" [{default}]: "
1076
+
1077
+ if subject:
1078
+ self.tool_output()
1079
+ if "\n" in subject:
1080
+ lines = subject.splitlines()
1081
+ max_length = max(len(line) for line in lines)
1082
+ padded_lines = [line.ljust(max_length) for line in lines]
1083
+ padded_subject = "\n".join(padded_lines)
1084
+ self.tool_output(padded_subject, bold=True)
1085
+ else:
1086
+ self.tool_output(subject, bold=True)
930
1087
 
931
- if subject:
932
- self.tool_output()
933
- if "\n" in subject:
934
- lines = subject.splitlines()
935
- max_length = max(len(line) for line in lines)
936
- padded_lines = [line.ljust(max_length) for line in lines]
937
- padded_subject = "\n".join(padded_lines)
938
- self.tool_output(padded_subject, bold=True)
1088
+ style = self._get_style()
1089
+
1090
+ if self.yes is True:
1091
+ res = "n" if explicit_yes_required else "y"
1092
+ elif self.yes is False:
1093
+ res = "n"
1094
+ elif group and group.preference:
1095
+ res = group.preference
1096
+ self.user_input(f"{question}{res}", log_only=False)
939
1097
  else:
940
- self.tool_output(subject, bold=True)
1098
+ while True:
1099
+ try:
1100
+ if self.prompt_session:
1101
+ coder = self.coder() if self.coder else None
1102
+ if (
1103
+ coder
1104
+ and hasattr(coder, "input_task")
1105
+ and coder.input_task
1106
+ and not coder.input_task.done()
1107
+ ):
1108
+ self.prompt_session.message = question
1109
+ self.prompt_session.app.invalidate()
1110
+ res = await coder.input_task
1111
+ else:
1112
+ prompt_task = asyncio.create_task(
1113
+ self.prompt_session.prompt_async(
1114
+ question,
1115
+ style=style,
1116
+ complete_while_typing=False,
1117
+ )
1118
+ )
1119
+ done, pending = await asyncio.wait(
1120
+ {prompt_task, confirmation_future},
1121
+ return_when=asyncio.FIRST_COMPLETED,
1122
+ )
1123
+
1124
+ if confirmation_future in done:
1125
+ prompt_task.cancel()
1126
+ return await confirmation_future
1127
+
1128
+ res = await prompt_task
1129
+ else:
1130
+ res = await asyncio.get_event_loop().run_in_executor(
1131
+ None, input, question
1132
+ )
1133
+ except EOFError:
1134
+ # Treat EOF (Ctrl+D) as if the user pressed Enter
1135
+ res = default
1136
+ break
1137
+ except asyncio.CancelledError:
1138
+ if not confirmation_future.done():
1139
+ confirmation_future.set_result(False)
1140
+ raise
941
1141
 
942
- style = self._get_style()
1142
+ if not res:
1143
+ res = default
1144
+ break
1145
+ res = res.lower()
1146
+ good = any(valid_response.startswith(res) for valid_response in valid_responses)
1147
+ if good:
1148
+ break
943
1149
 
944
- def is_valid_response(text):
945
- if not text:
946
- return True
947
- return text.lower() in valid_responses
1150
+ error_message = f"Please answer with one of: {', '.join(valid_responses)}"
1151
+ self.tool_error(error_message)
948
1152
 
949
- if self.yes is True:
950
- res = "n" if explicit_yes_required else "y"
951
- elif self.yes is False:
952
- res = "n"
953
- elif group and group.preference:
954
- res = group.preference
955
- self.user_input(f"{question}{res}", log_only=False)
956
- else:
957
- while True:
958
- try:
959
- if self.prompt_session:
960
- res = self.prompt_session.prompt(
961
- question,
962
- style=style,
963
- complete_while_typing=False,
964
- )
965
- else:
966
- res = input(question)
967
- except EOFError:
968
- # Treat EOF (Ctrl+D) as if the user pressed Enter
969
- res = default
970
- break
1153
+ res = res.lower()[0]
971
1154
 
972
- if not res:
973
- res = default
974
- break
975
- res = res.lower()
976
- good = any(valid_response.startswith(res) for valid_response in valid_responses)
977
- if good:
978
- break
1155
+ if res == "d" and allow_never:
1156
+ self.never_prompts.add(question_id)
1157
+ hist = f"{question.strip()} {res}"
1158
+ self.append_chat_history(hist, linebreak=True, blockquote=True)
1159
+ if not confirmation_future.done():
1160
+ confirmation_future.set_result(False)
1161
+ return await confirmation_future
1162
+
1163
+ if explicit_yes_required:
1164
+ is_yes = res == "y"
1165
+ else:
1166
+ is_yes = res in ("y", "a")
979
1167
 
980
- error_message = f"Please answer with one of: {', '.join(valid_responses)}"
981
- self.tool_error(error_message)
1168
+ is_all = res == "a" and group is not None and not explicit_yes_required
1169
+ is_skip = res == "s" and group is not None
982
1170
 
983
- res = res.lower()[0]
1171
+ if group:
1172
+ if is_all and not explicit_yes_required:
1173
+ group.preference = "all"
1174
+ elif is_skip:
1175
+ group.preference = "skip"
984
1176
 
985
- if res == "d" and allow_never:
986
- self.never_prompts.add(question_id)
987
1177
  hist = f"{question.strip()} {res}"
988
1178
  self.append_chat_history(hist, linebreak=True, blockquote=True)
989
- return False
990
-
991
- if explicit_yes_required:
992
- is_yes = res == "y"
993
- else:
994
- is_yes = res in ("y", "a")
995
1179
 
996
- is_all = res == "a" and group is not None and not explicit_yes_required
997
- is_skip = res == "s" and group is not None
1180
+ if not confirmation_future.done():
1181
+ confirmation_future.set_result(is_yes)
998
1182
 
999
- if group:
1000
- if is_all and not explicit_yes_required:
1001
- group.preference = "all"
1002
- elif is_skip:
1003
- group.preference = "skip"
1004
-
1005
- hist = f"{question.strip()} {res}"
1006
- self.append_chat_history(hist, linebreak=True, blockquote=True)
1183
+ except asyncio.CancelledError:
1184
+ if not confirmation_future.done():
1185
+ confirmation_future.set_result(False)
1186
+ raise
1187
+ finally:
1188
+ if confirmation_future in self.outstanding_confirmations:
1189
+ self.outstanding_confirmations.remove(confirmation_future)
1007
1190
 
1008
- return is_yes
1191
+ return await confirmation_future
1009
1192
 
1010
1193
  @restore_multiline
1011
1194
  def prompt_ask(self, question, default="", subject=None):
@@ -1059,14 +1242,18 @@ class InputOutput:
1059
1242
  message = Text(message)
1060
1243
  color = ensure_hash_prefix(color) if color else None
1061
1244
  style = dict(style=color) if self.pretty and color else dict()
1245
+
1062
1246
  try:
1063
- self.console.print(message, **style)
1247
+ self.stream_print(message, **style)
1064
1248
  except UnicodeEncodeError:
1065
1249
  # Fallback to ASCII-safe output
1066
1250
  if isinstance(message, Text):
1067
1251
  message = message.plain
1068
1252
  message = str(message).encode("ascii", errors="replace").decode("ascii")
1069
- self.console.print(message, **style)
1253
+ self.stream_print(message, **style)
1254
+
1255
+ if self.prompt_session and self.prompt_session.app:
1256
+ self.prompt_session.app.invalidate()
1070
1257
 
1071
1258
  def tool_error(self, message="", strip=True):
1072
1259
  self.num_error_outputs += 1
@@ -1089,19 +1276,12 @@ class InputOutput:
1089
1276
  if self.pretty:
1090
1277
  if self.tool_output_color:
1091
1278
  style["color"] = ensure_hash_prefix(self.tool_output_color)
1092
- style["reverse"] = bold
1279
+ # if bold:
1280
+ # style["bold"] = True
1093
1281
 
1094
1282
  style = RichStyle(**style)
1095
- self.console.print(*messages, style=style)
1096
1283
 
1097
- def get_assistant_mdstream(self):
1098
- mdargs = dict(
1099
- style=self.assistant_output_color,
1100
- code_theme=self.code_theme,
1101
- inline_code_lexer="text",
1102
- )
1103
- mdStream = MarkdownStream(mdargs=mdargs)
1104
- return mdStream
1284
+ self.stream_print(*messages, style=style)
1105
1285
 
1106
1286
  def assistant_output(self, message, pretty=None):
1107
1287
  if not message:
@@ -1121,7 +1301,64 @@ class InputOutput:
1121
1301
  else:
1122
1302
  show_resp = Text(message or "(empty response)")
1123
1303
 
1124
- self.console.print(show_resp)
1304
+ self.stream_print(show_resp)
1305
+
1306
+ def render_markdown(self, text):
1307
+ output = StringIO()
1308
+ console = Console(file=output, force_terminal=True, color_system="truecolor")
1309
+ md = Markdown(text, style=self.assistant_output_color, code_theme=self.code_theme)
1310
+ console.print(md)
1311
+ return output.getvalue()
1312
+
1313
+ def stream_output(self, text, final=False):
1314
+ """
1315
+ Stream output using Rich console to respect pretty print settings.
1316
+ This preserves formatting, colors, and other Rich features during streaming.
1317
+ """
1318
+ # Initialize buffer if not exists
1319
+ if not hasattr(self, "_stream_buffer"):
1320
+ self._stream_buffer = ""
1321
+
1322
+ # Initialize buffer if not exists
1323
+ if not hasattr(self, "_stream_line_count"):
1324
+ self._stream_line_count = 0
1325
+
1326
+ self._stream_buffer += text
1327
+
1328
+ # Process the buffer to find complete lines
1329
+ lines = self._stream_buffer.split("\n")
1330
+ complete_lines = []
1331
+ incomplete_line = ""
1332
+ output = ""
1333
+
1334
+ if len(lines) > 1 or final:
1335
+ # All lines except the last one are complete
1336
+ complete_lines = lines[:-1] if not final else lines
1337
+ incomplete_line = lines[-1] if not final else ""
1338
+
1339
+ for complete_line in complete_lines:
1340
+ output += complete_line
1341
+ self._stream_line_count += 1
1342
+
1343
+ self._stream_buffer = incomplete_line
1344
+
1345
+ if not final:
1346
+ if len(lines) > 1:
1347
+ self.console.print(output)
1348
+ else:
1349
+ # Ensure any remaining buffered content is printed using the full response
1350
+ self.console.print(output)
1351
+ self.reset_streaming_response()
1352
+
1353
+ def reset_streaming_response(self):
1354
+ self._stream_buffer = ""
1355
+ self._stream_line_count = 0
1356
+
1357
+ def stream_print(self, *messages, **kwargs):
1358
+ with self.console.capture() as capture:
1359
+ self.console.print(*messages, **kwargs)
1360
+ capture_text = capture.get()
1361
+ self.stream_output(capture_text, final=False)
1125
1362
 
1126
1363
  def set_placeholder(self, placeholder):
1127
1364
  """Set a one-time placeholder text for the next input prompt."""