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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +257 -123
- ripperdoc/cli/commands/__init__.py +2 -1
- ripperdoc/cli/commands/agents_cmd.py +138 -8
- ripperdoc/cli/commands/clear_cmd.py +9 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +27 -10
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +9 -3
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +403 -81
- ripperdoc/cli/ui/spinner.py +54 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +213 -0
- ripperdoc/core/agents.py +19 -6
- ripperdoc/core/config.py +51 -17
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +101 -12
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +27 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +118 -12
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +89 -24
- ripperdoc/core/query.py +273 -68
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +17 -8
- ripperdoc/sdk/client.py +79 -4
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +307 -135
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +63 -24
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +167 -54
- ripperdoc/tools/file_read_tool.py +28 -4
- ripperdoc/tools/file_write_tool.py +13 -10
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +519 -69
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +9 -5
- ripperdoc/utils/file_watch.py +214 -5
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +14 -7
- ripperdoc/utils/messages.py +126 -67
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/session_stats.py +293 -0
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- ripperdoc-0.2.10.dist-info/RECORD +129 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ==
|
|
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 ==
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
config_yolo_mode = getattr(config, "yolo_mode", False)
|
|
109
109
|
config_verbose = getattr(config, "verbose", False)
|
|
110
|
-
if
|
|
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.
|
|
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__,
|
|
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__,
|
|
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__,
|
|
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__,
|
|
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] = {
|
|
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
|
|
ripperdoc/cli/ui/panels.py
CHANGED
|
@@ -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:
|