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.
@@ -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}")