ripperdoc 0.2.8__py3-none-any.whl → 0.2.10__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 (94) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +257 -123
  3. ripperdoc/cli/commands/__init__.py +2 -1
  4. ripperdoc/cli/commands/agents_cmd.py +138 -8
  5. ripperdoc/cli/commands/clear_cmd.py +9 -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/exit_cmd.py +1 -0
  10. ripperdoc/cli/commands/hooks_cmd.py +27 -53
  11. ripperdoc/cli/commands/models_cmd.py +27 -10
  12. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  13. ripperdoc/cli/commands/resume_cmd.py +9 -3
  14. ripperdoc/cli/commands/stats_cmd.py +244 -0
  15. ripperdoc/cli/commands/status_cmd.py +4 -4
  16. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  17. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  18. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  19. ripperdoc/cli/ui/message_display.py +4 -2
  20. ripperdoc/cli/ui/panels.py +1 -0
  21. ripperdoc/cli/ui/provider_options.py +247 -0
  22. ripperdoc/cli/ui/rich_ui.py +403 -81
  23. ripperdoc/cli/ui/spinner.py +54 -18
  24. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  25. ripperdoc/cli/ui/tool_renderers.py +8 -2
  26. ripperdoc/cli/ui/wizard.py +213 -0
  27. ripperdoc/core/agents.py +19 -6
  28. ripperdoc/core/config.py +51 -17
  29. ripperdoc/core/custom_commands.py +7 -6
  30. ripperdoc/core/default_tools.py +101 -12
  31. ripperdoc/core/hooks/config.py +1 -3
  32. ripperdoc/core/hooks/events.py +27 -28
  33. ripperdoc/core/hooks/executor.py +4 -6
  34. ripperdoc/core/hooks/integration.py +12 -21
  35. ripperdoc/core/hooks/llm_callback.py +59 -0
  36. ripperdoc/core/hooks/manager.py +40 -15
  37. ripperdoc/core/permissions.py +118 -12
  38. ripperdoc/core/providers/anthropic.py +109 -36
  39. ripperdoc/core/providers/gemini.py +70 -5
  40. ripperdoc/core/providers/openai.py +89 -24
  41. ripperdoc/core/query.py +273 -68
  42. ripperdoc/core/query_utils.py +2 -0
  43. ripperdoc/core/skills.py +9 -3
  44. ripperdoc/core/system_prompt.py +4 -2
  45. ripperdoc/core/tool.py +17 -8
  46. ripperdoc/sdk/client.py +79 -4
  47. ripperdoc/tools/ask_user_question_tool.py +5 -3
  48. ripperdoc/tools/background_shell.py +307 -135
  49. ripperdoc/tools/bash_output_tool.py +1 -1
  50. ripperdoc/tools/bash_tool.py +63 -24
  51. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  52. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  53. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  54. ripperdoc/tools/file_edit_tool.py +167 -54
  55. ripperdoc/tools/file_read_tool.py +28 -4
  56. ripperdoc/tools/file_write_tool.py +13 -10
  57. ripperdoc/tools/glob_tool.py +3 -2
  58. ripperdoc/tools/grep_tool.py +3 -2
  59. ripperdoc/tools/kill_bash_tool.py +1 -1
  60. ripperdoc/tools/ls_tool.py +1 -1
  61. ripperdoc/tools/lsp_tool.py +615 -0
  62. ripperdoc/tools/mcp_tools.py +13 -10
  63. ripperdoc/tools/multi_edit_tool.py +8 -7
  64. ripperdoc/tools/notebook_edit_tool.py +7 -4
  65. ripperdoc/tools/skill_tool.py +1 -1
  66. ripperdoc/tools/task_tool.py +519 -69
  67. ripperdoc/tools/todo_tool.py +2 -2
  68. ripperdoc/tools/tool_search_tool.py +3 -2
  69. ripperdoc/utils/conversation_compaction.py +9 -5
  70. ripperdoc/utils/file_watch.py +214 -5
  71. ripperdoc/utils/json_utils.py +2 -1
  72. ripperdoc/utils/lsp.py +806 -0
  73. ripperdoc/utils/mcp.py +11 -3
  74. ripperdoc/utils/memory.py +4 -2
  75. ripperdoc/utils/message_compaction.py +21 -7
  76. ripperdoc/utils/message_formatting.py +14 -7
  77. ripperdoc/utils/messages.py +126 -67
  78. ripperdoc/utils/path_ignore.py +35 -8
  79. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  80. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  81. ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
  82. ripperdoc/utils/safe_get_cwd.py +2 -1
  83. ripperdoc/utils/session_heatmap.py +244 -0
  84. ripperdoc/utils/session_history.py +13 -6
  85. ripperdoc/utils/session_stats.py +293 -0
  86. ripperdoc/utils/todo.py +2 -1
  87. ripperdoc/utils/token_estimation.py +6 -1
  88. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
  89. ripperdoc-0.2.10.dist-info/RECORD +129 -0
  90. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  91. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
  92. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
  93. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
  94. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
