deepy-cli 0.2.18__tar.gz → 0.2.20__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 (106) hide show
  1. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/PKG-INFO +9 -3
  2. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/README.md +8 -2
  3. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/pyproject.toml +1 -1
  4. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/cli.py +5 -6
  6. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/config/settings.py +11 -5
  7. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/sessions/jsonl.py +0 -1
  8. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/app.py +2 -3
  9. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/commands.py +20 -8
  10. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/screens.py +1 -1
  11. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/widgets.py +8 -4
  12. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/prompt_input.py +41 -4
  13. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/slash_commands.py +106 -4
  14. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/status_footer.py +1 -1
  15. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/terminal.py +583 -349
  16. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/theme_picker.py +1 -2
  17. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/__main__.py +0 -0
  18. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/background_tasks.py +0 -0
  19. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/config/__init__.py +0 -0
  20. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/__init__.py +0 -0
  21. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  22. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  23. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  24. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/Search.md +0 -0
  25. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/WebFetch.md +0 -0
  26. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/WebSearch.md +0 -0
  27. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/__init__.py +0 -0
  28. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/apply_patch.md +0 -0
  29. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/edit_text.md +0 -0
  30. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/read_file.md +0 -0
  31. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/shell.md +0 -0
  32. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/task_list.md +0 -0
  33. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/task_output.md +0 -0
  34. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/task_stop.md +0 -0
  35. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/test_shell.md +0 -0
  36. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/todo_write.md +0 -0
  37. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/data/tools/write_file.md +0 -0
  38. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/errors.py +0 -0
  39. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/input_suggestions.py +0 -0
  40. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/__init__.py +0 -0
  41. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/agent.py +0 -0
  42. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/compaction.py +0 -0
  43. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/context.py +0 -0
  44. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/events.py +0 -0
  45. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/model_capabilities.py +0 -0
  46. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/provider.py +0 -0
  47. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/replay.py +0 -0
  48. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/runner.py +0 -0
  49. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/llm/thinking.py +0 -0
  50. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/mcp.py +0 -0
  51. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/__init__.py +0 -0
  52. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/compact.py +0 -0
  53. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/init_agents.py +0 -0
  54. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/rules.py +0 -0
  55. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/runtime_context.py +0 -0
  56. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/system.py +0 -0
  57. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/prompts/tool_docs.py +0 -0
  58. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/session_cost.py +0 -0
  59. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/sessions/__init__.py +0 -0
  60. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/sessions/manager.py +0 -0
  61. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/skill_market.py +0 -0
  62. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/skills.py +0 -0
  63. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/status.py +0 -0
  64. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/subagents.py +0 -0
  65. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/todos.py +0 -0
  66. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/__init__.py +0 -0
  67. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/agents.py +0 -0
  68. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/builtin.py +0 -0
  69. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/file_state.py +0 -0
  70. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/result.py +0 -0
  71. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/search.py +0 -0
  72. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/shell_output.py +0 -0
  73. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/shell_utils.py +0 -0
  74. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tools/test_shell.py +0 -0
  75. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/__init__.py +0 -0
  76. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/compat.py +0 -0
  77. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/diff.py +0 -0
  78. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/runner.py +0 -0
  79. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/tui/state.py +0 -0
  80. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/types/__init__.py +0 -0
  81. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/types/sdk.py +0 -0
  82. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/types/tool_payloads.py +0 -0
  83. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/__init__.py +0 -0
  84. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/app.py +0 -0
  85. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/ask_user_question.py +0 -0
  86. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/exit_summary.py +0 -0
  87. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/file_mentions.py +0 -0
  88. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/loading_text.py +0 -0
  89. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/local_command.py +0 -0
  90. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/markdown.py +0 -0
  91. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/message_view.py +0 -0
  92. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/model_picker.py +0 -0
  93. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/prompt_buffer.py +0 -0
  94. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/session_list.py +0 -0
  95. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/session_picker.py +0 -0
  96. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/skill_picker.py +0 -0
  97. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/styles.py +0 -0
  98. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/thinking_state.py +0 -0
  99. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/ui/welcome.py +0 -0
  100. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/update_check.py +0 -0
  101. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/usage.py +0 -0
  102. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/utils/__init__.py +0 -0
  103. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/utils/debug_logger.py +0 -0
  104. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/utils/error_logger.py +0 -0
  105. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/utils/json.py +0 -0
  106. {deepy_cli-0.2.18 → deepy_cli-0.2.20}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.2.18
