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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +33 -115
  3. ripperdoc/cli/commands/__init__.py +70 -6
  4. ripperdoc/cli/commands/agents_cmd.py +6 -3
  5. ripperdoc/cli/commands/clear_cmd.py +1 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/help_cmd.py +11 -1
  10. ripperdoc/cli/commands/hooks_cmd.py +610 -0
  11. ripperdoc/cli/commands/models_cmd.py +26 -9
  12. ripperdoc/cli/commands/permissions_cmd.py +57 -37
  13. ripperdoc/cli/commands/resume_cmd.py +6 -4
  14. ripperdoc/cli/commands/status_cmd.py +4 -4
  15. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  16. ripperdoc/cli/ui/file_mention_completer.py +64 -8
  17. ripperdoc/cli/ui/interrupt_handler.py +3 -4
  18. ripperdoc/cli/ui/message_display.py +5 -3
  19. ripperdoc/cli/ui/panels.py +13 -10
  20. ripperdoc/cli/ui/provider_options.py +247 -0
  21. ripperdoc/cli/ui/rich_ui.py +196 -77
  22. ripperdoc/cli/ui/spinner.py +25 -1
  23. ripperdoc/cli/ui/tool_renderers.py +8 -2
  24. ripperdoc/cli/ui/wizard.py +215 -0
  25. ripperdoc/core/agents.py +9 -3
  26. ripperdoc/core/config.py +49 -12
  27. ripperdoc/core/custom_commands.py +412 -0
  28. ripperdoc/core/default_tools.py +11 -2
  29. ripperdoc/core/hooks/__init__.py +99 -0
  30. ripperdoc/core/hooks/config.py +301 -0
  31. ripperdoc/core/hooks/events.py +535 -0
  32. ripperdoc/core/hooks/executor.py +496 -0
  33. ripperdoc/core/hooks/integration.py +344 -0
  34. ripperdoc/core/hooks/manager.py +745 -0
  35. ripperdoc/core/permissions.py +40 -8
  36. ripperdoc/core/providers/anthropic.py +548 -68
  37. ripperdoc/core/providers/gemini.py +70 -5
  38. ripperdoc/core/providers/openai.py +60 -5
  39. ripperdoc/core/query.py +140 -39
  40. ripperdoc/core/query_utils.py +2 -0
  41. ripperdoc/core/skills.py +9 -3
  42. ripperdoc/core/system_prompt.py +4 -2
  43. ripperdoc/core/tool.py +9 -5
  44. ripperdoc/sdk/client.py +2 -2
  45. ripperdoc/tools/ask_user_question_tool.py +5 -3
  46. ripperdoc/tools/background_shell.py +2 -1
  47. ripperdoc/tools/bash_output_tool.py +1 -1
  48. ripperdoc/tools/bash_tool.py +30 -20
  49. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  50. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  51. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  52. ripperdoc/tools/file_edit_tool.py +8 -4
  53. ripperdoc/tools/file_read_tool.py +9 -5
  54. ripperdoc/tools/file_write_tool.py +9 -5
  55. ripperdoc/tools/glob_tool.py +3 -2
  56. ripperdoc/tools/grep_tool.py +3 -2
  57. ripperdoc/tools/kill_bash_tool.py +1 -1
  58. ripperdoc/tools/ls_tool.py +1 -1
  59. ripperdoc/tools/mcp_tools.py +13 -10
  60. ripperdoc/tools/multi_edit_tool.py +8 -7
  61. ripperdoc/tools/notebook_edit_tool.py +7 -4
  62. ripperdoc/tools/skill_tool.py +1 -1
  63. ripperdoc/tools/task_tool.py +5 -4
  64. ripperdoc/tools/todo_tool.py +2 -2
  65. ripperdoc/tools/tool_search_tool.py +3 -2
  66. ripperdoc/utils/conversation_compaction.py +11 -7
  67. ripperdoc/utils/file_watch.py +8 -2
  68. ripperdoc/utils/json_utils.py +2 -1
  69. ripperdoc/utils/mcp.py +11 -3
  70. ripperdoc/utils/memory.py +4 -2
  71. ripperdoc/utils/message_compaction.py +21 -7
  72. ripperdoc/utils/message_formatting.py +11 -7
  73. ripperdoc/utils/messages.py +105 -66
  74. ripperdoc/utils/path_ignore.py +38 -12
  75. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  76. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  77. ripperdoc/utils/safe_get_cwd.py +2 -1
  78. ripperdoc/utils/session_history.py +13 -6
  79. ripperdoc/utils/todo.py +2 -1
  80. ripperdoc/utils/token_estimation.py +6 -1
  81. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
  82. ripperdoc-0.2.9.dist-info/RECORD +123 -0
  83. ripperdoc-0.2.7.dist-info/RECORD +0 -113
  84. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
  85. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
  86. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
  87. {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -50,19 +52,17 @@ def _get_scope_info(scope: ScopeType, project_path: Path) -> tuple[str, str]:
50
52
  return "Local (private)", str(project_path / ".ripperdoc" / "config.local.json")
51
53
 
52
54
 
53
- def _get_rules_for_scope(
54
- scope: ScopeType, project_path: Path
55
- ) -> tuple[List[str], List[str]]:
55
+ def _get_rules_for_scope(scope: ScopeType, project_path: Path) -> tuple[List[str], List[str]]:
56
56
  """Return (allow_rules, deny_rules) for a given scope."""
57
57
  if scope == "user":
58
- config = get_global_config()
59
- return list(config.user_allow_rules), list(config.user_deny_rules)
58
+ user_config: GlobalConfig = get_global_config()
59
+ return list(user_config.user_allow_rules), list(user_config.user_deny_rules)
60
60
  elif scope == "project":
61
- config = get_project_config(project_path)
62
- return list(config.bash_allow_rules), list(config.bash_deny_rules)
61
+ project_config: ProjectConfig = get_project_config(project_path)
62
+ return list(project_config.bash_allow_rules), list(project_config.bash_deny_rules)
63
63
  else: # local
64
- config = get_project_local_config(project_path)
65
- return list(config.local_allow_rules), list(config.local_deny_rules)
64
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
65
+ return list(local_config.local_allow_rules), list(local_config.local_deny_rules)
66
66
 
67
67
 
68
68
  def _add_rule(
@@ -73,26 +73,36 @@ def _add_rule(
73
73
  ) -> bool:
74
74
  """Add a rule to the specified scope. Returns True if added, False if already exists."""
75
75
  if scope == "user":
76
- config = get_global_config()
77
- rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
76
+ user_config: GlobalConfig = get_global_config()
77
+ rules = (
78
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
79
+ )
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 = (
87
+ project_config.bash_allow_rules
88
+ if rule_type == "allow"
89
+ else project_config.bash_deny_rules
90
+ )
85
91
  if rule in rules:
86
92
  return False
87
93
  rules.append(rule)
88
- save_project_config(config, project_path)
94
+ save_project_config(project_config, project_path)
89
95
  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
96
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
97
+ rules = (
98
+ local_config.local_allow_rules
99
+ if rule_type == "allow"
100
+ else local_config.local_deny_rules
101
+ )
92
102
  if rule in rules:
93
103
  return False
94
104
  rules.append(rule)
95
- save_project_local_config(config, project_path)
105
+ save_project_local_config(local_config, project_path)
96
106
  return True
97
107
 
98
108
 
@@ -104,26 +114,36 @@ def _remove_rule(
104
114
  ) -> bool:
105
115
  """Remove a rule from the specified scope. Returns True if removed, False if not found."""
106
116
  if scope == "user":
107
- config = get_global_config()
108
- rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
117
+ user_config: GlobalConfig = get_global_config()
118
+ rules = (
119
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
120
+ )
109
121
  if rule not in rules:
110
122
  return False
111
123
  rules.remove(rule)
112
- save_global_config(config)
124
+ save_global_config(user_config)
113
125
  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
126
+ project_config: ProjectConfig = get_project_config(project_path)
127
+ rules = (
128
+ project_config.bash_allow_rules
129
+ if rule_type == "allow"
130
+ else project_config.bash_deny_rules
131
+ )
116
132
  if rule not in rules:
117
133
  return False
118
134
  rules.remove(rule)
119
- save_project_config(config, project_path)
135
+ save_project_config(project_config, project_path)
120
136
  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
137
+ local_config: ProjectLocalConfig = get_project_local_config(project_path)
138
+ rules = (
139
+ local_config.local_allow_rules
140
+ if rule_type == "allow"
141
+ else local_config.local_deny_rules
142
+ )
123
143
  if rule not in rules:
124
144
  return False
125
145
  rules.remove(rule)
126
- save_project_local_config(config, project_path)
146
+ save_project_local_config(local_config, project_path)
127
147
  return True
128
148
 
129
149
 
@@ -204,7 +224,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
204
224
 
205
225
  # Parse command
206
226
  action = args[0].lower()
207
- scope_aliases = {
227
+ scope_aliases: Dict[str, ScopeType] = {
208
228
  "user": "user",
209
229
  "global": "user",
210
230
  "project": "project",
@@ -215,8 +235,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
215
235
 
216
236
  # Single scope display
217
237
  if action in scope_aliases:
218
- scope = scope_aliases[action]
219
- _render_scope_rules(ui.console, scope, project_path) # type: ignore
238
+ display_scope: ScopeType = scope_aliases[action]
239
+ _render_scope_rules(ui.console, display_scope, project_path)
220
240
  return True
221
241
 
222
242
  # Add rule
@@ -231,14 +251,14 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
231
251
  ui.console.print(f"[red]Unknown scope: {escape(scope_arg)}[/red]")
232
252
  ui.console.print("[dim]Available scopes: user, project, local[/dim]")
233
253
  return True
234
- scope: ScopeType = scope_aliases[scope_arg] # type: ignore
254
+ scope: ScopeType = scope_aliases[scope_arg]
235
255
 
236
256
  rule_type_arg = args[2].lower()
237
257
  if rule_type_arg not in ("allow", "deny"):
238
258
  ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
239
259
  ui.console.print("[dim]Available types: allow, deny[/dim]")
240
260
  return True
241
- rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore
261
+ rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore[assignment]
242
262
 
243
263
  rule = " ".join(args[3:])
244
264
  if _add_rule(scope, rule_type, rule, project_path):
@@ -271,13 +291,13 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
271
291
  ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
272
292
  ui.console.print("[dim]Available types: allow, deny[/dim]")
273
293
  return True
274
- rule_type = rule_type_arg # type: ignore
294
+ remove_rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore[assignment]
275
295
 
276
296
  rule = " ".join(args[3:])
277
- if _remove_rule(scope, rule_type, rule, project_path):
297
+ if _remove_rule(scope, remove_rule_type, rule, project_path):
278
298
  ui.console.print(
279
299
  Panel(
280
- f"Removed [{'green' if rule_type == 'allow' else 'red'}]{rule_type}[/] rule from {scope}:\n{escape(rule)}",
300
+ f"Removed [{'green' if remove_rule_type == 'allow' else 'red'}]{remove_rule_type}[/] rule from {scope}:\n{escape(rule)}",
281
301
  title="/permissions",
282
302
  )
283
303
  )
@@ -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 += ": "
@@ -72,14 +72,14 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
72
72
  return None
73
73
 
74
74
  # Handle pagination commands
75
- if choice_text == 'n':
75
+ if choice_text == "n":
76
76
  if current_page < total_pages - 1:
77
77
  current_page += 1
78
78
  continue
79
79
  else:
80
80
  ui.console.print("[yellow]Already at the last page.[/yellow]")
81
81
  continue
82
- elif choice_text == 'p':
82
+ elif choice_text == "p":
83
83
  if current_page > 0:
84
84
  current_page -= 1
85
85
  continue
@@ -89,7 +89,9 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
89
89
 
90
90
  # Handle session selection
91
91
  if not choice_text.isdigit():
92
- ui.console.print("[red]Please enter a session index number, 'n' for next page, or 'p' for previous page.[/red]")
92
+ ui.console.print(
93
+ "[red]Please enter a session index number, 'n' for next page, or 'p' for previous page.[/red]"
94
+ )
93
95
  continue
94
96
 
95
97
  idx = int(choice_text)
@@ -85,7 +85,7 @@ def _setting_sources_summary(
85
85
  profile: Optional[ModelProfile],
86
86
  memory_files: List[MemoryFile],
87
87
  auth_env_var: Optional[str],
88
- safe_mode: bool,
88
+ yolo_mode: bool,
89
89
  verbose: bool,
90
90
  project_path: Path,
91
91
  ) -> str:
@@ -105,9 +105,9 @@ def _setting_sources_summary(
105
105
  if auth_env_var:
106
106
  sources.append("Environment variables")
107
107
 
108
- config_safe_mode = getattr(config, "safe_mode", True)
108
+ config_yolo_mode = getattr(config, "yolo_mode", False)
109
109
  config_verbose = getattr(config, "verbose", False)
110
- if safe_mode != config_safe_mode or verbose != config_verbose:
110
+ if yolo_mode != config_yolo_mode or verbose != config_verbose:
111
111
  sources.append("Command line arguments")
112
112
 
113
113
  if profile and profile.api_key and not auth_env_var:
@@ -133,7 +133,7 @@ def _handle(ui: Any, _: str) -> bool:
133
133
  profile,
134
134
  memory_files,
135
135
  auth_env_var,
136
- ui.safe_mode,
136
+ ui.yolo_mode,
137
137
  ui.verbose,
138
138
  ui.project_path,
139
139
  )
@@ -109,7 +109,8 @@ def _list_tasks(ui: Any) -> bool:
109
109
  table.add_row(escape(task_id), "[red]error[/]", escape(str(exc)), "-")
110
110
  logger.warning(
111
111
  "[tasks_cmd] Failed to read background task status: %s: %s",
112
- type(exc).__name__, exc,
112
+ type(exc).__name__,
113
+ exc,
113
114
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
114
115
  )
115
116
  continue
@@ -148,7 +149,8 @@ def _kill_task(ui: Any, task_id: str) -> bool:
148
149
  console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
149
150
  logger.warning(
150
151
  "[tasks_cmd] Failed to read task before kill: %s: %s",
151
- type(exc).__name__, exc,
152
+ type(exc).__name__,
153
+ exc,
152
154
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
153
155
  )
154
156
  return True
@@ -172,7 +174,8 @@ def _kill_task(ui: Any, task_id: str) -> bool:
172
174
  console.print(f"[red]Error stopping task {escape(task_id)}: {escape(str(exc))}[/red]")
173
175
  logger.warning(
174
176
  "[tasks_cmd] Error stopping background task: %s: %s",
175
- type(exc).__name__, exc,
177
+ type(exc).__name__,
178
+ exc,
176
179
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
177
180
  )
178
181
  return True
@@ -197,7 +200,8 @@ def _show_task(ui: Any, task_id: str) -> bool:
197
200
  console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
198
201
  logger.warning(
199
202
  "[tasks_cmd] Failed to read task for detail view: %s: %s",
200
- type(exc).__name__, exc,
203
+ type(exc).__name__,
204
+ exc,
201
205
  extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
202
206
  )
203
207
  return True
@@ -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
 
@@ -85,10 +85,17 @@ class FileMentionCompleter(Completer):
85
85
  return
86
86
 
87
87
  # Extract the query after the @ symbol
88
- query = text[at_pos + 1:].strip()
88
+ query = text[at_pos + 1 :].strip()
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:
@@ -136,6 +143,7 @@ class FileMentionCompleter(Completer):
136
143
  continue
137
144
 
138
145
  import fnmatch
146
+
139
147
  if fnmatch.fnmatch(item.name.lower(), pattern.lower()):
140
148
  try:
141
149
  rel_path = item.relative_to(self.project_path)
@@ -146,7 +154,7 @@ class FileMentionCompleter(Completer):
146
154
  # Right side: show type only
147
155
  meta = "📁 directory" if item.is_dir() else "📄 file"
148
156
 
149
- matches.append((display_path, item, meta, 0))
157
+ _add_match(display_path, item, meta, 0)
150
158
  except ValueError:
151
159
  continue
152
160
  else:
@@ -170,13 +178,61 @@ class FileMentionCompleter(Completer):
170
178
 
171
179
  # Right side: show type only
172
180
  meta = "📁 directory" if item.is_dir() else "📄 file"
173
- matches.append((display_path, item, meta, 0))
181
+ _add_match(display_path, item, meta, 0)
174
182
  except ValueError:
175
183
  continue
176
184
  else:
185
+ # First, suggest top-level entries that match the prefix to support step-by-step navigation
186
+ query_lower = query.lower()
187
+ for item in sorted(self.project_path.iterdir()):
188
+ if should_skip_path(
189
+ item,
190
+ self.project_path,
191
+ ignore_filter=self.ignore_filter,
192
+ skip_hidden=True,
193
+ ):
194
+ continue
195
+
196
+ name_lower = item.name.lower()
197
+ if query_lower in name_lower:
198
+ score = 500
199
+ if name_lower.startswith(query_lower):
200
+ score += 50
201
+ if name_lower == query_lower:
202
+ score += 100
203
+
204
+ rel_path = item.relative_to(self.project_path)
205
+ display_path = str(rel_path)
206
+ if item.is_dir():
207
+ display_path += "/"
208
+
209
+ meta = "📁 directory" if item.is_dir() else "📄 file"
210
+ _add_match(display_path, item, meta, score)
211
+
212
+ # If the query exactly matches a directory, also surface its children for quicker drilling
213
+ dir_candidate = self.project_path / query
214
+ if dir_candidate.exists() and dir_candidate.is_dir():
215
+ for item in sorted(dir_candidate.iterdir()):
216
+ if should_skip_path(
217
+ item,
218
+ self.project_path,
219
+ ignore_filter=self.ignore_filter,
220
+ skip_hidden=True,
221
+ ):
222
+ continue
223
+
224
+ try:
225
+ rel_path = item.relative_to(self.project_path)
226
+ display_path = str(rel_path)
227
+ if item.is_dir():
228
+ display_path += "/"
229
+ meta = "📁 directory" if item.is_dir() else "📄 file"
230
+ _add_match(display_path, item, meta, 400)
231
+ except ValueError:
232
+ continue
233
+
177
234
  # Recursively search for files matching the query
178
235
  all_files = self._collect_files_recursive(self.project_path)
179
- query_lower = query.lower()
180
236
 
181
237
  for file_path in all_files:
182
238
  try:
@@ -198,7 +254,7 @@ class FileMentionCompleter(Completer):
198
254
  # Right side: show type only
199
255
  meta = "📄 file"
200
256
 
201
- matches.append((display_path, file_path, meta, score))
257
+ _add_match(display_path, file_path, meta, score)
202
258
  except ValueError:
203
259
  continue
204
260
 
@@ -14,13 +14,13 @@ from ripperdoc.utils.log import get_logger
14
14
  logger = get_logger()
15
15
 
16
16
  # Keys that trigger interrupt
17
- INTERRUPT_KEYS: Set[str] = {'\x1b', '\x03'} # ESC, Ctrl+C
17
+ INTERRUPT_KEYS: Set[str] = {"\x1b", "\x03"} # ESC, Ctrl+C
18
18
 
19
19
 
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
@@ -149,8 +149,7 @@ class InterruptHandler:
149
149
 
150
150
  try:
151
151
  done, _ = await asyncio.wait(
152
- {query_task, interrupt_task},
153
- return_when=asyncio.FIRST_COMPLETED
152
+ {query_task, interrupt_task}, return_when=asyncio.FIRST_COMPLETED
154
153
  )
155
154
 
156
155
  # Check if interrupted
@@ -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
@@ -22,15 +22,17 @@ ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
22
22
  class MessageDisplay:
23
23
  """Handles message rendering and display operations."""
24
24
 
25
- def __init__(self, console: Console, verbose: bool = False):
25
+ def __init__(self, console: Console, verbose: bool = False, show_full_thinking: bool = False):
26
26
  """Initialize the message display handler.
27
27
 
28
28
  Args:
29
29
  console: Rich console for output
30
30
  verbose: Whether to show verbose output
31
+ show_full_thinking: Whether to show full reasoning content instead of truncated preview
31
32
  """
32
33
  self.console = console
33
34
  self.verbose = verbose
35
+ self.show_full_thinking = show_full_thinking
34
36
 
35
37
  def format_tool_args(self, tool_name: str, tool_args: Optional[dict]) -> List[str]:
36
38
  """Render tool arguments into concise display-friendly parts."""
@@ -212,7 +214,7 @@ class MessageDisplay:
212
214
 
213
215
  def print_reasoning(self, reasoning: Any) -> None:
214
216
  """Display a collapsed preview of reasoning/thinking blocks."""
215
- preview = format_reasoning_preview(reasoning)
217
+ preview = format_reasoning_preview(reasoning, self.show_full_thinking)
216
218
  if preview:
217
219
  self.console.print(f"[dim italic]Thinking: {escape(preview)}[/]")
218
220
 
@@ -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: