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.
Files changed (57) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +75 -15
  3. ripperdoc/cli/commands/__init__.py +4 -0
  4. ripperdoc/cli/commands/agents_cmd.py +23 -1
  5. ripperdoc/cli/commands/context_cmd.py +13 -3
  6. ripperdoc/cli/commands/cost_cmd.py +1 -1
  7. ripperdoc/cli/commands/doctor_cmd.py +200 -0
  8. ripperdoc/cli/commands/memory_cmd.py +209 -0
  9. ripperdoc/cli/commands/models_cmd.py +25 -0
  10. ripperdoc/cli/commands/resume_cmd.py +3 -3
  11. ripperdoc/cli/commands/status_cmd.py +5 -5
  12. ripperdoc/cli/commands/tasks_cmd.py +32 -5
  13. ripperdoc/cli/ui/context_display.py +4 -3
  14. ripperdoc/cli/ui/rich_ui.py +205 -43
  15. ripperdoc/cli/ui/spinner.py +3 -4
  16. ripperdoc/core/agents.py +10 -6
  17. ripperdoc/core/config.py +48 -3
  18. ripperdoc/core/default_tools.py +26 -6
  19. ripperdoc/core/permissions.py +19 -0
  20. ripperdoc/core/query.py +238 -302
  21. ripperdoc/core/query_utils.py +537 -0
  22. ripperdoc/core/system_prompt.py +2 -1
  23. ripperdoc/core/tool.py +14 -1
  24. ripperdoc/sdk/client.py +1 -1
  25. ripperdoc/tools/background_shell.py +9 -3
  26. ripperdoc/tools/bash_tool.py +19 -4
  27. ripperdoc/tools/file_edit_tool.py +9 -2
  28. ripperdoc/tools/file_read_tool.py +9 -2
  29. ripperdoc/tools/file_write_tool.py +15 -2
  30. ripperdoc/tools/glob_tool.py +57 -17
  31. ripperdoc/tools/grep_tool.py +9 -2
  32. ripperdoc/tools/ls_tool.py +244 -75
  33. ripperdoc/tools/mcp_tools.py +47 -19
  34. ripperdoc/tools/multi_edit_tool.py +13 -2
  35. ripperdoc/tools/notebook_edit_tool.py +9 -6
  36. ripperdoc/tools/task_tool.py +20 -5
  37. ripperdoc/tools/todo_tool.py +163 -29
  38. ripperdoc/tools/tool_search_tool.py +15 -4
  39. ripperdoc/utils/git_utils.py +276 -0
  40. ripperdoc/utils/json_utils.py +28 -0
  41. ripperdoc/utils/log.py +130 -29
  42. ripperdoc/utils/mcp.py +83 -10
  43. ripperdoc/utils/memory.py +14 -1
  44. ripperdoc/utils/message_compaction.py +51 -14
  45. ripperdoc/utils/messages.py +63 -4
  46. ripperdoc/utils/output_utils.py +36 -9
  47. ripperdoc/utils/permissions/path_validation_utils.py +6 -0
  48. ripperdoc/utils/safe_get_cwd.py +4 -0
  49. ripperdoc/utils/session_history.py +27 -9
  50. ripperdoc/utils/todo.py +2 -2
  51. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/METADATA +4 -2
  52. ripperdoc-0.2.2.dist-info/RECORD +86 -0
  53. ripperdoc-0.1.0.dist-info/RECORD +0 -81
  54. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/WHEEL +0 -0
  55. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/entry_points.txt +0 -0
  56. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
  57. {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/top_level.txt +0 -0
@@ -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, PermissionResult
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__(self, safe_mode: bool = False, verbose: bool = False):
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[Dict[str, Any]] = []
109
- self._saved_conversation: Optional[List[Dict[str, Any]]] = None
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
- return
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
- return
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(self, sender: str, content: str, tool_data: Any) -> None:
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[Dict[str, Any]]) -> str:
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(model_profile, config.context_token_limit)
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
- return await base_permission_checker(tool, parsed_input)
642
- return PermissionResult(result=True)
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, system_prompt, context, self.query_context, permission_checker
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
- pass
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[Dict[str, Any]],
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(safe_mode: bool = False, verbose: bool = False) -> None:
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(safe_mode=safe_mode, verbose=verbose)
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
 
@@ -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 = None # type: Optional[object]
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, object], 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.error(f"Invalid frontmatter in agent file: {exc}")
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.error(f"Failed to read agent file {path}: {exc}")
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
- model = frontmatter.get("model") if isinstance(frontmatter.get("model"), str) else None
155
- color = frontmatter.get("color") if isinstance(frontmatter.get("color"), str) else None
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(),