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.
- agent_config.yaml +34 -0
- siada/__init__.py +13 -0
- siada/agent_hub/__init__.py +10 -0
- siada/agent_hub/coder/bug_fix_agent.py +398 -0
- siada/agent_hub/coder/code_gen_agent.py +235 -0
- siada/agent_hub/coder/fe_gen_agent.py +37 -0
- siada/agent_hub/coder/issue_review_agent.py +68 -0
- siada/agent_hub/coder/prompt/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
- siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
- siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
- siada/agent_hub/coder/prompt/base/rules.py +40 -0
- siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
- siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
- siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
- siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
- siada/agent_hub/coder/tracing/__init__.py +22 -0
- siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
- siada/agent_hub/hooks/__init__.py +0 -0
- siada/agent_hub/hooks/io_hooks.py +30 -0
- siada/agent_hub/siada_agent.py +214 -0
- siada/config/__init__.py +0 -0
- siada/config/config_loader.py +67 -0
- siada/entrypoint/__init__.py +3 -0
- siada/entrypoint/args_parser/__init__.py +0 -0
- siada/entrypoint/args_parser/args.py +195 -0
- siada/entrypoint/interaction/__init__.py +0 -0
- siada/entrypoint/interaction/base_turn.py +107 -0
- siada/entrypoint/interaction/config.py +30 -0
- siada/entrypoint/interaction/controller.py +122 -0
- siada/entrypoint/interaction/nointeractive_controller.py +27 -0
- siada/entrypoint/interaction/turn/interface.py +116 -0
- siada/entrypoint/interaction/turn/models.py +34 -0
- siada/entrypoint/interaction/turn/run_turn.py +641 -0
- siada/entrypoint/interaction/turn_factory.py +35 -0
- siada/entrypoint/interaction/turn_models.py +28 -0
- siada/entrypoint/siadahub.py +413 -0
- siada/foundation/__init__.py +0 -0
- siada/foundation/code_agent_context.py +48 -0
- siada/foundation/config.py +59 -0
- siada/foundation/constants.py +2 -0
- siada/foundation/context.py +76 -0
- siada/foundation/logging.py +341 -0
- siada/foundation/tools/__init__.py +0 -0
- siada/foundation/tools/get_git_diff.py +105 -0
- siada/io/__init__.py +0 -0
- siada/io/banner.py +148 -0
- siada/io/color_settings.py +207 -0
- siada/io/color_utils.py +16 -0
- siada/io/components/__init__.py +0 -0
- siada/io/components/mdstream.py +204 -0
- siada/io/console_printer.py +68 -0
- siada/io/io.py +661 -0
- siada/io/key_bindings.py +129 -0
- siada/io/notification_command.py +40 -0
- siada/io/test_banner.py +64 -0
- siada/models/__init__.py +0 -0
- siada/models/agent.py +47 -0
- siada/models/coder/__init__.py +0 -0
- siada/models/converter.py +27 -0
- siada/models/model_base_config.py +121 -0
- siada/models/model_run_config.py +153 -0
- siada/provider/__init__.py +0 -0
- siada/provider/client_factory.py +112 -0
- siada/provider/llm_client.py +11 -0
- siada/provider/openrouter/__init__.py +0 -0
- siada/provider/openrouter/coverter.py +17 -0
- siada/provider/openrouter/openroute_provider.py +33 -0
- siada/provider/provider_factory.py +53 -0
- siada/queries/tree-sitter-language-pack/README.md +7 -0
- siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- siada/queries/tree-sitter-languages/README.md +23 -0
- siada/queries/tree-sitter-languages/c-tags.scm +9 -0
- siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
- siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
- siada/queries/tree-sitter-languages/go-tags.scm +30 -0
- siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- siada/queries/tree-sitter-languages/java-tags.scm +20 -0
- siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-languages/php-tags.scm +26 -0
- siada/queries/tree-sitter-languages/python-tags.scm +12 -0
- siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
- siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
- siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
- siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- siada/services/__init__.py +30 -0
- siada/services/at_file_recommendation_design_document.md +1213 -0
- siada/services/code_context_manager.py +136 -0
- siada/services/enhanced_fix_result_check.py +546 -0
- siada/services/execution_trace_collector.py +207 -0
- siada/services/file_recommendation/__init__.py +136 -0
- siada/services/file_recommendation/core/__init__.py +17 -0
- siada/services/file_recommendation/core/completion_engine.py +351 -0
- siada/services/file_recommendation/core/config.py +62 -0
- siada/services/file_recommendation/core/file_discovery.py +353 -0
- siada/services/file_recommendation/core/suggestion.py +110 -0
- siada/services/file_recommendation/utils/__init__.py +22 -0
- siada/services/file_recommendation/utils/path_utils.py +138 -0
- siada/services/file_recommendation/utils/text_utils.py +253 -0
- siada/services/fix_result_check.py +377 -0
- siada/services/handle_at_command/README.md +247 -0
- siada/services/handle_at_command/__init__.py +115 -0
- siada/services/handle_at_command/exceptions.py +46 -0
- siada/services/handle_at_command/models.py +92 -0
- siada/services/handle_at_command/parser.py +189 -0
- siada/services/handle_at_command/processor.py +321 -0
- siada/services/handle_at_command/resolver.py +318 -0
- siada/services/handle_at_command/utils.py +345 -0
- siada/services/model_info_service.py +103 -0
- siada/services/siada_runner.py +170 -0
- siada/session/__init__.py +29 -0
- siada/session/session_manager.py +66 -0
- siada/session/session_models.py +43 -0
- siada/support/__init__.py +0 -0
- siada/support/completer.py +204 -0
- siada/support/editor.py +146 -0
- siada/support/envprocessor.py +54 -0
- siada/support/repo.py +51 -0
- siada/support/slash_commands.py +410 -0
- siada/support/spinner.py +320 -0
- siada/tools/__init__.py +0 -0
- siada/tools/ast/README.md +345 -0
- siada/tools/ast/ast_tool.py +341 -0
- siada/tools/ast/models.py +11 -0
- siada/tools/coder/__init__.py +0 -0
- siada/tools/coder/ask_followup_question.py +70 -0
- siada/tools/coder/cmd_runner.py +132 -0
- siada/tools/coder/file_operator.py +284 -0
- siada/tools/coder/file_search/README.md +223 -0
- siada/tools/coder/file_search/__init__.py +10 -0
- siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
- siada/tools/coder/file_search/bin/rg.exe +0 -0
- siada/tools/coder/file_search/search.py +437 -0
- siada/tools/coder/file_search/setup_package.py +150 -0
- siada/tools/coder/file_search/test_search.py +142 -0
- siada/tools/coder/files.py +147 -0
- siada/tools/coder/fix_attempt_completion.py +58 -0
- siada/tools/coder/issue_review_completion.py +29 -0
- siada/tools/coder/observation/__init__.py +0 -0
- siada/tools/coder/observation/error.py +22 -0
- siada/tools/coder/observation/file_observation.py +216 -0
- siada/tools/coder/observation/observation.py +71 -0
- siada/tools/coder/repo_map/__init__.py +157 -0
- siada/tools/coder/repo_map/dump.py +29 -0
- siada/tools/coder/repo_map/io.py +226 -0
- siada/tools/coder/repo_map/repo_map.py +881 -0
- siada/tools/coder/repo_map/special.py +203 -0
- siada/tools/coder/repo_map/token_counter.py +154 -0
- siada/tools/coder/repo_map/waiting.py +221 -0
- siada/tools/coder/reproduce_completion.py +39 -0
- siada/tools/coder/run_cmd.py +60 -0
- siada/tools/coder/test_completion.py +25 -0
- siada/tools/coder/tool_docs.py +44 -0
- siada/tools/compression_tool.py +200 -0
- siada/tools/read_many_files/README.md +220 -0
- siada/tools/read_many_files/__init__.py +25 -0
- siada/tools/read_many_files/file_processor.py +486 -0
- siada/tools/read_many_files/filters.py +326 -0
- siada/tools/read_many_files/formatters.py +204 -0
- siada/tools/read_many_files/models.py +189 -0
- siada/tools/read_many_files_tool.py +233 -0
- siada/tools/tool_call_format/__init__.py +55 -0
- siada/tools/tool_call_format/file_to_language.py +231 -0
- siada/tools/tool_call_format/formatter_factory.py +80 -0
- siada/tools/tool_call_format/formatters.py +282 -0
- siada/tools/tool_call_format/tool_call_formatter.py +54 -0
- siada/tools/web/__init__.py +0 -0
- siada/tools/web/web_crawl.py +208 -0
- siada/utils.py +194 -0
- siada_cli-0.0.1.dist-info/LICENSE +201 -0
- siada_cli-0.0.1.dist-info/METADATA +457 -0
- siada_cli-0.0.1.dist-info/RECORD +215 -0
- siada_cli-0.0.1.dist-info/WHEEL +4 -0
- 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
|
+
)
|