3
+ Version: 0.2.20
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -43,7 +43,9 @@ Description-Content-Type: text/markdown
43
43
  </p>
44
44
 
45
45
  <p align="center">
46
- <a href="https://kirineko.github.io/deepy/">Website</a>
46
+ <a href="https://deepy.kirineko.tech/"><strong>Install Website</strong></a>
47
+ ·
48
+ <a href="https://kirineko.github.io/deepy/">GitHub Pages</a>
47
49
  ·
48
50
  <a href="README.zh-CN.md">中文文档</a>
49
51
  ·
@@ -54,6 +56,8 @@ Description-Content-Type: text/markdown
54
56
 
55
57
  ![Deepy terminal welcome screen](https://raw.githubusercontent.com/kirineko/deepy/main/asset/welcome.webp)
56
58
 
59
+ > Install and setup guide: **https://deepy.kirineko.tech/**
60
+
57
61
  ## What Deepy Does
58
62
 
59
63
  Deepy is a Python CLI coding agent for DeepSeek and supported
@@ -113,6 +117,8 @@ for direct local commands.
113
117
 
114
118
  ## Quick Start
115
119
 
120
+ For the guided installation page, open **https://deepy.kirineko.tech/**.
121
+
116
122
  1. Install `uv`:
117
123
 
118
124
  ```bash
@@ -300,7 +306,7 @@ reserved_context_tokens = 50000
300
306
  compact_preserve_recent_messages = 2
301
307
 
302
308
  [ui]
303
- theme = "auto" # auto, dark, or light
309
+ theme = "dark" # dark or light
304
310
  ```
305
311
 
306
312
  Set config without the interactive wizard:
@@ -11,7 +11,9 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
- <a href="https://kirineko.github.io/deepy/">Website</a>
14
+ <a href="https://deepy.kirineko.tech/"><strong>Install Website</strong></a>
15
+ ·
16
+ <a href="https://kirineko.github.io/deepy/">GitHub Pages</a>
15
17
  ·
16
18
  <a href="README.zh-CN.md">中文文档</a>
17
19
  ·
@@ -22,6 +24,8 @@
22
24
 
23
25
  ![Deepy terminal welcome screen](https://raw.githubusercontent.com/kirineko/deepy/main/asset/welcome.webp)
24
26
 
27
+ > Install and setup guide: **https://deepy.kirineko.tech/**
28
+
25
29
  ## What Deepy Does
26
30
 
27
31
  Deepy is a Python CLI coding agent for DeepSeek and supported
@@ -81,6 +85,8 @@ for direct local commands.
81
85
 
82
86
  ## Quick Start
83
87
 
88
+ For the guided installation page, open **https://deepy.kirineko.tech/**.
89
+
84
90
  1. Install `uv`:
85
91
 
86
92
  ```bash
@@ -268,7 +274,7 @@ reserved_context_tokens = 50000
268
274
  compact_preserve_recent_messages = 2
269
275
 
270
276
  [ui]
271
- theme = "auto" # auto, dark, or light
277
+ theme = "dark" # dark or light
272
278
  ```
273
279
 
274
280
  Set config without the interactive wizard:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.18"
3
+ version = "0.2.20"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.2.18"
3
+ __version__ = "0.2.20"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -61,13 +61,13 @@ def _build_parser() -> argparse.ArgumentParser:
61
61
  init_parser.add_argument("--model", help="Model name.")
62
62
  init_parser.add_argument("--base-url", help="OpenAI-compatible base URL.")
63
63
  init_parser.add_argument("--thinking", help="Thinking mode for the provider.")
64
- init_parser.add_argument("--theme", default=DEFAULT_UI_THEME, help="UI theme: auto, dark, or light.")
64
+ init_parser.add_argument("--theme", default=DEFAULT_UI_THEME, help="UI theme: dark or light.")
65
65
  init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
66
66
  setup_parser = config_sub.add_parser("setup", help="Interactively configure Deepy.")
67
67
  setup_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
68
68
  config_sub.add_parser("reset", help="Delete local config and run interactive setup again.")
69
69
  theme_parser = config_sub.add_parser("theme", help="Show or update terminal UI theme.")
70
- theme_parser.add_argument("theme", nargs="?", help="Theme to save: auto, dark, or light.")
70
+ theme_parser.add_argument("theme", nargs="?", help="Theme to save: dark or light.")
71
71
 
72
72
  doctor_parser = subparsers.add_parser("doctor", help="Validate local Deepy setup.")
73
73
  doctor_parser.add_argument("--json", action="store_true", help="Print JSON diagnostics.")
@@ -381,9 +381,8 @@ def _thinking_mode_from_selection(provider: str, value: str, *, default: str) ->
381
381
 
382
382
  def _prompt_theme_value(*, default: str = DEFAULT_UI_THEME) -> str:
383
383
  print("UI theme:")
384
- print("1. auto Detect when possible; falls back to dark")
385
- print("2. dark Optimized for dark terminal backgrounds")
386
- print("3. light Optimized for light terminal backgrounds")
384
+ print("1. dark Optimized for dark terminal backgrounds")
385
+ print("2. light Optimized for light terminal backgrounds")
387
386
  value = _prompt_config_value("UI theme number", default=ui_theme_number(default))
388
387
  return ui_theme_from_selection(value, default=default)
389
388
 
@@ -417,7 +416,7 @@ def _cmd_config_theme(args: argparse.Namespace) -> int:
417
416
  print(f"resolved: {palette.name}")
418
417
  return 0
419
418
  if args.theme not in UI_THEMES:
420
- print("Invalid theme. Usage: deepy config theme [auto|dark|light]", file=sys.stderr)
419
+ print("Invalid theme. Usage: deepy config theme [dark|light]", file=sys.stderr)
421
420
  return 1
422
421
  config_path = settings.path or (args.config.expanduser() if args.config else Path.home() / ".deepy" / "config.toml")
423
422
  update_config_theme(config_path, args.theme)
@@ -16,7 +16,7 @@ DEFAULT_COMPACT_TRIGGER_RATIO = 0.8
16
16
  DEFAULT_RESERVED_CONTEXT_TOKENS = 50_000
17
17
  DEFAULT_COMPACT_PRESERVE_RECENT_MESSAGES = 2
18
18
  DEFAULT_WEB_SEARCH_SEARXNG_URL = "https://s.kirineko.tech/"
19
- DEFAULT_UI_THEME = "auto"
19
+ DEFAULT_UI_THEME = "dark"
20
20
  DEFAULT_MCP_ENABLED = True
21
21
  DEFAULT_MCP_CONNECT_TIMEOUT_SECONDS = 10.0
22
22
  DEFAULT_MCP_CLEANUP_TIMEOUT_SECONDS = 10.0
@@ -45,8 +45,8 @@ SWITCH_ONLY_THINKING_MODES = ("disabled", "enabled")
45
45
  REASONING_MODES = set(DEEPSEEK_REASONING_MODES)
46
46
  THINKING_MODES = set(DEEPSEEK_REASONING_MODES) | set(SWITCH_ONLY_THINKING_MODES) | OPENROUTER_REASONING_EFFORTS
47
47
  PROVIDERS = {"deepseek", "openrouter", "xiaomi"}
48
- UI_THEMES = {"auto", "dark", "light"}
49
- UI_THEME_OPTIONS = (("1", "auto"), ("2", "dark"), ("3", "light"))
48
+ UI_THEMES = {"dark", "light"}
49
+ UI_THEME_OPTIONS = (("1", "dark"), ("2", "light"))
50
50
 
51
51
 
52
52
  @dataclass(frozen=True)
@@ -570,6 +570,12 @@ class UiConfig:
570
570
  raw.get("input_suggestions_enabled"),
571
571
  DEFAULT_INPUT_SUGGESTIONS_ENABLED,
572
572
  )
573
+ if isinstance(theme, str) and theme.strip() == "auto":
574
+ return cls(
575
+ theme=DEFAULT_UI_THEME,
576
+ theme_configured=True,
577
+ input_suggestions_enabled=input_suggestions_enabled,
578
+ )
573
579
  if isinstance(theme, str) and theme.strip() in UI_THEMES:
574
580
  return cls(
575
581
  theme=theme.strip(),
@@ -690,7 +696,7 @@ def write_config(
690
696
  thinking_mode: str | None = None,
691
697
  ) -> None:
692
698
  if not is_valid_ui_theme(theme):
693
- raise ValueError("UI theme must be one of: auto, dark, light.")
699
+ raise ValueError("UI theme must be one of: dark, light.")
694
700
  if not is_supported_provider(provider):
695
701
  raise ValueError("Provider must be one of: deepseek, openrouter, xiaomi.")
696
702
  provider_info = provider_info_for(provider)
@@ -816,7 +822,7 @@ def update_config_model_settings(
816
822
 
817
823
  def update_config_theme(config_path: Path, theme: str) -> None:
818
824
  if not is_valid_ui_theme(theme):
819
- raise ValueError("UI theme must be one of: auto, dark, light.")
825
+ raise ValueError("UI theme must be one of: dark, light.")
820
826
  path = config_path.expanduser()
821
827
  if path.suffix == ".json":
822
828
  raise ValueError("Deepy only supports TOML config files; JSON config is not supported.")
@@ -143,7 +143,6 @@ class DeepyJsonlSession:
143
143
  state = self.context_token_state(records)
144
144
  self._touch_index(
145
145
  active_tokens=state.active_tokens,
146
- latest_context_window_tokens=state.active_tokens,
147
146
  last_usage_tokens=state.last_usage_tokens,
148
147
  pending_tokens=state.pending_tokens,
149
148
  last_usage_record_count=state.last_usage_record_count,
@@ -823,7 +823,6 @@ class DeepyTuiApp(App[None]):
823
823
  ChoiceScreen(
824
824
  "Select theme",
825
825
  [
826
- Choice("auto", "auto", "Follow saved/default theme behavior"),
827
826
  Choice("dark", "dark", "Use Textual dark theme"),
828
827
  Choice("light", "light", "Use Textual light theme"),
829
828
  ],
@@ -833,7 +832,7 @@ class DeepyTuiApp(App[None]):
833
832
  self._update_status("Theme unchanged")
834
833
  return
835
834
  if not is_valid_ui_theme(theme):
836
- await self._append_block(ErrorBlock("Usage: /theme auto|dark|light"))
835
+ await self._append_block(ErrorBlock("Usage: /theme dark|light"))
837
836
  return
838
837
  if self.settings.path is None:
839
838
  await self._append_block(ErrorBlock("Cannot persist theme: config path is unknown."))
@@ -2040,7 +2039,7 @@ def _reset_config_validation_error(result: ResetConfigResult) -> str:
2040
2039
  if not result.theme:
2041
2040
  return "Theme is required."
2042
2041
  if not is_valid_ui_theme(result.theme):
2043
- return "Usage: theme must be auto|dark|light"
2042
+ return "Usage: theme must be dark|light"
2044
2043
  return ""
2045
2044
 
2046
2045
 
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from textual.command import DiscoveryHit, Hit, Hits, Provider
8
8
 
9
+ from deepy.ui.slash_commands import slash_command_priority
10
+
9
11
  if TYPE_CHECKING:
10
12
  from deepy.tui.app import DeepyTuiApp
11
13
 
@@ -59,24 +61,34 @@ def command_by_name(name: str) -> TuiCommand | None:
59
61
  return next((command for command in TUI_COMMANDS if command.name == name), None)
60
62
 
61
63
 
64
+ def ranked_tui_commands() -> list[TuiCommand]:
65
+ return sorted(TUI_COMMANDS, key=lambda command: (slash_command_priority(command.name), command.name))
66
+
67
+
62
68
  class DeepyCommandProvider(Provider):
63
69
  async def search(self, query: str) -> Hits:
64
70
  matcher = self.matcher(query)
65
71
  app = self.app
66
- for command in TUI_COMMANDS:
72
+ matches = []
73
+ for command in ranked_tui_commands():
67
74
  candidate = f"{command.label} {command.description} {command.group}"
68
75
  score = matcher.match(candidate)
69
76
  if score > 0:
70
- yield Hit(
71
- score,
72
- matcher.highlight(command.label),
73
- partial(app.invoke_tui_command, command.name),
74
- help=f"{command.group}: {command.description}",
75
- )
77
+ matches.append((score, command))
78
+ for score, command in sorted(
79
+ matches,
80
+ key=lambda item: (-item[0], slash_command_priority(item[1].name), item[1].name),
81
+ ):
82
+ yield Hit(
83
+ score,
84
+ matcher.highlight(command.label),
85
+ partial(app.invoke_tui_command, command.name),
86
+ help=f"{command.group}: {command.description}",
87
+ )
76
88
 
77
89
  async def discover(self) -> Hits:
78
90
  app = self.app
79
- for command in TUI_COMMANDS:
91
+ for command in ranked_tui_commands():
80
92
  yield DiscoveryHit(
81
93
  command.label,
82
94
  partial(app.invoke_tui_command, command.name),
@@ -191,7 +191,7 @@ class ResetConfigScreen(ModalScreen[ResetConfigResult | None]):
191
191
  yield Input(value=self.model, placeholder="Model", id="reset-model")
192
192
  yield Input(value=self.base_url, placeholder="Base URL", id="reset-base-url")
193
193
  yield Input(value=self.thinking, placeholder="Thinking", id="reset-thinking")
194
- yield Input(value=self.theme, placeholder="Theme: auto|dark|light", id="reset-theme")
194
+ yield Input(value=self.theme, placeholder="Theme: dark|light", id="reset-theme")
195
195
  yield Footer()
196
196
 
197
197
  def on_mount(self) -> None:
@@ -35,7 +35,7 @@ from deepy.ui.message_view import (
35
35
  from deepy.ui.slash_commands import (
36
36
  SlashCommandItem,
37
37
  filter_slash_commands,
38
- format_slash_command_label,
38
+ format_slash_command_completion_label,
39
39
  )
40
40
 
41
41
 
@@ -244,7 +244,7 @@ class PromptPanel(Vertical):
244
244
  self._refresh_input_suggestion_display()
245
245
  return
246
246
  option_list.display = True
247
- option_list.add_options([Option(suggestion, id=suggestion) for suggestion in suggestions[:8]])
247
+ option_list.add_options([Option(suggestion, id=suggestion) for suggestion in suggestions])
248
248
  option_list.highlighted = 0
249
249
  self._refresh_input_suggestion_display()
250
250
 
@@ -308,10 +308,14 @@ class PromptPanel(Vertical):
308
308
  and token == text
309
309
  and not any(char.isspace() for char in token)
310
310
  ):
311
- if any(item.label == token for item in self.slash_commands):
311
+ if any(
312
+ item.label == token
313
+ or (item.kind == "skill" and f"/skill:{item.name}" == token)
314
+ for item in self.slash_commands
315
+ ):
312
316
  return []
313
317
  return [
314
- f"{format_slash_command_label(item)} {item.description}"
318
+ f"{format_slash_command_completion_label(item, token)} {item.description}"
315
319
  for item in filter_slash_commands(self.slash_commands, token)
316
320
  ]
317
321
  mention = extract_file_mention_fragment(text)
@@ -7,7 +7,7 @@ from unicodedata import normalize
7
7
 
8
8
  from prompt_toolkit import PromptSession
9
9
  from prompt_toolkit.auto_suggest import AutoSuggest, Suggestion
10
- from prompt_toolkit.completion import Completer, CompleteEvent, WordCompleter, merge_completers
10
+ from prompt_toolkit.completion import Completer, CompleteEvent, Completion, merge_completers
11
11
  from prompt_toolkit.document import Document
12
12
  from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
13
13
  from prompt_toolkit.history import FileHistory
@@ -18,7 +18,13 @@ from deepy.input_suggestions import InputSuggestionController
18
18
  from deepy.skills import SkillInfo
19
19
  from deepy.ui.file_mentions import FileMentionCompleter
20
20
  from deepy.ui.prompt_buffer import PromptBufferState
21
- from deepy.ui.slash_commands import SlashCommandItem
21
+ from deepy.ui.slash_commands import (
22
+ SlashCommandItem,
23
+ find_exact_slash_command,
24
+ format_slash_command_completion_label,
25
+ format_slash_command_description,
26
+ rank_slash_commands,
27
+ )
22
28
  from deepy.ui.status_footer import StatusFooter
23
29
  from deepy.ui.styles import DARK_PALETTE, UiPalette
24
30
 
@@ -52,11 +58,10 @@ def create_prompt_session(
52
58
  path = history_path or DEFAULT_PROMPT_HISTORY
53
59
  path.parent.mkdir(parents=True, exist_ok=True)
54
60
  path.touch(exist_ok=True)
55
- labels = [item.label for item in slash_commands or []]
56
61
  root = project_root or Path.cwd()
57
62
  completer = merge_completers(
58
63
  [
59
- WordCompleter(labels, ignore_case=True, sentence=True),
64
+ SlashCommandCompleter(slash_commands or []),
60
65
  FileMentionCompleter(root),
61
66
  ],
62
67
  deduplicate=True,
@@ -109,6 +114,38 @@ class InputSuggestionAwareCompleter(Completer):
109
114
  yield from self.completer.get_completions(document, complete_event)
110
115
 
111
116
 
117
+ class SlashCommandCompleter(Completer):
118
+ def __init__(self, slash_commands: list[SlashCommandItem]) -> None:
119
+ self.slash_commands = slash_commands
120
+
121
+ def get_completions(self, document: Document, complete_event: CompleteEvent):
122
+ del complete_event
123
+ token = _slash_token_before_cursor(document)
124
+ if token is None:
125
+ return
126
+ if find_exact_slash_command(self.slash_commands, token) is not None:
127
+ return
128
+ for item in rank_slash_commands(self.slash_commands, token):
129
+ label = format_slash_command_completion_label(item, token)
130
+ yield Completion(
131
+ label.removesuffix(" *"),
132
+ start_position=-len(token),
133
+ display=label,
134
+ display_meta=format_slash_command_description(item.description),
135
+ )
136
+
137
+
138
+ def _slash_token_before_cursor(document: Document) -> str | None:
139
+ before = document.text_before_cursor
140
+ start = len(before)
141
+ while start > 0 and not before[start - 1].isspace():
142
+ start -= 1
143
+ token = before[start:]
144
+ if not token.startswith("/") or not token:
145
+ return None
146
+ return token
147
+
148
+
112
149
  def build_prompt_key_bindings(
113
150
  *,
114
151
  on_interrupt: Callable[[], None] | None = None,
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from dataclasses import dataclass, replace
5
+ from enum import IntEnum
5
6
 
6
7
  from deepy.skills import SkillInfo
7
8
  from deepy.subagents import built_in_subagents
@@ -44,6 +45,36 @@ SUBAGENT_SLASH_COMMANDS = tuple(
44
45
  )
45
46
  BUILTIN_SLASH_COMMAND_NAMES = frozenset(item.name for item in BUILTIN_SLASH_COMMANDS)
46
47
  SUBAGENT_SLASH_COMMAND_NAMES = frozenset(item.name for item in SUBAGENT_SLASH_COMMANDS)
48
+ COMMON_WORKFLOW_COMMAND_ORDER = {
49
+ "help": 0,
50
+ "new": 1,
51
+ "resume": 2,
52
+ "model": 3,
53
+ "skills": 4,
54
+ "status": 5,
55
+ "compact": 6,
56
+ "mcp": 7,
57
+ "exit": 8,
58
+ }
59
+ LOW_FREQUENCY_COMMAND_ORDER = {
60
+ "init": 0,
61
+ "theme": 1,
62
+ "input-suggestion": 2,
63
+ "ps": 3,
64
+ "stop": 4,
65
+ "reset": 5,
66
+ }
67
+ SKILL_SCOPE_PRIORITY = {
68
+ "project": 0,
69
+ "user": 1,
70
+ "builtin": 2,
71
+ }
72
+
73
+
74
+ class SlashCommandMatch(IntEnum):
75
+ EXACT = 0
76
+ PREFIX = 1
77
+ WEAK = 2
47
78
 
48
79
 
49
80
  def build_slash_commands(
@@ -65,16 +96,79 @@ def build_slash_commands(
65
96
 
66
97
 
67
98
  def filter_slash_commands(items: list[SlashCommandItem], token: str) -> list[SlashCommandItem]:
99
+ return rank_slash_commands(items, token)
100
+
101
+
102
+ def rank_slash_commands(items: list[SlashCommandItem], token: str) -> list[SlashCommandItem]:
68
103
  if not token.startswith("/"):
69
104
  return []
70
105
  query = token[1:].lower()
71
106
  if not query:
72
- return items
73
- return [
74
- item
107
+ return sorted(items, key=slash_command_sort_key)
108
+ scored = [
109
+ (match, slash_command_sort_key(item), item)
75
110
  for item in items
76
- if item.name.lower().startswith(query) or item.label[1:].lower().startswith(query)
111
+ if (match := slash_command_match(item, query)) is not None
77
112
  ]
113
+ scored.sort(key=lambda scored_item: (scored_item[0], scored_item[1]))
114
+ return [item for _match, _sort_key, item in scored]
115
+
116
+
117
+ def slash_command_match(item: SlashCommandItem, query: str) -> SlashCommandMatch | None:
118
+ normalized = query.lower()
119
+ name = item.name.lower()
120
+ label = item.label[1:].lower()
121
+ description = item.description.lower()
122
+ legacy_skill_name = f"skill:{name}" if item.kind == "skill" else ""
123
+ values = [name, label]
124
+ if legacy_skill_name:
125
+ values.append(legacy_skill_name)
126
+ if any(value == normalized for value in values):
127
+ return SlashCommandMatch.EXACT
128
+ if any(value.startswith(normalized) for value in values):
129
+ return SlashCommandMatch.PREFIX
130
+ if normalized in name or normalized in label or normalized in description:
131
+ return SlashCommandMatch.WEAK
132
+ return None
133
+
134
+
135
+ def slash_command_sort_key(item: SlashCommandItem) -> tuple[int, int, int, str]:
136
+ return (
137
+ slash_command_priority(item),
138
+ slash_command_loaded_priority(item),
139
+ slash_command_scope_priority(item),
140
+ item.name.lower(),
141
+ )
142
+
143
+
144
+ def slash_command_priority(item: SlashCommandItem | str) -> int:
145
+ if isinstance(item, str):
146
+ name = item
147
+ kind = "builtin"
148
+ else:
149
+ name = item.name
150
+ kind = item.kind
151
+ if name in COMMON_WORKFLOW_COMMAND_ORDER:
152
+ return COMMON_WORKFLOW_COMMAND_ORDER[name]
153
+ if kind == "subagent":
154
+ return 100
155
+ if kind == "skill":
156
+ return 200
157
+ if name in LOW_FREQUENCY_COMMAND_ORDER:
158
+ return 300 + LOW_FREQUENCY_COMMAND_ORDER[name]
159
+ return 250
160
+
161
+
162
+ def slash_command_loaded_priority(item: SlashCommandItem) -> int:
163
+ if item.kind == "skill" and item.skill and item.skill.is_loaded:
164
+ return 0
165
+ return 1
166
+
167
+
168
+ def slash_command_scope_priority(item: SlashCommandItem) -> int:
169
+ if item.kind != "skill" or item.skill is None:
170
+ return 0
171
+ return SKILL_SCOPE_PRIORITY.get(item.skill.scope, 3)
78
172
 
79
173
 
80
174
  def find_exact_slash_command(
@@ -102,6 +196,14 @@ def format_slash_command_label(item: SlashCommandItem) -> str:
102
196
  return f"{item.label} *" if item.kind == "skill" and loaded else item.label
103
197
 
104
198
 
199
+ def format_slash_command_completion_label(item: SlashCommandItem, token: str = "") -> str:
200
+ label = item.label
201
+ if item.kind == "skill" and token[1:].lower().startswith("skill:"):
202
+ label = f"/skill:{item.name}"
203
+ loaded = bool(item.skill and item.skill.is_loaded)
204
+ return f"{label} *" if item.kind == "skill" and loaded else label
205
+
206
+
105
207
  def is_builtin_slash_command(name: str) -> bool:
106
208
  return name in BUILTIN_SLASH_COMMAND_NAMES
107
209
 
@@ -83,7 +83,7 @@ def _help_parts(help_text: str) -> list[tuple[FooterPartRole, str]]:
83
83
 
84
84
 
85
85
  def _known_title(text: str) -> str | None:
86
- for title in ("provider", "model", "cwd", "mcp", "bg", "ctx", "newline"):
86
+ for title in ("provider", "model", "cwd", "mcp", "update", "bg", "ctx", "newline"):
87
87
  if text == title or text.startswith(f"{title} ") or text.startswith(f"{title}:"):
88
88
  return title
89
89
  return None