aider-ce 0.87.13.dev3__py3-none-any.whl → 0.88.1__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.
- aider/__init__.py +1 -1
- aider/_version.py +2 -2
- aider/args.py +6 -0
- aider/coders/architect_coder.py +3 -3
- aider/coders/base_coder.py +511 -190
- aider/coders/context_coder.py +1 -1
- aider/coders/editblock_func_coder.py +2 -2
- aider/coders/navigator_coder.py +451 -649
- aider/coders/navigator_legacy_prompts.py +49 -284
- aider/coders/navigator_prompts.py +46 -473
- aider/coders/search_replace.py +0 -0
- aider/coders/wholefile_func_coder.py +2 -2
- aider/commands.py +56 -44
- aider/exceptions.py +1 -0
- aider/history.py +14 -12
- aider/io.py +354 -117
- aider/llm.py +12 -4
- aider/main.py +32 -29
- aider/mcp/__init__.py +65 -2
- aider/mcp/server.py +37 -11
- aider/models.py +45 -20
- aider/onboarding.py +5 -5
- aider/repo.py +7 -7
- aider/resources/model-metadata.json +8 -8
- aider/scrape.py +2 -2
- aider/sendchat.py +185 -15
- aider/tools/__init__.py +44 -23
- aider/tools/command.py +18 -0
- aider/tools/command_interactive.py +18 -0
- aider/tools/delete_block.py +23 -0
- aider/tools/delete_line.py +19 -1
- aider/tools/delete_lines.py +20 -1
- aider/tools/extract_lines.py +25 -2
- aider/tools/git.py +142 -0
- aider/tools/grep.py +47 -2
- aider/tools/indent_lines.py +25 -0
- aider/tools/insert_block.py +26 -0
- aider/tools/list_changes.py +15 -0
- aider/tools/ls.py +24 -1
- aider/tools/make_editable.py +18 -0
- aider/tools/make_readonly.py +19 -0
- aider/tools/remove.py +22 -0
- aider/tools/replace_all.py +21 -0
- aider/tools/replace_line.py +20 -1
- aider/tools/replace_lines.py +21 -1
- aider/tools/replace_text.py +22 -0
- aider/tools/show_numbered_context.py +18 -0
- aider/tools/undo_change.py +15 -0
- aider/tools/update_todo_list.py +131 -0
- aider/tools/view.py +23 -0
- aider/tools/view_files_at_glob.py +32 -27
- aider/tools/view_files_matching.py +51 -37
- aider/tools/view_files_with_symbol.py +41 -54
- aider/tools/view_todo_list.py +57 -0
- aider/waiting.py +20 -203
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/METADATA +21 -5
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/RECORD +60 -57
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/WHEEL +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/entry_points.txt +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.dist-info}/licenses/LICENSE.txt +0 -0
- {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.1.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
|
-
|
|
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.
|
|
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
|
|
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
|
-
@
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
907
|
-
|
|
1046
|
+
confirmation_future = asyncio.get_running_loop().create_future()
|
|
1047
|
+
self.outstanding_confirmations.append(confirmation_future)
|
|
908
1048
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
options
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
if
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
997
|
-
|
|
1180
|
+
if not confirmation_future.done():
|
|
1181
|
+
confirmation_future.set_result(is_yes)
|
|
998
1182
|
|
|
999
|
-
|
|
1000
|
-
if
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1279
|
+
# if bold:
|
|
1280
|
+
# style["bold"] = True
|
|
1093
1281
|
|
|
1094
1282
|
style = RichStyle(**style)
|
|
1095
|
-
self.console.print(*messages, style=style)
|
|
1096
1283
|
|
|
1097
|
-
|
|
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.
|
|
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."""
|