ripperdoc 0.2.6__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 +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Literal, Optional
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.markup import escape
|
|
4
|
+
from rich.status import Status
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Spinner:
|
|
8
|
+
"""Lightweight spinner wrapper for Rich status."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, console: Console, text: str = "Thinking...", spinner: str = "dots"):
|
|
11
|
+
self.console = console
|
|
12
|
+
self.text = text
|
|
13
|
+
self.spinner = spinner
|
|
14
|
+
self._status: Optional[Status] = None
|
|
15
|
+
|
|
16
|
+
def start(self) -> None:
|
|
17
|
+
"""Start the spinner if not already running."""
|
|
18
|
+
|
|
19
|
+
if self._status is not None:
|
|
20
|
+
return
|
|
21
|
+
self._status = self.console.status(
|
|
22
|
+
f"[cyan]{escape(self.text)}[/cyan]", spinner=self.spinner
|
|
23
|
+
)
|
|
24
|
+
self._status.__enter__()
|
|
25
|
+
|
|
26
|
+
def update(self, text: Optional[str] = None) -> None:
|
|
27
|
+
"""Update spinner text."""
|
|
28
|
+
|
|
29
|
+
if self._status is None:
|
|
30
|
+
return
|
|
31
|
+
new_text = text if text is not None else self.text
|
|
32
|
+
self._status.update(f"[cyan]{escape(new_text)}[/cyan]")
|
|
33
|
+
|
|
34
|
+
def stop(self) -> None:
|
|
35
|
+
"""Stop the spinner if running."""
|
|
36
|
+
|
|
37
|
+
if self._status is None:
|
|
38
|
+
return
|
|
39
|
+
self._status.__exit__(None, None, None)
|
|
40
|
+
self._status = None
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> "Spinner":
|
|
43
|
+
self.start()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
|
|
47
|
+
self.stop()
|
|
48
|
+
# Do not suppress exceptions
|
|
49
|
+
return False
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Specialized spinner that shows token progress with playful verbs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from ripperdoc.cli.ui.spinner import Spinner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
THINKING_WORDS: list[str] = [
|
|
15
|
+
"Accomplishing",
|
|
16
|
+
"Actioning",
|
|
17
|
+
"Actualizing",
|
|
18
|
+
"Baking",
|
|
19
|
+
"Booping",
|
|
20
|
+
"Brewing",
|
|
21
|
+
"Calculating",
|
|
22
|
+
"Cerebrating",
|
|
23
|
+
"Channelling",
|
|
24
|
+
"Churning",
|
|
25
|
+
"Clauding",
|
|
26
|
+
"Coalescing",
|
|
27
|
+
"Cogitating",
|
|
28
|
+
"Computing",
|
|
29
|
+
"Combobulating",
|
|
30
|
+
"Concocting",
|
|
31
|
+
"Conjuring",
|
|
32
|
+
"Considering",
|
|
33
|
+
"Contemplating",
|
|
34
|
+
"Cooking",
|
|
35
|
+
"Crafting",
|
|
36
|
+
"Creating",
|
|
37
|
+
"Crunching",
|
|
38
|
+
"Deciphering",
|
|
39
|
+
"Deliberating",
|
|
40
|
+
"Determining",
|
|
41
|
+
"Discombobulating",
|
|
42
|
+
"Divining",
|
|
43
|
+
"Doing",
|
|
44
|
+
"Effecting",
|
|
45
|
+
"Elucidating",
|
|
46
|
+
"Enchanting",
|
|
47
|
+
"Envisioning",
|
|
48
|
+
"Finagling",
|
|
49
|
+
"Flibbertigibbeting",
|
|
50
|
+
"Forging",
|
|
51
|
+
"Forming",
|
|
52
|
+
"Frolicking",
|
|
53
|
+
"Generating",
|
|
54
|
+
"Germinating",
|
|
55
|
+
"Hatching",
|
|
56
|
+
"Herding",
|
|
57
|
+
"Honking",
|
|
58
|
+
"Ideating",
|
|
59
|
+
"Imagining",
|
|
60
|
+
"Incubating",
|
|
61
|
+
"Inferring",
|
|
62
|
+
"Manifesting",
|
|
63
|
+
"Marinating",
|
|
64
|
+
"Meandering",
|
|
65
|
+
"Moseying",
|
|
66
|
+
"Mulling",
|
|
67
|
+
"Mustering",
|
|
68
|
+
"Musing",
|
|
69
|
+
"Noodling",
|
|
70
|
+
"Percolating",
|
|
71
|
+
"Perusing",
|
|
72
|
+
"Philosophising",
|
|
73
|
+
"Pontificating",
|
|
74
|
+
"Pondering",
|
|
75
|
+
"Processing",
|
|
76
|
+
"Puttering",
|
|
77
|
+
"Puzzling",
|
|
78
|
+
"Reticulating",
|
|
79
|
+
"Ruminating",
|
|
80
|
+
"Scheming",
|
|
81
|
+
"Schlepping",
|
|
82
|
+
"Shimmying",
|
|
83
|
+
"Simmering",
|
|
84
|
+
"Smooshing",
|
|
85
|
+
"Spelunking",
|
|
86
|
+
"Spinning",
|
|
87
|
+
"Stewing",
|
|
88
|
+
"Sussing",
|
|
89
|
+
"Synthesizing",
|
|
90
|
+
"Thinking",
|
|
91
|
+
"Tinkering",
|
|
92
|
+
"Transmuting",
|
|
93
|
+
"Unfurling",
|
|
94
|
+
"Unravelling",
|
|
95
|
+
"Vibing",
|
|
96
|
+
"Wandering",
|
|
97
|
+
"Whirring",
|
|
98
|
+
"Wibbling",
|
|
99
|
+
"Wizarding",
|
|
100
|
+
"Working",
|
|
101
|
+
"Wrangling",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ThinkingSpinner(Spinner):
|
|
106
|
+
"""Spinner that shows elapsed time and token progress."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, console: Console, prompt_tokens: int) -> None:
|
|
109
|
+
self.prompt_tokens = prompt_tokens
|
|
110
|
+
self.start_time = time.monotonic()
|
|
111
|
+
self.out_tokens = 0
|
|
112
|
+
self.thinking_word = random.choice(THINKING_WORDS)
|
|
113
|
+
super().__init__(console, self._format_text(), spinner="dots")
|
|
114
|
+
|
|
115
|
+
def _format_text(self, suffix: Optional[str] = None) -> str:
|
|
116
|
+
elapsed = int(time.monotonic() - self.start_time)
|
|
117
|
+
base = f"✽ {self.thinking_word}… (esc to interrupt · {elapsed}s"
|
|
118
|
+
if self.out_tokens > 0:
|
|
119
|
+
base += f" · ↓ {self.out_tokens} tokens"
|
|
120
|
+
else:
|
|
121
|
+
base += f" · ↑ {self.prompt_tokens} tokens"
|
|
122
|
+
if suffix:
|
|
123
|
+
base += f" · {suffix}"
|
|
124
|
+
return base + ")"
|
|
125
|
+
|
|
126
|
+
def update_tokens(self, out_tokens: int, suffix: Optional[str] = None) -> None:
|
|
127
|
+
self.out_tokens = max(0, out_tokens)
|
|
128
|
+
self.update(self._format_text(suffix))
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Tool result renderers for CLI display.
|
|
2
|
+
|
|
3
|
+
This module provides a strategy pattern implementation for rendering different
|
|
4
|
+
tool results in the Rich CLI interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, List, Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.markup import escape
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolResultRenderer:
|
|
14
|
+
"""Base class for rendering tool results to console."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, console: Console, verbose: bool = False):
|
|
17
|
+
self.console = console
|
|
18
|
+
self.verbose = verbose
|
|
19
|
+
|
|
20
|
+
def can_handle(self, _sender: str) -> bool:
|
|
21
|
+
"""Return True if this renderer handles the given tool name."""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def render(self, _content: str, _tool_data: Any) -> None:
|
|
25
|
+
"""Render the tool result to console."""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def _get_field(self, data: Any, key: str, default: Any = None) -> Any:
|
|
29
|
+
"""Safely fetch a field from either an object or a dict."""
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
return data.get(key, default)
|
|
32
|
+
return getattr(data, key, default)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TodoResultRenderer(ToolResultRenderer):
|
|
36
|
+
"""Render Todo tool results."""
|
|
37
|
+
|
|
38
|
+
def can_handle(self, sender: str) -> bool:
|
|
39
|
+
return "Todo" in sender
|
|
40
|
+
|
|
41
|
+
def render(self, content: str, _tool_data: Any) -> None:
|
|
42
|
+
lines = content.splitlines()
|
|
43
|
+
if lines:
|
|
44
|
+
self.console.print(f" ⎿ [dim]{escape(lines[0])}[/]")
|
|
45
|
+
for line in lines[1:]:
|
|
46
|
+
self.console.print(f" {line}", markup=False)
|
|
47
|
+
else:
|
|
48
|
+
self.console.print(" ⎿ [dim]Todo update[/]")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReadResultRenderer(ToolResultRenderer):
|
|
52
|
+
"""Render Read/View tool results."""
|
|
53
|
+
|
|
54
|
+
def can_handle(self, sender: str) -> bool:
|
|
55
|
+
return "Read" in sender or "View" in sender
|
|
56
|
+
|
|
57
|
+
def render(self, content: str, _tool_data: Any) -> None:
|
|
58
|
+
lines = content.split("\n")
|
|
59
|
+
line_count = len(lines)
|
|
60
|
+
self.console.print(f" ⎿ [dim]Read {line_count} lines[/]")
|
|
61
|
+
if self.verbose:
|
|
62
|
+
preview = lines[:30]
|
|
63
|
+
for line in preview:
|
|
64
|
+
self.console.print(line, markup=False)
|
|
65
|
+
if len(lines) > len(preview):
|
|
66
|
+
self.console.print(f"[dim]... ({len(lines) - len(preview)} more lines)[/]")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EditResultRenderer(ToolResultRenderer):
|
|
70
|
+
"""Render Write/Edit/MultiEdit tool results."""
|
|
71
|
+
|
|
72
|
+
def can_handle(self, sender: str) -> bool:
|
|
73
|
+
return "Write" in sender or "Edit" in sender or "MultiEdit" in sender
|
|
74
|
+
|
|
75
|
+
def render(self, _content: str, tool_data: Any) -> None:
|
|
76
|
+
if tool_data and (hasattr(tool_data, "file_path") or isinstance(tool_data, dict)):
|
|
77
|
+
file_path = self._get_field(tool_data, "file_path")
|
|
78
|
+
additions = self._get_field(tool_data, "additions", 0)
|
|
79
|
+
deletions = self._get_field(tool_data, "deletions", 0)
|
|
80
|
+
diff_with_line_numbers = self._get_field(tool_data, "diff_with_line_numbers", [])
|
|
81
|
+
|
|
82
|
+
if not file_path:
|
|
83
|
+
self.console.print(" ⎿ [dim]File updated successfully[/]")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
self.console.print(
|
|
87
|
+
f" ⎿ [dim]Updated {escape(str(file_path))} with {additions} additions and {deletions} removals[/]"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if self.verbose:
|
|
91
|
+
for line in diff_with_line_numbers:
|
|
92
|
+
self.console.print(line, markup=False)
|
|
93
|
+
else:
|
|
94
|
+
self.console.print(" ⎿ [dim]File updated successfully[/]")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GlobResultRenderer(ToolResultRenderer):
|
|
98
|
+
"""Render Glob tool results."""
|
|
99
|
+
|
|
100
|
+
def can_handle(self, sender: str) -> bool:
|
|
101
|
+
return "Glob" in sender
|
|
102
|
+
|
|
103
|
+
def render(self, content: str, _tool_data: Any) -> None:
|
|
104
|
+
files = content.split("\n")
|
|
105
|
+
file_count = len([f for f in files if f.strip()])
|
|
106
|
+
self.console.print(f" ⎿ [dim]Found {file_count} files[/]")
|
|
107
|
+
if self.verbose:
|
|
108
|
+
for line in files[:30]:
|
|
109
|
+
if line.strip():
|
|
110
|
+
self.console.print(f" {line}", markup=False)
|
|
111
|
+
if file_count > 30:
|
|
112
|
+
self.console.print(f"[dim]... ({file_count - 30} more)[/]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class GrepResultRenderer(ToolResultRenderer):
|
|
116
|
+
"""Render Grep tool results."""
|
|
117
|
+
|
|
118
|
+
def can_handle(self, sender: str) -> bool:
|
|
119
|
+
return "Grep" in sender
|
|
120
|
+
|
|
121
|
+
def render(self, content: str, _tool_data: Any) -> None:
|
|
122
|
+
matches = content.split("\n")
|
|
123
|
+
match_count = len([m for m in matches if m.strip()])
|
|
124
|
+
self.console.print(f" ⎿ [dim]Found {match_count} matches[/]")
|
|
125
|
+
if self.verbose:
|
|
126
|
+
for line in matches[:30]:
|
|
127
|
+
if line.strip():
|
|
128
|
+
self.console.print(f" {line}", markup=False)
|
|
129
|
+
if match_count > 30:
|
|
130
|
+
self.console.print(f"[dim]... ({match_count - 30} more)[/]")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class LSResultRenderer(ToolResultRenderer):
|
|
134
|
+
"""Render LS tool results."""
|
|
135
|
+
|
|
136
|
+
def can_handle(self, sender: str) -> bool:
|
|
137
|
+
return "LS" in sender
|
|
138
|
+
|
|
139
|
+
def render(self, content: str, _tool_data: Any) -> None:
|
|
140
|
+
tree_lines = content.splitlines()
|
|
141
|
+
self.console.print(f" ⎿ [dim]Directory tree ({len(tree_lines)} lines)[/]")
|
|
142
|
+
if self.verbose:
|
|
143
|
+
preview = tree_lines[:40]
|
|
144
|
+
for line in preview:
|
|
145
|
+
self.console.print(f" {line}", markup=False)
|
|
146
|
+
if len(tree_lines) > len(preview):
|
|
147
|
+
self.console.print(f"[dim]... ({len(tree_lines) - len(preview)} more)[/]")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Type alias for bash output parser callback
|
|
151
|
+
BashOutputParser = Callable[[str], tuple[List[str], List[str]]]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class BashResultRenderer(ToolResultRenderer):
|
|
155
|
+
"""Render Bash tool results."""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self, console: Console, verbose: bool = False, parse_fallback: Optional[BashOutputParser] = None
|
|
159
|
+
):
|
|
160
|
+
super().__init__(console, verbose)
|
|
161
|
+
self._parse_fallback = parse_fallback
|
|
162
|
+
|
|
163
|
+
def can_handle(self, sender: str) -> bool:
|
|
164
|
+
return "Bash" in sender
|
|
165
|
+
|
|
166
|
+
def render(self, content: str, tool_data: Any) -> None:
|
|
167
|
+
stdout_lines: List[str] = []
|
|
168
|
+
stderr_lines: List[str] = []
|
|
169
|
+
exit_code = 0
|
|
170
|
+
duration_ms = 0
|
|
171
|
+
timeout_ms = 0
|
|
172
|
+
|
|
173
|
+
if tool_data:
|
|
174
|
+
exit_code = self._get_field(tool_data, "exit_code", 0)
|
|
175
|
+
stdout = self._get_field(tool_data, "stdout", "") or ""
|
|
176
|
+
stderr = self._get_field(tool_data, "stderr", "") or ""
|
|
177
|
+
duration_ms = self._get_field(tool_data, "duration_ms", 0) or 0
|
|
178
|
+
timeout_ms = self._get_field(tool_data, "timeout_ms", 0) or 0
|
|
179
|
+
stdout_lines = stdout.splitlines() if stdout else []
|
|
180
|
+
stderr_lines = stderr.splitlines() if stderr else []
|
|
181
|
+
|
|
182
|
+
if not stdout_lines and not stderr_lines and content and self._parse_fallback:
|
|
183
|
+
stdout_lines, stderr_lines = self._parse_fallback(content)
|
|
184
|
+
|
|
185
|
+
show_inline_stdout = (
|
|
186
|
+
stdout_lines and not stderr_lines and exit_code == 0 and not self.verbose
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if show_inline_stdout:
|
|
190
|
+
preview = stdout_lines if self.verbose else stdout_lines[:5]
|
|
191
|
+
self.console.print(f" ⎿ {preview[0]}", markup=False)
|
|
192
|
+
for line in preview[1:]:
|
|
193
|
+
self.console.print(f" {line}", markup=False)
|
|
194
|
+
if not self.verbose and len(stdout_lines) > len(preview):
|
|
195
|
+
self.console.print(f"[dim]... ({len(stdout_lines) - len(preview)} more lines)[/]")
|
|
196
|
+
else:
|
|
197
|
+
self._render_detailed_output(
|
|
198
|
+
tool_data, exit_code, duration_ms, timeout_ms, stdout_lines, stderr_lines
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _render_detailed_output(
|
|
202
|
+
self,
|
|
203
|
+
tool_data: Any,
|
|
204
|
+
exit_code: int,
|
|
205
|
+
duration_ms: float,
|
|
206
|
+
timeout_ms: int,
|
|
207
|
+
stdout_lines: List[str],
|
|
208
|
+
stderr_lines: List[str],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Render detailed Bash output with exit code, stdout, stderr."""
|
|
211
|
+
if tool_data:
|
|
212
|
+
timing = ""
|
|
213
|
+
if duration_ms:
|
|
214
|
+
timing = f" ({duration_ms / 1000:.2f}s"
|
|
215
|
+
if timeout_ms:
|
|
216
|
+
timing += f" / timeout {timeout_ms / 1000:.0f}s"
|
|
217
|
+
timing += ")"
|
|
218
|
+
elif timeout_ms:
|
|
219
|
+
timing = f" (timeout {timeout_ms / 1000:.0f}s)"
|
|
220
|
+
self.console.print(f" ⎿ [dim]Exit code {exit_code}{timing}[/]")
|
|
221
|
+
else:
|
|
222
|
+
self.console.print(" ⎿ [dim]Command executed[/]")
|
|
223
|
+
|
|
224
|
+
# Render stdout
|
|
225
|
+
if stdout_lines:
|
|
226
|
+
preview = stdout_lines if self.verbose else stdout_lines[:5]
|
|
227
|
+
self.console.print("[dim]stdout:[/]")
|
|
228
|
+
for line in preview:
|
|
229
|
+
self.console.print(f" {line}", markup=False)
|
|
230
|
+
if not self.verbose and len(stdout_lines) > len(preview):
|
|
231
|
+
self.console.print(
|
|
232
|
+
f"[dim]... ({len(stdout_lines) - len(preview)} more stdout lines)[/]"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
self.console.print("[dim]stdout:[/]")
|
|
236
|
+
self.console.print(" [dim](no stdout)[/]")
|
|
237
|
+
|
|
238
|
+
# Render stderr
|
|
239
|
+
if stderr_lines:
|
|
240
|
+
preview = stderr_lines if self.verbose else stderr_lines[:5]
|
|
241
|
+
self.console.print("[dim]stderr:[/]")
|
|
242
|
+
for line in preview:
|
|
243
|
+
self.console.print(f" {line}", markup=False)
|
|
244
|
+
if not self.verbose and len(stderr_lines) > len(preview):
|
|
245
|
+
self.console.print(
|
|
246
|
+
f"[dim]... ({len(stderr_lines) - len(preview)} more stderr lines)[/]"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
self.console.print("[dim]stderr:[/]")
|
|
250
|
+
self.console.print(" [dim](no stderr)[/]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ToolResultRendererRegistry:
|
|
254
|
+
"""Registry that selects the appropriate renderer for a tool result."""
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self, console: Console, verbose: bool = False, parse_bash_fallback: Optional[BashOutputParser] = None
|
|
258
|
+
):
|
|
259
|
+
self.console = console
|
|
260
|
+
self.verbose = verbose
|
|
261
|
+
self._renderers: List[ToolResultRenderer] = [
|
|
262
|
+
TodoResultRenderer(console, verbose),
|
|
263
|
+
ReadResultRenderer(console, verbose),
|
|
264
|
+
EditResultRenderer(console, verbose),
|
|
265
|
+
GlobResultRenderer(console, verbose),
|
|
266
|
+
GrepResultRenderer(console, verbose),
|
|
267
|
+
LSResultRenderer(console, verbose),
|
|
268
|
+
BashResultRenderer(console, verbose, parse_bash_fallback),
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
def get_renderer(self, sender: str) -> Optional[ToolResultRenderer]:
|
|
272
|
+
"""Get the appropriate renderer for the given tool name."""
|
|
273
|
+
for renderer in self._renderers:
|
|
274
|
+
if renderer.can_handle(sender):
|
|
275
|
+
return renderer
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def render(self, sender: str, content: str, tool_data: Any) -> bool:
|
|
279
|
+
"""Render the tool result. Returns True if rendered, False otherwise."""
|
|
280
|
+
renderer = self.get_renderer(sender)
|
|
281
|
+
if renderer:
|
|
282
|
+
renderer.render(content, tool_data)
|
|
283
|
+
return True
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
__all__ = [
|
|
288
|
+
"ToolResultRenderer",
|
|
289
|
+
"ToolResultRendererRegistry",
|
|
290
|
+
"TodoResultRenderer",
|
|
291
|
+
"ReadResultRenderer",
|
|
292
|
+
"EditResultRenderer",
|
|
293
|
+
"GlobResultRenderer",
|
|
294
|
+
"GrepResultRenderer",
|
|
295
|
+
"LSResultRenderer",
|
|
296
|
+
"BashResultRenderer",
|
|
297
|
+
"BashOutputParser",
|
|
298
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core functionality for Ripperdoc."""
|