siada-cli 0.0.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.
Files changed (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
siada/io/io.py ADDED
@@ -0,0 +1,661 @@
1
+ import functools
2
+ import os
3
+ import re
4
+ import subprocess
5
+ import webbrowser
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ from prompt_toolkit.completion import Completer, ThreadedCompleter
10
+ from prompt_toolkit.cursor_shapes import ModalCursorShapeConfig
11
+ from prompt_toolkit.enums import EditingMode
12
+ from prompt_toolkit.lexers import Lexer
13
+ from prompt_toolkit.output.vt100 import is_dumb_terminal
14
+ from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
15
+ from prompt_toolkit.styles import Style
16
+ from rich.color import ColorParseError
17
+ from rich.console import Console
18
+ from rich.markdown import Markdown
19
+ from rich.style import Style as RichStyle
20
+ from rich.text import Text
21
+
22
+ from siada.io.components.mdstream import MarkdownRender
23
+ from siada.io.console_printer import ConsolePrinter
24
+ from siada.io.notification_command import NotificationCommandUtil
25
+ from .color_settings import ColorSettings, RunningConfigColorSettings
26
+ from .key_bindings import KeyBindingsFactory
27
+
28
+ # from .editor import pipe_editor
29
+
30
+ # Constants
31
+ NOTIFICATION_MESSAGE = "Siada is waiting for your input"
32
+
33
+
34
+ from siada.io.color_utils import ColorUtils
35
+
36
+
37
+ class AtFileReferenceLexer(Lexer):
38
+ """Custom lexer for highlighting @ file references"""
39
+
40
+ def __init__(self):
41
+ self.at_pattern = re.compile(r'@[^\s]+')
42
+
43
+ def lex_document(self, document):
44
+ def get_line(lineno):
45
+ if lineno >= len(document.lines):
46
+ return []
47
+
48
+ line = document.lines[lineno]
49
+ result = []
50
+ last_end = 0
51
+
52
+ # Find all @ commands in the line
53
+ for match in self.at_pattern.finditer(line):
54
+ start_pos = match.start()
55
+ end_pos = match.end()
56
+
57
+ # Add text before @ command with default style
58
+ if start_pos > last_end:
59
+ result.append(('', line[last_end:start_pos]))
60
+
61
+ # Add @ command with special style
62
+ at_command = match.group()
63
+ result.append(('class:at-file-reference', at_command))
64
+
65
+ last_end = end_pos
66
+
67
+ # Add remaining text after last @ command
68
+ if last_end < len(line):
69
+ result.append(('', line[last_end:]))
70
+
71
+ return result
72
+
73
+ return get_line
74
+
75
+
76
+ @dataclass
77
+ class ConfirmGroup:
78
+ preference: str = None
79
+ show_group: bool = True
80
+
81
+ def __init__(self, items=None):
82
+ if items is not None:
83
+ self.show_group = len(items) > 1
84
+
85
+ class InputOutput:
86
+ num_error_outputs = 0
87
+ num_user_asks = 0
88
+ clipboard_watcher = None
89
+ bell_on_next_input = False
90
+ notifications_command = None
91
+
92
+ @staticmethod
93
+ def _restore_multiline(func):
94
+ """Decorator to restore multiline mode after function execution"""
95
+
96
+ @functools.wraps(func)
97
+ def wrapper(self, *args, **kwargs):
98
+ orig_multiline = self.multiline_mode
99
+ self.multiline_mode = False
100
+ try:
101
+ return func(self, *args, **kwargs)
102
+ except Exception:
103
+ raise
104
+ finally:
105
+ self.multiline_mode = orig_multiline
106
+
107
+ return wrapper
108
+
109
+ def __init__(
110
+ self,
111
+ pretty=True,
112
+ yes=None,
113
+ input=None,
114
+ output=None,
115
+ running_color_settings: "RunningConfigColorSettings" = None,
116
+ encoding="utf-8",
117
+ line_endings="platform",
118
+ editingmode=EditingMode.EMACS,
119
+ fancy_input=True,
120
+ multiline_mode=False,
121
+ notifications=False,
122
+ notifications_command=None,
123
+ ):
124
+ self.placeholder = None
125
+ self.interrupted = False
126
+ self.never_prompts = set()
127
+ self.editingmode = editingmode
128
+ self.multiline_mode = multiline_mode
129
+ self.bell_on_next_input = False
130
+ self.notifications = notifications
131
+ if notifications and notifications_command is None:
132
+ self.notifications_command = (
133
+ NotificationCommandUtil.get_default_notification_command()
134
+ )
135
+ else:
136
+ self.notifications_command = notifications_command
137
+
138
+ no_color = os.environ.get("NO_COLOR")
139
+ if no_color is not None and no_color != "":
140
+ pretty = False
141
+
142
+ # Initialize running color settings or create default
143
+ if running_color_settings is None:
144
+ running_color_settings = RunningConfigColorSettings(pretty=pretty)
145
+
146
+ self.running_color_settings = running_color_settings
147
+
148
+ self.input = input
149
+ self.output = output
150
+
151
+ self.pretty = pretty
152
+ if self.output:
153
+ self.pretty = False
154
+
155
+ self.yes = yes
156
+
157
+ self.encoding = encoding
158
+ valid_line_endings = {"platform", "lf", "crlf"}
159
+ if line_endings not in valid_line_endings:
160
+ raise ValueError(
161
+ f"Invalid line_endings value: {line_endings}. "
162
+ f"Must be one of: {', '.join(valid_line_endings)}"
163
+ )
164
+ self.newline = (
165
+ None if line_endings == "platform" else "\n" if line_endings == "lf" else "\r\n"
166
+ )
167
+
168
+ self.prompt_session = None
169
+ self.is_dumb_terminal = is_dumb_terminal()
170
+
171
+ if self.is_dumb_terminal:
172
+ self.pretty = False
173
+ fancy_input = False
174
+
175
+ if fancy_input:
176
+ # Initialize PromptSession only if we have a capable terminal
177
+ session_kwargs = {
178
+ "input": self.input,
179
+ "output": self.output,
180
+ "lexer": None,
181
+ "editing_mode": self.editingmode,
182
+ }
183
+ if self.editingmode == EditingMode.VI:
184
+ session_kwargs["cursor"] = ModalCursorShapeConfig()
185
+ try:
186
+ self.prompt_session = PromptSession(**session_kwargs)
187
+ self.console = Console() # pretty console
188
+ self._initialize_printer()
189
+ except Exception as err:
190
+ self.console = Console(force_terminal=False, no_color=True)
191
+ self._initialize_printer()
192
+ self.print_error(f"Can't initialize prompt toolkit: {err}") # non-pretty
193
+ else:
194
+ self.console = Console(force_terminal=False, no_color=True) # non-pretty
195
+ self._initialize_printer()
196
+ if self.is_dumb_terminal:
197
+ self.print_info("Detected dumb terminal, disabling fancy input and pretty output.")
198
+
199
+ def _initialize_printer(self):
200
+ """Initialize the console printer."""
201
+ printer_colors = {
202
+ "error": self.running_color_settings.tool_error_color,
203
+ "warning": self.running_color_settings.tool_warning_color,
204
+ "output": self.running_color_settings.tool_output_color,
205
+ "result": self.running_color_settings.tool_result_color,
206
+ "call": self.running_color_settings.tool_call_color,
207
+ }
208
+ self.printer = ConsolePrinter(self.console, self.pretty, colors=printer_colors)
209
+
210
+ def _get_style(self, input_color=None):
211
+ style_dict = {}
212
+ if not self.pretty:
213
+ return Style.from_dict(style_dict)
214
+
215
+ user_input_color = input_color or self.running_color_settings.user_input_color
216
+ if user_input_color:
217
+ style_dict.setdefault("", user_input_color)
218
+ style_dict.update(
219
+ {
220
+ "pygments.literal.string": f"bold italic {user_input_color}",
221
+ }
222
+ )
223
+
224
+ # Conditionally add 'completion-menu' style
225
+ completion_menu_style = []
226
+ if self.running_color_settings.completion_menu_bg_color:
227
+ completion_menu_style.append(f"bg:{self.running_color_settings.completion_menu_bg_color}")
228
+ if self.running_color_settings.completion_menu_color:
229
+ completion_menu_style.append(self.running_color_settings.completion_menu_color)
230
+ if completion_menu_style:
231
+ style_dict["completion-menu"] = " ".join(completion_menu_style)
232
+
233
+ # Conditionally add 'completion-menu.completion.current' style
234
+ completion_menu_current_style = []
235
+ if self.running_color_settings.completion_menu_current_bg_color:
236
+ completion_menu_current_style.append(self.running_color_settings.completion_menu_current_bg_color)
237
+ if self.running_color_settings.completion_menu_current_color:
238
+ completion_menu_current_style.append(f"bg:{self.running_color_settings.completion_menu_current_color}")
239
+ if completion_menu_current_style:
240
+ style_dict["completion-menu.completion.current"] = " ".join(
241
+ completion_menu_current_style
242
+ )
243
+
244
+ # Add @ file reference style
245
+ if self.running_color_settings.at_file_reference_color:
246
+ style_dict["at-file-reference"] = self.running_color_settings.at_file_reference_color
247
+
248
+ return Style.from_dict(style_dict)
249
+
250
+ def rule(self, color=None):
251
+ if self.pretty:
252
+ style = (
253
+ dict(style=color)
254
+ if color
255
+ else (
256
+ dict(style=self.running_color_settings.user_input_color)
257
+ if self.running_color_settings.user_input_color
258
+ else dict()
259
+ )
260
+ )
261
+ self.console.rule(**style)
262
+ else:
263
+ print()
264
+
265
+ def interrupt_input(self):
266
+ if self.prompt_session and self.prompt_session.app:
267
+ # Store any partial input before interrupting
268
+ self.placeholder = self.prompt_session.app.current_buffer.text
269
+ self.interrupted = True
270
+ self.prompt_session.app.exit()
271
+
272
+ def get_input(
273
+ self,
274
+ completer: Optional[Completer] = None,
275
+ display_rule: bool = True,
276
+ color: str = None,
277
+ ):
278
+ if display_rule:
279
+ self.rule(color=color)
280
+
281
+ # Ring the bell if needed
282
+ self.ring_bell()
283
+
284
+ show = ""
285
+
286
+ prompt_prefix = ""
287
+ if self.multiline_mode:
288
+ prompt_prefix += "multi"
289
+ prompt_prefix += "> "
290
+
291
+ show += prompt_prefix
292
+ self.prompt_prefix = prompt_prefix
293
+
294
+ inp = ""
295
+ multiline_input = False
296
+
297
+ style = self._get_style(input_color=color)
298
+ completer_instance = ThreadedCompleter(completer=completer) if completer else None
299
+
300
+ kb_factory = KeyBindingsFactory(self)
301
+ kb = kb_factory.create_key_bindings()
302
+
303
+ while True:
304
+ if multiline_input:
305
+ show = self.prompt_prefix
306
+
307
+ try:
308
+ if self.prompt_session:
309
+ # Use placeholder if set, then clear it
310
+ default = self.placeholder or ""
311
+ self.placeholder = None
312
+
313
+ self.interrupted = False
314
+ if not multiline_input:
315
+ if self.clipboard_watcher:
316
+ self.clipboard_watcher.start()
317
+
318
+ def get_continuation(width, line_number, is_soft_wrap):
319
+ return self.prompt_prefix
320
+
321
+ # Build prompt parameters, explicitly set completer (including None case)
322
+ prompt_kwargs = {
323
+ "default": default,
324
+ "style": style,
325
+ "key_bindings": kb,
326
+ "prompt_continuation": get_continuation,
327
+ "completer": completer_instance, # Explicitly set, even if None
328
+ "complete_while_typing": bool(completer), # Only enable when completer exists
329
+ "lexer": AtFileReferenceLexer() if self.pretty else None, # Enable @ file reference highlighting
330
+ }
331
+
332
+ # Only add completion-related extra configurations when completer exists
333
+ if completer:
334
+ prompt_kwargs.update({
335
+ "reserve_space_for_menu": 8,
336
+ "complete_style": CompleteStyle.COLUMN,
337
+ })
338
+
339
+ line = self.prompt_session.prompt(show, **prompt_kwargs)
340
+ else:
341
+ line = input(show)
342
+
343
+ # Check if we were interrupted by a file change
344
+ if self.interrupted:
345
+ line = line or ""
346
+
347
+ except EOFError:
348
+ raise
349
+ except Exception as err:
350
+ import traceback
351
+
352
+ self.print_error(str(err))
353
+ self.print_error(traceback.format_exc())
354
+ return ""
355
+ except UnicodeEncodeError as err:
356
+ self.print_error(str(err))
357
+ return ""
358
+ finally:
359
+ if self.clipboard_watcher:
360
+ self.clipboard_watcher.stop()
361
+
362
+ if line.strip("\r\n") and not multiline_input:
363
+ stripped = line.strip("\r\n")
364
+ if stripped == "{":
365
+ multiline_input = True
366
+ multiline_tag = None
367
+ inp += ""
368
+ elif stripped[0] == "{":
369
+ # Extract tag if it exists (only alphanumeric chars)
370
+ tag = "".join(c for c in stripped[1:] if c.isalnum())
371
+ if stripped == "{" + tag:
372
+ multiline_input = True
373
+ multiline_tag = tag
374
+ inp += ""
375
+ else:
376
+ inp = line
377
+ break
378
+ else:
379
+ inp = line
380
+ break
381
+ continue
382
+ elif multiline_input and line.strip():
383
+ if multiline_tag:
384
+ # Check if line is exactly "tag}"
385
+ if line.strip("\r\n") == f"{multiline_tag}}}":
386
+ break
387
+ else:
388
+ inp += line + "\n"
389
+ # Check if line is exactly "}"
390
+ elif line.strip("\r\n") == "}":
391
+ break
392
+ else:
393
+ inp += line + "\n"
394
+ elif multiline_input:
395
+ inp += line + "\n"
396
+ else:
397
+ inp = line
398
+ break
399
+
400
+ print()
401
+ # self.display_user_input(inp)
402
+ return inp
403
+
404
+ def display_user_input(self, inp):
405
+ if self.pretty and self.running_color_settings.user_input_color:
406
+ style = dict(style=self.running_color_settings.user_input_color)
407
+ else:
408
+ style = dict()
409
+
410
+ self.console.print(Text(inp), **style)
411
+
412
+ def offer_url(self, url, prompt="Open URL for more info?", allow_never=True):
413
+ """Offer to open a URL in the browser, returns True if opened."""
414
+ if url in self.never_prompts:
415
+ return False
416
+ if self.confirm_ask(prompt, subject=url, allow_never=allow_never):
417
+ webbrowser.open(url)
418
+ return True
419
+ return False
420
+
421
+ @_restore_multiline
422
+ def confirm_ask(
423
+ self,
424
+ question,
425
+ default="y",
426
+ subject=None,
427
+ explicit_yes_required=False,
428
+ group=None,
429
+ allow_never=False,
430
+ ):
431
+ self.num_user_asks += 1
432
+
433
+ # Ring the bell if needed
434
+ self.ring_bell()
435
+
436
+ question_id = (question, subject)
437
+
438
+ if question_id in self.never_prompts:
439
+ return False
440
+
441
+ if group and not group.show_group:
442
+ group = None
443
+ if group:
444
+ allow_never = True
445
+
446
+ valid_responses = ["yes", "no", "skip", "all"]
447
+ options = " (Y)es/(N)o"
448
+ if group:
449
+ if not explicit_yes_required:
450
+ options += "/(A)ll"
451
+ options += "/(S)kip all"
452
+ if allow_never:
453
+ options += "/(D)on't ask again"
454
+ valid_responses.append("don't")
455
+
456
+ if default.lower().startswith("y"):
457
+ question += options + " [Yes]: "
458
+ elif default.lower().startswith("n"):
459
+ question += options + " [No]: "
460
+ else:
461
+ question += options + f" [{default}]: "
462
+
463
+ if subject:
464
+ self.print_info()
465
+ if "\n" in subject:
466
+ lines = subject.splitlines()
467
+ max_length = max(len(line) for line in lines)
468
+ padded_lines = [line.ljust(max_length) for line in lines]
469
+ padded_subject = "\n".join(padded_lines)
470
+ self.print_info(padded_subject, bold=True)
471
+ else:
472
+ self.print_info(subject, bold=True)
473
+
474
+ style = self._get_style()
475
+
476
+ def is_valid_response(text):
477
+ if not text:
478
+ return True
479
+ return text.lower() in valid_responses
480
+
481
+ if self.yes is True:
482
+ res = "n" if explicit_yes_required else "y"
483
+ elif self.yes is False:
484
+ res = "n"
485
+ elif group and group.preference:
486
+ res = group.preference
487
+ self.user_input(f"{question}{res}", log_only=False)
488
+ else:
489
+ while True:
490
+ try:
491
+ if self.prompt_session:
492
+ res = self.prompt_session.prompt(
493
+ question,
494
+ style=style,
495
+ complete_while_typing=False,
496
+ )
497
+ else:
498
+ res = input(question)
499
+ except EOFError:
500
+ # Treat EOF (Ctrl+D) as if the user pressed Enter
501
+ res = default
502
+ break
503
+
504
+ if not res:
505
+ res = default
506
+ break
507
+ res = res.lower()
508
+ good = any(valid_response.startswith(res) for valid_response in valid_responses)
509
+ if good:
510
+ break
511
+
512
+ error_message = f"Please answer with one of: {', '.join(valid_responses)}"
513
+ self.print_error(error_message)
514
+
515
+ res = res.lower()[0]
516
+
517
+ if res == "d" and allow_never:
518
+ self.never_prompts.add(question_id)
519
+ hist = f"{question.strip()} {res}"
520
+ return False
521
+
522
+ if explicit_yes_required:
523
+ is_yes = res == "y"
524
+ else:
525
+ is_yes = res in ("y", "a")
526
+
527
+ is_all = res == "a" and group is not None and not explicit_yes_required
528
+ is_skip = res == "s" and group is not None
529
+
530
+ if group:
531
+ if is_all and not explicit_yes_required:
532
+ group.preference = "all"
533
+ elif is_skip:
534
+ group.preference = "skip"
535
+
536
+ hist = f"{question.strip()} {res}"
537
+
538
+ return is_yes
539
+
540
+ @_restore_multiline
541
+ def prompt_ask(self, question, default="", subject=None):
542
+ self.num_user_asks += 1
543
+
544
+ # Ring the bell if needed
545
+ self.ring_bell()
546
+
547
+ if subject:
548
+ self.print_info()
549
+ self.print_info(subject, bold=True)
550
+
551
+ style = self._get_style()
552
+
553
+ if self.yes is True:
554
+ res = "yes"
555
+ elif self.yes is False:
556
+ res = "no"
557
+ else:
558
+ try:
559
+ if self.prompt_session:
560
+ res = self.prompt_session.prompt(
561
+ question + " ",
562
+ default=default,
563
+ style=style,
564
+ complete_while_typing=True,
565
+ )
566
+ else:
567
+ res = input(question + " ")
568
+ except EOFError:
569
+ # Treat EOF (Ctrl+D) as if the user pressed Enter
570
+ res = default
571
+
572
+ hist = f"{question.strip()} {res.strip()}"
573
+ if self.yes in (True, False):
574
+ self.print_info(hist)
575
+
576
+ return res
577
+
578
+ def print_error(self, message="", strip=True):
579
+ self.num_error_outputs += 1
580
+ self.printer.error(message)
581
+
582
+ def print_warning(self, message="", strip=True):
583
+ self.printer.warning(message)
584
+
585
+ def print_tool_result(self, message="", strip=True):
586
+ self.printer.result(message)
587
+
588
+ def print_tool_call(self, message="", strip=True):
589
+ self.printer.call(message)
590
+
591
+ def print_info(self, *messages, bold=False):
592
+ self.printer.output(*messages, bold=bold)
593
+
594
+ def get_assistant_mdstream(self):
595
+ mdargs = dict(
596
+ style=self.running_color_settings.assistant_output_color,
597
+ code_theme=self.running_color_settings.code_theme,
598
+ inline_code_lexer="text",
599
+ )
600
+ mdStream = MarkdownRender(mdargs=mdargs)
601
+ return mdStream
602
+
603
+ def assistant_output(self, message, pretty=None):
604
+ if not message:
605
+ self.print_warning("Empty response received from LLM. Check your provider account?")
606
+ return
607
+
608
+ show_resp = message
609
+
610
+ # Coder will force pretty off if fence is not triple-backticks
611
+ if pretty is None:
612
+ pretty = self.pretty
613
+
614
+ if pretty:
615
+ show_resp = Markdown(
616
+ message, style=self.running_color_settings.assistant_output_color, code_theme=self.running_color_settings.code_theme
617
+ )
618
+ else:
619
+ show_resp = Text(message or "(empty response)")
620
+
621
+ self.console.print(show_resp)
622
+
623
+ def set_placeholder(self, placeholder):
624
+ """Set a one-time placeholder text for the next input prompt."""
625
+ self.placeholder = placeholder
626
+
627
+ def print(self, message=""):
628
+ print(message)
629
+
630
+ def llm_started(self):
631
+ """Mark that the LLM has started processing, so we should ring the bell on next input"""
632
+ self.bell_on_next_input = True
633
+
634
+ def ring_bell(self):
635
+ """Ring the terminal bell if needed and clear the flag"""
636
+ if self.bell_on_next_input and self.notifications:
637
+ if self.notifications_command:
638
+ try:
639
+ result = subprocess.run(
640
+ self.notifications_command, shell=True, capture_output=True
641
+ )
642
+ if result.returncode != 0 and result.stderr:
643
+ error_msg = result.stderr.decode("utf-8", errors="replace")
644
+ self.print_warning(f"Failed to run notifications command: {error_msg}")
645
+ except Exception as e:
646
+ self.print_warning(f"Failed to run notifications command: {e}")
647
+ else:
648
+ print("\a", end="", flush=True) # Ring the bell
649
+ self.bell_on_next_input = False # Clear the flag
650
+
651
+ def toggle_multiline_mode(self):
652
+ """Toggle between normal and multiline input modes"""
653
+ self.multiline_mode = not self.multiline_mode
654
+ if self.multiline_mode:
655
+ self.print_info(
656
+ "Multiline mode: Enabled. Enter inserts newline, Alt-Enter submits text"
657
+ )
658
+ else:
659
+ self.print_info(
660
+ "Multiline mode: Disabled. Alt-Enter inserts newline, Enter submits text"
661
+ )