ripperdoc 0.2.6__py3-none-any.whl → 0.2.7__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/commands/clear_cmd.py +1 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -1
- ripperdoc/cli/commands/resume_cmd.py +71 -37
- ripperdoc/cli/ui/file_mention_completer.py +221 -0
- ripperdoc/cli/ui/helpers.py +100 -3
- ripperdoc/cli/ui/interrupt_handler.py +175 -0
- ripperdoc/cli/ui/message_display.py +249 -0
- ripperdoc/cli/ui/panels.py +60 -0
- ripperdoc/cli/ui/rich_ui.py +147 -630
- ripperdoc/cli/ui/tool_renderers.py +2 -2
- ripperdoc/core/agents.py +4 -4
- ripperdoc/core/query_utils.py +1 -1
- ripperdoc/core/tool.py +1 -1
- ripperdoc/tools/bash_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +2 -2
- ripperdoc/tools/file_read_tool.py +1 -1
- ripperdoc/tools/multi_edit_tool.py +1 -1
- ripperdoc/utils/conversation_compaction.py +476 -0
- ripperdoc/utils/message_compaction.py +109 -154
- ripperdoc/utils/message_formatting.py +216 -0
- ripperdoc/utils/messages.py +31 -9
- ripperdoc/utils/session_history.py +19 -7
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/METADATA +1 -1
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/RECORD +29 -23
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.6.dist-info → ripperdoc-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Message display and rendering utilities for RichUI.
|
|
2
|
+
|
|
3
|
+
This module handles rendering conversation messages to the terminal, including:
|
|
4
|
+
- Tool call and result formatting
|
|
5
|
+
- Assistant/user message display
|
|
6
|
+
- Reasoning block rendering
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown
|
|
13
|
+
from rich.markup import escape
|
|
14
|
+
|
|
15
|
+
from ripperdoc.cli.ui.tool_renderers import ToolResultRendererRegistry
|
|
16
|
+
from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
|
|
17
|
+
from ripperdoc.utils.message_formatting import format_reasoning_preview
|
|
18
|
+
|
|
19
|
+
ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MessageDisplay:
|
|
23
|
+
"""Handles message rendering and display operations."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Console, verbose: bool = False):
|
|
26
|
+
"""Initialize the message display handler.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
console: Rich console for output
|
|
30
|
+
verbose: Whether to show verbose output
|
|
31
|
+
"""
|
|
32
|
+
self.console = console
|
|
33
|
+
self.verbose = verbose
|
|
34
|
+
|
|
35
|
+
def format_tool_args(self, tool_name: str, tool_args: Optional[dict]) -> List[str]:
|
|
36
|
+
"""Render tool arguments into concise display-friendly parts."""
|
|
37
|
+
if not tool_args:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
args_parts: List[str] = []
|
|
41
|
+
|
|
42
|
+
def _format_arg(arg_key: str, arg_value: Any) -> str:
|
|
43
|
+
if arg_key == "todos" and isinstance(arg_value, list):
|
|
44
|
+
counts = {"pending": 0, "in_progress": 0, "completed": 0}
|
|
45
|
+
for item in arg_value:
|
|
46
|
+
status = ""
|
|
47
|
+
if isinstance(item, dict):
|
|
48
|
+
status = item.get("status", "")
|
|
49
|
+
elif hasattr(item, "get"):
|
|
50
|
+
status = item.get("status", "")
|
|
51
|
+
elif hasattr(item, "status"):
|
|
52
|
+
status = getattr(item, "status")
|
|
53
|
+
if status in counts:
|
|
54
|
+
counts[status] += 1
|
|
55
|
+
total = len(arg_value)
|
|
56
|
+
return f"{arg_key}: {total} items"
|
|
57
|
+
if isinstance(arg_value, (list, dict)):
|
|
58
|
+
return f"{arg_key}: {len(arg_value)} items"
|
|
59
|
+
if isinstance(arg_value, str) and len(arg_value) > 50:
|
|
60
|
+
return f'{arg_key}: "{arg_value[:50]}..."'
|
|
61
|
+
return f"{arg_key}: {arg_value}"
|
|
62
|
+
|
|
63
|
+
if tool_name == "Bash":
|
|
64
|
+
command_value = tool_args.get("command")
|
|
65
|
+
if command_value is not None:
|
|
66
|
+
args_parts.append(_format_arg("command", command_value))
|
|
67
|
+
|
|
68
|
+
background_value = tool_args.get("run_in_background", tool_args.get("runInBackground"))
|
|
69
|
+
background_value = bool(background_value) if background_value is not None else False
|
|
70
|
+
args_parts.append(f"background: {background_value}")
|
|
71
|
+
|
|
72
|
+
sandbox_value = tool_args.get("sandbox")
|
|
73
|
+
sandbox_value = bool(sandbox_value) if sandbox_value is not None else False
|
|
74
|
+
args_parts.append(f"sandbox: {sandbox_value}")
|
|
75
|
+
|
|
76
|
+
for key, value in tool_args.items():
|
|
77
|
+
if key in {"command", "run_in_background", "runInBackground", "sandbox"}:
|
|
78
|
+
continue
|
|
79
|
+
args_parts.append(_format_arg(key, value))
|
|
80
|
+
return args_parts
|
|
81
|
+
|
|
82
|
+
# Special handling for Edit and MultiEdit tools - don't show old_string
|
|
83
|
+
if tool_name in ["Edit", "MultiEdit"]:
|
|
84
|
+
for key, value in tool_args.items():
|
|
85
|
+
if key == "new_string":
|
|
86
|
+
continue # Skip new_string for Edit/MultiEdit tools
|
|
87
|
+
if key == "old_string":
|
|
88
|
+
continue # Skip old_string for Edit/MultiEdit tools
|
|
89
|
+
# For MultiEdit, also handle edits array
|
|
90
|
+
if key == "edits" and isinstance(value, list):
|
|
91
|
+
args_parts.append(f"edits: {len(value)} operations")
|
|
92
|
+
continue
|
|
93
|
+
args_parts.append(_format_arg(key, value))
|
|
94
|
+
return args_parts
|
|
95
|
+
|
|
96
|
+
for key, value in tool_args.items():
|
|
97
|
+
args_parts.append(_format_arg(key, value))
|
|
98
|
+
return args_parts
|
|
99
|
+
|
|
100
|
+
def print_tool_call(self, sender: str, content: str, tool_args: Optional[dict]) -> None:
|
|
101
|
+
"""Render a tool invocation line."""
|
|
102
|
+
if sender == "Task":
|
|
103
|
+
subagent = ""
|
|
104
|
+
if isinstance(tool_args, dict):
|
|
105
|
+
subagent = tool_args.get("subagent_type") or tool_args.get("subagent") or ""
|
|
106
|
+
desc = ""
|
|
107
|
+
if isinstance(tool_args, dict):
|
|
108
|
+
raw_desc = tool_args.get("description") or tool_args.get("prompt") or ""
|
|
109
|
+
desc = raw_desc if len(str(raw_desc)) <= 120 else str(raw_desc)[:117] + "..."
|
|
110
|
+
label = f"-> Launching subagent: {subagent or 'unknown'}"
|
|
111
|
+
if desc:
|
|
112
|
+
label += f" — {desc}"
|
|
113
|
+
self.console.print(f"[cyan]{escape(label)}[/cyan]")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
tool_name = sender if sender != "Ripperdoc" else content
|
|
117
|
+
tool_display = f"● {tool_name}("
|
|
118
|
+
|
|
119
|
+
args_parts = self.format_tool_args(tool_name, tool_args)
|
|
120
|
+
if args_parts:
|
|
121
|
+
tool_display += ", ".join(args_parts)
|
|
122
|
+
tool_display += ")"
|
|
123
|
+
|
|
124
|
+
self.console.print(f"[dim cyan]{escape(tool_display)}[/]")
|
|
125
|
+
|
|
126
|
+
def print_tool_result(
|
|
127
|
+
self,
|
|
128
|
+
sender: str,
|
|
129
|
+
content: str,
|
|
130
|
+
tool_data: Any,
|
|
131
|
+
tool_error: bool = False,
|
|
132
|
+
parse_bash_output_fn: Optional[Callable] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Render a tool result summary using the renderer registry."""
|
|
135
|
+
# Check for failure states
|
|
136
|
+
failed = tool_error
|
|
137
|
+
if tool_data is not None:
|
|
138
|
+
if isinstance(tool_data, dict):
|
|
139
|
+
failed = failed or (tool_data.get("success") is False)
|
|
140
|
+
else:
|
|
141
|
+
success = getattr(tool_data, "success", None)
|
|
142
|
+
failed = failed or (success is False)
|
|
143
|
+
failed = failed or bool(self._get_tool_field(tool_data, "is_error"))
|
|
144
|
+
|
|
145
|
+
# Extract warning/token info
|
|
146
|
+
warning_text = None
|
|
147
|
+
token_estimate = None
|
|
148
|
+
if tool_data is not None:
|
|
149
|
+
warning_text = self._get_tool_field(tool_data, "warning")
|
|
150
|
+
token_estimate = self._get_tool_field(tool_data, "token_estimate")
|
|
151
|
+
|
|
152
|
+
# Handle failure case
|
|
153
|
+
if failed:
|
|
154
|
+
if content:
|
|
155
|
+
self.console.print(f" ⎿ [red]{escape(content)}[/red]")
|
|
156
|
+
else:
|
|
157
|
+
self.console.print(f" ⎿ [red]{escape(sender)} failed[/red]")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Display warnings and token estimates
|
|
161
|
+
if warning_text:
|
|
162
|
+
self.console.print(f" ⎿ [yellow]{escape(str(warning_text))}[/yellow]")
|
|
163
|
+
if token_estimate:
|
|
164
|
+
self.console.print(
|
|
165
|
+
f" [dim]Estimated tokens: {escape(str(token_estimate))}[/dim]"
|
|
166
|
+
)
|
|
167
|
+
elif token_estimate and self.verbose:
|
|
168
|
+
self.console.print(f" ⎿ [dim]Estimated tokens: {escape(str(token_estimate))}[/dim]")
|
|
169
|
+
|
|
170
|
+
# Handle empty content
|
|
171
|
+
if not content:
|
|
172
|
+
self.console.print(" ⎿ [dim]Tool completed[/]")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Use renderer registry for tool-specific rendering
|
|
176
|
+
registry = ToolResultRendererRegistry(
|
|
177
|
+
self.console, self.verbose, parse_bash_output_fn or self._default_parse_bash
|
|
178
|
+
)
|
|
179
|
+
if registry.render(sender, content, tool_data):
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# Fallback for unhandled tools
|
|
183
|
+
self.console.print(" ⎿ [dim]Tool completed[/]")
|
|
184
|
+
|
|
185
|
+
def print_generic_tool(self, sender: str, content: str) -> None:
|
|
186
|
+
"""Fallback rendering for miscellaneous tool messages."""
|
|
187
|
+
if sender == "Task" and isinstance(content, str) and content.startswith("[subagent:"):
|
|
188
|
+
agent_label = content.split("]", 1)[0].replace("[subagent:", "").strip()
|
|
189
|
+
summary = content.split("]", 1)[1].strip() if "]" in content else ""
|
|
190
|
+
self.console.print(f"[green]↳ Subagent {escape(agent_label)} finished[/green]")
|
|
191
|
+
if summary:
|
|
192
|
+
self.console.print(f" {summary}", markup=False)
|
|
193
|
+
return
|
|
194
|
+
self.console.print(f"[dim cyan][Tool] {escape(sender)}: {escape(content)}[/]")
|
|
195
|
+
|
|
196
|
+
def print_human_or_assistant(self, sender: str, content: str) -> None:
|
|
197
|
+
"""Render messages from the user or assistant."""
|
|
198
|
+
if sender.lower() == "you":
|
|
199
|
+
self.console.print(f"[bold green]{escape(sender)}:[/] {escape(content)}")
|
|
200
|
+
return
|
|
201
|
+
self.console.print(Markdown(content))
|
|
202
|
+
|
|
203
|
+
def _get_tool_field(self, data: Any, key: str, default: Any = None) -> Any:
|
|
204
|
+
"""Safely fetch a field from either an object or a dict."""
|
|
205
|
+
if isinstance(data, dict):
|
|
206
|
+
return data.get(key, default)
|
|
207
|
+
return getattr(data, key, default)
|
|
208
|
+
|
|
209
|
+
def _default_parse_bash(self, content: str) -> Tuple[List[str], List[str]]:
|
|
210
|
+
"""Default bash output parser."""
|
|
211
|
+
return parse_bash_output_sections(content)
|
|
212
|
+
|
|
213
|
+
def print_reasoning(self, reasoning: Any) -> None:
|
|
214
|
+
"""Display a collapsed preview of reasoning/thinking blocks."""
|
|
215
|
+
preview = format_reasoning_preview(reasoning)
|
|
216
|
+
if preview:
|
|
217
|
+
self.console.print(f"[dim italic]Thinking: {escape(preview)}[/]")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def parse_bash_output_sections(content: str) -> Tuple[List[str], List[str]]:
|
|
221
|
+
"""Parse stdout/stderr sections from a bash output text block."""
|
|
222
|
+
stdout_lines: List[str] = []
|
|
223
|
+
stderr_lines: List[str] = []
|
|
224
|
+
if not content:
|
|
225
|
+
return stdout_lines, stderr_lines
|
|
226
|
+
|
|
227
|
+
current: Optional[str] = None
|
|
228
|
+
for line in content.splitlines():
|
|
229
|
+
stripped = line.strip()
|
|
230
|
+
if stripped.startswith("stdout:"):
|
|
231
|
+
current = "stdout"
|
|
232
|
+
remainder = line.split("stdout:", 1)[1].strip()
|
|
233
|
+
if remainder:
|
|
234
|
+
stdout_lines.append(remainder)
|
|
235
|
+
continue
|
|
236
|
+
if stripped.startswith("stderr:"):
|
|
237
|
+
current = "stderr"
|
|
238
|
+
remainder = line.split("stderr:", 1)[1].strip()
|
|
239
|
+
if remainder:
|
|
240
|
+
stderr_lines.append(remainder)
|
|
241
|
+
continue
|
|
242
|
+
if stripped.startswith("exit code:"):
|
|
243
|
+
break
|
|
244
|
+
if current == "stdout":
|
|
245
|
+
stdout_lines.append(line)
|
|
246
|
+
elif current == "stderr":
|
|
247
|
+
stderr_lines.append(line)
|
|
248
|
+
|
|
249
|
+
return stdout_lines, stderr_lines
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""UI panels and visual components for RichUI.
|
|
2
|
+
|
|
3
|
+
This module contains welcome panels, status bars, and other
|
|
4
|
+
visual UI elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich import box
|
|
13
|
+
|
|
14
|
+
from ripperdoc import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_welcome_panel() -> Panel:
|
|
18
|
+
"""Create a welcome panel for the CLI startup."""
|
|
19
|
+
welcome_content = """
|
|
20
|
+
[bold cyan]Welcome to Ripperdoc![/bold cyan]
|
|
21
|
+
|
|
22
|
+
Ripperdoc is an AI-powered coding assistant that helps with software development tasks.
|
|
23
|
+
You can read files, edit code, run commands, and help with various programming tasks.
|
|
24
|
+
|
|
25
|
+
[dim]Type your questions below. Press Ctrl+C to exit.[/dim]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
return Panel(
|
|
29
|
+
welcome_content,
|
|
30
|
+
title=f"Ripperdoc v{__version__}",
|
|
31
|
+
border_style="cyan",
|
|
32
|
+
box=box.ROUNDED,
|
|
33
|
+
padding=(1, 2),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
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
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def print_shortcuts(console: Console) -> None:
|
|
51
|
+
"""Show common keyboard shortcuts and prefixes."""
|
|
52
|
+
pairs: List[Tuple[str, str]] = [
|
|
53
|
+
("? for shortcuts", "! for bash mode"),
|
|
54
|
+
("/ for commands", "@ for file mention"),
|
|
55
|
+
]
|
|
56
|
+
console.print("[dim]Shortcuts[/dim]")
|
|
57
|
+
for left, right in pairs:
|
|
58
|
+
left_text = f" {left}".ljust(32)
|
|
59
|
+
right_text = f"{right}" if right else ""
|
|
60
|
+
console.print(f"{left_text}{right_text}")
|