ripperdoc 0.2.7__py3-none-any.whl → 0.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import Any, List, Literal
6
+ from typing import Any, Dict, List, Literal
7
7
 
8
8
  from rich.markup import escape
9
9
  from rich.panel import Panel
10
10
  from rich.table import Table
11
11
 
12
12
  from ripperdoc.core.config import (
13
- config_manager,
13
+ GlobalConfig,
14
+ ProjectConfig,
15
+ ProjectLocalConfig,
14
16
  get_global_config,
15
17
  get_project_config,
16
18
  get_project_local_config,
@@ -55,14 +57,14 @@ def _get_rules_for_scope(
55
57
  ) -> tuple[List[str], List[str]]:
56
58
  """Return (allow_rules, deny_rules) for a given scope."""
57
59
  if scope == "user":
58
- config = get_global_config()
59
- return list(config.user_allow_rules), list(config.user_deny_rules)
60
+ user_config: GlobalConfig = get_global_config()
61
+ return list(user_config.user_allow_rules), list(user_config.user_deny_rules)
60
62
  elif scope == "project":
61
- config = get_project_config(project_path)
62
- return list(config.bash_allow_rules), list(config.bash_deny_rules)
63
+ project_config: ProjectConfig = get_project_config(project_path)
64
+ return list(project_config.bash_allow_rules), list(project_config.bash_deny_rules)
63
65
  else: # local
64
- config = get_project_local_config(project_path)
65
- return list(config.local_allow_rules), list(config.local_deny_rules)
66
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
67
+ return list(local_config.local_allow_rules), list(local_config.local_deny_rules)
66
68
 
67
69
 
68
70
  def _add_rule(
@@ -73,26 +75,26 @@ def _add_rule(
73
75
  ) -> bool:
74
76
  """Add a rule to the specified scope. Returns True if added, False if already exists."""
75
77
  if scope == "user":
76
- config = get_global_config()
77
- rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
78
+ user_config: GlobalConfig = get_global_config()
79
+ rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
78
80
  if rule in rules:
79
81
  return False
80
82
  rules.append(rule)
81
- save_global_config(config)
83
+ save_global_config(user_config)
82
84
  elif scope == "project":
83
- config = get_project_config(project_path)
84
- rules = config.bash_allow_rules if rule_type == "allow" else config.bash_deny_rules
85
+ project_config: ProjectConfig = get_project_config(project_path)
86
+ rules = project_config.bash_allow_rules if rule_type == "allow" else project_config.bash_deny_rules
85
87
  if rule in rules:
86
88
  return False
87
89
  rules.append(rule)
88
- save_project_config(config, project_path)
90
+ save_project_config(project_config, project_path)
89
91
  else: # local
90
- config = get_project_local_config(project_path)
91
- rules = config.local_allow_rules if rule_type == "allow" else config.local_deny_rules
92
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
93
+ rules = local_config.local_allow_rules if rule_type == "allow" else local_config.local_deny_rules
92
94
  if rule in rules:
93
95
  return False
94
96
  rules.append(rule)
95
- save_project_local_config(config, project_path)
97
+ save_project_local_config(local_config, project_path)
96
98
  return True
97
99
 
98
100
 
@@ -104,26 +106,26 @@ def _remove_rule(
104
106
  ) -> bool:
105
107
  """Remove a rule from the specified scope. Returns True if removed, False if not found."""
106
108
  if scope == "user":
107
- config = get_global_config()
108
- rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
109
+ user_config: GlobalConfig = get_global_config()
110
+ rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
109
111
  if rule not in rules:
110
112
  return False
111
113
  rules.remove(rule)
112
- save_global_config(config)
114
+ save_global_config(user_config)
113
115
  elif scope == "project":
114
- config = get_project_config(project_path)
115
- rules = config.bash_allow_rules if rule_type == "allow" else config.bash_deny_rules
116
+ project_config: ProjectConfig = get_project_config(project_path)
117
+ rules = project_config.bash_allow_rules if rule_type == "allow" else project_config.bash_deny_rules
116
118
  if rule not in rules:
117
119
  return False
118
120
  rules.remove(rule)
119
- save_project_config(config, project_path)
121
+ save_project_config(project_config, project_path)
120
122
  else: # local
121
- config = get_project_local_config(project_path)
122
- rules = config.local_allow_rules if rule_type == "allow" else config.local_deny_rules
123
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
124
+ rules = local_config.local_allow_rules if rule_type == "allow" else local_config.local_deny_rules
123
125
  if rule not in rules:
124
126
  return False
125
127
  rules.remove(rule)
126
- save_project_local_config(config, project_path)
128
+ save_project_local_config(local_config, project_path)
127
129
  return True
128
130
 
129
131
 
@@ -204,7 +206,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
204
206
 
205
207
  # Parse command
206
208
  action = args[0].lower()
207
- scope_aliases = {
209
+ scope_aliases: Dict[str, ScopeType] = {
208
210
  "user": "user",
209
211
  "global": "user",
210
212
  "project": "project",
@@ -215,8 +217,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
215
217
 
216
218
  # Single scope display
217
219
  if action in scope_aliases:
218
- scope = scope_aliases[action]
219
- _render_scope_rules(ui.console, scope, project_path) # type: ignore
220
+ display_scope: ScopeType = scope_aliases[action]
221
+ _render_scope_rules(ui.console, display_scope, project_path)
220
222
  return True
221
223
 
222
224
  # Add rule
@@ -231,14 +233,14 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
231
233
  ui.console.print(f"[red]Unknown scope: {escape(scope_arg)}[/red]")
232
234
  ui.console.print("[dim]Available scopes: user, project, local[/dim]")
233
235
  return True
234
- scope: ScopeType = scope_aliases[scope_arg] # type: ignore
236
+ scope: ScopeType = scope_aliases[scope_arg]
235
237
 
236
238
  rule_type_arg = args[2].lower()
237
239
  if rule_type_arg not in ("allow", "deny"):
238
240
  ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
239
241
  ui.console.print("[dim]Available types: allow, deny[/dim]")
240
242
  return True
241
- rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore
243
+ rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore[assignment]
242
244
 
243
245
  rule = " ".join(args[3:])
244
246
  if _add_rule(scope, rule_type, rule, project_path):
@@ -271,13 +273,13 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
271
273
  ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
272
274
  ui.console.print("[dim]Available types: allow, deny[/dim]")
273
275
  return True
274
- rule_type = rule_type_arg # type: ignore
276
+ remove_rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore[assignment]
275
277
 
276
278
  rule = " ".join(args[3:])
277
- if _remove_rule(scope, rule_type, rule, project_path):
279
+ if _remove_rule(scope, remove_rule_type, rule, project_path):
278
280
  ui.console.print(
279
281
  Panel(
280
- f"Removed [{'green' if rule_type == 'allow' else 'red'}]{rule_type}[/] rule from {scope}:\n{escape(rule)}",
282
+ f"Removed [{'green' if remove_rule_type == 'allow' else 'red'}]{remove_rule_type}[/] rule from {scope}:\n{escape(rule)}",
281
283
  title="/permissions",
282
284
  )
283
285
  )
@@ -61,7 +61,7 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
61
61
  nav_hints.append("'n' for next page")
62
62
  nav_hints.append("Enter to cancel")
63
63
 
64
- prompt = f"\nSelect session index"
64
+ prompt = "\nSelect session index"
65
65
  if nav_hints:
66
66
  prompt += f" ({', '.join(nav_hints)})"
67
67
  prompt += ": "
@@ -5,7 +5,7 @@ Supports recursive search across the entire project.
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
- from typing import Any, Iterable, List
8
+ from typing import Any, Iterable, List, Set
9
9
 
10
10
  from prompt_toolkit.completion import Completer, Completion
11
11
 
@@ -40,7 +40,7 @@ class FileMentionCompleter(Completer):
40
40
  """
41
41
  files = []
42
42
 
43
- def _walk(current_dir: Path, depth: int):
43
+ def _walk(current_dir: Path, depth: int) -> None:
44
44
  if depth > max_depth:
45
45
  return
46
46
 
@@ -89,6 +89,13 @@ class FileMentionCompleter(Completer):
89
89
 
90
90
  try:
91
91
  matches = []
92
+ seen: Set[str] = set()
93
+
94
+ def _add_match(display_path: str, item: Path, meta: str, score: int) -> None:
95
+ if display_path in seen:
96
+ return
97
+ seen.add(display_path)
98
+ matches.append((display_path, item, meta, score))
92
99
 
93
100
  # If query contains path separator, do directory-based search
94
101
  if "/" in query or "\\" in query:
@@ -117,7 +124,7 @@ class FileMentionCompleter(Completer):
117
124
  # Right side: show type only
118
125
  meta = "📁 directory" if item.is_dir() else "📄 file"
119
126
 
120
- matches.append((display_path, item, meta, 0))
127
+ _add_match(display_path, item, meta, 0)
121
128
  except ValueError:
122
129
  continue
123
130
  else:
@@ -146,7 +153,7 @@ class FileMentionCompleter(Completer):
146
153
  # Right side: show type only
147
154
  meta = "📁 directory" if item.is_dir() else "📄 file"
148
155
 
149
- matches.append((display_path, item, meta, 0))
156
+ _add_match(display_path, item, meta, 0)
150
157
  except ValueError:
151
158
  continue
152
159
  else:
@@ -170,13 +177,61 @@ class FileMentionCompleter(Completer):
170
177
 
171
178
  # Right side: show type only
172
179
  meta = "📁 directory" if item.is_dir() else "📄 file"
173
- matches.append((display_path, item, meta, 0))
180
+ _add_match(display_path, item, meta, 0)
174
181
  except ValueError:
175
182
  continue
176
183
  else:
184
+ # First, suggest top-level entries that match the prefix to support step-by-step navigation
185
+ query_lower = query.lower()
186
+ for item in sorted(self.project_path.iterdir()):
187
+ if should_skip_path(
188
+ item,
189
+ self.project_path,
190
+ ignore_filter=self.ignore_filter,
191
+ skip_hidden=True,
192
+ ):
193
+ continue
194
+
195
+ name_lower = item.name.lower()
196
+ if query_lower in name_lower:
197
+ score = 500
198
+ if name_lower.startswith(query_lower):
199
+ score += 50
200
+ if name_lower == query_lower:
201
+ score += 100
202
+
203
+ rel_path = item.relative_to(self.project_path)
204
+ display_path = str(rel_path)
205
+ if item.is_dir():
206
+ display_path += "/"
207
+
208
+ meta = "📁 directory" if item.is_dir() else "📄 file"
209
+ _add_match(display_path, item, meta, score)
210
+
211
+ # If the query exactly matches a directory, also surface its children for quicker drilling
212
+ dir_candidate = self.project_path / query
213
+ if dir_candidate.exists() and dir_candidate.is_dir():
214
+ for item in sorted(dir_candidate.iterdir()):
215
+ if should_skip_path(
216
+ item,
217
+ self.project_path,
218
+ ignore_filter=self.ignore_filter,
219
+ skip_hidden=True,
220
+ ):
221
+ continue
222
+
223
+ try:
224
+ rel_path = item.relative_to(self.project_path)
225
+ display_path = str(rel_path)
226
+ if item.is_dir():
227
+ display_path += "/"
228
+ meta = "📁 directory" if item.is_dir() else "📄 file"
229
+ _add_match(display_path, item, meta, 400)
230
+ except ValueError:
231
+ continue
232
+
177
233
  # Recursively search for files matching the query
178
234
  all_files = self._collect_files_recursive(self.project_path)
179
- query_lower = query.lower()
180
235
 
181
236
  for file_path in all_files:
182
237
  try:
@@ -198,7 +253,7 @@ class FileMentionCompleter(Completer):
198
253
  # Right side: show type only
199
254
  meta = "📄 file"
200
255
 
201
- matches.append((display_path, file_path, meta, score))
256
+ _add_match(display_path, file_path, meta, score)
202
257
  except ValueError:
203
258
  continue
204
259
 
@@ -20,7 +20,7 @@ INTERRUPT_KEYS: Set[str] = {'\x1b', '\x03'} # ESC, Ctrl+C
20
20
  class InterruptHandler:
21
21
  """Handles keyboard interrupt detection during async operations."""
22
22
 
23
- def __init__(self):
23
+ def __init__(self) -> None:
24
24
  """Initialize the interrupt handler."""
25
25
  self._query_interrupted: bool = False
26
26
  self._esc_listener_active: bool = False
@@ -6,7 +6,7 @@ This module handles rendering conversation messages to the terminal, including:
6
6
  - Reasoning block rendering
7
7
  """
8
8
 
9
- from typing import Any, Callable, Dict, List, Optional, Tuple, Union
9
+ from typing import Any, Callable, List, Optional, Tuple, Union
10
10
 
11
11
  from rich.console import Console
12
12
  from rich.markdown import Markdown
@@ -12,6 +12,7 @@ from rich.text import Text
12
12
  from rich import box
13
13
 
14
14
  from ripperdoc import __version__
15
+ from ripperdoc.cli.ui.helpers import get_profile_for_pointer
15
16
 
16
17
 
17
18
  def create_welcome_panel() -> Panel:
@@ -35,16 +36,18 @@ You can read files, edit code, run commands, and help with various programming t
35
36
 
36
37
 
37
38
  def create_status_bar() -> Text:
38
- """Create a status bar text for display."""
39
- text = Text()
40
- text.append("Type ", style="dim")
41
- text.append("/help", style="cyan")
42
- text.append(" for commands | ", style="dim")
43
- text.append("ESC", style="cyan")
44
- text.append(" to interrupt | ", style="dim")
45
- text.append("Ctrl+C", style="cyan")
46
- text.append(" to exit", style="dim")
47
- return text
39
+ """Create a status bar with current model information."""
40
+ profile = get_profile_for_pointer("main")
41
+ model_name = profile.model if profile else "Not configured"
42
+
43
+ status_text = Text()
44
+ status_text.append("Ripperdoc", style="bold cyan")
45
+ status_text.append(" ")
46
+ status_text.append(model_name, style="dim")
47
+ status_text.append(" ")
48
+ status_text.append("Ready", style="green")
49
+
50
+ return status_text
48
51
 
49
52
 
50
53
  def print_shortcuts(console: Console) -> None:
@@ -4,37 +4,37 @@ This module provides a clean, minimal terminal UI using Rich for the Ripperdoc a
4
4
  """
5
5
 
6
6
  import asyncio
7
- import contextlib
8
7
  import json
9
- import os
10
8
  import sys
11
9
  import uuid
12
- import re
13
10
  from typing import List, Dict, Any, Optional, Union, Iterable
14
11
  from pathlib import Path
15
12
 
16
13
  from rich.console import Console
17
- from rich.markdown import Markdown
18
14
  from rich.markup import escape
19
15
 
20
16
  from prompt_toolkit import PromptSession
21
17
  from prompt_toolkit.completion import Completer, Completion, merge_completers
22
- from prompt_toolkit.shortcuts.prompt import CompleteStyle
23
18
  from prompt_toolkit.history import InMemoryHistory
24
19
  from prompt_toolkit.key_binding import KeyBindings
25
- from prompt_toolkit.document import Document
20
+ from prompt_toolkit.shortcuts.prompt import CompleteStyle
26
21
 
27
22
  from ripperdoc.core.config import get_global_config, provider_protocol
28
23
  from ripperdoc.core.default_tools import get_default_tools
29
24
  from ripperdoc.core.query import query, QueryContext
30
25
  from ripperdoc.core.system_prompt import build_system_prompt
31
26
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
27
+ from ripperdoc.core.hooks.manager import hook_manager
32
28
  from ripperdoc.cli.commands import (
33
29
  get_slash_command,
30
+ get_custom_command,
34
31
  list_slash_commands,
32
+ list_custom_commands,
35
33
  slash_command_completions,
34
+ expand_command_content,
35
+ CustomCommandDefinition,
36
36
  )
37
- from ripperdoc.cli.ui.helpers import get_profile_for_pointer, THINKING_WORDS
37
+ from ripperdoc.cli.ui.helpers import get_profile_for_pointer
38
38
  from ripperdoc.core.permissions import make_permission_checker
39
39
  from ripperdoc.cli.ui.spinner import Spinner
40
40
  from ripperdoc.cli.ui.thinking_spinner import ThinkingSpinner
@@ -72,7 +72,6 @@ from ripperdoc.utils.messages import (
72
72
  AssistantMessage,
73
73
  ProgressMessage,
74
74
  create_user_message,
75
- create_assistant_message,
76
75
  )
77
76
  from ripperdoc.utils.log import enable_session_file_logging, get_logger
78
77
  from ripperdoc.utils.path_ignore import build_ignore_filter
@@ -113,7 +112,7 @@ class RichUI:
113
112
  self._current_tool: Optional[str] = None
114
113
  self._should_exit: bool = False
115
114
  self.command_list = list_slash_commands()
116
- self._command_completions = slash_command_completions()
115
+ self._custom_command_list = list_custom_commands()
117
116
  self._prompt_session: Optional[PromptSession] = None
118
117
  self.project_path = Path.cwd()
119
118
  # Track a stable session identifier for the current UI run.
@@ -162,6 +161,17 @@ class RichUI:
162
161
  extra={"session_id": self.session_id},
163
162
  )
164
163
 
164
+ # Initialize hook manager with project context
165
+ hook_manager.set_project_dir(self.project_path)
166
+ hook_manager.set_session_id(self.session_id)
167
+ logger.debug(
168
+ "[ui] Initialized hook manager",
169
+ extra={
170
+ "session_id": self.session_id,
171
+ "project_path": str(self.project_path),
172
+ },
173
+ )
174
+
165
175
  # ─────────────────────────────────────────────────────────────────────────────
166
176
  # Properties for backward compatibility with interrupt handler
167
177
  # ─────────────────────────────────────────────────────────────────────────────
@@ -452,7 +462,7 @@ class RichUI:
452
462
  "tokens_saved": result.tokens_saved,
453
463
  },
454
464
  )
455
- return result.messages # type: ignore[return-value]
465
+ return result.messages
456
466
  elif isinstance(result, CompactionError):
457
467
  logger.warning(
458
468
  "[ui] Auto-compaction failed: %s",
@@ -789,8 +799,9 @@ class RichUI:
789
799
  """Public wrapper for running coroutines on the UI event loop."""
790
800
  return self._run_async(coro)
791
801
 
792
- def handle_slash_command(self, user_input: str) -> bool:
793
- """Handle slash commands. Returns True if the input was handled."""
802
+ def handle_slash_command(self, user_input: str) -> bool | str:
803
+ """Handle slash commands. Returns True if handled as built-in, False if not a command,
804
+ or a string if it's a custom command that should be sent to the AI."""
794
805
 
795
806
  if not user_input.startswith("/"):
796
807
  return False
@@ -802,12 +813,32 @@ class RichUI:
802
813
 
803
814
  command_name = parts[0].lower()
804
815
  trimmed_arg = " ".join(parts[1:]).strip()
816
+
817
+ # First, try built-in commands
805
818
  command = get_slash_command(command_name)
806
- if command is None:
807
- self.console.print(f"[red]Unknown command: {escape(command_name)}[/red]")
808
- return True
819
+ if command is not None:
820
+ return command.handler(self, trimmed_arg)
821
+
822
+ # Then, try custom commands
823
+ custom_cmd = get_custom_command(command_name, self.project_path)
824
+ if custom_cmd is not None:
825
+ # Expand the custom command content
826
+ expanded_content = expand_command_content(
827
+ custom_cmd, trimmed_arg, self.project_path
828
+ )
809
829
 
810
- return command.handler(self, trimmed_arg)
830
+ # Show a hint that this is from a custom command
831
+ self.console.print(
832
+ f"[dim]Running custom command: /{command_name}[/dim]"
833
+ )
834
+ if custom_cmd.argument_hint and trimmed_arg:
835
+ self.console.print(f"[dim]Arguments: {trimmed_arg}[/dim]")
836
+
837
+ # Return the expanded content to be processed as a query
838
+ return expanded_content
839
+
840
+ self.console.print(f"[red]Unknown command: {escape(command_name)}[/red]")
841
+ return True
811
842
 
812
843
  def get_prompt_session(self) -> PromptSession:
813
844
  """Create (or return) the prompt session with command completion."""
@@ -815,35 +846,68 @@ class RichUI:
815
846
  return self._prompt_session
816
847
 
817
848
  class SlashCommandCompleter(Completer):
818
- """Autocomplete for slash commands."""
849
+ """Autocomplete for slash commands including custom commands."""
819
850
 
820
- def __init__(self, completions: List):
821
- self.completions = completions
851
+ def __init__(self, project_path: Path):
852
+ self.project_path = project_path
822
853
 
823
854
  def get_completions(self, document: Any, complete_event: Any) -> Iterable[Completion]:
824
855
  text = document.text_before_cursor
825
856
  if not text.startswith("/"):
826
857
  return
827
858
  query = text[1:]
828
- for name, cmd in self.completions:
859
+ # Get completions including custom commands
860
+ completions = slash_command_completions(self.project_path)
861
+ for name, cmd in completions:
829
862
  if name.startswith(query):
863
+ # Handle both SlashCommand and CustomCommandDefinition
864
+ description = cmd.description
865
+ # Add hint for custom commands
866
+ if isinstance(cmd, CustomCommandDefinition):
867
+ hint = cmd.argument_hint or ""
868
+ display = f"{name} {hint}".strip() if hint else name
869
+ display_meta = f"[custom] {description}"
870
+ else:
871
+ display = name
872
+ display_meta = description
830
873
  yield Completion(
831
874
  name,
832
875
  start_position=-len(query),
833
- display=name,
834
- display_meta=cmd.description,
876
+ display=display,
877
+ display_meta=display_meta,
835
878
  )
836
879
 
837
880
  # Merge both completers
838
- slash_completer = SlashCommandCompleter(self._command_completions)
881
+ slash_completer = SlashCommandCompleter(self.project_path)
839
882
  file_completer = FileMentionCompleter(self.project_path, self._ignore_filter)
840
883
  combined_completer = merge_completers([slash_completer, file_completer])
841
884
 
885
+ key_bindings = KeyBindings()
886
+
887
+ @key_bindings.add("enter")
888
+ def _(event: Any) -> None:
889
+ """Accept completion if menu is open; otherwise submit line."""
890
+ buf = event.current_buffer
891
+ if buf.complete_state and buf.complete_state.current_completion:
892
+ buf.apply_completion(buf.complete_state.current_completion)
893
+ return
894
+ buf.validate_and_handle()
895
+
896
+ @key_bindings.add("tab")
897
+ def _(event: Any) -> None:
898
+ """Use Tab to accept the highlighted completion when visible."""
899
+ buf = event.current_buffer
900
+ if buf.complete_state and buf.complete_state.current_completion:
901
+ buf.apply_completion(buf.complete_state.current_completion)
902
+ else:
903
+ buf.start_completion(select_first=True)
904
+
842
905
  self._prompt_session = PromptSession(
843
906
  completer=combined_completer,
844
907
  complete_style=CompleteStyle.COLUMN,
845
908
  complete_while_typing=True,
846
909
  history=InMemoryHistory(),
910
+ key_bindings=key_bindings,
847
911
  )
848
912
  return self._prompt_session
849
913
 
@@ -888,7 +952,11 @@ class RichUI:
888
952
  handled = self.handle_slash_command(user_input)
889
953
  if self._should_exit:
890
954
  break
891
- if handled:
955
+ # If handled is a string, it's expanded custom command content
956
+ if isinstance(handled, str):
957
+ # Process the expanded custom command as a query
958
+ user_input = handled
959
+ elif handled:
892
960
  console.print() # spacing
893
961
  continue
894
962