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.
Files changed (57) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +24 -3
  3. jarvis/jarvis_agent/config_editor.py +5 -1
  4. jarvis/jarvis_agent/edit_file_handler.py +15 -9
  5. jarvis/jarvis_agent/jarvis.py +99 -3
  6. jarvis/jarvis_agent/memory_manager.py +3 -3
  7. jarvis/jarvis_agent/share_manager.py +3 -1
  8. jarvis/jarvis_agent/task_analyzer.py +0 -1
  9. jarvis/jarvis_agent/task_manager.py +15 -5
  10. jarvis/jarvis_agent/tool_executor.py +2 -2
  11. jarvis/jarvis_code_agent/code_agent.py +42 -18
  12. jarvis/jarvis_git_utils/git_commiter.py +3 -6
  13. jarvis/jarvis_mcp/sse_mcp_client.py +9 -3
  14. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -5
  15. jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
  16. jarvis/jarvis_methodology/main.py +4 -4
  17. jarvis/jarvis_multi_agent/__init__.py +3 -3
  18. jarvis/jarvis_platform/base.py +10 -5
  19. jarvis/jarvis_platform/kimi.py +18 -6
  20. jarvis/jarvis_platform/tongyi.py +18 -5
  21. jarvis/jarvis_platform/yuanbao.py +10 -3
  22. jarvis/jarvis_platform_manager/main.py +21 -7
  23. jarvis/jarvis_platform_manager/service.py +4 -3
  24. jarvis/jarvis_rag/cli.py +61 -22
  25. jarvis/jarvis_rag/embedding_manager.py +10 -3
  26. jarvis/jarvis_rag/llm_interface.py +4 -1
  27. jarvis/jarvis_rag/query_rewriter.py +3 -1
  28. jarvis/jarvis_rag/rag_pipeline.py +47 -3
  29. jarvis/jarvis_rag/retriever.py +240 -2
  30. jarvis/jarvis_smart_shell/main.py +59 -18
  31. jarvis/jarvis_stats/cli.py +11 -9
  32. jarvis/jarvis_stats/stats.py +14 -8
  33. jarvis/jarvis_stats/storage.py +23 -6
  34. jarvis/jarvis_tools/cli/main.py +63 -29
  35. jarvis/jarvis_tools/edit_file.py +17 -90
  36. jarvis/jarvis_tools/file_analyzer.py +0 -1
  37. jarvis/jarvis_tools/generate_new_tool.py +3 -3
  38. jarvis/jarvis_tools/read_code.py +0 -1
  39. jarvis/jarvis_tools/read_webpage.py +14 -4
  40. jarvis/jarvis_tools/registry.py +16 -9
  41. jarvis/jarvis_tools/retrieve_memory.py +0 -1
  42. jarvis/jarvis_tools/save_memory.py +0 -1
  43. jarvis/jarvis_tools/search_web.py +0 -2
  44. jarvis/jarvis_tools/sub_agent.py +197 -0
  45. jarvis/jarvis_tools/sub_code_agent.py +194 -0
  46. jarvis/jarvis_tools/virtual_tty.py +21 -13
  47. jarvis/jarvis_utils/config.py +35 -5
  48. jarvis/jarvis_utils/input.py +297 -56
  49. jarvis/jarvis_utils/methodology.py +3 -1
  50. jarvis/jarvis_utils/output.py +5 -2
  51. jarvis/jarvis_utils/utils.py +483 -170
  52. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/METADATA +10 -2
  53. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/RECORD +57 -55
  54. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/WHEEL +0 -0
  55. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/entry_points.txt +0 -0
  56. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/licenses/LICENSE +0 -0
  57. {jarvis_ai_assistant-0.3.20.dist-info → jarvis_ai_assistant-0.3.22.dist-info}/top_level.txt +0 -0
@@ -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({"prompt": "ansicyan", "bottom-toolbar": "fg:#888888"})
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
- at_positions = [i for i, char in enumerate(text) if char == "@"]
247
- if not at_positions:
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
- current_at_pos = at_positions[-1]
250
- if cursor_pos <= current_at_pos:
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
- text_after_at = text[current_at_pos + 1 : cursor_pos]
253
- if " " in text_after_at:
266
+
267
+ text_after = text[current_pos + 1 : cursor_pos]
268
+ if " " in text_after:
254
269
  return
255
270
 
256
- file_path = text_after_at.strip()
257
- replace_length = len(text_after_at) + 1
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
- import subprocess
290
+ if current_sym == "@":
291
+ import subprocess
275
292
 
276
- result = subprocess.run(
277
- ["git", "ls-files"],
278
- stdout=subprocess.PIPE,
279
- stderr=subprocess.PIPE,
280
- text=True,
281
- )
282
- if result.returncode == 0:
283
- all_completions.extend(
284
- [
285
- (path, "File")
286
- for path in result.stdout.splitlines()
287
- if path.strip()
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 file_path:
329
+ if token:
294
330
  scored_items = process.extract(
295
- file_path,
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 text, score in scored_items:
304
- display_text = f"{text} ({score}%)" if score < 100 else text
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"'{text}'",
342
+ text=f"'{t}'",
307
343
  start_position=-replace_length,
308
344
  display=display_text,
309
- display_meta=completion_map.get(text, ""),
345
+ display_meta=completion_map.get(t, ""),
310
346
  )
311
347
  else:
312
- for text, desc in all_completions[: self.max_suggestions]:
348
+ for t, desc in all_completions[: self.max_suggestions]:
313
349
  yield Completion(
314
- text=f"'{text}'",
350
+ text=f"'{t}'",
315
351
  start_position=-replace_length,
316
- display=text,
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(tip: str, preset: str | None = None, preset_cursor: int | None = None) -> str:
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
- return f"!{name}"
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 = f"{cursor}:{base64.b64encode(text.encode('utf-8')).decode('ascii')}"
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(tip, preset=preset, preset_cursor=preset_cursor)
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(FZF_REQUEST_SENTINEL_PREFIX):
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(payload[sep_index + 1 :].encode("ascii")).decode("utf-8")
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("未检测到 fzf,无法打开文件选择器。", OutputType.WARNING)
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 = [line for line in r.stdout.splitlines() if line.strip()]
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(_os.path.relpath(_os.path.join(root, name), "."))
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 = [ot("Summary"), ot("Clear"), ot("ToolUsage"), ot("ReloadConfig"), ot("SaveSession")]
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
- items = [s for s in specials if isinstance(s, str) and s.strip()] + files
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
- ["fzf", "--prompt", "Files> ", "--height", "40%", "--border"],
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(FZF_INSERT_SENTINEL_PREFIX):
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(f"加载方法论文件完成 (共 {len(methodologies)} 个)", OutputType.SUCCESS)
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)
@@ -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 event.output_type == OutputType.ERROR:
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: