kapsel-cli 0.5.7__tar.gz → 0.6.3__tar.gz

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 (124) hide show
  1. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/PKG-INFO +1 -1
  2. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/__init__.py +1 -1
  3. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/cli.py +2 -0
  4. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/completer.py +1 -0
  5. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/add.py +204 -25
  6. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/status.py +3 -1
  7. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/upgrade.py +102 -32
  8. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/spec_manager.py +0 -1
  9. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/detector.py +32 -0
  10. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/config.py +6 -2
  11. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/history.py +15 -9
  12. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/banner.py +3 -1
  13. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/card.py +4 -0
  14. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/theme.py +1 -0
  15. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/viewport.py +4 -0
  16. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/PKG-INFO +1 -1
  17. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/pyproject.toml +1 -1
  18. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_add_concurrent.py +2 -1
  19. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_history.py +22 -4
  20. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_ai.py +192 -35
  21. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_fetcher.py +2 -1
  22. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_install.py +17 -0
  23. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_preview.py +2 -2
  24. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/README.md +0 -0
  25. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/__main__.py +0 -0
  26. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/__init__.py +0 -0
  27. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/carapace_engine.py +0 -0
  28. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/carapace_installer.py +0 -0
  29. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/__init__.py +0 -0
  30. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/__init__.py +0 -0
  31. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/completion.py +0 -0
  32. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/config.py +0 -0
  33. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/datadir.py +0 -0
  34. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/dir_nav.py +0 -0
  35. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/help.py +0 -0
  36. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/language.py +0 -0
  37. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/plugin_switch.py +0 -0
  38. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/restart.py +0 -0
  39. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/search.py +0 -0
  40. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/builtins/toggle.py +0 -0
  41. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/dispatcher.py +0 -0
  42. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/completion/kps/registry.py +0 -0
  43. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/__init__.py +0 -0
  44. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/__init__.py +0 -0
  45. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/model.py +0 -0
  46. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/registry.py +0 -0
  47. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/block/runner.py +0 -0
  48. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/engine.py +0 -0
  49. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/executor.py +0 -0
  50. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/i18n.py +0 -0
  51. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/__init__.py +0 -0
  52. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/base.py +0 -0
  53. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/catalog.py +0 -0
  54. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/context.py +0 -0
  55. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/fetcher.py +0 -0
  56. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/hooks.py +0 -0
  57. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/plugin/manager.py +0 -0
  58. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/__init__.py +0 -0
  59. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/installer.py +0 -0
  60. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/tools/registry.py +0 -0
  61. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/core/update_check.py +0 -0
  62. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/i18n.py +0 -0
  63. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/LC_MESSAGES/kapsel.mo +0 -0
  64. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/LC_MESSAGES/kapsel.po +0 -0
  65. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/help.yaml +0 -0
  66. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/de/tips.yaml +0 -0
  67. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/en/help.yaml +0 -0
  68. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/en/tips.yaml +0 -0
  69. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/LC_MESSAGES/kapsel.mo +0 -0
  70. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/LC_MESSAGES/kapsel.po +0 -0
  71. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/help.yaml +0 -0
  72. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/es/tips.yaml +0 -0
  73. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/LC_MESSAGES/kapsel.mo +0 -0
  74. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/LC_MESSAGES/kapsel.po +0 -0
  75. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/help.yaml +0 -0
  76. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/fr/tips.yaml +0 -0
  77. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/LC_MESSAGES/kapsel.mo +0 -0
  78. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/LC_MESSAGES/kapsel.po +0 -0
  79. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/help.yaml +0 -0
  80. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ja/tips.yaml +0 -0
  81. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/kapsel.pot +0 -0
  82. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/LC_MESSAGES/kapsel.mo +0 -0
  83. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/LC_MESSAGES/kapsel.po +0 -0
  84. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/help.yaml +0 -0
  85. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/ru/tips.yaml +0 -0
  86. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.mo +0 -0
  87. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/LC_MESSAGES/kapsel.po +0 -0
  88. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/help.yaml +0 -0
  89. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/locales/zh_CN/tips.yaml +0 -0
  90. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/__init__.py +0 -0
  91. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/logger.py +0 -0
  92. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/storage/migrate.py +0 -0
  93. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/__init__.py +0 -0
  94. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/prompt.py +0 -0
  95. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel/ui/tips.py +0 -0
  96. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/SOURCES.txt +0 -0
  97. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/dependency_links.txt +0 -0
  98. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/entry_points.txt +0 -0
  99. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/requires.txt +0 -0
  100. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/kapsel_cli.egg-info/top_level.txt +0 -0
  101. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/setup.cfg +0 -0
  102. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_block_system.py +0 -0
  103. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_carapace_installer.py +0 -0
  104. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_config.py +0 -0
  105. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_dir_nav.py +0 -0
  106. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_i18n.py +0 -0
  107. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_alias.py +0 -0
  108. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_autopilot.py +0 -0
  109. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_backup.py +0 -0
  110. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_commit.py +0 -0
  111. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_help.py +0 -0
  112. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_init.py +0 -0
  113. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_lang.py +0 -0
  114. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_mend.py +0 -0
  115. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_portal.py +0 -0
  116. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_queue.py +0 -0
  117. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_template.py +0 -0
  118. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_plugin_trans.py +0 -0
  119. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_restart_and_pypi.py +0 -0
  120. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_search_and_completions.py +0 -0
  121. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_spec_manager.py +0 -0
  122. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_toggle.py +0 -0
  123. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_tools_registry.py +0 -0
  124. {kapsel_cli-0.5.7 → kapsel_cli-0.6.3}/tests/test_upgrade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kapsel-cli
3
- Version: 0.5.7
3
+ Version: 0.6.3
4
4
  Summary: Next-generation cross-platform terminal capsule & ergonomic shell multiplexer with context-aware command translation and 1,000+ CLI completions.
5
5
  Author-email: MrEiu <k648888@vip.qq.com>
6
6
  License: MIT
@@ -3,4 +3,4 @@ Kapsel: Cross-platform adaptive smart terminal capsule.
3
3
  Wrap complexity, expose simplicity.
4
4
  """
5
5
 
6
- __version__ = "0.5.7"
6
+ __version__ = "0.6.3"
@@ -104,11 +104,13 @@ def main(args: Optional[List[str]] = None) -> int:
104
104
 
105
105
  normalized = " ".join(stripped.lower().split())
106
106
  if normalized in ("exit", "quit", "bye"):
107
+ engine.history_mgr.record_command(command=user_input, mode="builtin")
107
108
  os.environ.pop("KAPSEL_ACTIVE", None)
108
109
  console.print("[dim]Exited Kapsel.[/]")
109
110
  break
110
111
 
111
112
  if normalized in ("restart", "kapsel restart", "reload"):
113
+ engine.history_mgr.record_command(command=user_input, mode="builtin")
112
114
  from kapsel.completion.kps.builtins.restart import handle_restart
113
115
  return handle_restart(console=console)
114
116
 
@@ -312,6 +312,7 @@ class DualStateCompleter(Completer):
312
312
  ("cd", _("Change directory")),
313
313
  ("clear", _("Clear terminal screen")),
314
314
  ("exit", _("Exit session")),
315
+ ("restart", _("Cleanly restart Kapsel interactive shell session")),
315
316
  ("git", _("Git version control")),
316
317
  ("docker", _("Docker container platform")),
317
318
  ("scoop", _("Windows command-line installer")),
@@ -26,6 +26,40 @@ _CONFIG_LOCK = threading.Lock()
26
26
  _CONSOLE_LOCK = threading.Lock()
27
27
 
28
28
 
29
+ def _check_plugin_already_installed(
30
+ target_raw: str,
31
+ is_dev: bool = False,
32
+ ) -> Tuple[bool, Optional[str], Optional[Path]]:
33
+ """
34
+ Checks if a plugin is already installed on the system by verifying its directory and version.
35
+ Returns: (is_installed: bool, version: Optional[str], plugin_path: Optional[Path]).
36
+ """
37
+ has_path_qualifier = any(sep in target_raw for sep in ("/", "\\")) or target_raw.startswith(".")
38
+ if has_path_qualifier:
39
+ p = Path(target_raw).expanduser()
40
+ target_name = p.name.lower()
41
+ else:
42
+ target_name = target_raw.strip().lower()
43
+
44
+ from kapsel.core.plugin.catalog import _extract_plugin_version
45
+
46
+ # 1. Check user global plugins directory (~/.kapsel/plugins/<name>)
47
+ global_plugins_dir = get_kapsel_dir() / "plugins"
48
+ dest_plugin_dir = global_plugins_dir / target_name
49
+ if dest_plugin_dir.is_dir() and ((dest_plugin_dir / "__init__.py").exists() or (dest_plugin_dir / "plugin.py").exists()):
50
+ ver = _extract_plugin_version(dest_plugin_dir)
51
+ return True, ver, dest_plugin_dir
52
+
53
+ # 2. Check local workspace dev directory if in dev mode
54
+ if is_dev:
55
+ dev_plugin_dir = Path.cwd() / "plugins" / target_name
56
+ if dev_plugin_dir.is_dir() and ((dev_plugin_dir / "__init__.py").exists() or (dev_plugin_dir / "plugin.py").exists()):
57
+ ver = _extract_plugin_version(dev_plugin_dir)
58
+ return True, ver, dev_plugin_dir
59
+
60
+ return False, None, None
61
+
62
+
29
63
  def _install_single_plugin(
30
64
  target_raw: str,
31
65
  con: Console,
@@ -34,12 +68,26 @@ def _install_single_plugin(
34
68
  ) -> Tuple[bool, str]:
35
69
  """
36
70
  Installs a single plugin by name or path:
