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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +33 -115
- ripperdoc/cli/commands/__init__.py +70 -6
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -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/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +610 -0
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +57 -37
- ripperdoc/cli/commands/resume_cmd.py +6 -4
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +64 -8
- ripperdoc/cli/ui/interrupt_handler.py +3 -4
- ripperdoc/cli/ui/message_display.py +5 -3
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +196 -77
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +412 -0
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +301 -0
- ripperdoc/core/hooks/events.py +535 -0
- ripperdoc/core/hooks/executor.py +496 -0
- ripperdoc/core/hooks/integration.py +344 -0
- ripperdoc/core/hooks/manager.py +745 -0
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +548 -68
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +140 -39
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +30 -20
- 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 +8 -4
- ripperdoc/tools/file_read_tool.py +9 -5
- ripperdoc/tools/file_write_tool.py +9 -5
- 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/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 +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +11 -7
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +38 -12
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.7.dist-info/RECORD +0 -113
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
59
|
-
return list(
|
|
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
|
-
|
|
62
|
-
return list(
|
|
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
|
-
|
|
65
|
-
return list(
|
|
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
|
-
|
|
77
|
-
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(
|
|
83
|
+
save_global_config(user_config)
|
|
82
84
|
elif scope == "project":
|
|
83
|
-
|
|
84
|
-
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(
|
|
94
|
+
save_project_config(project_config, project_path)
|
|
89
95
|
else: # local
|
|
90
|
-
|
|
91
|
-
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(
|
|
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
|
-
|
|
108
|
-
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(
|
|
124
|
+
save_global_config(user_config)
|
|
113
125
|
elif scope == "project":
|
|
114
|
-
|
|
115
|
-
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(
|
|
135
|
+
save_project_config(project_config, project_path)
|
|
120
136
|
else: # local
|
|
121
|
-
|
|
122
|
-
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(
|
|
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
|
-
|
|
219
|
-
_render_scope_rules(ui.console,
|
|
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]
|
|
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
|
-
|
|
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,
|
|
297
|
+
if _remove_rule(scope, remove_rule_type, rule, project_path):
|
|
278
298
|
ui.console.print(
|
|
279
299
|
Panel(
|
|
280
|
-
f"Removed [{'green' if
|
|
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 =
|
|
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 ==
|
|
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)
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] = {
|
|
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,
|
|
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
|
|
ripperdoc/cli/ui/panels.py
CHANGED
|
@@ -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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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:
|