jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +1143 -245
- jarvis/jarvis_agent/agent_manager.py +97 -0
- jarvis/jarvis_agent/builtin_input_handler.py +12 -10
- jarvis/jarvis_agent/config_editor.py +57 -0
- jarvis/jarvis_agent/edit_file_handler.py +392 -99
- jarvis/jarvis_agent/event_bus.py +48 -0
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/file_methodology_manager.py +117 -0
- jarvis/jarvis_agent/jarvis.py +1117 -147
- jarvis/jarvis_agent/main.py +78 -34
- jarvis/jarvis_agent/memory_manager.py +195 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompt_manager.py +82 -0
- jarvis/jarvis_agent/prompts.py +46 -9
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +146 -0
- jarvis/jarvis_agent/session_manager.py +9 -9
- jarvis/jarvis_agent/share_manager.py +228 -0
- jarvis/jarvis_agent/shell_input_handler.py +23 -3
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +212 -0
- jarvis/jarvis_agent/task_manager.py +154 -0
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/tool_executor.py +8 -4
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_agent/user_interaction.py +42 -0
- jarvis/jarvis_agent/utils.py +54 -0
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1605 -178
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +275 -13
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
- jarvis/jarvis_code_analysis/code_review.py +583 -548
- jarvis/jarvis_data/config_schema.json +339 -28
- jarvis/jarvis_git_squash/main.py +22 -13
- jarvis/jarvis_git_utils/git_commiter.py +171 -55
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
- jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
- jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
- jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
- jarvis/jarvis_methodology/main.py +48 -63
- jarvis/jarvis_multi_agent/__init__.py +302 -43
- jarvis/jarvis_multi_agent/main.py +70 -24
- jarvis/jarvis_platform/ai8.py +40 -23
- jarvis/jarvis_platform/base.py +210 -49
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +82 -76
- jarvis/jarvis_platform/openai.py +73 -1
- jarvis/jarvis_platform/registry.py +8 -15
- jarvis/jarvis_platform/tongyi.py +115 -101
- jarvis/jarvis_platform/yuanbao.py +89 -63
- jarvis/jarvis_platform_manager/main.py +194 -132
- jarvis/jarvis_platform_manager/service.py +122 -86
- jarvis/jarvis_rag/cli.py +156 -53
- jarvis/jarvis_rag/embedding_manager.py +155 -12
- jarvis/jarvis_rag/llm_interface.py +10 -13
- jarvis/jarvis_rag/query_rewriter.py +63 -12
- jarvis/jarvis_rag/rag_pipeline.py +222 -40
- jarvis/jarvis_rag/reranker.py +26 -3
- jarvis/jarvis_rag/retriever.py +270 -14
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_smart_shell/main.py +405 -137
- jarvis/jarvis_stats/__init__.py +13 -0
- jarvis/jarvis_stats/cli.py +387 -0
- jarvis/jarvis_stats/stats.py +711 -0
- jarvis/jarvis_stats/storage.py +612 -0
- jarvis/jarvis_stats/visualizer.py +282 -0
- jarvis/jarvis_tools/ask_user.py +1 -0
- jarvis/jarvis_tools/base.py +18 -2
- jarvis/jarvis_tools/clear_memory.py +239 -0
- jarvis/jarvis_tools/cli/main.py +220 -144
- jarvis/jarvis_tools/execute_script.py +52 -12
- jarvis/jarvis_tools/file_analyzer.py +17 -12
- jarvis/jarvis_tools/generate_new_tool.py +46 -24
- jarvis/jarvis_tools/read_code.py +277 -18
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +86 -13
- jarvis/jarvis_tools/registry.py +294 -90
- jarvis/jarvis_tools/retrieve_memory.py +227 -0
- jarvis/jarvis_tools/save_memory.py +194 -0
- jarvis/jarvis_tools/search_web.py +62 -28
- jarvis/jarvis_tools/sub_agent.py +205 -0
- jarvis/jarvis_tools/sub_code_agent.py +217 -0
- jarvis/jarvis_tools/virtual_tty.py +330 -62
- jarvis/jarvis_utils/builtin_replace_map.py +4 -5
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +607 -50
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/fzf.py +57 -0
- jarvis/jarvis_utils/git_utils.py +251 -29
- jarvis/jarvis_utils/globals.py +174 -17
- jarvis/jarvis_utils/http.py +58 -79
- jarvis/jarvis_utils/input.py +899 -153
- jarvis/jarvis_utils/methodology.py +210 -83
- jarvis/jarvis_utils/output.py +220 -137
- jarvis/jarvis_utils/utils.py +1906 -135
- jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
- jarvis/jarvis_git_details/main.py +0 -265
- jarvis/jarvis_platform/oyi.py +0 -357
- jarvis/jarvis_tools/edit_file.py +0 -255
- jarvis/jarvis_tools/rewrite_file.py +0 -195
- jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
- jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
- /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
jarvis/jarvis_utils/input.py
CHANGED
|
@@ -8,99 +8,270 @@
|
|
|
8
8
|
- 带有模糊匹配的文件路径补全
|
|
9
9
|
- 用于输入控制的自定义键绑定
|
|
10
10
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import base64
|
|
14
|
+
from typing import Iterable, List, Optional
|
|
15
|
+
import wcwidth
|
|
16
|
+
|
|
17
|
+
from colorama import Fore
|
|
18
|
+
from colorama import Style as ColoramaStyle
|
|
19
|
+
from fuzzywuzzy import process
|
|
20
|
+
from prompt_toolkit import PromptSession
|
|
21
|
+
from prompt_toolkit.application import Application, run_in_terminal
|
|
22
|
+
from prompt_toolkit.completion import CompleteEvent
|
|
15
23
|
from prompt_toolkit.completion import (
|
|
16
|
-
CompleteEvent,
|
|
17
24
|
Completer,
|
|
18
|
-
Completion,
|
|
25
|
+
Completion,
|
|
19
26
|
PathCompleter,
|
|
20
|
-
)
|
|
21
|
-
from prompt_toolkit.document import Document
|
|
22
|
-
from prompt_toolkit.formatted_text import FormattedText
|
|
23
|
-
from prompt_toolkit.
|
|
24
|
-
from prompt_toolkit.
|
|
27
|
+
)
|
|
28
|
+
from prompt_toolkit.document import Document
|
|
29
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
30
|
+
from prompt_toolkit.history import FileHistory
|
|
31
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
32
|
+
from prompt_toolkit.enums import DEFAULT_BUFFER
|
|
33
|
+
from prompt_toolkit.filters import has_focus
|
|
34
|
+
from prompt_toolkit.layout.containers import Window
|
|
35
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
36
|
+
from prompt_toolkit.layout.layout import Layout
|
|
37
|
+
from prompt_toolkit.styles import Style as PromptStyle
|
|
25
38
|
|
|
26
|
-
from jarvis.jarvis_utils.
|
|
39
|
+
from jarvis.jarvis_utils.clipboard import copy_to_clipboard
|
|
40
|
+
from jarvis.jarvis_utils.config import get_data_dir, get_replace_map
|
|
41
|
+
from jarvis.jarvis_utils.globals import get_message_history
|
|
27
42
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
28
43
|
from jarvis.jarvis_utils.tag import ot
|
|
29
|
-
|
|
44
|
+
|
|
45
|
+
# Sentinel value to indicate that Ctrl+O was pressed
|
|
46
|
+
CTRL_O_SENTINEL = "__CTRL_O_PRESSED__"
|
|
47
|
+
# Sentinel prefix to indicate that Ctrl+F (fzf) inserted content should prefill next prompt
|
|
48
|
+
FZF_INSERT_SENTINEL_PREFIX = "__FZF_INSERT__::"
|
|
49
|
+
# Sentinel to request running fzf outside the prompt and then prefill next prompt
|
|
50
|
+
FZF_REQUEST_SENTINEL_PREFIX = "__FZF_REQUEST__::"
|
|
51
|
+
# Sentinel to request running fzf outside the prompt for all-files mode (exclude .git)
|
|
52
|
+
FZF_REQUEST_ALL_SENTINEL_PREFIX = "__FZF_REQUEST_ALL__::"
|
|
53
|
+
|
|
54
|
+
# Persistent hint marker for multiline input (shown only once across runs)
|
|
55
|
+
_MULTILINE_HINT_MARK_FILE = os.path.join(get_data_dir(), "multiline_enter_hint_shown")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _display_width(s: str) -> int:
|
|
59
|
+
"""Calculate printable width of a string in terminal columns (handles wide chars)."""
|
|
60
|
+
try:
|
|
61
|
+
w = 0
|
|
62
|
+
for ch in s:
|
|
63
|
+
cw = wcwidth.wcwidth(ch)
|
|
64
|
+
if cw is None or cw < 0:
|
|
65
|
+
# Fallback for unknown width chars (e.g. emoji on some terminals)
|
|
66
|
+
cw = 1
|
|
67
|
+
w += cw
|
|
68
|
+
return w
|
|
69
|
+
except Exception:
|
|
70
|
+
return len(s)
|
|
30
71
|
|
|
31
72
|
|
|
32
|
-
def
|
|
73
|
+
def _calc_prompt_rows(prev_text: str) -> int:
|
|
33
74
|
"""
|
|
34
|
-
|
|
75
|
+
Estimate how many terminal rows the previous prompt occupied.
|
|
76
|
+
Considers prompt prefix and soft-wrapping across terminal columns.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
cols = os.get_terminal_size().columns
|
|
80
|
+
except Exception:
|
|
81
|
+
cols = 80
|
|
82
|
+
prefix = "👤 > "
|
|
83
|
+
prefix_w = _display_width(prefix)
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
85
|
+
if prev_text is None:
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
lines = prev_text.splitlines()
|
|
89
|
+
if not lines:
|
|
90
|
+
lines = [""]
|
|
91
|
+
# If the text ends with a newline, there is a visible empty line at the end.
|
|
92
|
+
if prev_text.endswith("\n"):
|
|
93
|
+
lines.append("")
|
|
94
|
+
total_rows = 0
|
|
95
|
+
for i, line in enumerate(lines):
|
|
96
|
+
lw = _display_width(line)
|
|
97
|
+
if i == 0:
|
|
98
|
+
width = prefix_w + lw
|
|
99
|
+
else:
|
|
100
|
+
width = lw
|
|
101
|
+
rows = max(1, (width + cols - 1) // cols)
|
|
102
|
+
total_rows += rows
|
|
103
|
+
return max(1, total_rows)
|
|
38
104
|
|
|
39
|
-
|
|
40
|
-
|
|
105
|
+
|
|
106
|
+
def _multiline_hint_already_shown() -> bool:
|
|
107
|
+
"""Check if the multiline Enter hint has been shown before (persisted)."""
|
|
108
|
+
try:
|
|
109
|
+
return os.path.exists(_MULTILINE_HINT_MARK_FILE)
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _mark_multiline_hint_shown() -> None:
|
|
115
|
+
"""Persist that the multiline Enter hint has been shown."""
|
|
116
|
+
try:
|
|
117
|
+
os.makedirs(os.path.dirname(_MULTILINE_HINT_MARK_FILE), exist_ok=True)
|
|
118
|
+
with open(_MULTILINE_HINT_MARK_FILE, "w", encoding="utf-8") as f:
|
|
119
|
+
f.write("1")
|
|
120
|
+
except Exception:
|
|
121
|
+
# Non-critical persistence failure; ignore to avoid breaking input flow
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_single_line_input(tip: str, default: str = "") -> str:
|
|
126
|
+
"""
|
|
127
|
+
获取支持历史记录的单行输入。
|
|
41
128
|
"""
|
|
42
129
|
session: PromptSession = PromptSession(history=None)
|
|
130
|
+
style = PromptStyle.from_dict(
|
|
131
|
+
{"prompt": "ansicyan", "bottom-toolbar": "fg:#888888"}
|
|
132
|
+
)
|
|
133
|
+
prompt = FormattedText([("class:prompt", f"👤 > {tip}")])
|
|
134
|
+
return session.prompt(prompt, default=default, style=style)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_choice(tip: str, choices: List[str]) -> str:
|
|
138
|
+
"""
|
|
139
|
+
提供一个可滚动的选择列表供用户选择。
|
|
140
|
+
"""
|
|
141
|
+
if not choices:
|
|
142
|
+
raise ValueError("Choices cannot be empty.")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
terminal_height = os.get_terminal_size().lines
|
|
146
|
+
except OSError:
|
|
147
|
+
terminal_height = 25 # 如果无法确定终端大小,则使用默认高度
|
|
148
|
+
|
|
149
|
+
# 为提示和缓冲区保留行
|
|
150
|
+
max_visible_choices = max(5, terminal_height - 4)
|
|
151
|
+
|
|
152
|
+
bindings = KeyBindings()
|
|
153
|
+
selected_index = 0
|
|
154
|
+
start_index = 0
|
|
155
|
+
|
|
156
|
+
@bindings.add("up")
|
|
157
|
+
def _(event):
|
|
158
|
+
nonlocal selected_index, start_index
|
|
159
|
+
selected_index = (selected_index - 1 + len(choices)) % len(choices)
|
|
160
|
+
if selected_index < start_index:
|
|
161
|
+
start_index = selected_index
|
|
162
|
+
elif selected_index == len(choices) - 1: # 支持从第一项上翻到最后一项时滚动
|
|
163
|
+
start_index = max(0, len(choices) - max_visible_choices)
|
|
164
|
+
event.app.invalidate()
|
|
165
|
+
|
|
166
|
+
@bindings.add("down")
|
|
167
|
+
def _(event):
|
|
168
|
+
nonlocal selected_index, start_index
|
|
169
|
+
selected_index = (selected_index + 1) % len(choices)
|
|
170
|
+
if selected_index >= start_index + max_visible_choices:
|
|
171
|
+
start_index = selected_index - max_visible_choices + 1
|
|
172
|
+
elif selected_index == 0: # 支持从最后一项下翻到第一项时滚动
|
|
173
|
+
start_index = 0
|
|
174
|
+
event.app.invalidate()
|
|
175
|
+
|
|
176
|
+
@bindings.add("enter")
|
|
177
|
+
def _(event):
|
|
178
|
+
event.app.exit(result=choices[selected_index])
|
|
179
|
+
|
|
180
|
+
def get_prompt_tokens():
|
|
181
|
+
tokens = [("class:question", f"{tip} (使用上下箭头选择, Enter确认)\n")]
|
|
182
|
+
|
|
183
|
+
end_index = min(start_index + max_visible_choices, len(choices))
|
|
184
|
+
visible_choices_slice = choices[start_index:end_index]
|
|
185
|
+
|
|
186
|
+
if start_index > 0:
|
|
187
|
+
tokens.append(("class:indicator", " ... (更多选项在上方) ...\n"))
|
|
188
|
+
|
|
189
|
+
for i, choice in enumerate(visible_choices_slice, start=start_index):
|
|
190
|
+
if i == selected_index:
|
|
191
|
+
tokens.append(("class:selected", f"> {choice}\n"))
|
|
192
|
+
else:
|
|
193
|
+
tokens.append(("", f" {choice}\n"))
|
|
194
|
+
|
|
195
|
+
if end_index < len(choices):
|
|
196
|
+
tokens.append(("class:indicator", " ... (更多选项在下方) ...\n"))
|
|
197
|
+
|
|
198
|
+
return FormattedText(tokens)
|
|
199
|
+
|
|
43
200
|
style = PromptStyle.from_dict(
|
|
44
201
|
{
|
|
45
|
-
"
|
|
202
|
+
"question": "bold",
|
|
203
|
+
"selected": "bg:#696969 #ffffff",
|
|
204
|
+
"indicator": "fg:gray",
|
|
46
205
|
}
|
|
47
206
|
)
|
|
48
|
-
|
|
207
|
+
|
|
208
|
+
layout = Layout(
|
|
209
|
+
container=Window(
|
|
210
|
+
content=FormattedTextControl(
|
|
211
|
+
text=get_prompt_tokens,
|
|
212
|
+
focusable=True,
|
|
213
|
+
key_bindings=bindings,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
app: Application = Application(
|
|
219
|
+
layout=layout,
|
|
220
|
+
key_bindings=bindings,
|
|
221
|
+
style=style,
|
|
222
|
+
mouse_support=True,
|
|
223
|
+
full_screen=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
result = app.run()
|
|
228
|
+
return result if result is not None else ""
|
|
229
|
+
except (KeyboardInterrupt, EOFError):
|
|
230
|
+
return ""
|
|
49
231
|
|
|
50
232
|
|
|
51
233
|
class FileCompleter(Completer):
|
|
52
234
|
"""
|
|
53
235
|
带有模糊匹配的文件路径自定义补全器。
|
|
54
|
-
|
|
55
|
-
属性:
|
|
56
|
-
path_completer: 基础路径补全器
|
|
57
|
-
max_suggestions: 显示的最大建议数量
|
|
58
|
-
min_score: 建议的最小匹配分数
|
|
59
236
|
"""
|
|
60
237
|
|
|
61
238
|
def __init__(self):
|
|
62
|
-
"""使用默认设置初始化文件补全器。"""
|
|
63
239
|
self.path_completer = PathCompleter()
|
|
64
240
|
self.max_suggestions = 10
|
|
65
241
|
self.min_score = 10
|
|
66
242
|
self.replace_map = get_replace_map()
|
|
243
|
+
# Caches for file lists to avoid repeated expensive scans
|
|
244
|
+
self._git_files_cache = None
|
|
245
|
+
self._all_files_cache = None
|
|
246
|
+
self._max_walk_files = 10000
|
|
67
247
|
|
|
68
|
-
def get_completions(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
参数:
|
|
73
|
-
document: 当前正在编辑的文档
|
|
74
|
-
complete_event: 补全事件
|
|
75
|
-
|
|
76
|
-
生成:
|
|
77
|
-
Completion: 建议的补全项
|
|
78
|
-
"""
|
|
248
|
+
def get_completions(
|
|
249
|
+
self, document: Document, _: CompleteEvent
|
|
250
|
+
) -> Iterable[Completion]:
|
|
79
251
|
text = document.text_before_cursor
|
|
80
252
|
cursor_pos = document.cursor_position
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if
|
|
253
|
+
|
|
254
|
+
# Support both '@' (git files) and '#' (all files excluding .git)
|
|
255
|
+
sym_positions = [(i, ch) for i, ch in enumerate(text) if ch in ("@", "#")]
|
|
256
|
+
if not sym_positions:
|
|
84
257
|
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
258
|
+
current_pos = None
|
|
259
|
+
current_sym = None
|
|
260
|
+
for i, ch in sym_positions:
|
|
261
|
+
if i < cursor_pos:
|
|
262
|
+
current_pos = i
|
|
263
|
+
current_sym = ch
|
|
264
|
+
if current_pos is None:
|
|
89
265
|
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if " " in
|
|
266
|
+
|
|
267
|
+
text_after = text[current_pos + 1 : cursor_pos]
|
|
268
|
+
if " " in text_after:
|
|
93
269
|
return
|
|
94
270
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# 计算替换长度
|
|
98
|
-
replace_length = len(text_after_at) + 1
|
|
271
|
+
token = text_after.strip()
|
|
272
|
+
replace_length = len(text_after) + 1
|
|
99
273
|
|
|
100
|
-
# 获取所有可能的补全项
|
|
101
274
|
all_completions = []
|
|
102
|
-
|
|
103
|
-
# 1. 添加特殊标记
|
|
104
275
|
all_completions.extend(
|
|
105
276
|
[(ot(tag), self._get_description(tag)) for tag in self.replace_map.keys()]
|
|
106
277
|
)
|
|
@@ -114,63 +285,75 @@ class FileCompleter(Completer):
|
|
|
114
285
|
]
|
|
115
286
|
)
|
|
116
287
|
|
|
117
|
-
#
|
|
288
|
+
# File path candidates
|
|
118
289
|
try:
|
|
119
|
-
|
|
290
|
+
if current_sym == "@":
|
|
291
|
+
import subprocess
|
|
120
292
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
293
|
+
if self._git_files_cache is None:
|
|
294
|
+
result = subprocess.run(
|
|
295
|
+
["git", "ls-files"],
|
|
296
|
+
stdout=subprocess.PIPE,
|
|
297
|
+
stderr=subprocess.PIPE,
|
|
298
|
+
text=True,
|
|
299
|
+
)
|
|
300
|
+
if result.returncode == 0:
|
|
301
|
+
self._git_files_cache = [
|
|
302
|
+
p for p in result.stdout.splitlines() if p.strip()
|
|
303
|
+
]
|
|
304
|
+
else:
|
|
305
|
+
self._git_files_cache = []
|
|
306
|
+
paths = self._git_files_cache or []
|
|
307
|
+
else:
|
|
308
|
+
import os as _os
|
|
309
|
+
|
|
310
|
+
if self._all_files_cache is None:
|
|
311
|
+
files: List[str] = []
|
|
312
|
+
for root, dirs, fnames in _os.walk(".", followlinks=False):
|
|
313
|
+
# Exclude .git directory
|
|
314
|
+
dirs[:] = [d for d in dirs if d != ".git"]
|
|
315
|
+
for name in fnames:
|
|
316
|
+
files.append(
|
|
317
|
+
_os.path.relpath(_os.path.join(root, name), ".")
|
|
318
|
+
)
|
|
319
|
+
if len(files) > self._max_walk_files:
|
|
320
|
+
break
|
|
321
|
+
if len(files) > self._max_walk_files:
|
|
322
|
+
break
|
|
323
|
+
self._all_files_cache = files
|
|
324
|
+
paths = self._all_files_cache or []
|
|
325
|
+
all_completions.extend([(path, "File") for path in paths])
|
|
135
326
|
except Exception:
|
|
136
327
|
pass
|
|
137
328
|
|
|
138
|
-
|
|
139
|
-
if file_path:
|
|
140
|
-
# 使用模糊匹配过滤
|
|
329
|
+
if token:
|
|
141
330
|
scored_items = process.extract(
|
|
142
|
-
|
|
331
|
+
token,
|
|
143
332
|
[item[0] for item in all_completions],
|
|
144
333
|
limit=self.max_suggestions,
|
|
145
334
|
)
|
|
146
335
|
scored_items = [
|
|
147
336
|
(item[0], item[1]) for item in scored_items if item[1] > self.min_score
|
|
148
337
|
]
|
|
149
|
-
# 创建映射以便查找描述
|
|
150
338
|
completion_map = {item[0]: item[1] for item in all_completions}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
display_text = text
|
|
154
|
-
if score < 100:
|
|
155
|
-
display_text = f"{text} ({score}%)"
|
|
339
|
+
for t, score in scored_items:
|
|
340
|
+
display_text = f"{t} ({score}%)" if score < 100 else t
|
|
156
341
|
yield Completion(
|
|
157
|
-
text=f"'{
|
|
342
|
+
text=f"'{t}'",
|
|
158
343
|
start_position=-replace_length,
|
|
159
344
|
display=display_text,
|
|
160
|
-
display_meta=completion_map.get(
|
|
161
|
-
)
|
|
345
|
+
display_meta=completion_map.get(t, ""),
|
|
346
|
+
)
|
|
162
347
|
else:
|
|
163
|
-
|
|
164
|
-
for text, desc in all_completions[: self.max_suggestions]:
|
|
348
|
+
for t, desc in all_completions[: self.max_suggestions]:
|
|
165
349
|
yield Completion(
|
|
166
|
-
text=f"'{
|
|
350
|
+
text=f"'{t}'",
|
|
167
351
|
start_position=-replace_length,
|
|
168
|
-
display=
|
|
352
|
+
display=t,
|
|
169
353
|
display_meta=desc,
|
|
170
|
-
)
|
|
354
|
+
)
|
|
171
355
|
|
|
172
356
|
def _get_description(self, tag: str) -> str:
|
|
173
|
-
"""获取标记的描述信息"""
|
|
174
357
|
if tag in self.replace_map:
|
|
175
358
|
return (
|
|
176
359
|
self.replace_map[tag].get("description", tag) + "(Append)"
|
|
@@ -180,17 +363,51 @@ class FileCompleter(Completer):
|
|
|
180
363
|
return tag
|
|
181
364
|
|
|
182
365
|
|
|
183
|
-
|
|
184
|
-
|
|
366
|
+
# ---------------------
|
|
367
|
+
# 公共判定辅助函数(按当前Agent优先)
|
|
368
|
+
# ---------------------
|
|
369
|
+
def _get_current_agent_for_input():
|
|
370
|
+
try:
|
|
371
|
+
import jarvis.jarvis_utils.globals as g
|
|
372
|
+
current_name = getattr(g, "current_agent_name", "")
|
|
373
|
+
if current_name:
|
|
374
|
+
return g.get_agent(current_name)
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
return None
|
|
185
378
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
379
|
+
def _is_non_interactive_for_current_agent() -> bool:
|
|
380
|
+
try:
|
|
381
|
+
from jarvis.jarvis_utils.config import is_non_interactive
|
|
382
|
+
ag = _get_current_agent_for_input()
|
|
383
|
+
try:
|
|
384
|
+
return bool(getattr(ag, "non_interactive", False)) if ag else bool(is_non_interactive())
|
|
385
|
+
except Exception:
|
|
386
|
+
return bool(is_non_interactive())
|
|
387
|
+
except Exception:
|
|
388
|
+
return False
|
|
189
389
|
|
|
190
|
-
|
|
191
|
-
bool: 用户确认返回True,否则返回False
|
|
192
|
-
"""
|
|
390
|
+
def _is_auto_complete_for_current_agent() -> bool:
|
|
193
391
|
try:
|
|
392
|
+
from jarvis.jarvis_utils.config import GLOBAL_CONFIG_DATA
|
|
393
|
+
ag = _get_current_agent_for_input()
|
|
394
|
+
if ag is not None and hasattr(ag, "auto_complete"):
|
|
395
|
+
try:
|
|
396
|
+
return bool(getattr(ag, "auto_complete", False))
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
env_v = os.getenv("JARVIS_AUTO_COMPLETE")
|
|
400
|
+
if env_v is not None:
|
|
401
|
+
return str(env_v).strip().lower() in ("1", "true", "yes", "on")
|
|
402
|
+
return bool(GLOBAL_CONFIG_DATA.get("JARVIS_AUTO_COMPLETE", False))
|
|
403
|
+
except Exception:
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def user_confirm(tip: str, default: bool = True) -> bool:
|
|
407
|
+
"""提示用户确认是/否问题(按当前Agent优先判断非交互)"""
|
|
408
|
+
try:
|
|
409
|
+
if _is_non_interactive_for_current_agent():
|
|
410
|
+
return default
|
|
194
411
|
suffix = "[Y/n]" if default else "[y/N]"
|
|
195
412
|
ret = get_single_line_input(f"{tip} {suffix}: ")
|
|
196
413
|
return default if ret == "" else ret.lower() == "y"
|
|
@@ -198,86 +415,615 @@ def user_confirm(tip: str, default: bool = True) -> bool:
|
|
|
198
415
|
return False
|
|
199
416
|
|
|
200
417
|
|
|
201
|
-
def
|
|
418
|
+
def _show_history_and_copy():
|
|
202
419
|
"""
|
|
203
|
-
|
|
420
|
+
Displays message history and handles copying to clipboard.
|
|
421
|
+
This function uses standard I/O and is safe to call outside a prompt session.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
history = get_message_history()
|
|
425
|
+
if not history:
|
|
426
|
+
PrettyOutput.print("没有可复制的消息", OutputType.INFO)
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# 为避免 PrettyOutput 在循环中为每行加框,先拼接后统一打印
|
|
430
|
+
lines = []
|
|
431
|
+
lines.append("\n" + "=" * 20 + " 消息历史记录 " + "=" * 20)
|
|
432
|
+
for i, msg in enumerate(history):
|
|
433
|
+
cleaned_msg = msg.replace("\n", r"\n")
|
|
434
|
+
display_msg = (
|
|
435
|
+
(cleaned_msg[:70] + "...") if len(cleaned_msg) > 70 else cleaned_msg
|
|
436
|
+
)
|
|
437
|
+
lines.append(f" {i + 1}: {display_msg.strip()}")
|
|
438
|
+
lines.append("=" * 58 + "\n")
|
|
439
|
+
PrettyOutput.print("\n".join(lines), OutputType.INFO)
|
|
440
|
+
|
|
441
|
+
while True:
|
|
442
|
+
try:
|
|
443
|
+
prompt_text = f"{Fore.CYAN}请输入要复制的条目序号 (或输入c取消, 直接回车选择最后一条): {ColoramaStyle.RESET_ALL}"
|
|
444
|
+
choice_str = input(prompt_text)
|
|
445
|
+
|
|
446
|
+
if not choice_str: # User pressed Enter
|
|
447
|
+
if not history:
|
|
448
|
+
PrettyOutput.print("没有历史记录可供选择。", OutputType.INFO)
|
|
449
|
+
break
|
|
450
|
+
choice = len(history) - 1
|
|
451
|
+
elif choice_str.lower() == "c":
|
|
452
|
+
PrettyOutput.print("已取消", OutputType.INFO)
|
|
453
|
+
break
|
|
454
|
+
else:
|
|
455
|
+
choice = int(choice_str) - 1
|
|
456
|
+
|
|
457
|
+
if 0 <= choice < len(history):
|
|
458
|
+
selected_msg = history[choice]
|
|
459
|
+
copy_to_clipboard(selected_msg)
|
|
460
|
+
PrettyOutput.print(
|
|
461
|
+
f"已复制消息: {selected_msg[:70]}...", OutputType.SUCCESS
|
|
462
|
+
)
|
|
463
|
+
break
|
|
464
|
+
else:
|
|
465
|
+
PrettyOutput.print("无效的序号,请重试。", OutputType.WARNING)
|
|
466
|
+
except ValueError:
|
|
467
|
+
PrettyOutput.print("无效的输入,请输入数字。", OutputType.WARNING)
|
|
468
|
+
except (KeyboardInterrupt, EOFError):
|
|
469
|
+
PrettyOutput.print("\n操作取消", OutputType.INFO)
|
|
470
|
+
break
|
|
204
471
|
|
|
205
|
-
参数:
|
|
206
|
-
tip: 要显示的提示信息
|
|
207
472
|
|
|
208
|
-
|
|
209
|
-
|
|
473
|
+
def _get_multiline_input_internal(
|
|
474
|
+
tip: str, preset: Optional[str] = None, preset_cursor: Optional[int] = None
|
|
475
|
+
) -> str:
|
|
476
|
+
"""
|
|
477
|
+
Internal function to get multiline input using prompt_toolkit.
|
|
478
|
+
Returns a sentinel value if Ctrl+O is pressed.
|
|
210
479
|
"""
|
|
211
|
-
# 显示输入说明
|
|
212
|
-
PrettyOutput.section(
|
|
213
|
-
"用户输入 - 使用 @ 触发文件补全,Tab 选择补全项,Ctrl+J 提交,Ctrl+O 复制最后一条消息,按 Ctrl+C 取消输入",
|
|
214
|
-
OutputType.USER,
|
|
215
|
-
)
|
|
216
|
-
print(f"{Fore.GREEN}{tip}{ColoramaStyle.RESET_ALL}")
|
|
217
|
-
# 配置键绑定
|
|
218
480
|
bindings = KeyBindings()
|
|
219
481
|
|
|
482
|
+
# Show a one-time hint on the first Enter press in this invocation (disabled; using inlay toolbar instead)
|
|
483
|
+
first_enter_hint_shown = True
|
|
484
|
+
|
|
220
485
|
@bindings.add("enter")
|
|
221
486
|
def _(event):
|
|
222
|
-
|
|
487
|
+
nonlocal first_enter_hint_shown
|
|
488
|
+
if not first_enter_hint_shown and not _multiline_hint_already_shown():
|
|
489
|
+
first_enter_hint_shown = True
|
|
490
|
+
|
|
491
|
+
def _show_notice():
|
|
492
|
+
PrettyOutput.print(
|
|
493
|
+
"提示:当前支持多行输入。输入完成请使用 Ctrl+J 确认;Enter 仅用于换行。",
|
|
494
|
+
OutputType.INFO,
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
input("按回车继续...")
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
# Persist the hint so it won't be shown again in future runs
|
|
501
|
+
try:
|
|
502
|
+
_mark_multiline_hint_shown()
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
run_in_terminal(_show_notice)
|
|
507
|
+
return
|
|
508
|
+
|
|
223
509
|
if event.current_buffer.complete_state:
|
|
224
|
-
event.current_buffer.
|
|
225
|
-
|
|
226
|
-
|
|
510
|
+
completion = event.current_buffer.complete_state.current_completion
|
|
511
|
+
if completion:
|
|
512
|
+
event.current_buffer.apply_completion(completion)
|
|
513
|
+
else:
|
|
514
|
+
event.current_buffer.insert_text("\n")
|
|
227
515
|
else:
|
|
228
516
|
event.current_buffer.insert_text("\n")
|
|
229
517
|
|
|
230
|
-
@bindings.add("c-j")
|
|
518
|
+
@bindings.add("c-j", filter=has_focus(DEFAULT_BUFFER))
|
|
231
519
|
def _(event):
|
|
232
|
-
"""处理Ctrl+J以提交输入。"""
|
|
233
520
|
event.current_buffer.validate_and_handle()
|
|
234
521
|
|
|
235
|
-
@bindings.add("c-o")
|
|
522
|
+
@bindings.add("c-o", filter=has_focus(DEFAULT_BUFFER))
|
|
236
523
|
def _(event):
|
|
237
|
-
"""
|
|
238
|
-
|
|
524
|
+
"""Handle Ctrl+O by exiting the prompt and returning the sentinel value."""
|
|
525
|
+
event.app.exit(result=CTRL_O_SENTINEL)
|
|
239
526
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
copy_to_clipboard(last_msg)
|
|
244
|
-
else:
|
|
245
|
-
PrettyOutput.print("没有可复制的消息", OutputType.INFO)
|
|
527
|
+
@bindings.add("c-t", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
|
528
|
+
def _(event):
|
|
529
|
+
"""Return a shell command like '!bash' for upper input_handler to execute."""
|
|
246
530
|
|
|
247
|
-
|
|
531
|
+
def _gen_shell_cmd() -> str: # type: ignore
|
|
532
|
+
try:
|
|
533
|
+
import os
|
|
534
|
+
import shutil
|
|
535
|
+
|
|
536
|
+
if os.name == "nt":
|
|
537
|
+
# Prefer PowerShell if available, otherwise fallback to cmd
|
|
538
|
+
for name in ("pwsh", "powershell", "cmd"):
|
|
539
|
+
if name == "cmd" or shutil.which(name):
|
|
540
|
+
if name == "cmd":
|
|
541
|
+
# Keep session open with /K and set env for the spawned shell
|
|
542
|
+
return "!cmd /K set JARVIS_TERMINAL=1"
|
|
543
|
+
else:
|
|
544
|
+
# PowerShell or pwsh: set env then remain in session
|
|
545
|
+
return f"!{name} -NoExit -Command \"$env:JARVIS_TERMINAL='1'\""
|
|
546
|
+
else:
|
|
547
|
+
shell_path = os.environ.get("SHELL", "")
|
|
548
|
+
if shell_path:
|
|
549
|
+
base = os.path.basename(shell_path)
|
|
550
|
+
if base:
|
|
551
|
+
return f"!env JARVIS_TERMINAL=1 {base}"
|
|
552
|
+
for name in ("fish", "zsh", "bash", "sh"):
|
|
553
|
+
if shutil.which(name):
|
|
554
|
+
return f"!env JARVIS_TERMINAL=1 {name}"
|
|
555
|
+
return "!env JARVIS_TERMINAL=1 bash"
|
|
556
|
+
except Exception:
|
|
557
|
+
return "!env JARVIS_TERMINAL=1 bash"
|
|
558
|
+
|
|
559
|
+
# Append a special marker to indicate no-confirm execution in shell_input_handler
|
|
560
|
+
event.app.exit(result=_gen_shell_cmd() + " # JARVIS-NOCONFIRM")
|
|
561
|
+
|
|
562
|
+
@bindings.add("@", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
|
563
|
+
def _(event):
|
|
564
|
+
"""
|
|
565
|
+
使用 @ 触发 fzf(当 fzf 存在);否则仅插入 @ 以启用内置补全
|
|
566
|
+
逻辑:
|
|
567
|
+
- 若检测到系统存在 fzf,则先插入 '@',随后请求外层运行 fzf 并在返回后进行替换/插入
|
|
568
|
+
- 若不存在 fzf 或发生异常,则直接插入 '@'
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
import shutil
|
|
572
|
+
|
|
573
|
+
buf = event.current_buffer
|
|
574
|
+
if shutil.which("fzf") is None:
|
|
575
|
+
buf.insert_text("@")
|
|
576
|
+
return
|
|
577
|
+
# 先插入 '@',以便外层根据最后一个 '@' 进行片段替换
|
|
578
|
+
buf.insert_text("@")
|
|
579
|
+
doc = buf.document
|
|
580
|
+
text = doc.text
|
|
581
|
+
cursor = doc.cursor_position
|
|
582
|
+
payload = (
|
|
583
|
+
f"{cursor}:{base64.b64encode(text.encode('utf-8')).decode('ascii')}"
|
|
584
|
+
)
|
|
585
|
+
event.app.exit(result=FZF_REQUEST_SENTINEL_PREFIX + payload)
|
|
586
|
+
return
|
|
587
|
+
except Exception:
|
|
588
|
+
try:
|
|
589
|
+
event.current_buffer.insert_text("@")
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
@bindings.add("#", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
|
594
|
+
def _(event):
|
|
595
|
+
"""
|
|
596
|
+
使用 # 触发 fzf(当 fzf 存在),以“全量文件模式”进行选择(排除 .git);否则仅插入 # 启用内置补全
|
|
597
|
+
"""
|
|
598
|
+
try:
|
|
599
|
+
import shutil
|
|
600
|
+
|
|
601
|
+
buf = event.current_buffer
|
|
602
|
+
if shutil.which("fzf") is None:
|
|
603
|
+
buf.insert_text("#")
|
|
604
|
+
return
|
|
605
|
+
# 先插入 '#'
|
|
606
|
+
buf.insert_text("#")
|
|
607
|
+
doc = buf.document
|
|
608
|
+
text = doc.text
|
|
609
|
+
cursor = doc.cursor_position
|
|
610
|
+
payload = (
|
|
611
|
+
f"{cursor}:{base64.b64encode(text.encode('utf-8')).decode('ascii')}"
|
|
612
|
+
)
|
|
613
|
+
event.app.exit(result=FZF_REQUEST_ALL_SENTINEL_PREFIX + payload)
|
|
614
|
+
return
|
|
615
|
+
except Exception:
|
|
616
|
+
try:
|
|
617
|
+
event.current_buffer.insert_text("#")
|
|
618
|
+
except Exception:
|
|
619
|
+
pass
|
|
248
620
|
|
|
249
|
-
# 配置提示会话
|
|
250
621
|
style = PromptStyle.from_dict(
|
|
251
622
|
{
|
|
252
|
-
"prompt": "
|
|
623
|
+
"prompt": "ansibrightmagenta bold",
|
|
624
|
+
"bottom-toolbar": "bg:#4b145b #ffd6ff bold",
|
|
625
|
+
"bt.tip": "bold fg:#ff5f87",
|
|
626
|
+
"bt.sep": "fg:#ffb3de",
|
|
627
|
+
"bt.key": "bg:#d7005f #ffffff bold",
|
|
628
|
+
"bt.label": "fg:#ffd6ff",
|
|
253
629
|
}
|
|
254
630
|
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
631
|
+
|
|
632
|
+
def _bottom_toolbar():
|
|
633
|
+
return FormattedText(
|
|
634
|
+
[
|
|
635
|
+
("class:bt.tip", f" {tip} "),
|
|
636
|
+
("class:bt.sep", " • "),
|
|
637
|
+
("class:bt.label", "快捷键: "),
|
|
638
|
+
("class:bt.key", "@"),
|
|
639
|
+
("class:bt.label", " 文件补全 "),
|
|
640
|
+
("class:bt.sep", " • "),
|
|
641
|
+
("class:bt.key", "Tab"),
|
|
642
|
+
("class:bt.label", " 选择 "),
|
|
643
|
+
("class:bt.sep", " • "),
|
|
644
|
+
("class:bt.key", "Ctrl+J"),
|
|
645
|
+
("class:bt.label", " 确认 "),
|
|
646
|
+
("class:bt.sep", " • "),
|
|
647
|
+
("class:bt.key", "Ctrl+O"),
|
|
648
|
+
("class:bt.label", " 历史复制 "),
|
|
649
|
+
("class:bt.sep", " • "),
|
|
650
|
+
("class:bt.key", "Ctrl+T"),
|
|
651
|
+
("class:bt.label", " 终端(!SHELL) "),
|
|
652
|
+
("class:bt.sep", " • "),
|
|
653
|
+
("class:bt.key", "Ctrl+C/D"),
|
|
654
|
+
("class:bt.label", " 取消 "),
|
|
655
|
+
]
|
|
273
656
|
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
657
|
+
|
|
658
|
+
history_dir = get_data_dir()
|
|
659
|
+
session: PromptSession = PromptSession(
|
|
660
|
+
history=FileHistory(os.path.join(history_dir, "multiline_input_history")),
|
|
661
|
+
completer=FileCompleter(),
|
|
662
|
+
key_bindings=bindings,
|
|
663
|
+
complete_while_typing=True,
|
|
664
|
+
multiline=True,
|
|
665
|
+
vi_mode=False,
|
|
666
|
+
mouse_support=False,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
# Tip is shown in bottom toolbar; avoid extra print
|
|
670
|
+
prompt = FormattedText([("class:prompt", "👤 > ")])
|
|
671
|
+
|
|
672
|
+
def _pre_run():
|
|
673
|
+
try:
|
|
674
|
+
from prompt_toolkit.application.current import get_app as _ga
|
|
675
|
+
|
|
676
|
+
app = _ga()
|
|
677
|
+
buf = app.current_buffer
|
|
678
|
+
if preset is not None and preset_cursor is not None:
|
|
679
|
+
cp = max(0, min(len(buf.text), preset_cursor))
|
|
680
|
+
buf.cursor_position = cp
|
|
681
|
+
except Exception:
|
|
682
|
+
pass
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
return session.prompt(
|
|
277
686
|
prompt,
|
|
278
687
|
style=style,
|
|
688
|
+
pre_run=_pre_run,
|
|
689
|
+
bottom_toolbar=_bottom_toolbar,
|
|
690
|
+
default=(preset or ""),
|
|
279
691
|
).strip()
|
|
280
|
-
|
|
281
|
-
except KeyboardInterrupt:
|
|
282
|
-
PrettyOutput.print("输入已取消", OutputType.INFO)
|
|
692
|
+
except (KeyboardInterrupt, EOFError):
|
|
283
693
|
return ""
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
697
|
+
"""
|
|
698
|
+
获取带有增强补全和确认功能的多行输入。
|
|
699
|
+
此函数处理控制流,允许在不破坏终端状态的情况下处理历史记录复制。
|
|
700
|
+
|
|
701
|
+
参数:
|
|
702
|
+
tip: 提示文本,将显示在底部工具栏中
|
|
703
|
+
print_on_empty: 当输入为空字符串时,是否打印“输入已取消”提示。默认打印。
|
|
704
|
+
"""
|
|
705
|
+
preset: Optional[str] = None
|
|
706
|
+
preset_cursor: Optional[int] = None
|
|
707
|
+
while True:
|
|
708
|
+
# 基于“当前Agent”精确判断非交互与自动完成,避免多Agent相互干扰
|
|
709
|
+
if _is_non_interactive_for_current_agent():
|
|
710
|
+
# 在多Agent系统中,无论是否启用自动完成,均提示可用智能体并建议使用 SEND_MESSAGE 转移控制权
|
|
711
|
+
hint = ""
|
|
712
|
+
try:
|
|
713
|
+
ag = _get_current_agent_for_input()
|
|
714
|
+
ohs = getattr(ag, "output_handler", [])
|
|
715
|
+
available_agents: List[str] = []
|
|
716
|
+
for oh in (ohs or []):
|
|
717
|
+
cfgs = getattr(oh, "agents_config", None)
|
|
718
|
+
if isinstance(cfgs, list):
|
|
719
|
+
for c in cfgs:
|
|
720
|
+
try:
|
|
721
|
+
name = c.get("name")
|
|
722
|
+
except Exception:
|
|
723
|
+
name = None
|
|
724
|
+
if isinstance(name, str) and name.strip():
|
|
725
|
+
available_agents.append(name.strip())
|
|
726
|
+
if available_agents:
|
|
727
|
+
# 去重但保留顺序
|
|
728
|
+
seen = set()
|
|
729
|
+
ordered = []
|
|
730
|
+
for n in available_agents:
|
|
731
|
+
if n not in seen:
|
|
732
|
+
seen.add(n)
|
|
733
|
+
ordered.append(n)
|
|
734
|
+
hint = "\n当前可用智能体: " + ", ".join(ordered) + f"\n如需将任务交给其他智能体,请使用 {ot('SEND_MESSAGE')} 块。"
|
|
735
|
+
except Exception:
|
|
736
|
+
hint = ""
|
|
737
|
+
if _is_auto_complete_for_current_agent():
|
|
738
|
+
base_msg = "我无法与你交互,所有的事情你都自我决策,如果无法决策,就完成任务。输出" + ot("!!!COMPLETE!!!")
|
|
739
|
+
return base_msg + hint
|
|
740
|
+
else:
|
|
741
|
+
return "我无法与你交互,所有的事情你都自我决策" + hint
|
|
742
|
+
user_input = _get_multiline_input_internal(
|
|
743
|
+
tip, preset=preset, preset_cursor=preset_cursor
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
if user_input == CTRL_O_SENTINEL:
|
|
747
|
+
_show_history_and_copy()
|
|
748
|
+
tip = "请继续输入(或按Ctrl+J确认):"
|
|
749
|
+
continue
|
|
750
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
|
751
|
+
FZF_REQUEST_SENTINEL_PREFIX
|
|
752
|
+
):
|
|
753
|
+
# Handle fzf request outside the prompt, then prefill new text.
|
|
754
|
+
try:
|
|
755
|
+
payload = user_input[len(FZF_REQUEST_SENTINEL_PREFIX) :]
|
|
756
|
+
sep_index = payload.find(":")
|
|
757
|
+
cursor = int(payload[:sep_index])
|
|
758
|
+
text = base64.b64decode(
|
|
759
|
+
payload[sep_index + 1 :].encode("ascii")
|
|
760
|
+
).decode("utf-8")
|
|
761
|
+
except Exception:
|
|
762
|
+
# Malformed payload; just continue without change.
|
|
763
|
+
preset = None
|
|
764
|
+
tip = "FZF 预填失败,继续输入:"
|
|
765
|
+
continue
|
|
766
|
+
|
|
767
|
+
# Run fzf to get a file selection synchronously (outside prompt)
|
|
768
|
+
selected_path = ""
|
|
769
|
+
try:
|
|
770
|
+
import shutil
|
|
771
|
+
import subprocess
|
|
772
|
+
|
|
773
|
+
if shutil.which("fzf") is None:
|
|
774
|
+
PrettyOutput.print(
|
|
775
|
+
"未检测到 fzf,无法打开文件选择器。", OutputType.WARNING
|
|
776
|
+
)
|
|
777
|
+
else:
|
|
778
|
+
files = []
|
|
779
|
+
try:
|
|
780
|
+
r = subprocess.run(
|
|
781
|
+
["git", "ls-files"],
|
|
782
|
+
stdout=subprocess.PIPE,
|
|
783
|
+
stderr=subprocess.PIPE,
|
|
784
|
+
text=True,
|
|
785
|
+
)
|
|
786
|
+
if r.returncode == 0:
|
|
787
|
+
files = [
|
|
788
|
+
line for line in r.stdout.splitlines() if line.strip()
|
|
789
|
+
]
|
|
790
|
+
except Exception:
|
|
791
|
+
files = []
|
|
792
|
+
|
|
793
|
+
if not files:
|
|
794
|
+
import os as _os
|
|
795
|
+
|
|
796
|
+
for root, _, fnames in _os.walk(".", followlinks=False):
|
|
797
|
+
for name in fnames:
|
|
798
|
+
files.append(
|
|
799
|
+
_os.path.relpath(_os.path.join(root, name), ".")
|
|
800
|
+
)
|
|
801
|
+
if len(files) > 10000:
|
|
802
|
+
break
|
|
803
|
+
|
|
804
|
+
if not files:
|
|
805
|
+
PrettyOutput.print("未找到可选择的文件。", OutputType.INFO)
|
|
806
|
+
else:
|
|
807
|
+
try:
|
|
808
|
+
specials = [
|
|
809
|
+
ot("Summary"),
|
|
810
|
+
ot("Clear"),
|
|
811
|
+
ot("ToolUsage"),
|
|
812
|
+
ot("ReloadConfig"),
|
|
813
|
+
ot("SaveSession"),
|
|
814
|
+
]
|
|
815
|
+
except Exception:
|
|
816
|
+
specials = []
|
|
817
|
+
try:
|
|
818
|
+
replace_map = get_replace_map()
|
|
819
|
+
builtin_tags = [
|
|
820
|
+
ot(tag)
|
|
821
|
+
for tag in replace_map.keys()
|
|
822
|
+
if isinstance(tag, str) and tag.strip()
|
|
823
|
+
]
|
|
824
|
+
except Exception:
|
|
825
|
+
builtin_tags = []
|
|
826
|
+
items = (
|
|
827
|
+
[s for s in specials if isinstance(s, str) and s.strip()]
|
|
828
|
+
+ builtin_tags
|
|
829
|
+
+ files
|
|
830
|
+
)
|
|
831
|
+
proc = subprocess.run(
|
|
832
|
+
[
|
|
833
|
+
"fzf",
|
|
834
|
+
"--prompt",
|
|
835
|
+
"Files> ",
|
|
836
|
+
"--height",
|
|
837
|
+
"40%",
|
|
838
|
+
"--border",
|
|
839
|
+
],
|
|
840
|
+
input="\n".join(items),
|
|
841
|
+
stdout=subprocess.PIPE,
|
|
842
|
+
stderr=subprocess.PIPE,
|
|
843
|
+
text=True,
|
|
844
|
+
)
|
|
845
|
+
sel = proc.stdout.strip()
|
|
846
|
+
if sel:
|
|
847
|
+
selected_path = sel
|
|
848
|
+
except Exception as e:
|
|
849
|
+
PrettyOutput.print(f"FZF 执行失败: {e}", OutputType.ERROR)
|
|
850
|
+
|
|
851
|
+
# Compute new text based on selection (or keep original if none)
|
|
852
|
+
if selected_path:
|
|
853
|
+
text_before = text[:cursor]
|
|
854
|
+
last_at = text_before.rfind("@")
|
|
855
|
+
if last_at != -1 and " " not in text_before[last_at + 1 :]:
|
|
856
|
+
# Replace @... segment
|
|
857
|
+
inserted = f"'{selected_path}'"
|
|
858
|
+
new_text = text[:last_at] + inserted + text[cursor:]
|
|
859
|
+
new_cursor = last_at + len(inserted)
|
|
860
|
+
else:
|
|
861
|
+
# Plain insert
|
|
862
|
+
inserted = f"'{selected_path}'"
|
|
863
|
+
new_text = text[:cursor] + inserted + text[cursor:]
|
|
864
|
+
new_cursor = cursor + len(inserted)
|
|
865
|
+
preset = new_text
|
|
866
|
+
preset_cursor = new_cursor
|
|
867
|
+
tip = "已插入文件,继续编辑或按Ctrl+J确认:"
|
|
868
|
+
else:
|
|
869
|
+
# No selection; keep original text and cursor
|
|
870
|
+
preset = text
|
|
871
|
+
preset_cursor = cursor
|
|
872
|
+
tip = "未选择文件或已取消,继续编辑:"
|
|
873
|
+
# 清除上一条输入行(多行安全),避免多清,保守仅按提示行估算
|
|
874
|
+
try:
|
|
875
|
+
rows_total = _calc_prompt_rows(text)
|
|
876
|
+
for _ in range(rows_total):
|
|
877
|
+
sys.stdout.write("\x1b[1A") # 光标上移一行
|
|
878
|
+
sys.stdout.write("\x1b[2K\r") # 清除整行
|
|
879
|
+
sys.stdout.flush()
|
|
880
|
+
except Exception:
|
|
881
|
+
pass
|
|
882
|
+
continue
|
|
883
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
|
884
|
+
FZF_REQUEST_ALL_SENTINEL_PREFIX
|
|
885
|
+
):
|
|
886
|
+
# Handle fzf request (all-files mode, excluding .git) outside the prompt, then prefill new text.
|
|
887
|
+
try:
|
|
888
|
+
payload = user_input[len(FZF_REQUEST_ALL_SENTINEL_PREFIX) :]
|
|
889
|
+
sep_index = payload.find(":")
|
|
890
|
+
cursor = int(payload[:sep_index])
|
|
891
|
+
text = base64.b64decode(
|
|
892
|
+
payload[sep_index + 1 :].encode("ascii")
|
|
893
|
+
).decode("utf-8")
|
|
894
|
+
except Exception:
|
|
895
|
+
# Malformed payload; just continue without change.
|
|
896
|
+
preset = None
|
|
897
|
+
tip = "FZF 预填失败,继续输入:"
|
|
898
|
+
continue
|
|
899
|
+
|
|
900
|
+
# Run fzf to get a file selection synchronously (outside prompt) with all files (exclude .git)
|
|
901
|
+
selected_path = ""
|
|
902
|
+
try:
|
|
903
|
+
import shutil
|
|
904
|
+
import subprocess
|
|
905
|
+
|
|
906
|
+
if shutil.which("fzf") is None:
|
|
907
|
+
PrettyOutput.print(
|
|
908
|
+
"未检测到 fzf,无法打开文件选择器。", OutputType.WARNING
|
|
909
|
+
)
|
|
910
|
+
else:
|
|
911
|
+
files = []
|
|
912
|
+
try:
|
|
913
|
+
import os as _os
|
|
914
|
+
|
|
915
|
+
for root, dirs, fnames in _os.walk(".", followlinks=False):
|
|
916
|
+
# Exclude .git directories
|
|
917
|
+
dirs[:] = [d for d in dirs if d != ".git"]
|
|
918
|
+
for name in fnames:
|
|
919
|
+
files.append(
|
|
920
|
+
_os.path.relpath(_os.path.join(root, name), ".")
|
|
921
|
+
)
|
|
922
|
+
if len(files) > 10000:
|
|
923
|
+
break
|
|
924
|
+
if len(files) > 10000:
|
|
925
|
+
break
|
|
926
|
+
except Exception:
|
|
927
|
+
files = []
|
|
928
|
+
|
|
929
|
+
if not files:
|
|
930
|
+
PrettyOutput.print("未找到可选择的文件。", OutputType.INFO)
|
|
931
|
+
else:
|
|
932
|
+
try:
|
|
933
|
+
specials = [
|
|
934
|
+
ot("Summary"),
|
|
935
|
+
ot("Clear"),
|
|
936
|
+
ot("ToolUsage"),
|
|
937
|
+
ot("ReloadConfig"),
|
|
938
|
+
ot("SaveSession"),
|
|
939
|
+
]
|
|
940
|
+
except Exception:
|
|
941
|
+
specials = []
|
|
942
|
+
try:
|
|
943
|
+
replace_map = get_replace_map()
|
|
944
|
+
builtin_tags = [
|
|
945
|
+
ot(tag)
|
|
946
|
+
for tag in replace_map.keys()
|
|
947
|
+
if isinstance(tag, str) and tag.strip()
|
|
948
|
+
]
|
|
949
|
+
except Exception:
|
|
950
|
+
builtin_tags = []
|
|
951
|
+
items = (
|
|
952
|
+
[s for s in specials if isinstance(s, str) and s.strip()]
|
|
953
|
+
+ builtin_tags
|
|
954
|
+
+ files
|
|
955
|
+
)
|
|
956
|
+
proc = subprocess.run(
|
|
957
|
+
[
|
|
958
|
+
"fzf",
|
|
959
|
+
"--prompt",
|
|
960
|
+
"Files(all)> ",
|
|
961
|
+
"--height",
|
|
962
|
+
"40%",
|
|
963
|
+
"--border",
|
|
964
|
+
],
|
|
965
|
+
input="\n".join(items),
|
|
966
|
+
stdout=subprocess.PIPE,
|
|
967
|
+
stderr=subprocess.PIPE,
|
|
968
|
+
text=True,
|
|
969
|
+
)
|
|
970
|
+
sel = proc.stdout.strip()
|
|
971
|
+
if sel:
|
|
972
|
+
selected_path = sel
|
|
973
|
+
except Exception as e:
|
|
974
|
+
PrettyOutput.print(f"FZF 执行失败: {e}", OutputType.ERROR)
|
|
975
|
+
|
|
976
|
+
# Compute new text based on selection (or keep original if none)
|
|
977
|
+
if selected_path:
|
|
978
|
+
text_before = text[:cursor]
|
|
979
|
+
last_hash = text_before.rfind("#")
|
|
980
|
+
if last_hash != -1 and " " not in text_before[last_hash + 1 :]:
|
|
981
|
+
# Replace #... segment
|
|
982
|
+
inserted = f"'{selected_path}'"
|
|
983
|
+
new_text = text[:last_hash] + inserted + text[cursor:]
|
|
984
|
+
new_cursor = last_hash + len(inserted)
|
|
985
|
+
else:
|
|
986
|
+
# Plain insert
|
|
987
|
+
inserted = f"'{selected_path}'"
|
|
988
|
+
new_text = text[:cursor] + inserted + text[cursor:]
|
|
989
|
+
new_cursor = cursor + len(inserted)
|
|
990
|
+
preset = new_text
|
|
991
|
+
preset_cursor = new_cursor
|
|
992
|
+
tip = "已插入文件,继续编辑或按Ctrl+J确认:"
|
|
993
|
+
else:
|
|
994
|
+
# No selection; keep original text and cursor
|
|
995
|
+
preset = text
|
|
996
|
+
preset_cursor = cursor
|
|
997
|
+
tip = "未选择文件或已取消,继续编辑:"
|
|
998
|
+
# 清除上一条输入行(多行安全),避免多清,保守仅按提示行估算
|
|
999
|
+
try:
|
|
1000
|
+
rows_total = _calc_prompt_rows(text)
|
|
1001
|
+
for _ in range(rows_total):
|
|
1002
|
+
sys.stdout.write("\x1b[1A")
|
|
1003
|
+
sys.stdout.write("\x1b[2K\r")
|
|
1004
|
+
sys.stdout.flush()
|
|
1005
|
+
except Exception:
|
|
1006
|
+
pass
|
|
1007
|
+
continue
|
|
1008
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
|
1009
|
+
FZF_INSERT_SENTINEL_PREFIX
|
|
1010
|
+
):
|
|
1011
|
+
# 从哨兵载荷中提取新文本,作为下次进入提示的预填内容
|
|
1012
|
+
preset = user_input[len(FZF_INSERT_SENTINEL_PREFIX) :]
|
|
1013
|
+
preset_cursor = len(preset)
|
|
1014
|
+
|
|
1015
|
+
# 清除上一条输入行(多行安全),避免多清,保守仅按提示行估算
|
|
1016
|
+
try:
|
|
1017
|
+
rows_total = _calc_prompt_rows(preset)
|
|
1018
|
+
for _ in range(rows_total):
|
|
1019
|
+
sys.stdout.write("\x1b[1A")
|
|
1020
|
+
sys.stdout.write("\x1b[2K\r")
|
|
1021
|
+
sys.stdout.flush()
|
|
1022
|
+
except Exception:
|
|
1023
|
+
pass
|
|
1024
|
+
tip = "已插入文件,继续编辑或按Ctrl+J确认:"
|
|
1025
|
+
continue
|
|
1026
|
+
else:
|
|
1027
|
+
if not user_input and print_on_empty:
|
|
1028
|
+
PrettyOutput.print("输入已取消", OutputType.INFO)
|
|
1029
|
+
return user_input
|