37
- 1. Resolves source directory (dev local, user global, or remote fetch).
38
- 2. Copies to user global plugins directory.
39
- 3. Adds to config.yaml enabled plugins list in thread-safe manner.
40
- 4. Triggers declarative tools.yaml installer or legacy install.py.
71
+ 1. Checks if the plugin is already installed; if so, skips installation and prompts for upgrade.
72
+ 2. Resolves source directory (dev local, user global, or remote fetch).
73
+ 3. Copies to user global plugins directory.
74
+ 4. Adds to config.yaml enabled plugins list in thread-safe manner.
75
+ 5. Triggers declarative tools.yaml installer or legacy install.py.
41
76
  Returns (success: bool, message: str).
42
77
  """
78
+ is_installed, cur_ver, existing_dir = _check_plugin_already_installed(target_raw, is_dev=is_dev)
79
+ if is_installed:
80
+ target_name = existing_dir.name.lower() if existing_dir else target_raw.lower()
81
+ if not is_batch:
82
+ with _CONFIG_LOCK:
83
+ cfg = load_config()
84
+ current_enabled = cfg.enabled_plugins or []
85
+ if target_name not in current_enabled:
86
+ new_enabled = current_enabled + [target_name]
87
+ update_config_value("plugins", "enabled", new_enabled)
88
+ ver_str = f" (v{cur_ver})" if cur_ver else ""
89
+ return True, f"Plugin '{target_name}' is already installed{ver_str}. To upgrade this plugin, run: kapsel upgrade {target_name}"
90
+
43
91
  global_plugins_dir = get_kapsel_dir() / "plugins"
44
92
  global_plugins_dir.mkdir(parents=True, exist_ok=True)
45
93
 
@@ -87,13 +135,22 @@ def _install_single_plugin(
87
135
  con.print(f"[bold #f43f5e]Error:[/] {err}")
88
136
  return False, err
89
137
 
90
- # Thread-safe config.yaml update
91
- with _CONFIG_LOCK:
92
- cfg = load_config()
93
- current_enabled = cfg.enabled_plugins
94
- if target_name not in current_enabled:
95
- new_enabled = current_enabled + [target_name]
96
- update_config_value("plugins", "enabled", new_enabled)
138
+ # Thread-safe config.yaml update (skip during concurrent batch; batch handler syncs atomically at the end)
139
+ if not is_batch:
140
+ with _CONFIG_LOCK:
141
+ cfg = load_config()
142
+ current_enabled = cfg.enabled_plugins or []
143
+ if target_name not in current_enabled:
144
+ new_enabled = current_enabled + [target_name]
145
+ update_config_value("plugins", "enabled", new_enabled)
146
+
147
+ # Sync Carapace specs for single plugin
148
+ try:
149
+ from kapsel.completion.spec_manager import CarapaceSpecManager
150
+ spec_mgr = CarapaceSpecManager()
151
+ spec_mgr.sync_specs()
152
+ except Exception:
153
+ pass
97
154
 
98
155
  # Declarative tool dependency installation via tools.yaml:
99
156
  tools_file = plugin_dir / "tools.yaml"
@@ -147,14 +204,64 @@ def _handle_add_batch_concurrent(
147
204
  max_workers: int = 4,
148
205
  ) -> int:
149
206
  """
150
- Installs multiple plugins concurrently using a 4-thread worker pool.
207
+ Installs multiple plugins concurrently using an adaptive worker thread pool.
208
+ Dynamically scales thread count when fewer than 4 plugins need installation.
151
209
  Synchronizes output and config writing, and displays an aggregated summary table.
