deepy-cli 0.2.18__tar.gz → 0.2.19__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.19}/PKG-INFO +8 -2
  2. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/README.md +7 -1
  3. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/pyproject.toml +1 -1
  4. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/sessions/jsonl.py +0 -1
  6. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/commands.py +20 -8
  7. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/widgets.py +8 -4
  8. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/prompt_input.py +41 -4
  9. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/slash_commands.py +106 -4
  10. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/terminal.py +98 -1
  11. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/__main__.py +0 -0
  12. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/background_tasks.py +0 -0
  13. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/cli.py +0 -0
  14. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/config/__init__.py +0 -0
  15. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/config/settings.py +0 -0
  16. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/__init__.py +0 -0
  17. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
  18. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
  19. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  20. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/Search.md +0 -0
  21. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/WebFetch.md +0 -0
  22. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/WebSearch.md +0 -0
  23. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/__init__.py +0 -0
  24. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/apply_patch.md +0 -0
  25. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/edit_text.md +0 -0
  26. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/read_file.md +0 -0
  27. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/shell.md +0 -0
  28. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/task_list.md +0 -0
  29. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/task_output.md +0 -0
  30. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/task_stop.md +0 -0
  31. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/test_shell.md +0 -0
  32. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/todo_write.md +0 -0
  33. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/data/tools/write_file.md +0 -0
  34. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/errors.py +0 -0
  35. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/input_suggestions.py +0 -0
  36. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/__init__.py +0 -0
  37. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/agent.py +0 -0
  38. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/compaction.py +0 -0
  39. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/context.py +0 -0
  40. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/events.py +0 -0
  41. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/model_capabilities.py +0 -0
  42. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/provider.py +0 -0
  43. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/replay.py +0 -0
  44. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/runner.py +0 -0
  45. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/llm/thinking.py +0 -0
  46. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/mcp.py +0 -0
  47. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/__init__.py +0 -0
  48. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/compact.py +0 -0
  49. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/init_agents.py +0 -0
  50. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/rules.py +0 -0
  51. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/runtime_context.py +0 -0
  52. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/system.py +0 -0
  53. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/prompts/tool_docs.py +0 -0
  54. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/session_cost.py +0 -0
  55. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/sessions/__init__.py +0 -0
  56. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/sessions/manager.py +0 -0
  57. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/skill_market.py +0 -0
  58. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/skills.py +0 -0
  59. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/status.py +0 -0
  60. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/subagents.py +0 -0
  61. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/todos.py +0 -0
  62. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/__init__.py +0 -0
  63. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/agents.py +0 -0
  64. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/builtin.py +0 -0
  65. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/file_state.py +0 -0
  66. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/result.py +0 -0
  67. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/search.py +0 -0
  68. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/shell_output.py +0 -0
  69. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/shell_utils.py +0 -0
  70. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tools/test_shell.py +0 -0
  71. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/__init__.py +0 -0
  72. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/app.py +0 -0
  73. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/compat.py +0 -0
  74. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/diff.py +0 -0
  75. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/runner.py +0 -0
  76. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/screens.py +0 -0
  77. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/tui/state.py +0 -0
  78. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/types/__init__.py +0 -0
  79. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/types/sdk.py +0 -0
  80. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/types/tool_payloads.py +0 -0
  81. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/__init__.py +0 -0
  82. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/app.py +0 -0
  83. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/ask_user_question.py +0 -0
  84. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/exit_summary.py +0 -0
  85. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/file_mentions.py +0 -0
  86. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/loading_text.py +0 -0
  87. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/local_command.py +0 -0
  88. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/markdown.py +0 -0
  89. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/message_view.py +0 -0
  90. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/model_picker.py +0 -0
  91. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/prompt_buffer.py +0 -0
  92. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/session_list.py +0 -0
  93. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/session_picker.py +0 -0
  94. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/skill_picker.py +0 -0
  95. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/status_footer.py +0 -0
  96. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/styles.py +0 -0
  97. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/theme_picker.py +0 -0
  98. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/thinking_state.py +0 -0
  99. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/ui/welcome.py +0 -0
  100. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/update_check.py +0 -0
  101. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/usage.py +0 -0
  102. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/utils/__init__.py +0 -0
  103. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/utils/debug_logger.py +0 -0
  104. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/utils/error_logger.py +0 -0
  105. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/src/deepy/utils/json.py +0 -0
  106. {deepy_cli-0.2.18 → deepy_cli-0.2.19}/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.19
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.2.18"
3
+ version = "0.2.19"
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.19"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -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,
@@ -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),
@@ -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
 
@@ -2819,6 +2819,13 @@ class _SilentStatus:
2819
2819
  return None
2820
2820
 
2821
2821
 
2822
+ @dataclass(frozen=True)
2823
+ class _RuntimeStatusSegments:
2824
+ prefix: str
2825
+ label: str = ""
2826
+ payload: str = ""
2827
+
2828
+
2822
2829
  def _terminal_runtime_status_style(palette: UiPalette) -> str:
2823
2830
  foreground = _hex_color(palette.toolbar_background) or "#161821"
2824
2831
  background = _hex_color(palette.warning) or "#facc15"
