jarvis-ai-assistant 0.3.20__py3-none-any.whl → 0.3.22__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 +24 -3
- jarvis/jarvis_agent/config_editor.py +5 -1
- jarvis/jarvis_agent/edit_file_handler.py +15 -9
- jarvis/jarvis_agent/jarvis.py +99 -3
- jarvis/jarvis_agent/memory_manager.py +3 -3
- jarvis/jarvis_agent/share_manager.py +3 -1
- jarvis/jarvis_agent/task_analyzer.py +0 -1
- jarvis/jarvis_agent/task_manager.py +15 -5
- jarvis/jarvis_agent/tool_executor.py +2 -2
- jarvis/jarvis_code_agent/code_agent.py +42 -18
- jarvis/jarvis_git_utils/git_commiter.py +3 -6
- jarvis/jarvis_mcp/sse_mcp_client.py +9 -3
- jarvis/jarvis_mcp/streamable_mcp_client.py +15 -5
- jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
- jarvis/jarvis_methodology/main.py +4 -4
- jarvis/jarvis_multi_agent/__init__.py +3 -3
- jarvis/jarvis_platform/base.py +10 -5
- jarvis/jarvis_platform/kimi.py +18 -6
- jarvis/jarvis_platform/tongyi.py +18 -5
- jarvis/jarvis_platform/yuanbao.py +10 -3
- jarvis/jarvis_platform_manager/main.py +21 -7
- jarvis/jarvis_platform_manager/service.py +4 -3
- jarvis/jarvis_rag/cli.py +61 -22
- jarvis/jarvis_rag/embedding_manager.py +10 -3
- jarvis/jarvis_rag/llm_interface.py +4 -1
- jarvis/jarvis_rag/query_rewriter.py +3 -1
- jarvis/jarvis_rag/rag_pipeline.py +47 -3
- jarvis/jarvis_rag/retriever.py +240 -2
- jarvis/jarvis_smart_shell/main.py +59 -18
- jarvis/jarvis_stats/cli.py +11 -9
- jarvis/jarvis_stats/stats.py +14 -8
- jarvis/jarvis_stats/storage.py +23 -6
- jarvis/jarvis_tools/cli/main.py +63 -29
- jarvis/jarvis_tools/edit_file.py +17 -90
- jarvis/jarvis_tools/file_analyzer.py +0 -1
- jarvis/jarvis_tools/generate_new_tool.py +3 -3
- jarvis/jarvis_tools/read_code.py +0 -1
- jarvis/jarvis_tools/read_webpage.py +14 -4
- jarvis/jarvis_tools/registry.py +16 -9
- jarvis/jarvis_tools/retrieve_memory.py +0 -1
- jarvis/jarvis_tools/save_memory.py +0 -1
- jarvis/jarvis_tools/search_web.py +0 -2
- jarvis/jarvis_tools/sub_agent.py +197 -0
- jarvis/jarvis_tools/sub_code_agent.py +194 -0
- jarvis/jarvis_tools/virtual_tty.py +21 -13
- jarvis/jarvis_utils/config.py +35 -5
- jarvis/jarvis_utils/input.py +297 -56
- jarvis/jarvis_utils/methodology.py +3 -1
- jarvis/jarvis_utils/output.py +5 -2
- jarvis/jarvis_utils/utils.py +483 -170
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/METADATA +10 -2
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/RECORD +57 -55
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/top_level.txt +0 -0
jarvis/jarvis_utils/input.py
CHANGED
@@ -48,10 +48,13 @@ CTRL_O_SENTINEL = "__CTRL_O_PRESSED__"
|
|
48
48
|
FZF_INSERT_SENTINEL_PREFIX = "__FZF_INSERT__::"
|
49
49
|
# Sentinel to request running fzf outside the prompt and then prefill next prompt
|
50
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__::"
|
51
53
|
|
52
54
|
# Persistent hint marker for multiline input (shown only once across runs)
|
53
55
|
_MULTILINE_HINT_MARK_FILE = os.path.join(get_data_dir(), "multiline_enter_hint_shown")
|
54
56
|
|
57
|
+
|
55
58
|
def _display_width(s: str) -> int:
|
56
59
|
"""Calculate printable width of a string in terminal columns (handles wide chars)."""
|
57
60
|
try:
|
@@ -66,6 +69,7 @@ def _display_width(s: str) -> int:
|
|
66
69
|
except Exception:
|
67
70
|
return len(s)
|
68
71
|
|
72
|
+
|
69
73
|
def _calc_prompt_rows(prev_text: str) -> int:
|
70
74
|
"""
|
71
75
|
Estimate how many terminal rows the previous prompt occupied.
|
@@ -99,9 +103,6 @@ def _calc_prompt_rows(prev_text: str) -> int:
|
|
99
103
|
return max(1, total_rows)
|
100
104
|
|
101
105
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
106
|
def _multiline_hint_already_shown() -> bool:
|
106
107
|
"""Check if the multiline Enter hint has been shown before (persisted)."""
|
107
108
|
try:
|
@@ -126,7 +127,9 @@ def get_single_line_input(tip: str, default: str = "") -> str:
|
|
126
127
|
获取支持历史记录的单行输入。
|
127
128
|
"""
|
128
129
|
session: PromptSession = PromptSession(history=None)
|
129
|
-
style = PromptStyle.from_dict(
|
130
|
+
style = PromptStyle.from_dict(
|
131
|
+
{"prompt": "ansicyan", "bottom-toolbar": "fg:#888888"}
|
132
|
+
)
|
130
133
|
prompt = FormattedText([("class:prompt", f"👤 > {tip}")])
|
131
134
|
return session.prompt(prompt, default=default, style=style)
|
132
135
|
|
@@ -237,24 +240,36 @@ class FileCompleter(Completer):
|
|
237
240
|
self.max_suggestions = 10
|
238
241
|
self.min_score = 10
|
239
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
|
240
247
|
|
241
248
|
def get_completions(
|
242
249
|
self, document: Document, _: CompleteEvent
|
243
250
|
) -> Iterable[Completion]:
|
244
251
|
text = document.text_before_cursor
|
245
252
|
cursor_pos = document.cursor_position
|
246
|
-
|
247
|
-
|
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:
|
248
257
|
return
|
249
|
-
|
250
|
-
|
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:
|
251
265
|
return
|
252
|
-
|
253
|
-
|
266
|
+
|
267
|
+
text_after = text[current_pos + 1 : cursor_pos]
|
268
|
+
if " " in text_after:
|
254
269
|
return
|
255
270
|
|
256
|
-
|
257
|
-
replace_length = len(
|
271
|
+
token = text_after.strip()
|
272
|
+
replace_length = len(text_after) + 1
|
258
273
|
|
259
274
|
all_completions = []
|
260
275
|
all_completions.extend(
|
@@ -270,29 +285,50 @@ class FileCompleter(Completer):
|
|
270
285
|
]
|
271
286
|
)
|
272
287
|
|
288
|
+
# File path candidates
|
273
289
|
try:
|
274
|
-
|
290
|
+
if current_sym == "@":
|
291
|
+
import subprocess
|
275
292
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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])
|
290
326
|
except Exception:
|
291
327
|
pass
|
292
328
|
|
293
|
-
if
|
329
|
+
if token:
|
294
330
|
scored_items = process.extract(
|
295
|
-
|
331
|
+
token,
|
296
332
|
[item[0] for item in all_completions],
|
297
333
|
limit=self.max_suggestions,
|
298
334
|
)
|
@@ -300,20 +336,20 @@ class FileCompleter(Completer):
|
|
300
336
|
(item[0], item[1]) for item in scored_items if item[1] > self.min_score
|
301
337
|
]
|
302
338
|
completion_map = {item[0]: item[1] for item in all_completions}
|
303
|
-
for
|
304
|
-
display_text = f"{
|
339
|
+
for t, score in scored_items:
|
340
|
+
display_text = f"{t} ({score}%)" if score < 100 else t
|
305
341
|
yield Completion(
|
306
|
-
text=f"'{
|
342
|
+
text=f"'{t}'",
|
307
343
|
start_position=-replace_length,
|
308
344
|
display=display_text,
|
309
|
-
display_meta=completion_map.get(
|
345
|
+
display_meta=completion_map.get(t, ""),
|
310
346
|
)
|
311
347
|
else:
|
312
|
-
for
|
348
|
+
for t, desc in all_completions[: self.max_suggestions]:
|
313
349
|
yield Completion(
|
314
|
-
text=f"'{
|
350
|
+
text=f"'{t}'",
|
315
351
|
start_position=-replace_length,
|
316
|
-
display=
|
352
|
+
display=t,
|
317
353
|
display_meta=desc,
|
318
354
|
)
|
319
355
|
|
@@ -389,7 +425,9 @@ def _show_history_and_copy():
|
|
389
425
|
break
|
390
426
|
|
391
427
|
|
392
|
-
def _get_multiline_input_internal(
|
428
|
+
def _get_multiline_input_internal(
|
429
|
+
tip: str, preset: str | None = None, preset_cursor: int | None = None
|
430
|
+
) -> str:
|
393
431
|
"""
|
394
432
|
Internal function to get multiline input using prompt_toolkit.
|
395
433
|
Returns a sentinel value if Ctrl+O is pressed.
|
@@ -444,6 +482,7 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
444
482
|
@bindings.add("c-t", filter=has_focus(DEFAULT_BUFFER))
|
445
483
|
def _(event):
|
446
484
|
"""Return a shell command like '!bash' for upper input_handler to execute."""
|
485
|
+
|
447
486
|
def _gen_shell_cmd() -> str: # type: ignore
|
448
487
|
try:
|
449
488
|
import os
|
@@ -453,24 +492,28 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
453
492
|
# Prefer PowerShell if available, otherwise fallback to cmd
|
454
493
|
for name in ("pwsh", "powershell", "cmd"):
|
455
494
|
if name == "cmd" or shutil.which(name):
|
456
|
-
|
495
|
+
if name == "cmd":
|
496
|
+
# Keep session open with /K and set env for the spawned shell
|
497
|
+
return "!cmd /K set JARVIS_TERMINAL=1"
|
498
|
+
else:
|
499
|
+
# PowerShell or pwsh: set env then remain in session
|
500
|
+
return f"!{name} -NoExit -Command \"$env:JARVIS_TERMINAL='1'\""
|
457
501
|
else:
|
458
502
|
shell_path = os.environ.get("SHELL", "")
|
459
503
|
if shell_path:
|
460
504
|
base = os.path.basename(shell_path)
|
461
505
|
if base:
|
462
|
-
return f"!{base}"
|
506
|
+
return f"!env JARVIS_TERMINAL=1 {base}"
|
463
507
|
for name in ("fish", "zsh", "bash", "sh"):
|
464
508
|
if shutil.which(name):
|
465
|
-
return f"!{name}"
|
466
|
-
return "!bash"
|
509
|
+
return f"!env JARVIS_TERMINAL=1 {name}"
|
510
|
+
return "!env JARVIS_TERMINAL=1 bash"
|
467
511
|
except Exception:
|
468
|
-
return "!bash"
|
512
|
+
return "!env JARVIS_TERMINAL=1 bash"
|
469
513
|
|
470
514
|
# Append a special marker to indicate no-confirm execution in shell_input_handler
|
471
515
|
event.app.exit(result=_gen_shell_cmd() + " # JARVIS-NOCONFIRM")
|
472
516
|
|
473
|
-
|
474
517
|
@bindings.add("@", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
475
518
|
def _(event):
|
476
519
|
"""
|
@@ -481,6 +524,7 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
481
524
|
"""
|
482
525
|
try:
|
483
526
|
import shutil
|
527
|
+
|
484
528
|
buf = event.current_buffer
|
485
529
|
if shutil.which("fzf") is None:
|
486
530
|
buf.insert_text("@")
|
@@ -490,7 +534,9 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
490
534
|
doc = buf.document
|
491
535
|
text = doc.text
|
492
536
|
cursor = doc.cursor_position
|
493
|
-
payload =
|
537
|
+
payload = (
|
538
|
+
f"{cursor}:{base64.b64encode(text.encode('utf-8')).decode('ascii')}"
|
539
|
+
)
|
494
540
|
event.app.exit(result=FZF_REQUEST_SENTINEL_PREFIX + payload)
|
495
541
|
return
|
496
542
|
except Exception:
|
@@ -499,6 +545,34 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
499
545
|
except Exception:
|
500
546
|
pass
|
501
547
|
|
548
|
+
@bindings.add("#", filter=has_focus(DEFAULT_BUFFER), eager=True)
|
549
|
+
def _(event):
|
550
|
+
"""
|
551
|
+
使用 # 触发 fzf(当 fzf 存在),以“全量文件模式”进行选择(排除 .git);否则仅插入 # 启用内置补全
|
552
|
+
"""
|
553
|
+
try:
|
554
|
+
import shutil
|
555
|
+
|
556
|
+
buf = event.current_buffer
|
557
|
+
if shutil.which("fzf") is None:
|
558
|
+
buf.insert_text("#")
|
559
|
+
return
|
560
|
+
# 先插入 '#'
|
561
|
+
buf.insert_text("#")
|
562
|
+
doc = buf.document
|
563
|
+
text = doc.text
|
564
|
+
cursor = doc.cursor_position
|
565
|
+
payload = (
|
566
|
+
f"{cursor}:{base64.b64encode(text.encode('utf-8')).decode('ascii')}"
|
567
|
+
)
|
568
|
+
event.app.exit(result=FZF_REQUEST_ALL_SENTINEL_PREFIX + payload)
|
569
|
+
return
|
570
|
+
except Exception:
|
571
|
+
try:
|
572
|
+
event.current_buffer.insert_text("#")
|
573
|
+
except Exception:
|
574
|
+
pass
|
575
|
+
|
502
576
|
style = PromptStyle.from_dict(
|
503
577
|
{
|
504
578
|
"prompt": "ansibrightmagenta bold",
|
@@ -556,6 +630,7 @@ def _get_multiline_input_internal(tip: str, preset: str | None = None, preset_cu
|
|
556
630
|
def _pre_run():
|
557
631
|
try:
|
558
632
|
from prompt_toolkit.application.current import get_app as _ga
|
633
|
+
|
559
634
|
app = _ga()
|
560
635
|
buf = app.current_buffer
|
561
636
|
if preset is not None and preset_cursor is not None:
|
@@ -588,19 +663,25 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
588
663
|
preset: str | None = None
|
589
664
|
preset_cursor: int | None = None
|
590
665
|
while True:
|
591
|
-
user_input = _get_multiline_input_internal(
|
666
|
+
user_input = _get_multiline_input_internal(
|
667
|
+
tip, preset=preset, preset_cursor=preset_cursor
|
668
|
+
)
|
592
669
|
|
593
670
|
if user_input == CTRL_O_SENTINEL:
|
594
671
|
_show_history_and_copy()
|
595
672
|
tip = "请继续输入(或按Ctrl+J确认):"
|
596
673
|
continue
|
597
|
-
elif isinstance(user_input, str) and user_input.startswith(
|
674
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
675
|
+
FZF_REQUEST_SENTINEL_PREFIX
|
676
|
+
):
|
598
677
|
# Handle fzf request outside the prompt, then prefill new text.
|
599
678
|
try:
|
600
679
|
payload = user_input[len(FZF_REQUEST_SENTINEL_PREFIX) :]
|
601
680
|
sep_index = payload.find(":")
|
602
681
|
cursor = int(payload[:sep_index])
|
603
|
-
text = base64.b64decode(
|
682
|
+
text = base64.b64decode(
|
683
|
+
payload[sep_index + 1 :].encode("ascii")
|
684
|
+
).decode("utf-8")
|
604
685
|
except Exception:
|
605
686
|
# Malformed payload; just continue without change.
|
606
687
|
preset = None
|
@@ -614,7 +695,9 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
614
695
|
import subprocess
|
615
696
|
|
616
697
|
if shutil.which("fzf") is None:
|
617
|
-
PrettyOutput.print(
|
698
|
+
PrettyOutput.print(
|
699
|
+
"未检测到 fzf,无法打开文件选择器。", OutputType.WARNING
|
700
|
+
)
|
618
701
|
else:
|
619
702
|
files: list[str] = []
|
620
703
|
try:
|
@@ -625,15 +708,20 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
625
708
|
text=True,
|
626
709
|
)
|
627
710
|
if r.returncode == 0:
|
628
|
-
files = [
|
711
|
+
files = [
|
712
|
+
line for line in r.stdout.splitlines() if line.strip()
|
713
|
+
]
|
629
714
|
except Exception:
|
630
715
|
files = []
|
631
716
|
|
632
717
|
if not files:
|
633
718
|
import os as _os
|
719
|
+
|
634
720
|
for root, _, fnames in _os.walk(".", followlinks=False):
|
635
721
|
for name in fnames:
|
636
|
-
files.append(
|
722
|
+
files.append(
|
723
|
+
_os.path.relpath(_os.path.join(root, name), ".")
|
724
|
+
)
|
637
725
|
if len(files) > 10000:
|
638
726
|
break
|
639
727
|
|
@@ -641,12 +729,38 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
641
729
|
PrettyOutput.print("未找到可选择的文件。", OutputType.INFO)
|
642
730
|
else:
|
643
731
|
try:
|
644
|
-
specials = [
|
732
|
+
specials = [
|
733
|
+
ot("Summary"),
|
734
|
+
ot("Clear"),
|
735
|
+
ot("ToolUsage"),
|
736
|
+
ot("ReloadConfig"),
|
737
|
+
ot("SaveSession"),
|
738
|
+
]
|
645
739
|
except Exception:
|
646
740
|
specials = []
|
647
|
-
|
741
|
+
try:
|
742
|
+
replace_map = get_replace_map()
|
743
|
+
builtin_tags = [
|
744
|
+
ot(tag)
|
745
|
+
for tag in replace_map.keys()
|
746
|
+
if isinstance(tag, str) and tag.strip()
|
747
|
+
]
|
748
|
+
except Exception:
|
749
|
+
builtin_tags = []
|
750
|
+
items = (
|
751
|
+
[s for s in specials if isinstance(s, str) and s.strip()]
|
752
|
+
+ builtin_tags
|
753
|
+
+ files
|
754
|
+
)
|
648
755
|
proc = subprocess.run(
|
649
|
-
[
|
756
|
+
[
|
757
|
+
"fzf",
|
758
|
+
"--prompt",
|
759
|
+
"Files> ",
|
760
|
+
"--height",
|
761
|
+
"40%",
|
762
|
+
"--border",
|
763
|
+
],
|
650
764
|
input="\n".join(items),
|
651
765
|
stdout=subprocess.PIPE,
|
652
766
|
stderr=subprocess.PIPE,
|
@@ -684,13 +798,140 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
684
798
|
try:
|
685
799
|
rows_total = _calc_prompt_rows(text)
|
686
800
|
for _ in range(rows_total):
|
687
|
-
sys.stdout.write("\x1b[1A")
|
688
|
-
sys.stdout.write("\x1b[2K\r")
|
801
|
+
sys.stdout.write("\x1b[1A") # 光标上移一行
|
802
|
+
sys.stdout.write("\x1b[2K\r") # 清除整行
|
803
|
+
sys.stdout.flush()
|
804
|
+
except Exception:
|
805
|
+
pass
|
806
|
+
continue
|
807
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
808
|
+
FZF_REQUEST_ALL_SENTINEL_PREFIX
|
809
|
+
):
|
810
|
+
# Handle fzf request (all-files mode, excluding .git) outside the prompt, then prefill new text.
|
811
|
+
try:
|
812
|
+
payload = user_input[len(FZF_REQUEST_ALL_SENTINEL_PREFIX) :]
|
813
|
+
sep_index = payload.find(":")
|
814
|
+
cursor = int(payload[:sep_index])
|
815
|
+
text = base64.b64decode(
|
816
|
+
payload[sep_index + 1 :].encode("ascii")
|
817
|
+
).decode("utf-8")
|
818
|
+
except Exception:
|
819
|
+
# Malformed payload; just continue without change.
|
820
|
+
preset = None
|
821
|
+
tip = "FZF 预填失败,继续输入:"
|
822
|
+
continue
|
823
|
+
|
824
|
+
# Run fzf to get a file selection synchronously (outside prompt) with all files (exclude .git)
|
825
|
+
selected_path = ""
|
826
|
+
try:
|
827
|
+
import shutil
|
828
|
+
import subprocess
|
829
|
+
|
830
|
+
if shutil.which("fzf") is None:
|
831
|
+
PrettyOutput.print(
|
832
|
+
"未检测到 fzf,无法打开文件选择器。", OutputType.WARNING
|
833
|
+
)
|
834
|
+
else:
|
835
|
+
files: list[str] = []
|
836
|
+
try:
|
837
|
+
import os as _os
|
838
|
+
|
839
|
+
for root, dirs, fnames in _os.walk(".", followlinks=False):
|
840
|
+
# Exclude .git directories
|
841
|
+
dirs[:] = [d for d in dirs if d != ".git"]
|
842
|
+
for name in fnames:
|
843
|
+
files.append(
|
844
|
+
_os.path.relpath(_os.path.join(root, name), ".")
|
845
|
+
)
|
846
|
+
if len(files) > 10000:
|
847
|
+
break
|
848
|
+
if len(files) > 10000:
|
849
|
+
break
|
850
|
+
except Exception:
|
851
|
+
files = []
|
852
|
+
|
853
|
+
if not files:
|
854
|
+
PrettyOutput.print("未找到可选择的文件。", OutputType.INFO)
|
855
|
+
else:
|
856
|
+
try:
|
857
|
+
specials = [
|
858
|
+
ot("Summary"),
|
859
|
+
ot("Clear"),
|
860
|
+
ot("ToolUsage"),
|
861
|
+
ot("ReloadConfig"),
|
862
|
+
ot("SaveSession"),
|
863
|
+
]
|
864
|
+
except Exception:
|
865
|
+
specials = []
|
866
|
+
try:
|
867
|
+
replace_map = get_replace_map()
|
868
|
+
builtin_tags = [
|
869
|
+
ot(tag)
|
870
|
+
for tag in replace_map.keys()
|
871
|
+
if isinstance(tag, str) and tag.strip()
|
872
|
+
]
|
873
|
+
except Exception:
|
874
|
+
builtin_tags = []
|
875
|
+
items = (
|
876
|
+
[s for s in specials if isinstance(s, str) and s.strip()]
|
877
|
+
+ builtin_tags
|
878
|
+
+ files
|
879
|
+
)
|
880
|
+
proc = subprocess.run(
|
881
|
+
[
|
882
|
+
"fzf",
|
883
|
+
"--prompt",
|
884
|
+
"Files(all)> ",
|
885
|
+
"--height",
|
886
|
+
"40%",
|
887
|
+
"--border",
|
888
|
+
],
|
889
|
+
input="\n".join(items),
|
890
|
+
stdout=subprocess.PIPE,
|
891
|
+
stderr=subprocess.PIPE,
|
892
|
+
text=True,
|
893
|
+
)
|
894
|
+
sel = proc.stdout.strip()
|
895
|
+
if sel:
|
896
|
+
selected_path = sel
|
897
|
+
except Exception as e:
|
898
|
+
PrettyOutput.print(f"FZF 执行失败: {e}", OutputType.ERROR)
|
899
|
+
|
900
|
+
# Compute new text based on selection (or keep original if none)
|
901
|
+
if selected_path:
|
902
|
+
text_before = text[:cursor]
|
903
|
+
last_hash = text_before.rfind("#")
|
904
|
+
if last_hash != -1 and " " not in text_before[last_hash + 1 :]:
|
905
|
+
# Replace #... segment
|
906
|
+
inserted = f"'{selected_path}'"
|
907
|
+
new_text = text[:last_hash] + inserted + text[cursor:]
|
908
|
+
new_cursor = last_hash + len(inserted)
|
909
|
+
else:
|
910
|
+
# Plain insert
|
911
|
+
inserted = f"'{selected_path}'"
|
912
|
+
new_text = text[:cursor] + inserted + text[cursor:]
|
913
|
+
new_cursor = cursor + len(inserted)
|
914
|
+
preset = new_text
|
915
|
+
preset_cursor = new_cursor
|
916
|
+
tip = "已插入文件,继续编辑或按Ctrl+J确认:"
|
917
|
+
else:
|
918
|
+
# No selection; keep original text and cursor
|
919
|
+
preset = text
|
920
|
+
preset_cursor = cursor
|
921
|
+
tip = "未选择文件或已取消,继续编辑:"
|
922
|
+
# 清除上一条输入行(多行安全),避免多清,保守仅按提示行估算
|
923
|
+
try:
|
924
|
+
rows_total = _calc_prompt_rows(text)
|
925
|
+
for _ in range(rows_total):
|
926
|
+
sys.stdout.write("\x1b[1A")
|
927
|
+
sys.stdout.write("\x1b[2K\r")
|
689
928
|
sys.stdout.flush()
|
690
929
|
except Exception:
|
691
930
|
pass
|
692
931
|
continue
|
693
|
-
elif isinstance(user_input, str) and user_input.startswith(
|
932
|
+
elif isinstance(user_input, str) and user_input.startswith(
|
933
|
+
FZF_INSERT_SENTINEL_PREFIX
|
934
|
+
):
|
694
935
|
# 从哨兵载荷中提取新文本,作为下次进入提示的预填内容
|
695
936
|
preset = user_input[len(FZF_INSERT_SENTINEL_PREFIX) :]
|
696
937
|
preset_cursor = len(preset)
|
@@ -208,7 +208,9 @@ def load_methodology(
|
|
208
208
|
if not methodologies:
|
209
209
|
PrettyOutput.print("没有找到方法论文件", OutputType.WARNING)
|
210
210
|
return ""
|
211
|
-
PrettyOutput.print(
|
211
|
+
PrettyOutput.print(
|
212
|
+
f"加载方法论文件完成 (共 {len(methodologies)} 个)", OutputType.SUCCESS
|
213
|
+
)
|
212
214
|
|
213
215
|
if platform_name:
|
214
216
|
platform = PlatformRegistry().create_platform(platform_name)
|
jarvis/jarvis_utils/output.py
CHANGED
@@ -20,7 +20,7 @@ from rich.style import Style as RichStyle
|
|
20
20
|
from rich.syntax import Syntax
|
21
21
|
from rich.text import Text
|
22
22
|
|
23
|
-
from jarvis.jarvis_utils.config import get_pretty_output
|
23
|
+
from jarvis.jarvis_utils.config import get_pretty_output, is_print_error_traceback
|
24
24
|
from jarvis.jarvis_utils.globals import console, get_agent_list
|
25
25
|
from dataclasses import dataclass
|
26
26
|
from abc import ABC, abstractmethod
|
@@ -71,6 +71,7 @@ class OutputEvent:
|
|
71
71
|
- section: 若为章节标题输出,填入标题文本;否则为None
|
72
72
|
- context: 额外上下文(预留给TUI/日志等)
|
73
73
|
"""
|
74
|
+
|
74
75
|
text: str
|
75
76
|
output_type: OutputType
|
76
77
|
timestamp: bool = True
|
@@ -213,7 +214,9 @@ class ConsoleOutputSink(OutputSink):
|
|
213
214
|
console.print(panel)
|
214
215
|
else:
|
215
216
|
console.print(content)
|
216
|
-
if event.traceback or
|
217
|
+
if event.traceback or (
|
218
|
+
event.output_type == OutputType.ERROR and is_print_error_traceback()
|
219
|
+
):
|
217
220
|
try:
|
218
221
|
console.print_exception()
|
219
222
|
except Exception as e:
|