152
210
  """
153
- total = len(targets)
211
+ cfg = load_config()
212
+ already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
213
+
214
+ # Partition targets into uninstalled vs already-installed
215
+ uninstalled: List[str] = []
216
+ already_done: List[str] = []
217
+ for t in targets:
218
+ t_clean = t.strip()
219
+ if not t_clean:
220
+ continue
221
+ is_inst, _, _ = _check_plugin_already_installed(t_clean, is_dev=is_dev)
222
+ if is_inst or t_clean.lower() in already_enabled:
223
+ already_done.append(t_clean)
224
+ else:
225
+ uninstalled.append(t_clean)
226
+
227
+ uninstalled = list(dict.fromkeys(uninstalled))
228
+ already_done = list(dict.fromkeys(already_done))
229
+
230
+ # Ensure already_done are marked enabled in config.yaml
231
+ if already_done:
232
+ with _CONFIG_LOCK:
233
+ cfg = load_config()
234
+ current_enabled = cfg.enabled_plugins or []
235
+ to_enable = [p.lower() for p in already_done if p.lower() not in current_enabled]
236
+ if to_enable:
237
+ update_config_value("plugins", "enabled", current_enabled + to_enable)
238
+
239
+ if not uninstalled:
240
+ con.print(f"\n[bold #10b981]✔ All specified plugins ({len(already_done)}) are already installed.[/]")
241
+ con.print("[dim]Notice: To upgrade installed plugins, please run:[/] [bold white]kapsel upgrade[/]\n")
242
+ return 0
243
+
244
+ # If only 1 plugin actually needs installation, install directly without worker pool
245
+ if len(uninstalled) == 1:
246
+ if already_done:
247
+ con.print(f"[dim]Notice: {', '.join(already_done)} already installed/enabled.[/]")
248
+ success, msg = _install_single_plugin(uninstalled[0], con, is_dev=is_dev, is_batch=False)
249
+ if success:
250
+ con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
251
+ return 0
252
+ return 1
253
+
254
+ total = len(uninstalled)
255
+ # Dynamically adapt thread count: never spawn more workers than actual uninstalled plugins
256
+ actual_workers = max(1, min(max_workers, total))
257
+
154
258
  con.print(f"\n[bold #00f0ff]============================================================[/]")
155
- con.print(f"[bold #00f0ff] 📦 Kapsel Concurrent Batch Installer ({max_workers} Threads, {total} Plugins) [/]")
259
+ con.print(f"[bold #00f0ff] 📦 Kapsel Concurrent Batch Installer ({actual_workers} Threads, {total} Plugins) [/]")
156
260
  con.print(f"[bold #00f0ff]============================================================[/]\n")
157
261
 
262
+ if already_done:
263
+ con.print(f"[dim]Notice: {len(already_done)} plugin(s) already installed (skipped): {', '.join(already_done)}[/]\n")
264
+
158
265
  succeeded: List[str] = []
159
266
  failed: List[Tuple[str, str]] = []
160
267
  results_lock = threading.Lock()
@@ -183,8 +290,8 @@ def _handle_add_batch_concurrent(
183
290
  con.print(f" [bold #f43f5e]✘ [{plugin_name}] Unexpected error:[/] {e}")
184
291
  return False, plugin_name, str(e)
185
292
 
186
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
187
- futures = {executor.submit(_worker_task, name): name for name in targets}
293
+ with ThreadPoolExecutor(max_workers=actual_workers) as executor:
294
+ futures = {executor.submit(_worker_task, name): name for name in uninstalled}
188
295
  for fut in as_completed(futures):
189
296
  ok, name, msg = fut.result()
190
297
  with results_lock:
@@ -197,9 +304,17 @@ def _handle_add_batch_concurrent(
197
304
  if succeeded:
198
305
  with _CONFIG_LOCK:
199
306
  cfg = load_config()
200
- new_enabled = list(dict.fromkeys(cfg.enabled_plugins + succeeded))
307
+ new_enabled = list(dict.fromkeys((cfg.enabled_plugins or []) + succeeded))
201
308
  update_config_value("plugins", "enabled", new_enabled)
202
309
 
310
+ # Sync Carapace specs for newly enabled plugins
311
+ try:
312
+ from kapsel.completion.spec_manager import CarapaceSpecManager
313
+ spec_mgr = CarapaceSpecManager()
314
+ spec_mgr.sync_specs()
315
+ except Exception:
316
+ pass
317
+
203
318
  # Summary table
204
319
  table = Table(
205
320
  title=f"[bold #00f0ff]📊 Concurrent Batch Installation Summary ({len(succeeded)}/{total} Succeeded)[/]",
@@ -209,25 +324,31 @@ def _handle_add_batch_concurrent(
209
324
  table.add_column("Status", width=24)
210
325
  table.add_column("Details", style="dim")
211
326
 
212
- for name in targets:
327
+ for name in uninstalled:
213
328
  if name in succeeded:
214
329
  table.add_row(name, "[bold #10b981]✔ Installed & Enabled[/]", "Ready")
215
330
  else:
216
331
  err_msg = next((m for (n, m) in failed if n == name), "Failed")
217
332
  table.add_row(name, "[bold #f43f5e]✘ Failed[/]", err_msg[:50])
218
333
 
334
+ for name in already_done:
335
+ table.add_row(name, "[dim #10b981]✔ Already Installed[/]", "Skipped (Use 'kapsel upgrade' to update)")
336
+
219
337
  con.print("")
220
338
  con.print(table)
221
339
  summary_color = "#10b981" if not failed else "#f59e0b"
222
340
  con.print(f"\n[{summary_color}]Batch Completed:[/] [white]{len(succeeded)} succeeded[/], "
223
- f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/].\n")
341
+ f"[{'#f43f5e' if failed else 'white'}]{len(failed)} failed[/], "
342
+ f"[dim]{len(already_done)} already installed[/].\n")
224
343
 
225
- return 0 if succeeded else 1
344
+ return 0 if succeeded or (not failed and already_done) else 1
226
345
 
227
346
 
228
347
  def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) -> int:
229
348
  """