@@ -52,9 +52,7 @@ def _get_scope_info(scope: ScopeType, project_path: Path) -> tuple[str, str]:
52
52
  return "Local (private)", str(project_path / ".ripperdoc" / "config.local.json")
53
53
 
54
54
 
55
- def _get_rules_for_scope(
56
- scope: ScopeType, project_path: Path
57
- ) -> tuple[List[str], List[str]]:
55
+ def _get_rules_for_scope(scope: ScopeType, project_path: Path) -> tuple[List[str], List[str]]:
58
56
  """Return (allow_rules, deny_rules) for a given scope."""
59
57
  if scope == "user":
60
58
  user_config: GlobalConfig = get_global_config()
@@ -76,21 +74,31 @@ def _add_rule(
76
74
  """Add a rule to the specified scope. Returns True if added, False if already exists."""
77
75
  if scope == "user":
78
76
  user_config: GlobalConfig = get_global_config()
79
- rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
77
+ rules = (
78
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
79
+ )
80
80
  if rule in rules:
81
81
  return False
82
82
  rules.append(rule)
83
83
  save_global_config(user_config)
84
84
  elif scope == "project":
85
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
86
+ rules = (
87
+ project_config.bash_allow_rules
88
+ if rule_type == "allow"
89
+ else project_config.bash_deny_rules
90
+ )
87
91
  if rule in rules:
88
92
  return False
89
93
  rules.append(rule)
90
94
  save_project_config(project_config, project_path)
91
95
  else: # local
92
96
  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
97
+ rules = (
98
+ local_config.local_allow_rules
99
+ if rule_type == "allow"
100
+ else local_config.local_deny_rules
101
+ )
94
102
  if rule in rules:
95
103
  return False
96
104
  rules.append(rule)
@@ -107,21 +115,31 @@ def _remove_rule(
107
115
  """Remove a rule from the specified scope. Returns True if removed, False if not found."""
108
116
  if scope == "user":
109
117
  user_config: GlobalConfig = get_global_config()
110
- rules = user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
118
+ rules = (
119
+ user_config.user_allow_rules if rule_type == "allow" else user_config.user_deny_rules
120
+ )
111
121
  if rule not in rules:
112
122
  return False
113
123
  rules.remove(rule)
114
124
  save_global_config(user_config)
115
125
  elif scope == "project":
116
126
  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
127
+ rules = (
128
+ project_config.bash_allow_rules
129
+ if rule_type == "allow"
130
+ else project_config.bash_deny_rules
131
+ )
118
132
  if rule not in rules:
119
133
  return False
120
134
  rules.remove(rule)
121
135
  save_project_config(project_config, project_path)
122
136
  else: # local
123
137
  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
138
+ rules = (
139
+ local_config.local_allow_rules
140
+ if rule_type == "allow"
141
+ else local_config.local_deny_rules
142
+ )
125
143
  if rule not in rules:
126
144
  return False
127
145
  rules.remove(rule)
@@ -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)
@@ -114,6 +116,10 @@ def _handle(ui: Any, arg: str) -> bool:
114
116
  ui.conversation_messages = messages
115
117
  ui._saved_conversation = None
116
118
  ui._set_session(summary.session_id)
119
+ try:
120
+ ui._run_session_start("resume")
121
+ except (AttributeError, RuntimeError, ValueError):
122
+ pass
117
123
  ui.replay_conversation(messages)
118
124
  ui.console.print(
119
125
  f"[green]✓ Resumed session {escape(summary.session_id)} "
@@ -0,0 +1,244 @@
1
+ """Statistics command to show session usage patterns."""
2
+
3
+ from typing import Any
4
+
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+
8
+ from ripperdoc.utils.session_heatmap import render_heatmap
9
+ from ripperdoc.utils.session_stats import (
10
+ collect_session_stats,
11
+ format_duration,
12
+ format_large_number,
13
+ )
14
+
15
+ from .base import SlashCommand
16
+
17
+
18
+ def _format_number(n: int) -> str:
19
+ """Format number with thousands separator."""
20
+ return f"{n:,}"
21
+
22
+
23
+ def _truncate_model_name(model: str, max_len: int = 15) -> str:
24
+ """Truncate model name with ellipsis if too long."""
25
+ if len(model) <= max_len:
26
+ return model
27
+ return model[: max_len - 1] + "…"
28
+
29
+
30
+ def _get_token_comparison(total_tokens: int) -> str:
31
+ """Get a fun comparison for total tokens based on famous books.
32
+
33
+ Reference data from famous books' approximate token counts.
34
+ """
35
+ books = [
36
+ ("The Old Man and the Sea", 35000),
37
+ ("Animal Farm", 39000),
38
+ ("The Great Gatsby", 62000),
39
+ ("Brave New World", 83000),
40
+ ("Harry Potter and the Philosopher's Stone", 103000),
41
+ ("The Hobbit", 123000),
42
+ ("1984", 123000),
43
+ ("To Kill a Mockingbird", 130000),
44
+ ("Pride and Prejudice", 156000),
45
+ ("Anna Karenina", 468000),
46
+ ("Don Quixote", 520000),
47
+ ("The Lord of the Rings", 576000),
48
+ ("War and Peace", 730000),
49
+ ]
50
+
51
+ if total_tokens == 0:
52
+ return "Start chatting to build your stats!"
53
+
54
+ # Find the best matching book
55
+ for i, (name, tokens) in enumerate(books):
56
+ if total_tokens < tokens:
57
+ if i == 0:
58
+ # Less than the smallest book
59
+ percentage = (total_tokens / tokens) * 100
60
+ return f"You've used {percentage:.0f}% of the tokens in {name}"
61
+ else:
62
+ # Between two books
63
+ prev_name, prev_tokens = books[i - 1]
64
+ if total_tokens >= prev_tokens * 1.5:
65
+ # Closer to current book
66
+ multiplier = total_tokens / tokens
67
+ return f"You've used ~{multiplier:.1f}x the tokens in {name}"
68
+ else:
69
+ # Closer to previous book
70
+ multiplier = total_tokens / prev_tokens
71
+ return f"You've used ~{multiplier:.1f}x the tokens in {prev_name}"
72
+
73
+ # More than the largest book
74
+ largest_name, largest_tokens = books[-1]
75
+ multiplier = total_tokens / largest_tokens
76
+ return f"You've used ~{multiplier:.1f}x the tokens in {largest_name}!"
77
+
78
+
79
+ def _get_duration_comparison(duration_minutes: int) -> str:
80
+ """Get a fun comparison for session duration based on common activities.
81
+
82
+ Reference data for activity durations in minutes.
83
+ """
84
+ activities = [
85
+ ("a TED talk", 18),
86
+ ("an episode of The Office", 22),
87
+ ("a half marathon (average time)", 120),
88
+ ("the movie Inception", 148),
89
+ ("a transatlantic flight", 420),
90
+ ]
91
+
92
+ if duration_minutes == 0:
93
+ return ""
94
+
95
+ # Find the best matching activity
96
+ for i, (name, minutes) in enumerate(activities):
97
+ if duration_minutes < minutes:
98
+ if i == 0:
99
+ # Less than the shortest activity
100
+ percentage = (duration_minutes / minutes) * 100
101
+ return f"Longest session: {percentage:.0f}% of {name}"
102
+ else:
103
+ # Between two activities
104
+ prev_name, prev_minutes = activities[i - 1]
105
+ if duration_minutes >= prev_minutes * 1.5:
106
+ # Closer to current activity
107
+ multiplier = duration_minutes / minutes
108
+ return f"Longest session: ~{multiplier:.1f}x {name}"
109
+ else:
110
+ # Closer to previous activity
111
+ multiplier = duration_minutes / prev_minutes
112
+ return f"Longest session: ~{multiplier:.1f}x {prev_name}"
113
+
114
+ # More than the longest activity
115
+ longest_name, longest_minutes = activities[-1]
116
+ multiplier = duration_minutes / longest_minutes
117
+ return f"Longest session: ~{multiplier:.1f}x {longest_name}!"
118
+
119
+
120
+ def _handle(ui: Any, _args: str) -> bool:
121
+ """Handle the stats command."""
122
+ # Get project path from UI
123
+ project_path = getattr(ui, "project_path", None)
124
+ if project_path is None:
125
+ ui.console.print("[yellow]No project context available[/yellow]")
126
+ return True
127
+
128
+ # Collect statistics
129
+ console = ui.console
130
+ days = 365 # Look back full year for complete heatmap
131
+ weeks_count = 52 # Display 52 weeks (1 year)
132
+ stats = collect_session_stats(project_path, days=days)
133
+
134
+ if stats.total_sessions == 0:
135
+ console.print("[yellow]No session data found[/yellow]")
136
+ return True
137
+
138
+ # Create overview panel
139
+ console.print()
140
+ console.print("[bold]Activity Heatmap[/bold]")
141
+ console.print()
142
+
143
+ # Render heatmap with full year (52 weeks)
144
+ render_heatmap(console, stats.daily_activity, weeks_count=weeks_count)
145
+ console.print()
146
+
147
+ # Create two-column layout for statistics
148
+ # Left column: Model and tokens
149
+ # Right column: Sessions and streaks
150
+ table = Table.grid(padding=(0, 4))
151
+ table.add_column(style="cyan", justify="left")
152
+ table.add_column(justify="left")
153
+ table.add_column(style="cyan", justify="left")
154
+ table.add_column(justify="left")
155
+
156
+ # Row 1: Favorite model | Sessions
157
+ favorite_model_display = ""
158
+ if stats.favorite_model:
159
+ favorite_model_display = _truncate_model_name(stats.favorite_model)
160
+ table.add_row(
161
+ "Favorite model:",
162
+ favorite_model_display or "N/A",
163
+ "Sessions:",
164
+ f"{_format_number(stats.total_sessions)}",
165
+ )
166
+
167
+ # Row 2: Total tokens | Longest session
168
+ total_tokens_display = format_large_number(stats.total_tokens) if stats.total_tokens > 0 else "0"
169
+ longest_session_display = ""
170
+ if stats.longest_session_duration.total_seconds() > 0:
171
+ longest_session_display = format_duration(stats.longest_session_duration)
172
+ table.add_row(
173
+ "Total tokens:",
174
+ total_tokens_display,
175
+ "Longest session:",
176
+ longest_session_display or "N/A",
177
+ )
178
+
179
+ # Row 3: empty | Current streak
180
+ table.add_row(
181
+ "",
182
+ "",
183
+ "Current streak:",
184
+ f"{stats.current_streak} days",
185
+ )
186
+
187
+ # Row 4: empty | Longest streak
188
+ table.add_row(
189
+ "",
190
+ "",
191
+ "Longest streak:",
192
+ f"{stats.longest_streak} days",
193
+ )
194
+
195
+ # Row 5: empty | Active days
196
+ table.add_row(
197
+ "",
198
+ "",
199
+ "Active days:",
200
+ f"{stats.active_days}/{stats.total_days}",
201
+ )
202
+
203
+ # Row 6: empty | Peak hour
204
+ peak_hour_str = f"{stats.peak_hour:02d}:00-{stats.peak_hour + 1:02d}:00"
205
+ table.add_row(
206
+ "",
207
+ "",
208
+ "Peak hour:",
209
+ peak_hour_str,
210
+ )
211
+
212
+ # Create the main panel with statistics
213
+ # Match heatmap width: WEEKDAY_LABEL_WIDTH (8) + weeks_count (52) = 60
214
+ heatmap_width = 8 + weeks_count + 16
215
+ console.print(
216
+ Panel(table, title="Statistics", border_style="blue", width=heatmap_width)
217
+ )
218
+ console.print()
219
+
220
+ # Fun comparisons
221
+ # Token comparison
222
+ token_comparison = _get_token_comparison(stats.total_tokens)
223
+ if token_comparison:
224
+ console.print(f"[dim]{token_comparison}[/dim]")
225
+
226
+ # Duration comparison
227
+ duration_minutes = int(stats.longest_session_duration.total_seconds() / 60)
228
+ duration_comparison = _get_duration_comparison(duration_minutes)
229
+ if duration_comparison:
230
+ console.print(f"[dim]{duration_comparison}[/dim]")
231
+
232
+ console.print(f"[dim]Stats from the last {days} days[/dim]")
233
+ console.print()
234
+
235
+ return True
236
+
237
+
238
+ command = SlashCommand(
239
+ name="stats",
240
+ description="Show session statistics and activity patterns",
241
+ handler=_handle,
242
+ )
243
+
244
+ __all__ = ["command"]
@@ -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
@@ -85,7 +85,7 @@ 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 = []
@@ -143,6 +143,7 @@ class FileMentionCompleter(Completer):
143
143
  continue
144
144
 
145
145
  import fnmatch
146
+
146
147
  if fnmatch.fnmatch(item.name.lower(), pattern.lower()):
147
148
  try:
148
149
  rel_path = item.relative_to(self.project_path)
@@ -14,7 +14,7 @@ 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:
@@ -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
@@ -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
 
@@ -55,6 +55,7 @@ def print_shortcuts(console: Console) -> None:
55
55
  pairs: List[Tuple[str, str]] = [
56
56
  ("? for shortcuts", "! for bash mode"),
57
57
  ("/ for commands", "@ for file mention"),
58
+ ("Alt+Enter for newline", "Enter to submit"),
58
59
  ]
59
60
  console.print("[dim]Shortcuts[/dim]")
60
61
  for left, right in pairs: