ripperdoc 0.1.0__py3-none-any.whl → 0.2.2__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 +75 -15
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +23 -1
- ripperdoc/cli/commands/context_cmd.py +13 -3
- ripperdoc/cli/commands/cost_cmd.py +1 -1
- ripperdoc/cli/commands/doctor_cmd.py +200 -0
- ripperdoc/cli/commands/memory_cmd.py +209 -0
- ripperdoc/cli/commands/models_cmd.py +25 -0
- ripperdoc/cli/commands/resume_cmd.py +3 -3
- ripperdoc/cli/commands/status_cmd.py +5 -5
- ripperdoc/cli/commands/tasks_cmd.py +32 -5
- ripperdoc/cli/ui/context_display.py +4 -3
- ripperdoc/cli/ui/rich_ui.py +205 -43
- ripperdoc/cli/ui/spinner.py +3 -4
- ripperdoc/core/agents.py +10 -6
- ripperdoc/core/config.py +48 -3
- ripperdoc/core/default_tools.py +26 -6
- ripperdoc/core/permissions.py +19 -0
- ripperdoc/core/query.py +238 -302
- ripperdoc/core/query_utils.py +537 -0
- ripperdoc/core/system_prompt.py +2 -1
- ripperdoc/core/tool.py +14 -1
- ripperdoc/sdk/client.py +1 -1
- ripperdoc/tools/background_shell.py +9 -3
- ripperdoc/tools/bash_tool.py +19 -4
- ripperdoc/tools/file_edit_tool.py +9 -2
- ripperdoc/tools/file_read_tool.py +9 -2
- ripperdoc/tools/file_write_tool.py +15 -2
- ripperdoc/tools/glob_tool.py +57 -17
- ripperdoc/tools/grep_tool.py +9 -2
- ripperdoc/tools/ls_tool.py +244 -75
- ripperdoc/tools/mcp_tools.py +47 -19
- ripperdoc/tools/multi_edit_tool.py +13 -2
- ripperdoc/tools/notebook_edit_tool.py +9 -6
- ripperdoc/tools/task_tool.py +20 -5
- ripperdoc/tools/todo_tool.py +163 -29
- ripperdoc/tools/tool_search_tool.py +15 -4
- ripperdoc/utils/git_utils.py +276 -0
- ripperdoc/utils/json_utils.py +28 -0
- ripperdoc/utils/log.py +130 -29
- ripperdoc/utils/mcp.py +83 -10
- ripperdoc/utils/memory.py +14 -1
- ripperdoc/utils/message_compaction.py +51 -14
- ripperdoc/utils/messages.py +63 -4
- ripperdoc/utils/output_utils.py +36 -9
- ripperdoc/utils/permissions/path_validation_utils.py +6 -0
- ripperdoc/utils/safe_get_cwd.py +4 -0
- ripperdoc/utils/session_history.py +27 -9
- ripperdoc/utils/todo.py +2 -2
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/METADATA +4 -2
- ripperdoc-0.2.2.dist-info/RECORD +86 -0
- ripperdoc-0.1.0.dist-info/RECORD +0 -81
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/WHEEL +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/top_level.txt +0 -0
ripperdoc/cli/ui/rich_ui.py
CHANGED
|
@@ -6,7 +6,7 @@ This module provides a clean, minimal terminal UI using Rich for the Ripperdoc a
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import sys
|
|
8
8
|
import uuid
|
|
9
|
-
from typing import List, Dict, Any, Optional
|
|
9
|
+
from typing import List, Dict, Any, Optional, Union, Iterable
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from rich.console import Console
|
|
@@ -32,10 +32,9 @@ from ripperdoc.cli.commands import (
|
|
|
32
32
|
slash_command_completions,
|
|
33
33
|
)
|
|
34
34
|
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
35
|
-
from ripperdoc.core.permissions import make_permission_checker
|
|
35
|
+
from ripperdoc.core.permissions import make_permission_checker
|
|
36
36
|
from ripperdoc.cli.ui.spinner import Spinner
|
|
37
37
|
from ripperdoc.cli.ui.context_display import context_usage_lines
|
|
38
|
-
from ripperdoc.utils.messages import create_user_message, create_assistant_message
|
|
39
38
|
from ripperdoc.utils.message_compaction import (
|
|
40
39
|
compact_messages,
|
|
41
40
|
estimate_conversation_tokens,
|
|
@@ -53,9 +52,21 @@ from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_
|
|
|
53
52
|
from ripperdoc.utils.session_history import SessionHistory
|
|
54
53
|
from ripperdoc.utils.memory import build_memory_instructions
|
|
55
54
|
from ripperdoc.core.query import query_llm
|
|
55
|
+
from ripperdoc.utils.messages import (
|
|
56
|
+
UserMessage,
|
|
57
|
+
AssistantMessage,
|
|
58
|
+
ProgressMessage,
|
|
59
|
+
create_user_message,
|
|
60
|
+
create_assistant_message,
|
|
61
|
+
)
|
|
62
|
+
from ripperdoc.utils.log import enable_session_file_logging, get_logger
|
|
63
|
+
|
|
64
|
+
# Type alias for conversation messages
|
|
65
|
+
ConversationMessage = Union[UserMessage, AssistantMessage, ProgressMessage]
|
|
56
66
|
|
|
57
67
|
|
|
58
68
|
console = Console()
|
|
69
|
+
logger = get_logger()
|
|
59
70
|
|
|
60
71
|
# Keep a small window of recent messages alongside the summary after /compact so
|
|
61
72
|
# the model retains immediate context.
|
|
@@ -101,12 +112,18 @@ def create_status_bar() -> Text:
|
|
|
101
112
|
class RichUI:
|
|
102
113
|
"""Rich-based UI for Ripperdoc."""
|
|
103
114
|
|
|
104
|
-
def __init__(
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
safe_mode: bool = False,
|
|
118
|
+
verbose: bool = False,
|
|
119
|
+
session_id: Optional[str] = None,
|
|
120
|
+
log_file_path: Optional[Path] = None,
|
|
121
|
+
):
|
|
105
122
|
self.console = console
|
|
106
123
|
self.safe_mode = safe_mode
|
|
107
124
|
self.verbose = verbose
|
|
108
|
-
self.conversation_messages: List[
|
|
109
|
-
self._saved_conversation: Optional[List[
|
|
125
|
+
self.conversation_messages: List[ConversationMessage] = []
|
|
126
|
+
self._saved_conversation: Optional[List[ConversationMessage]] = None
|
|
110
127
|
self.query_context: Optional[QueryContext] = None
|
|
111
128
|
self._current_tool: Optional[str] = None
|
|
112
129
|
self._should_exit: bool = False
|
|
@@ -115,20 +132,44 @@ class RichUI:
|
|
|
115
132
|
self._prompt_session: Optional[PromptSession] = None
|
|
116
133
|
self.project_path = Path.cwd()
|
|
117
134
|
# Track a stable session identifier for the current UI run.
|
|
118
|
-
self.session_id = str(uuid.uuid4())
|
|
135
|
+
self.session_id = session_id or str(uuid.uuid4())
|
|
136
|
+
if log_file_path:
|
|
137
|
+
self.log_file_path = log_file_path
|
|
138
|
+
logger.attach_file_handler(self.log_file_path)
|
|
139
|
+
else:
|
|
140
|
+
self.log_file_path = enable_session_file_logging(self.project_path, self.session_id)
|
|
141
|
+
logger.info(
|
|
142
|
+
"[ui] Initialized Rich UI session",
|
|
143
|
+
extra={
|
|
144
|
+
"session_id": self.session_id,
|
|
145
|
+
"project_path": str(self.project_path),
|
|
146
|
+
"log_file": str(self.log_file_path),
|
|
147
|
+
"safe_mode": self.safe_mode,
|
|
148
|
+
"verbose": self.verbose,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
119
151
|
self._session_history = SessionHistory(self.project_path, self.session_id)
|
|
120
152
|
self._permission_checker = (
|
|
121
153
|
make_permission_checker(self.project_path, safe_mode) if safe_mode else None
|
|
122
154
|
)
|
|
123
155
|
|
|
124
156
|
def _context_usage_lines(
|
|
125
|
-
self, breakdown, model_label: str, auto_compact_enabled: bool
|
|
157
|
+
self, breakdown: Any, model_label: str, auto_compact_enabled: bool
|
|
126
158
|
) -> List[str]:
|
|
127
159
|
return context_usage_lines(breakdown, model_label, auto_compact_enabled)
|
|
128
160
|
|
|
129
161
|
def _set_session(self, session_id: str) -> None:
|
|
130
162
|
"""Switch to a different session id and reset logging."""
|
|
131
163
|
self.session_id = session_id
|
|
164
|
+
self.log_file_path = enable_session_file_logging(self.project_path, self.session_id)
|
|
165
|
+
logger.info(
|
|
166
|
+
"[ui] Switched session",
|
|
167
|
+
extra={
|
|
168
|
+
"session_id": self.session_id,
|
|
169
|
+
"project_path": str(self.project_path),
|
|
170
|
+
"log_file": str(self.log_file_path),
|
|
171
|
+
},
|
|
172
|
+
)
|
|
132
173
|
self._session_history = SessionHistory(self.project_path, session_id)
|
|
133
174
|
|
|
134
175
|
def _log_message(self, message: Any) -> None:
|
|
@@ -137,7 +178,10 @@ class RichUI:
|
|
|
137
178
|
self._session_history.append(message)
|
|
138
179
|
except Exception:
|
|
139
180
|
# Logging failures should never interrupt the UI flow
|
|
140
|
-
|
|
181
|
+
logger.exception(
|
|
182
|
+
"[ui] Failed to append message to session history",
|
|
183
|
+
extra={"session_id": self.session_id},
|
|
184
|
+
)
|
|
141
185
|
|
|
142
186
|
def _append_prompt_history(self, text: str) -> None:
|
|
143
187
|
"""Append text to the interactive prompt history."""
|
|
@@ -147,7 +191,10 @@ class RichUI:
|
|
|
147
191
|
try:
|
|
148
192
|
session.history.append_string(text)
|
|
149
193
|
except Exception:
|
|
150
|
-
|
|
194
|
+
logger.exception(
|
|
195
|
+
"[ui] Failed to append prompt history",
|
|
196
|
+
extra={"session_id": self.session_id},
|
|
197
|
+
)
|
|
151
198
|
|
|
152
199
|
def replay_conversation(self, messages: List[Dict[str, Any]]) -> None:
|
|
153
200
|
"""Render a conversation history in the console and seed prompt history."""
|
|
@@ -188,10 +235,11 @@ class RichUI:
|
|
|
188
235
|
sender: str,
|
|
189
236
|
content: str,
|
|
190
237
|
is_tool: bool = False,
|
|
191
|
-
tool_type: str = None,
|
|
192
|
-
tool_args: dict = None,
|
|
238
|
+
tool_type: Optional[str] = None,
|
|
239
|
+
tool_args: Optional[dict] = None,
|
|
193
240
|
tool_data: Any = None,
|
|
194
|
-
|
|
241
|
+
tool_error: bool = False,
|
|
242
|
+
) -> None:
|
|
195
243
|
"""Display a message in the conversation."""
|
|
196
244
|
if not is_tool:
|
|
197
245
|
self._print_human_or_assistant(sender, content)
|
|
@@ -202,7 +250,7 @@ class RichUI:
|
|
|
202
250
|
return
|
|
203
251
|
|
|
204
252
|
if tool_type == "result":
|
|
205
|
-
self._print_tool_result(sender, content, tool_data)
|
|
253
|
+
self._print_tool_result(sender, content, tool_data, tool_error)
|
|
206
254
|
return
|
|
207
255
|
|
|
208
256
|
self._print_generic_tool(sender, content)
|
|
@@ -298,8 +346,25 @@ class RichUI:
|
|
|
298
346
|
|
|
299
347
|
self.console.print(f"[dim cyan]{escape(tool_display)}[/]")
|
|
300
348
|
|
|
301
|
-
def _print_tool_result(
|
|
349
|
+
def _print_tool_result(
|
|
350
|
+
self, sender: str, content: str, tool_data: Any, tool_error: bool = False
|
|
351
|
+
) -> None:
|
|
302
352
|
"""Render a tool result summary."""
|
|
353
|
+
failed = tool_error
|
|
354
|
+
if tool_data is not None:
|
|
355
|
+
if isinstance(tool_data, dict):
|
|
356
|
+
failed = failed or (tool_data.get("success") is False)
|
|
357
|
+
else:
|
|
358
|
+
success = getattr(tool_data, "success", None)
|
|
359
|
+
failed = failed or (success is False)
|
|
360
|
+
|
|
361
|
+
if failed:
|
|
362
|
+
if content:
|
|
363
|
+
self.console.print(f" ⎿ [red]{escape(content)}[/red]")
|
|
364
|
+
else:
|
|
365
|
+
self.console.print(f" ⎿ [red]{escape(sender)} failed[/red]")
|
|
366
|
+
return
|
|
367
|
+
|
|
303
368
|
if not content:
|
|
304
369
|
self.console.print(" ⎿ [dim]Tool completed[/]")
|
|
305
370
|
return
|
|
@@ -426,12 +491,12 @@ class RichUI:
|
|
|
426
491
|
if tool_data:
|
|
427
492
|
timing = ""
|
|
428
493
|
if duration_ms:
|
|
429
|
-
timing = f" ({duration_ms/1000:.2f}s"
|
|
494
|
+
timing = f" ({duration_ms / 1000:.2f}s"
|
|
430
495
|
if timeout_ms:
|
|
431
|
-
timing += f" / timeout {timeout_ms/1000:.0f}s"
|
|
496
|
+
timing += f" / timeout {timeout_ms / 1000:.0f}s"
|
|
432
497
|
timing += ")"
|
|
433
498
|
elif timeout_ms:
|
|
434
|
-
timing = f" (timeout {timeout_ms/1000:.0f}s)"
|
|
499
|
+
timing = f" (timeout {timeout_ms / 1000:.0f}s)"
|
|
435
500
|
self.console.print(f" ⎿ [dim]Exit code {exit_code}{timing}[/]")
|
|
436
501
|
else:
|
|
437
502
|
self.console.print(" ⎿ [dim]Command executed[/]")
|
|
@@ -534,7 +599,7 @@ class RichUI:
|
|
|
534
599
|
return "\n".join(parts)
|
|
535
600
|
return ""
|
|
536
601
|
|
|
537
|
-
def _render_transcript(self, messages: List[
|
|
602
|
+
def _render_transcript(self, messages: List[ConversationMessage]) -> str:
|
|
538
603
|
"""Render a simple transcript for summarization."""
|
|
539
604
|
lines: List[str] = []
|
|
540
605
|
for msg in messages:
|
|
@@ -549,7 +614,7 @@ class RichUI:
|
|
|
549
614
|
lines.append(f"{label}: {text}")
|
|
550
615
|
return "\n".join(lines)
|
|
551
616
|
|
|
552
|
-
def _extract_assistant_text(self, assistant_message) -> str:
|
|
617
|
+
def _extract_assistant_text(self, assistant_message: Any) -> str:
|
|
553
618
|
"""Extract plain text from an AssistantMessage."""
|
|
554
619
|
if isinstance(assistant_message.message.content, str):
|
|
555
620
|
return assistant_message.message.content
|
|
@@ -561,12 +626,26 @@ class RichUI:
|
|
|
561
626
|
return "\n".join(parts)
|
|
562
627
|
return ""
|
|
563
628
|
|
|
564
|
-
async def process_query(self, user_input: str):
|
|
629
|
+
async def process_query(self, user_input: str) -> None:
|
|
565
630
|
"""Process a user query and display the response."""
|
|
566
631
|
if not self.query_context:
|
|
567
632
|
self.query_context = QueryContext(
|
|
568
633
|
tools=self.get_default_tools(), safe_mode=self.safe_mode, verbose=self.verbose
|
|
569
634
|
)
|
|
635
|
+
else:
|
|
636
|
+
# Clear any prior abort so new queries aren't immediately interrupted.
|
|
637
|
+
abort_controller = getattr(self.query_context, "abort_controller", None)
|
|
638
|
+
if abort_controller is not None:
|
|
639
|
+
abort_controller.clear()
|
|
640
|
+
|
|
641
|
+
logger.info(
|
|
642
|
+
"[ui] Starting query processing",
|
|
643
|
+
extra={
|
|
644
|
+
"session_id": self.session_id,
|
|
645
|
+
"prompt_length": len(user_input),
|
|
646
|
+
"prompt_preview": user_input[:200],
|
|
647
|
+
},
|
|
648
|
+
)
|
|
570
649
|
|
|
571
650
|
try:
|
|
572
651
|
context: Dict[str, str] = {}
|
|
@@ -576,6 +655,15 @@ class RichUI:
|
|
|
576
655
|
self.query_context.tools = merge_tools_with_dynamic(
|
|
577
656
|
self.query_context.tools, dynamic_tools
|
|
578
657
|
)
|
|
658
|
+
logger.debug(
|
|
659
|
+
"[ui] Prepared tools and MCP servers",
|
|
660
|
+
extra={
|
|
661
|
+
"session_id": self.session_id,
|
|
662
|
+
"tool_count": len(self.query_context.tools),
|
|
663
|
+
"mcp_servers": len(servers),
|
|
664
|
+
"dynamic_tools": len(dynamic_tools),
|
|
665
|
+
},
|
|
666
|
+
)
|
|
579
667
|
mcp_instructions = format_mcp_instructions(servers)
|
|
580
668
|
base_system_prompt = build_system_prompt(
|
|
581
669
|
self.query_context.tools,
|
|
@@ -598,14 +686,26 @@ class RichUI:
|
|
|
598
686
|
|
|
599
687
|
config = get_global_config()
|
|
600
688
|
model_profile = get_profile_for_pointer("main")
|
|
601
|
-
max_context_tokens = get_remaining_context_tokens(
|
|
689
|
+
max_context_tokens = get_remaining_context_tokens(
|
|
690
|
+
model_profile, config.context_token_limit
|
|
691
|
+
)
|
|
602
692
|
auto_compact_enabled = resolve_auto_compact_enabled(config)
|
|
603
693
|
protocol = provider_protocol(model_profile.provider) if model_profile else "openai"
|
|
604
694
|
|
|
605
|
-
used_tokens = estimate_used_tokens(messages, protocol=protocol)
|
|
695
|
+
used_tokens = estimate_used_tokens(messages, protocol=protocol) # type: ignore[arg-type]
|
|
606
696
|
usage_status = get_context_usage_status(
|
|
607
697
|
used_tokens, max_context_tokens, auto_compact_enabled
|
|
608
698
|
)
|
|
699
|
+
logger.debug(
|
|
700
|
+
"[ui] Context usage snapshot",
|
|
701
|
+
extra={
|
|
702
|
+
"session_id": self.session_id,
|
|
703
|
+
"used_tokens": used_tokens,
|
|
704
|
+
"max_context_tokens": max_context_tokens,
|
|
705
|
+
"percent_used": round(usage_status.percent_used, 2),
|
|
706
|
+
"auto_compact_enabled": auto_compact_enabled,
|
|
707
|
+
},
|
|
708
|
+
)
|
|
609
709
|
|
|
610
710
|
if usage_status.is_above_warning:
|
|
611
711
|
console.print(
|
|
@@ -619,27 +719,38 @@ class RichUI:
|
|
|
619
719
|
|
|
620
720
|
if usage_status.should_auto_compact:
|
|
621
721
|
original_messages = list(messages)
|
|
622
|
-
compaction = compact_messages(messages, protocol=protocol)
|
|
722
|
+
compaction = compact_messages(messages, protocol=protocol) # type: ignore[arg-type]
|
|
623
723
|
if compaction.was_compacted:
|
|
624
724
|
if self._saved_conversation is None:
|
|
625
|
-
self._saved_conversation = original_messages
|
|
626
|
-
messages = compaction.messages
|
|
725
|
+
self._saved_conversation = original_messages # type: ignore[assignment]
|
|
726
|
+
messages = compaction.messages # type: ignore[assignment]
|
|
627
727
|
console.print(
|
|
628
728
|
f"[yellow]Auto-compacted conversation (saved ~{compaction.tokens_saved} tokens). "
|
|
629
729
|
f"Estimated usage: {compaction.tokens_after}/{max_context_tokens} tokens.[/yellow]"
|
|
630
730
|
)
|
|
731
|
+
logger.info(
|
|
732
|
+
"[ui] Auto-compacted conversation",
|
|
733
|
+
extra={
|
|
734
|
+
"session_id": self.session_id,
|
|
735
|
+
"tokens_before": compaction.tokens_before,
|
|
736
|
+
"tokens_after": compaction.tokens_after,
|
|
737
|
+
"tokens_saved": compaction.tokens_saved,
|
|
738
|
+
"cleared_tool_ids": list(compaction.cleared_tool_ids),
|
|
739
|
+
},
|
|
740
|
+
)
|
|
631
741
|
|
|
632
742
|
spinner = Spinner(console, "Thinking...", spinner="dots")
|
|
633
743
|
# Wrap permission checker to pause the spinner while waiting for user input.
|
|
634
744
|
base_permission_checker = self._permission_checker
|
|
635
745
|
|
|
636
|
-
async def permission_checker(tool, parsed_input):
|
|
746
|
+
async def permission_checker(tool: Any, parsed_input: Any) -> bool:
|
|
637
747
|
if spinner:
|
|
638
748
|
spinner.stop()
|
|
639
749
|
try:
|
|
640
750
|
if base_permission_checker is not None:
|
|
641
|
-
|
|
642
|
-
|
|
751
|
+
result = await base_permission_checker(tool, parsed_input)
|
|
752
|
+
return result.result if hasattr(result, "result") else True
|
|
753
|
+
return True
|
|
643
754
|
finally:
|
|
644
755
|
if spinner:
|
|
645
756
|
spinner.start()
|
|
@@ -652,9 +763,13 @@ class RichUI:
|
|
|
652
763
|
try:
|
|
653
764
|
spinner.start()
|
|
654
765
|
async for message in query(
|
|
655
|
-
messages,
|
|
766
|
+
messages,
|
|
767
|
+
system_prompt,
|
|
768
|
+
context,
|
|
769
|
+
self.query_context,
|
|
770
|
+
permission_checker, # type: ignore[arg-type]
|
|
656
771
|
):
|
|
657
|
-
if message.type == "assistant":
|
|
772
|
+
if message.type == "assistant" and isinstance(message, AssistantMessage):
|
|
658
773
|
# Extract text content from assistant message
|
|
659
774
|
if isinstance(message.message.content, str):
|
|
660
775
|
self.display_message("Ripperdoc", message.message.content)
|
|
@@ -688,7 +803,7 @@ class RichUI:
|
|
|
688
803
|
tool_registry[tool_use_id]["printed"] = True
|
|
689
804
|
last_tool_name = tool_name
|
|
690
805
|
|
|
691
|
-
elif message.type == "user":
|
|
806
|
+
elif message.type == "user" and isinstance(message, UserMessage):
|
|
692
807
|
# Handle tool results - show summary instead of full content
|
|
693
808
|
if isinstance(message.message.content, list):
|
|
694
809
|
for block in message.message.content:
|
|
@@ -699,6 +814,7 @@ class RichUI:
|
|
|
699
814
|
):
|
|
700
815
|
tool_name = "Tool"
|
|
701
816
|
tool_data = getattr(message, "tool_use_result", None)
|
|
817
|
+
is_error = bool(getattr(block, "is_error", False))
|
|
702
818
|
|
|
703
819
|
tool_use_id = getattr(block, "tool_use_id", None)
|
|
704
820
|
entry = tool_registry.get(tool_use_id) if tool_use_id else None
|
|
@@ -722,9 +838,10 @@ class RichUI:
|
|
|
722
838
|
is_tool=True,
|
|
723
839
|
tool_type="result",
|
|
724
840
|
tool_data=tool_data,
|
|
841
|
+
tool_error=is_error,
|
|
725
842
|
)
|
|
726
843
|
|
|
727
|
-
elif message.type == "progress":
|
|
844
|
+
elif message.type == "progress" and isinstance(message, ProgressMessage):
|
|
728
845
|
if self.verbose:
|
|
729
846
|
self.display_message(
|
|
730
847
|
"System", f"Progress: {message.content}", is_tool=True
|
|
@@ -740,18 +857,32 @@ class RichUI:
|
|
|
740
857
|
|
|
741
858
|
# Add message to history
|
|
742
859
|
self._log_message(message)
|
|
743
|
-
messages.append(message)
|
|
860
|
+
messages.append(message) # type: ignore[arg-type]
|
|
744
861
|
except Exception as e:
|
|
862
|
+
logger.exception(
|
|
863
|
+
"[ui] Unhandled error while processing streamed query response",
|
|
864
|
+
extra={"session_id": self.session_id},
|
|
865
|
+
)
|
|
745
866
|
self.display_message("System", f"Error: {str(e)}", is_tool=True)
|
|
746
867
|
finally:
|
|
747
868
|
# Ensure spinner stops even on exceptions
|
|
748
869
|
try:
|
|
749
870
|
spinner.stop()
|
|
750
871
|
except Exception:
|
|
751
|
-
|
|
872
|
+
logger.exception(
|
|
873
|
+
"[ui] Failed to stop spinner", extra={"session_id": self.session_id}
|
|
874
|
+
)
|
|
752
875
|
|
|
753
876
|
# Update conversation history
|
|
754
877
|
self.conversation_messages = messages
|
|
878
|
+
logger.info(
|
|
879
|
+
"[ui] Query processing completed",
|
|
880
|
+
extra={
|
|
881
|
+
"session_id": self.session_id,
|
|
882
|
+
"conversation_messages": len(self.conversation_messages),
|
|
883
|
+
"project_path": str(self.project_path),
|
|
884
|
+
},
|
|
885
|
+
)
|
|
755
886
|
finally:
|
|
756
887
|
await shutdown_mcp_runtime()
|
|
757
888
|
await shutdown_mcp_runtime()
|
|
@@ -787,7 +918,7 @@ class RichUI:
|
|
|
787
918
|
def __init__(self, completions: List):
|
|
788
919
|
self.completions = completions
|
|
789
920
|
|
|
790
|
-
def get_completions(self, document, complete_event):
|
|
921
|
+
def get_completions(self, document: Any, complete_event: Any) -> Iterable[Completion]:
|
|
791
922
|
text = document.text_before_cursor
|
|
792
923
|
if not text.startswith("/"):
|
|
793
924
|
return
|
|
@@ -809,7 +940,7 @@ class RichUI:
|
|
|
809
940
|
)
|
|
810
941
|
return self._prompt_session
|
|
811
942
|
|
|
812
|
-
def run(self):
|
|
943
|
+
def run(self) -> None:
|
|
813
944
|
"""Run the Rich-based interface."""
|
|
814
945
|
# Display welcome panel
|
|
815
946
|
console.print()
|
|
@@ -822,6 +953,10 @@ class RichUI:
|
|
|
822
953
|
console.print("[dim]Tip: type '/' then press Tab to see available commands.[/dim]\n")
|
|
823
954
|
|
|
824
955
|
session = self.get_prompt_session()
|
|
956
|
+
logger.info(
|
|
957
|
+
"[ui] Starting interactive loop",
|
|
958
|
+
extra={"session_id": self.session_id, "log_file": str(self.log_file_path)},
|
|
959
|
+
)
|
|
825
960
|
|
|
826
961
|
while not self._should_exit:
|
|
827
962
|
try:
|
|
@@ -838,6 +973,10 @@ class RichUI:
|
|
|
838
973
|
|
|
839
974
|
# Handle slash commands locally
|
|
840
975
|
if user_input.startswith("/"):
|
|
976
|
+
logger.debug(
|
|
977
|
+
"[ui] Received slash command",
|
|
978
|
+
extra={"session_id": self.session_id, "command": user_input},
|
|
979
|
+
)
|
|
841
980
|
handled = self.handle_slash_command(user_input)
|
|
842
981
|
if self._should_exit:
|
|
843
982
|
break
|
|
@@ -846,6 +985,14 @@ class RichUI:
|
|
|
846
985
|
continue
|
|
847
986
|
|
|
848
987
|
# Process the query
|
|
988
|
+
logger.info(
|
|
989
|
+
"[ui] Processing interactive prompt",
|
|
990
|
+
extra={
|
|
991
|
+
"session_id": self.session_id,
|
|
992
|
+
"prompt_length": len(user_input),
|
|
993
|
+
"prompt_preview": user_input[:200],
|
|
994
|
+
},
|
|
995
|
+
)
|
|
849
996
|
asyncio.run(self.process_query(user_input))
|
|
850
997
|
|
|
851
998
|
console.print() # Add spacing between interactions
|
|
@@ -858,6 +1005,9 @@ class RichUI:
|
|
|
858
1005
|
break
|
|
859
1006
|
except Exception as e:
|
|
860
1007
|
console.print(f"[red]Error: {escape(str(e))}[/]")
|
|
1008
|
+
logger.exception(
|
|
1009
|
+
"[ui] Error in interactive loop", extra={"session_id": self.session_id}
|
|
1010
|
+
)
|
|
861
1011
|
if self.verbose:
|
|
862
1012
|
import traceback
|
|
863
1013
|
|
|
@@ -887,6 +1037,9 @@ class RichUI:
|
|
|
887
1037
|
)
|
|
888
1038
|
except Exception as e:
|
|
889
1039
|
console.print(f"[red]Error during compaction: {escape(str(e))}[/red]")
|
|
1040
|
+
logger.exception(
|
|
1041
|
+
"[ui] Error during manual compaction", extra={"session_id": self.session_id}
|
|
1042
|
+
)
|
|
890
1043
|
return
|
|
891
1044
|
finally:
|
|
892
1045
|
spinner.stop()
|
|
@@ -929,7 +1082,7 @@ class RichUI:
|
|
|
929
1082
|
|
|
930
1083
|
async def _summarize_conversation(
|
|
931
1084
|
self,
|
|
932
|
-
messages: List[
|
|
1085
|
+
messages: List[ConversationMessage],
|
|
933
1086
|
custom_instructions: str,
|
|
934
1087
|
) -> str:
|
|
935
1088
|
"""Summarize the given conversation using the configured model."""
|
|
@@ -949,12 +1102,11 @@ class RichUI:
|
|
|
949
1102
|
instructions += f"\nCustom instructions: {custom_instructions.strip()}"
|
|
950
1103
|
|
|
951
1104
|
user_content = (
|
|
952
|
-
"Summarize the following conversation between a user and an assistant:\n\n"
|
|
953
|
-
f"{transcript}"
|
|
1105
|
+
f"Summarize the following conversation between a user and an assistant:\n\n{transcript}"
|
|
954
1106
|
)
|
|
955
1107
|
|
|
956
1108
|
assistant_response = await query_llm(
|
|
957
|
-
messages=[{"role": "user", "content": user_content}],
|
|
1109
|
+
messages=[{"role": "user", "content": user_content}], # type: ignore[list-item]
|
|
958
1110
|
system_prompt=instructions,
|
|
959
1111
|
tools=[],
|
|
960
1112
|
max_thinking_tokens=0,
|
|
@@ -994,7 +1146,12 @@ def check_onboarding_rich() -> bool:
|
|
|
994
1146
|
return check_onboarding()
|
|
995
1147
|
|
|
996
1148
|
|
|
997
|
-
def main_rich(
|
|
1149
|
+
def main_rich(
|
|
1150
|
+
safe_mode: bool = False,
|
|
1151
|
+
verbose: bool = False,
|
|
1152
|
+
session_id: Optional[str] = None,
|
|
1153
|
+
log_file_path: Optional[Path] = None,
|
|
1154
|
+
) -> None:
|
|
998
1155
|
"""Main entry point for Rich interface."""
|
|
999
1156
|
|
|
1000
1157
|
# Ensure onboarding is complete
|
|
@@ -1002,7 +1159,12 @@ def main_rich(safe_mode: bool = False, verbose: bool = False) -> None:
|
|
|
1002
1159
|
sys.exit(1)
|
|
1003
1160
|
|
|
1004
1161
|
# Run the Rich UI
|
|
1005
|
-
ui = RichUI(
|
|
1162
|
+
ui = RichUI(
|
|
1163
|
+
safe_mode=safe_mode,
|
|
1164
|
+
verbose=verbose,
|
|
1165
|
+
session_id=session_id,
|
|
1166
|
+
log_file_path=log_file_path,
|
|
1167
|
+
)
|
|
1006
1168
|
ui.run()
|
|
1007
1169
|
|
|
1008
1170
|
|
ripperdoc/cli/ui/spinner.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
from typing import Any, Literal
|
|
1
|
+
from typing import Any, Literal, Optional
|
|
4
2
|
from rich.console import Console
|
|
5
3
|
from rich.markup import escape
|
|
4
|
+
from rich.status import Status
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class Spinner:
|
|
@@ -12,7 +11,7 @@ class Spinner:
|
|
|
12
11
|
self.console = console
|
|
13
12
|
self.text = text
|
|
14
13
|
self.spinner = spinner
|
|
15
|
-
self._status
|
|
14
|
+
self._status: Optional[Status] = None
|
|
16
15
|
|
|
17
16
|
def start(self) -> None:
|
|
18
17
|
"""Start the spinner if not already running."""
|
ripperdoc/core/agents.py
CHANGED
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, Iterable, List, Optional, Tuple
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
@@ -98,7 +98,7 @@ def _agent_dir_for_location(location: AgentLocation) -> Path:
|
|
|
98
98
|
raise ValueError(f"Unsupported agent location: {location}")
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def _split_frontmatter(raw_text: str) -> Tuple[Dict[str,
|
|
101
|
+
def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
|
|
102
102
|
"""Extract YAML frontmatter and body content."""
|
|
103
103
|
lines = raw_text.splitlines()
|
|
104
104
|
if len(lines) >= 3 and lines[0].strip() == "---":
|
|
@@ -109,7 +109,7 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, object], str]:
|
|
|
109
109
|
try:
|
|
110
110
|
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
111
111
|
except Exception as exc: # pragma: no cover - defensive
|
|
112
|
-
logger.
|
|
112
|
+
logger.exception("Invalid frontmatter in agent file", extra={"error": str(exc)})
|
|
113
113
|
return {"__error__": f"Invalid frontmatter: {exc}"}, body
|
|
114
114
|
return frontmatter, body
|
|
115
115
|
return {}, raw_text
|
|
@@ -136,7 +136,9 @@ def _parse_agent_file(
|
|
|
136
136
|
try:
|
|
137
137
|
text = path.read_text(encoding="utf-8")
|
|
138
138
|
except Exception as exc:
|
|
139
|
-
logger.
|
|
139
|
+
logger.exception(
|
|
140
|
+
"Failed to read agent file", extra={"error": str(exc), "path": str(path)}
|
|
141
|
+
)
|
|
140
142
|
return None, f"Failed to read agent file {path}: {exc}"
|
|
141
143
|
|
|
142
144
|
frontmatter, body = _split_frontmatter(text)
|
|
@@ -151,8 +153,10 @@ def _parse_agent_file(
|
|
|
151
153
|
return None, 'Missing required "description" field in frontmatter'
|
|
152
154
|
|
|
153
155
|
tools = _normalize_tools(frontmatter.get("tools"))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
model_value = frontmatter.get("model")
|
|
157
|
+
color_value = frontmatter.get("color")
|
|
158
|
+
model = model_value if isinstance(model_value, str) else None
|
|
159
|
+
color = color_value if isinstance(color_value, str) else None
|
|
156
160
|
|
|
157
161
|
agent = AgentDefinition(
|
|
158
162
|
agent_type=agent_name.strip(),
|