230
- Collects all official and dev Kapsel plugins and installs them concurrently across 4 threads.
349
+ Collects all uninstalled official and dev Kapsel plugins and installs them concurrently.
350
+ Dynamically scales thread count when fewer than 4 plugins need installation.
351
+ Filters out any already-installed plugins and directs user to 'kapsel upgrade'.
231
352
  """
232
353
  from kapsel.core.plugin.catalog import load_plugin_catalog_rich, DEFAULT_CATALOG
233
354
  catalog = load_plugin_catalog_rich()
@@ -247,7 +368,48 @@ def _handle_add_all(con: Console, is_dev: bool = False, max_workers: int = 4) ->
247
368
  plugin_names.add(item.name.lower())
248
369
 
249
370
  ordered_plugins = sorted(plugin_names)
250
- return _handle_add_batch_concurrent(ordered_plugins, con, is_dev=is_dev, max_workers=max_workers)
371
+
372
+ cfg = load_config()
373
+ already_enabled = set(p.lower() for p in (cfg.enabled_plugins or []))
374
+
375
+ # Check both config.enabled_plugins and actual on-disk installation status with version
376
+ uninstalled: List[str] = []
377
+ already_installed: List[str] = []
378
+ for p in ordered_plugins:
379
+ is_inst, _, _ = _check_plugin_already_installed(p, is_dev=is_dev)
380
+ if is_inst or p.lower() in already_enabled:
381
+ already_installed.append(p)
382
+ else:
383
+ uninstalled.append(p)
384
+
385
+ # Ensure all already_installed are marked enabled in config.yaml
386
+ if already_installed:
387
+ with _CONFIG_LOCK:
388
+ cfg = load_config()
389
+ current_enabled = cfg.enabled_plugins or []
390
+ to_enable = [p.lower() for p in already_installed if p.lower() not in current_enabled]
391
+ if to_enable:
392
+ update_config_value("plugins", "enabled", current_enabled + to_enable)
393
+
394
+ if not uninstalled:
395
+ con.print(f"\n[bold #10b981]✔ All official plugins ({len(already_installed)}) are already installed.[/]")
396
+ con.print("[dim]Notice: To check or upgrade installed plugins, please run:[/] [bold white]kapsel upgrade[/]\n")
397
+ return 0
398
+
399
+ con.print(f"[dim]Skipped {len(already_installed)} already-installed plugin(s). To upgrade, run: kapsel upgrade[/]")
400
+ con.print(f"[dim]Installing {len(uninstalled)} new plugin(s) ({len(ordered_plugins)} total in catalog).[/]\n")
401
+
402
+ # If only 1 uninstalled plugin, no need for thread pool
403
+ if len(uninstalled) == 1:
404
+ success, msg = _install_single_plugin(uninstalled[0], con, is_dev=is_dev, is_batch=False)
405
+ if success:
406
+ con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
407
+ return 0
408
+ return 1
409
+
410
+ # When fewer than max_workers (e.g. 2 or 3), dynamically adapt thread count to len(uninstalled)
411
+ workers = min(max_workers, len(uninstalled))
412
+ return _handle_add_batch_concurrent(uninstalled, con, is_dev=is_dev, max_workers=workers)
251
413
 
252
414
 
253
415
  def _select_plugins_interactive(con: Console, is_dev: bool = False) -> List[str]:
@@ -442,7 +604,7 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
442
604
  con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
443
605
  return 0
444
606
  return 1
445
- return _handle_add_batch_concurrent(chosen, con, is_dev=is_dev, max_workers=4)
607
+ return _handle_add_batch_concurrent(chosen, con, is_dev=is_dev, max_workers=min(4, len(chosen)))
446
608
 
447
609
  # 2. Parse target plugin names from CLI arguments (supporting spaces and commas)
448
610
  targets: List[str] = []
@@ -472,6 +634,23 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
472
634
 
473
635
  # 5. Handle single plugin installation
474
636
  if len(targets) == 1:
637
+ target = targets[0]
638
+ has_path_qualifier = any(sep in target for sep in ("/", "\\")) or target.startswith(".")
639
+ target_name = Path(target).name.lower() if has_path_qualifier else target.lower()
640
+
641
+ is_installed, cur_ver, _ = _check_plugin_already_installed(target, is_dev=is_dev)
642
+ if is_installed:
643
+ with _CONFIG_LOCK:
644
+ cfg = load_config()
645
+ current_enabled = cfg.enabled_plugins or []
646
+ if target_name not in current_enabled:
647
+ new_enabled = current_enabled + [target_name]
648
+ update_config_value("plugins", "enabled", new_enabled)
649
+ ver_str = f" (v{cur_ver})" if cur_ver else ""
650
+ con.print(f"\n[bold #f59e0b]●[/] Plugin '[bold #00f0ff]{target_name}[/]' is already installed{ver_str}.")
651
+ con.print(f"[dim]To upgrade this plugin, please run:[/] [bold white]kapsel upgrade {target_name}[/]\n")
652
+ return 0
653
+
475
654
  success, msg = _install_single_plugin(targets[0], con, is_dev=is_dev, is_batch=False)
476
655
  if success:
477
656
  con.print(Panel(msg, title="[bold #00f0ff]🔌 Plugin System[/]", border_style="#0891b2", expand=False))
@@ -479,5 +658,5 @@ def handle_add_command(args: List[str], console: Optional[Console] = None) -> in
479
658
  else:
480
659
  return 1
481
660
 
482
- # 6. Handle multiple plugins concurrent installation (4 workers)
483
- return _handle_add_batch_concurrent(targets, con, is_dev=is_dev, max_workers=4)
661
+ # 6. Handle multiple plugins concurrent installation (adapted worker count)
662
+ return _handle_add_batch_concurrent(targets, con, is_dev=is_dev, max_workers=min(4, len(targets)))
@@ -68,12 +68,14 @@ def handle_status(args: Optional[List[str]] = None, console: Optional[Console] =
68
68
  from kapsel.core.update_check import get_pypi_upgrade_notice
69
69
  up_ver = get_pypi_upgrade_notice()
70
70
  up_badge = f" [bold #f59e0b]🚀 upgrade (v{up_ver})[/]" if up_ver else ""
71
+ venv = detector.get_virtual_env()
72
+ venv_badge = f" [bold #10b981]({venv})[/]" if venv else ""
71
73
 
72
74
  grid.add_row(
73
75
  f"📂 {_('Working Dir:')}",
74
76
  f"[dim]{cwd_fmt}[/]",
75
77
  f"💊 {_('Kapsel Version:')}",
76
- f"[bold #00f0ff]v{__version__}[/]{up_badge} (Python {sys.version.split()[0]})",
78
+ f"[bold #00f0ff]v{__version__}[/]{up_badge} (Python {sys.version.split()[0]}){venv_badge}",
77
79
  )
78
80
 
79
81
  theme_name = cfg.theme.get("name", "cyber_dark") if isinstance(cfg.theme, dict) else str(cfg.theme)
@@ -9,6 +9,7 @@ All comments and descriptions are in English.
9
9
  Default interface language is English with i18n support.
10
10
  """