@@ -2851,6 +2858,18 @@ def _ansi_rgb(prefix: str, color: str) -> str:
2851
2858
  return f"{prefix};2;{red};{green};{blue}"
2852
2859
 
2853
2860
 
2861
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
2862
+ _CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
2863
+ _STATUS_SEPARATOR = " · "
2864
+
2865
+
2866
+ def _sanitize_status_line(text: str) -> str:
2867
+ stripped = _ANSI_ESCAPE_RE.sub("", text)
2868
+ stripped = stripped.replace("\r", " ").replace("\n", " ").replace("\t", " ")
2869
+ stripped = _CONTROL_CHAR_RE.sub("", stripped)
2870
+ return re.sub(r" {2,}", " ", stripped).strip()
2871
+
2872
+
2854
2873
  def _truncate_status_line(text: str, *, max_width: int) -> str:
2855
2874
  if cell_len(text) <= max_width:
2856
2875
  return text
@@ -2870,10 +2889,88 @@ def _truncate_status_line(text: str, *, max_width: int) -> str:
2870
2889
 
2871
2890
 
2872
2891
  def _fit_status_line(text: str, *, width: int) -> str:
2873
- line = _truncate_status_line(text, max_width=max(width, 0))
2892
+ width = max(width, 0)
2893
+ sanitized = _sanitize_status_line(text)
2894
+ segments = _parse_runtime_status_segments(sanitized)
2895
+ line = (
2896
+ _fit_runtime_status_segments(segments, width=width)
2897
+ if segments is not None
2898
+ else _truncate_status_line(sanitized, max_width=width)
2899
+ )
2874
2900
  return line + (" " * max(0, width - cell_len(line)))
2875
2901
 
2876
2902
 
2903
+ def _parse_runtime_status_segments(text: str) -> _RuntimeStatusSegments | None:
2904
+ interrupt = "esc to interrupt"
2905
+ interrupt_index = text.find(interrupt)
2906
+ if interrupt_index < 0:
2907
+ return None
2908
+
2909
+ prefix_end = interrupt_index + len(interrupt)
2910
+ prefix = text[:prefix_end].strip()
2911
+ detail = text[prefix_end:]
2912
+ if detail.startswith(_STATUS_SEPARATOR):
2913
+ detail = detail[len(_STATUS_SEPARATOR) :].strip()
2914
+ else:
2915
+ detail = detail.strip()
2916
+ if not detail:
2917
+ return _RuntimeStatusSegments(prefix=prefix)
2918
+
2919
+ if detail.startswith(f"local command{_STATUS_SEPARATOR}"):
2920
+ payload = detail.removeprefix(f"local command{_STATUS_SEPARATOR}").strip()
2921
+ return _RuntimeStatusSegments(prefix=prefix, label="local command", payload=payload)
2922
+
2923
+ tool_match = re.match(r"(tool \[[^\]]+\])(?:\s+(.*))?$", detail)
2924
+ if tool_match:
2925
+ label, payload = tool_match.groups()
2926
+ return _RuntimeStatusSegments(prefix=prefix, label=label, payload=(payload or "").strip())
2927
+
2928
+ return _RuntimeStatusSegments(prefix=prefix, label=detail)
2929
+
2930
+
2931
+ def _fit_runtime_status_segments(segments: _RuntimeStatusSegments, *, width: int) -> str:
2932
+ if width <= 0:
2933
+ return ""
2934
+
2935
+ full = _runtime_status_segments_text(segments)
2936
+ if cell_len(full) <= width:
2937
+ return full
2938
+
2939
+ if cell_len(segments.prefix) >= width:
2940
+ return _truncate_status_line(segments.prefix, max_width=width)
2941
+
2942
+ if not segments.label:
2943
+ return _truncate_status_line(segments.prefix, max_width=width)
2944
+
2945
+ prefix_label = f"{segments.prefix}{_STATUS_SEPARATOR}{segments.label}"
2946
+ if segments.payload:
2947
+ base = f"{prefix_label}{_STATUS_SEPARATOR}"
2948
+ payload_width = width - cell_len(base)
2949
+ if payload_width > 0:
2950
+ payload = _truncate_status_line(segments.payload, max_width=payload_width)
2951
+ return f"{base}{payload}".rstrip()
2952
+
2953
+ if cell_len(prefix_label) <= width:
2954
+ return prefix_label
2955
+
2956
+ label_base = f"{segments.prefix}{_STATUS_SEPARATOR}"
2957
+ label_width = width - cell_len(label_base)
2958
+ if label_width > 0:
2959
+ label = _truncate_status_line(segments.label, max_width=label_width)
2960
+ return f"{label_base}{label}".rstrip()
2961
+
2962
+ return _truncate_status_line(segments.prefix, max_width=width)
2963
+
2964
+
2965
+ def _runtime_status_segments_text(segments: _RuntimeStatusSegments) -> str:
2966
+ parts = [segments.prefix]
2967
+ if segments.label:
2968
+ parts.append(segments.label)
2969
+ if segments.payload:
2970
+ parts.append(segments.payload)
2971
+ return _STATUS_SEPARATOR.join(parts)
2972
+
2973
+
2877
2974
  def _working_status_text(
2878
2975
  started_at: float,
2879
2976
  detail: str = "",
File without changes
File without changes