11
11
 
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
13
  import importlib.util
13
14
  import json
14
15
  import os
@@ -17,6 +18,7 @@ import re
17
18
  import shutil
18
19
  import subprocess
19
20
  import sys
21
+ import threading
20
22
  from typing import Any, Dict, List, Optional, Tuple
21
23
  import urllib.request
22
24
  from rich.console import Console
@@ -35,6 +37,8 @@ from kapsel.ui.banner import ensure_utf8_io
35
37
 
36
38
  ensure_utf8_io()
37
39
 
40
+ _CONSOLE_LOCK = threading.Lock()
41
+
38
42
  # Remote endpoints for Kapsel Core releases
39
43
  KAPSEL_GITHUB_RELEASE_API = "https://api.github.com/repos/MrEiu/Kapsel/releases/latest"
40
44
  KAPSEL_GITHUB_RELEASE_MIRROR_API = "https://ghproxy.net/https://api.github.com/repos/MrEiu/Kapsel/releases/latest"
@@ -260,28 +264,58 @@ def check_plugins_update(
260
264
  return results
261
265
 
262
266
 
263
- def run_plugin_post_install(plugin_dir: Path, con: Console) -> bool:
267
+ def install_plugin_dependencies(plugin_dir: Path, con: Console) -> bool:
264
268
  """
265
- Executes the plugin's install.py script if present after update.
266
- Returns True on success or if no installer exists.
269
+ Installs missing declarative tools from tools.yaml and/or runs legacy install.py.
270
+ Ensures any uninstalled dependencies are properly installed during upgrade.
267
271
  """
268
- install_script = plugin_dir / "install.py"
269
- if not install_script.is_file():
270
- return True
272
+ all_ok = True
273
+ target_name = plugin_dir.name
271
274
 
272
- con.print(f" [dim]Running post-install setup for [bold #00f0ff]{plugin_dir.name}[/]...[/]")
273
- try:
274
- spec = importlib.util.spec_from_file_location(f"kapsel_install_{plugin_dir.name}", install_script)
275
- if spec and spec.loader:
276
- mod = importlib.util.module_from_spec(spec)
277
- spec.loader.exec_module(mod)
278
- if hasattr(mod, "install"):
279
- bin_dir = get_kapsel_dir() / "bin"
280
- res = mod.install(con, bin_dir)
281
- return bool(res)
282
- except Exception as e:
283
- con.print(f" [yellow]Notice: Post-install setup warning for {plugin_dir.name}: {e}[/]")
284
- return True
275
+ # 1. Declarative tool dependencies via tools.yaml
276
+ tools_file = plugin_dir / "tools.yaml"
277
+ if tools_file.is_file():
278
+ try:
279
+ from kapsel.core.tools.installer import install_plugin_tools
280
+ bin_dir = get_kapsel_dir() / "bin"
281
+ bin_dir.mkdir(parents=True, exist_ok=True)
282
+ with _CONSOLE_LOCK:
283
+ con.print(f" [dim]Checking tool dependencies for '[bold #00f0ff]{target_name}[/]' (tools.yaml)...[/]")
284
+ ok = install_plugin_tools(plugin_dir, console=con, bin_dir=bin_dir)
285
+ if not ok:
286
+ all_ok = False
287
+ except Exception as e:
288
+ with _CONSOLE_LOCK:
289
+ con.print(f" [yellow]Warning: Error installing tools.yaml dependencies for '{target_name}':[/] {e}")
290
+ all_ok = False
291
+
292
+ # 2. Legacy install.py script (if present)
293
+ install_script = plugin_dir / "install.py"
294
+ if install_script.is_file():
295
+ try:
296
+ with _CONSOLE_LOCK:
297
+ con.print(f" [dim]Running post-install setup for '[bold #00f0ff]{target_name}[/]' (install.py)...[/]")
298
+ spec = importlib.util.spec_from_file_location(f"kapsel_install_{target_name}", install_script)
299
+ if spec and spec.loader:
300
+ mod = importlib.util.module_from_spec(spec)
301
+ sys.modules[f"kapsel_install_{target_name}"] = mod
302
+ spec.loader.exec_module(mod)
303
+ if hasattr(mod, "install"):
304
+ bin_dir = get_kapsel_dir() / "bin"
305
+ bin_dir.mkdir(parents=True, exist_ok=True)
306
+ res = mod.install(con, bin_dir)
307
+ if not res:
308
+ all_ok = False
309
+ except Exception as e:
310
+ with _CONSOLE_LOCK:
311
+ con.print(f" [yellow]Notice: Post-install setup warning for '{target_name}':[/] {e}")
312
+ all_ok = False
313
+
314
+ return all_ok
315
+
316
+
317
+ # Alias for backward compatibility
318
+ run_plugin_post_install = install_plugin_dependencies
285
319
 
286
320
 
287
321
  def prompt_user_choice(prompt_msg: str, default: str = "y") -> str:
@@ -364,7 +398,7 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
364
398
  dest_dir = info["dir"] or (get_kapsel_dir() / "plugins" / target_plugin)
365
399
  con.print(f"[dim]{_('Downloading and updating plugin package...')}[/]")
366
400
  if fetch_plugin_from_remote(target_plugin, dest_dir, con):
367
- run_plugin_post_install(dest_dir, con)
401
+ install_plugin_dependencies(dest_dir, con)
368
402
  try:
369
403
  from kapsel.completion.spec_manager import CarapaceSpecManager
370
404
  CarapaceSpecManager().sync_specs()
@@ -490,17 +524,53 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
490
524
  con.print(f"[dim]{_('Cancelled.')}[/]\n")
491
525
  return 0
492
526
 
493
- # Apply plugin updates
494
- success_count = 0
495
- for p in upgradable_plugins:
496
- con.print(f"[dim]{_('Updating')} [bold #00f0ff]{p['name']}[/] {_('to')} v{p['latest']}...[/]")
497
- dest_dir = p["dir"] or (get_kapsel_dir() / "plugins" / p["name"])
498
- if fetch_plugin_from_remote(p["name"], dest_dir, con):
499
- run_plugin_post_install(dest_dir, con)
500
- success_count += 1
501
- con.print(f" [bold #10b981]✔ {_('Updated')} {p['name']} {_('to')} v{p['latest']}[/]")
527
+ # Apply plugin updates concurrently
528
+ actual_workers = max(1, min(4, len(upgradable_plugins)))
529
+ con.print(f"\n[bold #00f0ff]━━━ {_('Updating')} {len(upgradable_plugins)} {_('plugin(s) concurrently')} ({actual_workers} {_('Threads')}) ━━━[/]\n")
530
+
531
+ succeeded_plugins: List[str] = []
532
+ failed_plugins: List[Tuple[str, str]] = []
533
+ results_lock = threading.Lock()
534
+
535
+ def _upgrade_worker(p_info: Dict[str, Any]) -> Tuple[bool, str, str]:
536
+ name = p_info["name"]
537
+ latest_ver = p_info["latest"]
538
+ dest_dir = p_info["dir"] or (get_kapsel_dir() / "plugins" / name)
539
+
540
+ with _CONSOLE_LOCK:
541
+ con.print(f"[dim]{_('Updating')} [bold #00f0ff]{name}[/] {_('to')} v{latest_ver}...[/]")
542
+
543
+ try:
544
+ if fetch_plugin_from_remote(name, dest_dir, con):
545
+ install_plugin_dependencies(dest_dir, con)
546
+ with _CONSOLE_LOCK:
547
+ con.print(f" [bold #10b981]✔ {_('Updated')} {name} {_('to')} v{latest_ver}[/]")
548
+ return True, name, f"v{latest_ver}"
549
+ else:
550
+ with _CONSOLE_LOCK:
551
+ con.print(f" [bold #f43f5e]✘ {_('Failed to update')} {name}[/]")
552
+ return False, name, "Download failed"
553
+ except Exception as ex:
554
+ with _CONSOLE_LOCK:
555
+ con.print(f" [bold #f43f5e]✘ {_('Error updating')} {name}:[/] {ex}")
556
+ return False, name, str(ex)
557
+
558
+ if len(upgradable_plugins) == 1:
559
+ ok, name, detail = _upgrade_worker(upgradable_plugins[0])
560
+ if ok:
561
+ succeeded_plugins.append(name)
502
562
  else:
503
- con.print(f" [bold #f43f5e]✘ {_('Failed to update')} {p['name']}[/]")
563
+ failed_plugins.append((name, detail))
564
+ else:
565
+ with ThreadPoolExecutor(max_workers=actual_workers) as executor:
566
+ futures = {executor.submit(_upgrade_worker, p): p["name"] for p in upgradable_plugins}
567
+ for fut in as_completed(futures):
568
+ ok, name, detail = fut.result()
569
+ with results_lock:
570
+ if ok:
571
+ succeeded_plugins.append(name)
572
+ else:
573
+ failed_plugins.append((name, detail))
504
574
 
505
575
  # Re-sync completion specifications
506
576
  try:
@@ -509,5 +579,5 @@ def handle_upgrade(args: List[str], console: Optional[Console] = None) -> int:
509
579
  except Exception:
510
580
  pass
511
581
 
512
- con.print(f"\n[bold #10b981]✨ {_('Successfully updated')} {success_count}/{len(upgradable_plugins)} {_('plugin(s)!')}[/]\n")
513
- return 0
582
+ con.print(f"\n[bold #10b981]✨ {_('Successfully updated')} {len(succeeded_plugins)}/{len(upgradable_plugins)} {_('plugin(s)!')}[/]\n")
583
+ return 0 if not failed_plugins else 1
@@ -121,7 +121,6 @@ CORE_SYSTEM_COMMANDS: List[Dict[str, Any]] = [
121
121
  },
122
122
  {
123
123
  "name": "upgrade",
124
- "aliases": ["update"],
125
124
  "description": "Check and upgrade Kapsel Core and official plugins with release notes",
126
125
  "flags": {
127
126
  "-c, --check": "Check for updates without downloading or installing",
@@ -158,5 +158,37 @@ class EnvironmentDetector:
158
158
  except ValueError:
159
159
  return str(p)
160
160
 
161
+ def get_virtual_env(self) -> Optional[str]:
162
+ """
163
+ Detects active Python virtual environment or Conda environment.
164
+ Returns the environment name (e.g. 'base', 'myenv', '.venv') or None if not active.
165
+ """
166
+ # 1. Conda environment
167
+ conda_env = os.environ.get("CONDA_DEFAULT_ENV")
168
+ if conda_env:
169
+ clean = conda_env.strip()
170
+ if clean:
171
+ return clean
172
+
173
+ # 2. VIRTUAL_ENV_PROMPT (Python 3.11+ custom venv prompt)
174
+ venv_prompt = os.environ.get("VIRTUAL_ENV_PROMPT")
175
+ if venv_prompt:
176
+ clean = venv_prompt.strip().strip("()").strip()
177
+ if clean:
178
+ return clean
179
+
180
+ # 3. Standard VIRTUAL_ENV path
181
+ virtual_env = os.environ.get("VIRTUAL_ENV")
182
+ if virtual_env:
183
+ name = Path(virtual_env).name.strip()
184
+ if name:
185
+ return name
186
+
187
+ # 4. In-process virtual environment check (sys.prefix != sys.base_prefix)
188
+ if hasattr(sys, "base_prefix") and sys.prefix != sys.base_prefix:
189
+ return Path(sys.prefix).name.strip()
190
+
191
+ return None
192
+
161
193
 
162
194
  detector = EnvironmentDetector()
@@ -29,6 +29,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
29
29
  "enable_card_border": True,
30
30
  "show_git_branch": True,
31
31
  "show_shell_badge": True,
32
+ "show_venv_badge": True,
32
33
  "show_timestamp": True,
33
34
  "prompt_symbols": {
34
35
  "top": "╭─",
@@ -58,7 +59,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
58
59
  "auto_strip_spaces": True,
59
60
  },
60
61
  "history": {
61
- "max_memory_entries": 20,
62
+ "max_memory_entries": 150,
62
63
  "frequency_learning": True,
63
64
  },
64
65
  "cloud": {
@@ -137,6 +138,9 @@ ui:
137
138
  # 提示行是否显示当前外层 Shell 徽章 (如 [pwsh], [cmd])
138
139
  show_shell_badge: true
139
140
 
141
+ # 提示行是否显示当前 Python / Conda 虚拟环境徽标 (如 (base), (.venv))
142
+ show_venv_badge: true
143
+
140
144
  # 提示行是否显示当前执行时间戳
141
145
  show_timestamp: true
142
146
 
@@ -271,7 +275,7 @@ class KapselConfig:
271
275
 
272
276
  @property
273
277
  def history_entries(self) -> int:
274
- return int(self.raw.get("history", {}).get("max_memory_entries", 20))
278
+ return int(self.raw.get("history", {}).get("max_memory_entries", 150))
275
279
 
276
280
  @property
277
281
  def enabled_plugins(self) -> List[str]: