connectonion 0.5.8__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 (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1039 @@
1
+ """
2
+ Purpose: Provide Rich-formatted UI for interactive debugging with breakpoint display and user interaction
3
+ LLM-Note:
4
+ Dependencies: imports from [typing, dataclasses, enum, json, ast, inspect, pprint, questionary, rich.*] | imported by [interactive_debugger.py] | no dedicated test file found
5
+ Data flow: InteractiveDebugger creates DebuggerUI() → calls .show_welcome(agent_name) → .get_user_prompt() for input → .show_executing(prompt) → .show_breakpoint(context: BreakpointContext) → displays Rich panels/tables with tool execution details → returns BreakpointAction enum (CONTINUE, EDIT, WHY, QUIT) → .edit_value(context, agent) opens Python REPL for live modifications → returns modifications dict → .display_explanation(text, context) shows AI analysis
6
+ State/Effects: no persistent state (stateless UI) | uses Rich Console to write formatted output to terminal | uses questionary for interactive menus | REPL execution in .edit_value() can have arbitrary side effects (user code) | does not modify agent or trace data directly (returns modifications)
7
+ Integration: exposes DebuggerUI class, BreakpointContext dataclass, BreakpointAction enum | show_breakpoint() displays comprehensive debugging info: execution context, tool args/result, LLM next actions, source code, execution history | edit_value() provides REPL with access to trace_entry, tool_args, agent, result variables | display_explanation() formats AI-generated debugging insights
8
+ Performance: Rich rendering is fast | source code inspection uses inspect.getsource() | execution history can be large (shows all tools) | REPL evaluation is synchronous (blocks UI)
9
+ Errors: REPL errors caught and displayed with syntax highlighting | getsource() failures handled gracefully (shows "unavailable") | questionary keyboard interrupts propagate up | assumes terminal supports Rich formatting
10
+ """
11
+
12
+ from typing import Any, Dict, Optional, Tuple, List
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ import json
16
+ import ast
17
+ import inspect
18
+ from pprint import pformat
19
+
20
+ import questionary
21
+ from questionary import Style
22
+ from rich.console import Console as RichConsole
23
+ from rich.console import Group
24
+ from rich.panel import Panel
25
+ from rich.table import Table
26
+ from rich.syntax import Syntax
27
+ from rich.text import Text
28
+ from rich.tree import Tree
29
+ from rich import box
30
+
31
+
32
+ class BreakpointAction(Enum):
33
+ """User's choice at a breakpoint"""
34
+ CONTINUE = "continue"
35
+ EDIT = "edit"
36
+ WHY = "why"
37
+ QUIT = "quit"
38
+
39
+
40
+ @dataclass
41
+ class BreakpointContext:
42
+ """All data needed to display a breakpoint"""
43
+ tool_name: str
44
+ tool_args: Dict
45
+ trace_entry: Dict
46
+ user_prompt: str
47
+ iteration: int
48
+ max_iterations: int
49
+ previous_tools: List[str]
50
+ next_actions: Optional[List[Dict]] = None # Preview of next planned tools
51
+ tool_function: Optional[Any] = None # The actual tool function for source inspection
52
+
53
+
54
+ class DebuggerUI:
55
+ """Handles all user interaction and display for the debugger."""
56
+
57
+ def __init__(self):
58
+ """Initialize the UI with styling."""
59
+ self.console = RichConsole()
60
+ self.style = Style([
61
+ ('question', 'fg:#00ffff bold'),
62
+ ('pointer', 'fg:#00ff00 bold'),
63
+ ('highlighted', 'fg:#00ff00 bold'),
64
+ ('selected', 'fg:#00ffff'),
65
+ ('instruction', 'fg:#808080'),
66
+ ])
67
+
68
+ def show_welcome(self, agent_name: str) -> None:
69
+ """Display welcome panel for debug session.
70
+
71
+ Args:
72
+ agent_name: Name of the agent being debugged
73
+ """
74
+ self.console.print(Panel(
75
+ "[bold cyan]🔍 Interactive Debug Session Started[/bold cyan]\n\n"
76
+ f"Agent: [yellow]{agent_name}[/yellow]\n"
77
+ "Tools with @xray will pause for inspection\n"
78
+ "Interactive menu at breakpoints to continue or edit\n",
79
+ title="Auto Debug",
80
+ border_style="cyan"
81
+ ))
82
+
83
+ def get_user_prompt(self) -> Optional[str]:
84
+ """Get prompt from user or None if they want to quit.
85
+
86
+ Returns:
87
+ User's prompt string or None to quit
88
+ """
89
+ prompt = input("\nEnter prompt for agent (or 'quit' to exit): ").strip()
90
+
91
+ if prompt.lower() in ['quit', 'exit', 'q']:
92
+ self.console.print("[yellow]Debug session ended.[/yellow]")
93
+ return None
94
+
95
+ return prompt if prompt else self.get_user_prompt() # Retry if empty
96
+
97
+ def show_executing(self, prompt: str) -> None:
98
+ """Show that a prompt is being executed.
99
+
100
+ Args:
101
+ prompt: The prompt being executed
102
+ """
103
+ self.console.print(f"\n[cyan]→ Executing: {prompt}[/cyan]")
104
+
105
+ def show_result(self, result: str) -> None:
106
+ """Display the final result of task execution.
107
+
108
+ Args:
109
+ result: The result to display
110
+ """
111
+ self.console.print(f"\n[green]✓ Result:[/green] {result}")
112
+
113
+ def show_interrupted(self) -> None:
114
+ """Show that task was interrupted."""
115
+ self.console.print("\n[yellow]Task interrupted.[/yellow]")
116
+
117
+ def show_breakpoint(self, context: BreakpointContext) -> BreakpointAction:
118
+ """Display breakpoint UI and get user's choice.
119
+
120
+ Shows tool information, arguments, results, and a menu
121
+ for the user to choose their action.
122
+
123
+ Args:
124
+ context: All context data for the breakpoint
125
+
126
+ Returns:
127
+ User's chosen action
128
+ """
129
+ self._display_breakpoint_info(context)
130
+ return self._show_action_menu()
131
+
132
+ def edit_value(self, context: BreakpointContext, agent: Any = None) -> Dict[str, Any]:
133
+ """Start Python REPL to inspect and modify execution state.
134
+
135
+ Args:
136
+ context: Full breakpoint context with all execution data
137
+ agent: Optional agent instance for accessing agent context
138
+
139
+ Returns:
140
+ Dict of modified values (e.g., {'result': new_value, 'tool_args': {...}})
141
+ """
142
+ import code
143
+
144
+ # Build namespace with all debuggable variables
145
+ result = context.trace_entry.get('result')
146
+ namespace = {
147
+ # Primary execution
148
+ 'result': result,
149
+ 'tool_name': context.tool_name,
150
+ 'tool_args': context.tool_args.copy(), # Make it mutable
151
+
152
+ # Flow control
153
+ 'iteration': context.iteration,
154
+ 'max_iterations': context.max_iterations,
155
+
156
+ # Context
157
+ 'user_prompt': context.user_prompt,
158
+ 'next_actions': context.next_actions,
159
+
160
+ # Advanced
161
+ 'trace_entry': context.trace_entry,
162
+ 'previous_tools': context.previous_tools,
163
+ }
164
+
165
+ # Add agent context if available
166
+ if agent:
167
+ namespace.update({
168
+ 'agent_name': agent.name,
169
+ 'model': agent.llm.model if hasattr(agent.llm, 'model') else 'unknown',
170
+ 'tools_available': [tool.name for tool in agent.tools] if agent.tools else [],
171
+ 'turn': agent.current_session.get('turn', 0) if agent.current_session else 0,
172
+ 'messages': agent.current_session.get('messages', []) if agent.current_session else [],
173
+ })
174
+
175
+ # Add helper function for pretty printing in REPL
176
+ def pp(obj):
177
+ """Pretty print helper for explicit use"""
178
+ from rich.pretty import pprint
179
+ pprint(obj, expand_all=True)
180
+
181
+ namespace['pp'] = pp # Add to namespace
182
+
183
+ # Display REPL header
184
+ self._display_repl_header(context, namespace)
185
+
186
+ # Customize REPL display hook to auto pretty-print
187
+ import sys
188
+ from rich.pretty import pprint
189
+
190
+ original_displayhook = sys.displayhook
191
+
192
+ def rich_displayhook(value):
193
+ """Custom display hook that uses Rich pretty printing"""
194
+ if value is not None:
195
+ pprint(value, expand_all=True)
196
+ # Also store in _ for REPL access
197
+ import builtins
198
+ builtins._ = value
199
+
200
+ sys.displayhook = rich_displayhook
201
+
202
+ # Check if stdin is available for interactive REPL
203
+ if not sys.stdin or not hasattr(sys.stdin, 'isatty') or not sys.stdin.isatty():
204
+ self.console.print("\n[yellow]⚠️ Interactive REPL not available (stdin not connected)[/yellow]")
205
+ self.console.print("[dim]Tip: Run directly in a terminal (not through VSCode/IDE) to use EDIT feature[/dim]")
206
+ return {}
207
+
208
+ # Start interactive Python REPL
209
+ banner = "" # Empty banner since we show our own header
210
+ try:
211
+ code.interact(banner=banner, local=namespace, exitmsg="")
212
+ except SystemExit:
213
+ pass # Normal REPL exit
214
+ finally:
215
+ # Restore original displayhook
216
+ sys.displayhook = original_displayhook
217
+
218
+ # Extract modifications from namespace
219
+ modifications = {}
220
+ if namespace['result'] != result:
221
+ modifications['result'] = namespace['result']
222
+ if namespace['tool_args'] != context.tool_args:
223
+ modifications['tool_args'] = namespace['tool_args']
224
+ if namespace['iteration'] != context.iteration:
225
+ modifications['iteration'] = namespace['iteration']
226
+ if namespace['max_iterations'] != context.max_iterations:
227
+ modifications['max_iterations'] = namespace['max_iterations']
228
+
229
+ # Show what was modified
230
+ if modifications:
231
+ self._display_modifications(modifications)
232
+ else:
233
+ self.console.print("\n[dim]No modifications made[/dim]")
234
+
235
+ return modifications
236
+
237
+ # Private helper methods for cleaner code
238
+
239
+ def _display_breakpoint_info(self, context: BreakpointContext) -> None:
240
+ """Display complete debugging context from user prompt to execution result.
241
+
242
+ Shows a comprehensive panel with:
243
+ - User prompt and iteration context
244
+ - Execution flow tree (previous → current → next tools)
245
+ - Current execution details (function call, result, source code)
246
+ - Next planned action preview
247
+
248
+ Args:
249
+ context: All breakpoint data including tool info, execution state, and previews
250
+ """
251
+ # Clear some space
252
+ self.console.print("\n")
253
+
254
+ # Build sections without individual panels
255
+ sections = []
256
+
257
+ # 1. Context Section
258
+ prompt_display = context.user_prompt if len(context.user_prompt) <= 80 else f"{context.user_prompt[:80]}..."
259
+ sections.append(Text("CONTEXT", style="bold dim"))
260
+ sections.append(Text(f'User Prompt: "{prompt_display}"', style="cyan"))
261
+ sections.append(Text(f"Iteration: {context.iteration}/{context.max_iterations} | Model: o4-mini", style="dim"))
262
+ sections.append(Text("")) # Empty line for spacing
263
+
264
+ # 2. Execution Flow Section
265
+ sections.append(Text("EXECUTION FLOW", style="bold dim"))
266
+
267
+ tree = Tree("User Input")
268
+ llm_branch = tree.add("LLM Decision")
269
+
270
+ # Add all tools in the chain
271
+ all_tools = context.previous_tools + [context.tool_name]
272
+ for i, tool in enumerate(all_tools):
273
+ if tool == context.tool_name:
274
+ # Current tool (highlighted)
275
+ timing = context.trace_entry.get('timing', 0)
276
+ llm_branch.add(f"[bold yellow]⚡ {tool}() - {timing/1000:.4f}s ← PAUSED HERE[/bold yellow]")
277
+ elif i < len(context.previous_tools):
278
+ # Completed tools
279
+ llm_branch.add(f"✓ {tool}() - [dim]completed[/dim]")
280
+
281
+ # Add next planned actions based on LLM preview
282
+ if context.next_actions is not None:
283
+ if context.next_actions:
284
+ # Show the actual planned next tools
285
+ for i, action in enumerate(context.next_actions):
286
+ tool_name = action['name']
287
+ tool_args = action.get('args', {})
288
+
289
+ # Format arguments for display
290
+ args_display = []
291
+ for key, value in tool_args.items():
292
+ if isinstance(value, str) and len(value) > 20:
293
+ args_display.append(f"{key}='...'")
294
+ elif isinstance(value, str):
295
+ args_display.append(f"{key}='{value}'")
296
+ else:
297
+ args_display.append(f"{key}={value}")
298
+ args_str = ', '.join(args_display) if args_display else ''
299
+
300
+ llm_branch.add(f"📍 {tool_name}({args_str}) - [dim]planned next[/dim]")
301
+ else:
302
+ # No more tools planned - task complete
303
+ llm_branch.add("✅ Task complete - [dim]no more tools needed[/dim]")
304
+ else:
305
+ # Preview unavailable (error or couldn't determine)
306
+ llm_branch.add("❓ Next action - [dim]preview unavailable[/dim]")
307
+
308
+ sections.append(tree)
309
+ sections.append(Text("")) # Empty line for spacing
310
+
311
+ # 3. Current Execution Section (the main focus)
312
+ sections.append(Text("─" * 60, style="dim")) # Visual separator
313
+ sections.append(Text("CURRENT EXECUTION", style="bold yellow"))
314
+ sections.append(Text(""))
315
+
316
+ # Build the function call
317
+ args_str_parts = []
318
+ if context.tool_args:
319
+ for key, value in context.tool_args.items():
320
+ if isinstance(value, str):
321
+ args_str_parts.append(f'{key}="{value}"')
322
+ else:
323
+ args_str_parts.append(f'{key}={value}')
324
+ function_call = f"{context.tool_name}({', '.join(args_str_parts)})"
325
+
326
+ # Get the result
327
+ result = context.trace_entry.get('result', 'No result')
328
+ is_error = context.trace_entry.get('status') == 'error'
329
+
330
+ # REPL section
331
+ sections.append(Text(f">>> {function_call}", style="bold bright_cyan"))
332
+
333
+ if is_error:
334
+ error = context.trace_entry.get('error', str(result))
335
+ sections.append(Text(f"Error: {error}", style="red"))
336
+ else:
337
+ if isinstance(result, str):
338
+ display_result = result[:150] + ('...' if len(result) > 150 else '')
339
+ sections.append(Text(f"'{display_result}'", style="green"))
340
+ elif isinstance(result, (dict, list)):
341
+ try:
342
+ result_json = json.dumps(result, indent=2, ensure_ascii=False)
343
+ display_json = result_json[:200] + ('...' if len(result_json) > 200 else '')
344
+ sections.append(Text(display_json, style="green"))
345
+ except:
346
+ sections.append(Text(f"{str(result)[:100]}...", style="green"))
347
+ else:
348
+ sections.append(Text(str(result), style="green"))
349
+
350
+ sections.append(Text("")) # Spacing
351
+
352
+ # Source code section
353
+ source_code, file_info, start_line = self._get_tool_source(context)
354
+ sections.append(Text(f"Source ({file_info})", style="dim italic"))
355
+
356
+ if source_code:
357
+ # Use start_line_number to show actual file line numbers
358
+ syntax = Syntax(
359
+ source_code,
360
+ "python",
361
+ theme="monokai",
362
+ line_numbers=True,
363
+ start_line=start_line
364
+ )
365
+ sections.append(syntax)
366
+ else:
367
+ sections.append(Text(" Source code unavailable", style="dim"))
368
+ sections.append(Text("─" * 60, style="dim")) # Visual separator
369
+ sections.append(Text("")) # Spacing
370
+
371
+ # 4. Next Planned Action Section
372
+ sections.append(Text("NEXT PLANNED ACTION", style="bold dim"))
373
+
374
+ if context.next_actions is not None:
375
+ if context.next_actions:
376
+ # Show what LLM plans to do next
377
+ sections.append(Text("The LLM will call:", style="dim"))
378
+
379
+ for action in context.next_actions[:1]: # Show just the first one in detail
380
+ tool_name = action['name']
381
+ tool_args = action.get('args', {})
382
+
383
+ # Format the planned call
384
+ args_parts = []
385
+ for key, value in tool_args.items():
386
+ if isinstance(value, str):
387
+ # Show more of the string here since it's a preview
388
+ if len(value) > 50:
389
+ args_parts.append(f'{key}="{value[:50]}..."')
390
+ else:
391
+ args_parts.append(f'{key}="{value}"')
392
+ else:
393
+ args_parts.append(f'{key}={value}')
394
+
395
+ planned_call = f"{tool_name}({', '.join(args_parts)})"
396
+ sections.append(Text(planned_call, style="cyan bold"))
397
+
398
+ if len(context.next_actions) > 1:
399
+ sections.append(Text(f"(and {len(context.next_actions) - 1} more planned actions)", style="dim"))
400
+ else:
401
+ # Task complete
402
+ sections.append(Text("🎯 Task Complete", style="bold green"))
403
+ sections.append(Text("No further tools needed", style="green"))
404
+ else:
405
+ # Preview unavailable
406
+ sections.append(Text("Preview temporarily unavailable", style="dim italic"))
407
+
408
+ # 5. Add metadata footer
409
+ sections.append(Text("")) # Spacing
410
+ timing = context.trace_entry.get('timing', 0)
411
+ metadata = Text(
412
+ f"Execution time: {timing/1000:.4f}s | Iteration: {context.iteration}/{context.max_iterations} | Breakpoint: @xray",
413
+ style="dim italic",
414
+ justify="center"
415
+ )
416
+ sections.append(metadata)
417
+
418
+ # 6. Combine everything into a single panel with proper spacing
419
+ all_content = Group(*sections)
420
+
421
+ # 7. Create single main wrapper panel
422
+ if is_error:
423
+ title = "⚠️ Execution Paused - Error"
424
+ border_style = "red"
425
+ else:
426
+ title = "🔍 Execution Paused - Breakpoint"
427
+ border_style = "yellow"
428
+
429
+ main_panel = Panel(
430
+ all_content,
431
+ title=f"[bold {border_style}]{title}[/bold {border_style}]",
432
+ box=box.ROUNDED,
433
+ border_style=border_style,
434
+ padding=(1, 2)
435
+ )
436
+
437
+ self.console.print(main_panel)
438
+
439
+ def _get_tool_source(self, context: BreakpointContext) -> Tuple[Optional[str], str, int]:
440
+ """Get the source code of the actual tool function.
441
+
442
+ Unwraps decorators to get the original function and extracts:
443
+ - Source code using inspect.getsource()
444
+ - File location and starting line number
445
+ - File info formatted as "filename:line"
446
+
447
+ Args:
448
+ context: Breakpoint context containing tool_function
449
+
450
+ Returns:
451
+ Tuple of (source_code, file_info, start_line_number)
452
+ Returns (None, "source unavailable", 1) if function not available
453
+ """
454
+ if not context.tool_function:
455
+ return None, "source unavailable", 1
456
+
457
+ # Unwrap to get the original function (not the wrapper)
458
+ func = context.tool_function
459
+ while hasattr(func, '__wrapped__'):
460
+ func = func.__wrapped__
461
+
462
+ source = inspect.getsource(func)
463
+ file_path = inspect.getfile(func)
464
+ start_line = inspect.getsourcelines(func)[1]
465
+
466
+ # Show just filename:line
467
+ import os
468
+ file_name = os.path.basename(file_path)
469
+ file_info = f"{file_name}:{start_line}"
470
+
471
+ return source, file_info, start_line
472
+
473
+ def _show_action_menu(self) -> BreakpointAction:
474
+ """Show the action menu and get user's choice.
475
+
476
+ Tries multiple UI libraries in order of preference:
477
+ 1. simple-term-menu (best compatibility, no asyncio conflicts)
478
+ 2. questionary (may conflict with Playwright/asyncio)
479
+ 3. simple input fallback (when no TTY or event loop conflicts)
480
+
481
+ Returns:
482
+ User's chosen action (CONTINUE, EDIT, or QUIT)
483
+ """
484
+ # Try to use simple-term-menu (no asyncio conflicts, works with Playwright)
485
+ try:
486
+ from simple_term_menu import TerminalMenu
487
+
488
+ menu_entries = [
489
+ "[c] Continue execution 🚀",
490
+ "[e] Edit values 🔍",
491
+ "[w] Why this tool? 🤔",
492
+ "[q] Quit debugging 🚫"
493
+ ]
494
+
495
+ terminal_menu = TerminalMenu(
496
+ menu_entries,
497
+ title="\nAction:",
498
+ menu_cursor="→ ",
499
+ menu_cursor_style=("fg_green", "bold"),
500
+ menu_highlight_style=("fg_green", "bold"),
501
+ cycle_cursor=True,
502
+ clear_screen=False,
503
+ )
504
+
505
+ menu_index = terminal_menu.show()
506
+
507
+ # Handle Ctrl+C or None
508
+ if menu_index is None:
509
+ self.console.print("[yellow]→ Quitting debug session...[/yellow]")
510
+ return BreakpointAction.QUIT
511
+
512
+ # Map index to action
513
+ actions = [BreakpointAction.CONTINUE, BreakpointAction.EDIT, BreakpointAction.WHY, BreakpointAction.QUIT]
514
+ action = actions[menu_index]
515
+
516
+ if action == BreakpointAction.CONTINUE:
517
+ self.console.print("[green]→ Continuing execution...[/green]")
518
+ elif action == BreakpointAction.WHY:
519
+ self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
520
+ elif action == BreakpointAction.QUIT:
521
+ self.console.print("[yellow]→ Quitting debug session...[/yellow]")
522
+
523
+ return action
524
+
525
+ except (ImportError, OSError):
526
+ # simple-term-menu not installed, not supported (Windows), or no TTY available
527
+ # Fall back to questionary or simple input
528
+ pass
529
+
530
+ # Fallback: Use questionary (may have asyncio conflicts with Playwright)
531
+ choices = [
532
+ questionary.Choice("[c] Continue execution 🚀", value=BreakpointAction.CONTINUE, shortcut_key='c'),
533
+ questionary.Choice("[e] Edit values 🔍", value=BreakpointAction.EDIT, shortcut_key='e'),
534
+ questionary.Choice("[w] Why this tool? 🤔", value=BreakpointAction.WHY, shortcut_key='w'),
535
+ questionary.Choice("[q] Quit debugging 🚫", value=BreakpointAction.QUIT, shortcut_key='q'),
536
+ ]
537
+
538
+ try:
539
+ action = questionary.select(
540
+ "\nAction:",
541
+ choices=choices,
542
+ style=self.style,
543
+ instruction="(Press c/e/w/q)",
544
+ use_shortcuts=True,
545
+ use_indicator=False,
546
+ use_arrow_keys=True
547
+ ).ask()
548
+ except RuntimeError:
549
+ # Event loop conflict - use simple input fallback
550
+ return self._simple_input_fallback()
551
+
552
+ # Handle Ctrl+C
553
+ if action is None:
554
+ self.console.print("[yellow]→ Quitting debug session...[/yellow]")
555
+ return BreakpointAction.QUIT
556
+
557
+ if action == BreakpointAction.CONTINUE:
558
+ self.console.print("[green]→ Continuing execution...[/green]")
559
+ elif action == BreakpointAction.WHY:
560
+ self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
561
+ elif action == BreakpointAction.QUIT:
562
+ self.console.print("[yellow]→ Quitting debug session...[/yellow]")
563
+
564
+ return action
565
+
566
+ def _simple_input_fallback(self) -> BreakpointAction:
567
+ """Simple text input fallback when event loop conflicts occur.
568
+
569
+ Used when:
570
+ - Asyncio event loop is already running (Playwright, Jupyter)
571
+ - No TTY available
572
+ - Menu libraries not installed or not supported
573
+
574
+ Returns:
575
+ User's chosen action based on keyboard input (c/e/w/q)
576
+ """
577
+ self.console.print("\n[cyan bold]Action:[/cyan bold]")
578
+ self.console.print(" [c] Continue execution 🚀")
579
+ self.console.print(" [e] Edit values 🔍")
580
+ self.console.print(" [w] Why this tool? 🤔")
581
+ self.console.print(" [q] Quit debugging 🚫")
582
+
583
+ while True:
584
+ try:
585
+ choice = input("\nYour choice (c/e/w/q): ").strip().lower()
586
+ if choice == 'c':
587
+ self.console.print("[green]→ Continuing execution...[/green]")
588
+ return BreakpointAction.CONTINUE
589
+ elif choice == 'e':
590
+ return BreakpointAction.EDIT
591
+ elif choice == 'w':
592
+ self.console.print("[cyan]→ Analyzing why tool was chosen...[/cyan]")
593
+ return BreakpointAction.WHY
594
+ elif choice == 'q':
595
+ self.console.print("[yellow]→ Quitting debug session...[/yellow]")
596
+ return BreakpointAction.QUIT
597
+ else:
598
+ self.console.print("[yellow]Invalid choice. Please enter c, e, w, or q.[/yellow]")
599
+ except (KeyboardInterrupt, EOFError):
600
+ self.console.print("\n[yellow]→ Quitting debug session...[/yellow]")
601
+ return BreakpointAction.QUIT
602
+
603
+ def _display_current_value(self, value: Any) -> None:
604
+ """Display the current value nicely formatted.
605
+
606
+ Uses Rich syntax highlighting for JSON and appropriate
607
+ formatting for strings, dicts, lists, and other types.
608
+
609
+ Args:
610
+ value: The value to display (any type)
611
+ """
612
+ self.console.print("\n")
613
+
614
+ # Create a table for the value display
615
+ value_table = Table(show_header=False, box=None)
616
+ value_table.add_column()
617
+
618
+ # Format value based on type
619
+ if isinstance(value, (dict, list)):
620
+ try:
621
+ json_str = json.dumps(value, indent=2, ensure_ascii=False)
622
+ # Use syntax highlighting for JSON
623
+ syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
624
+ value_table.add_row(syntax)
625
+ except:
626
+ value_table.add_row(f"[green]{value}[/green]")
627
+ elif isinstance(value, str):
628
+ # For strings, show with quotes
629
+ if len(value) > 500:
630
+ value_table.add_row(f'[green]"{value[:500]}..."[/green]')
631
+ else:
632
+ value_table.add_row(f'[green]"{value}"[/green]')
633
+ else:
634
+ value_table.add_row(f"[green]{value}[/green]")
635
+
636
+ # Display in a panel
637
+ panel = Panel(
638
+ value_table,
639
+ title="[bold cyan]📝 Current Result[/bold cyan]",
640
+ border_style="cyan",
641
+ padding=(1, 2)
642
+ )
643
+ self.console.print(panel)
644
+
645
+ def _get_new_value(self) -> Optional[Any]:
646
+ """Get new value from user via text input.
647
+
648
+ Prompts user to enter a Python expression and attempts to
649
+ parse it using ast.literal_eval(). Falls back to treating
650
+ as string if parsing fails.
651
+
652
+ Returns:
653
+ Parsed Python value (str, dict, list, etc.) or None if empty
654
+ """
655
+ self.console.print("\n[cyan]Enter new result value:[/cyan]")
656
+ self.console.print("[dim]Tip: Enter valid Python expression (string, dict, list, etc.)[/dim]")
657
+ self.console.print("[dim]Examples: 'new text', {'key': 'value'}, [1, 2, 3][/dim]\n")
658
+
659
+ new_value_str = input("New result: ").strip()
660
+
661
+ if not new_value_str:
662
+ return None
663
+
664
+ try:
665
+ # Try to evaluate as Python expression
666
+ return ast.literal_eval(new_value_str)
667
+ except (ValueError, SyntaxError):
668
+ # If not valid Python literal, treat as string
669
+ return new_value_str
670
+
671
+ def _display_updated_value(self, value: Any) -> None:
672
+ """Display the updated value after successful modification.
673
+
674
+ Shows success message and formatted value in yellow panel
675
+ to distinguish from the original value display.
676
+
677
+ Args:
678
+ value: The newly updated value to display
679
+ """
680
+ self.console.print(f"\n[green]✅ Result updated successfully![/green]\n")
681
+
682
+ # Create a table for the updated value
683
+ value_table = Table(show_header=False, box=None)
684
+ value_table.add_column()
685
+
686
+ # Format value based on type
687
+ if isinstance(value, (dict, list)):
688
+ try:
689
+ json_str = json.dumps(value, indent=2, ensure_ascii=False)
690
+ # Use syntax highlighting
691
+ syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
692
+ value_table.add_row(syntax)
693
+ except:
694
+ value_table.add_row(f"[yellow]{value}[/yellow]")
695
+ elif isinstance(value, str):
696
+ if len(value) > 500:
697
+ value_table.add_row(f'[yellow]"{value[:500]}..."[/yellow]')
698
+ else:
699
+ value_table.add_row(f'[yellow]"{value}"[/yellow]')
700
+ else:
701
+ value_table.add_row(f"[yellow]{value}[/yellow]")
702
+
703
+ # Display in a panel with different style
704
+ panel = Panel(
705
+ value_table,
706
+ title="[bold yellow]✨ Updated Result[/bold yellow]",
707
+ border_style="yellow",
708
+ padding=(1, 2)
709
+ )
710
+ self.console.print(panel)
711
+ def _display_repl_header(self, context: BreakpointContext, namespace: Dict[str, Any]) -> None:
712
+ """Display Python REPL header with available variables.
713
+
714
+ Shows a clean table of all variables available in the REPL namespace,
715
+ organized by priority groups:
716
+ 1. Execution: result, tool_name, tool_args
717
+ 2. Control: iteration, max_iterations
718
+ 3. Context: user_prompt, next_actions
719
+ 4. Agent: agent_name, model, turn, tools_available
720
+ 5. Advanced: messages, trace_entry, previous_tools
721
+ 6. Helpers: pp (pretty print function)
722
+
723
+ Args:
724
+ context: Breakpoint context for reference
725
+ namespace: Dict of all variables available in REPL
726
+ """
727
+ self.console.print("\n")
728
+ self.console.print(Panel(
729
+ "[bold white]Python REPL - Interactive Debugging[/bold white]\n"
730
+ "[dim]Modify any variable and exit() to apply changes[/dim]",
731
+ title="🐍 Debug Console",
732
+ border_style="green"
733
+ ))
734
+
735
+ # Create clean two-column table
736
+ table = Table(
737
+ show_header=True,
738
+ header_style="bold cyan",
739
+ border_style="dim",
740
+ box=box.SIMPLE_HEAD, # Only header has border
741
+ padding=(0, 2), # 0 vertical, 2 horizontal
742
+ show_lines=False
743
+ )
744
+
745
+ # Two columns: Variable (fixed) and Value (flexible)
746
+ table.add_column("Variable", style="yellow", width=18, no_wrap=True)
747
+ table.add_column("Value", style="white", overflow="fold")
748
+
749
+ # Priority ordering for smart grouping
750
+ priority_order = [
751
+ 'result', 'tool_name', 'tool_args', # Group 1: Execution
752
+ 'iteration', 'max_iterations', # Group 2: Control
753
+ 'user_prompt', 'next_actions', # Group 3: Context
754
+ 'agent_name', 'model', 'turn', 'tools_available', # Group 4: Agent
755
+ 'messages', 'trace_entry', 'previous_tools', # Group 5: Advanced
756
+ 'pp', # Group 6: Helper (show last)
757
+ ]
758
+
759
+ # Sort variables by priority
760
+ sorted_items = []
761
+ for key in priority_order:
762
+ if key in namespace:
763
+ sorted_items.append((key, namespace[key]))
764
+
765
+ # Add any remaining variables not in priority list
766
+ for key, value in namespace.items():
767
+ if key not in priority_order:
768
+ sorted_items.append((key, value))
769
+
770
+ # Add rows with automatic grouping
771
+ group_breaks = [2, 4, 6, 10] # Add empty row after these indices (Execution, Control, Context, Agent, Advanced)
772
+
773
+ for i, (var_name, var_value) in enumerate(sorted_items):
774
+ # Add empty row for visual grouping
775
+ if i in group_breaks:
776
+ table.add_row("", "")
777
+
778
+ # Format value with smart formatting
779
+ formatted_value = self._format_value_for_repl(var_value)
780
+ table.add_row(var_name, formatted_value)
781
+
782
+ self.console.print(table)
783
+ self.console.print()
784
+
785
+ def _format_value_for_repl(self, value: Any) -> str:
786
+ """Format value with smart, consistent formatting for REPL display.
787
+
788
+ Handles different types intelligently:
789
+ - None/bools/numbers: Compact cyan format
790
+ - Strings: Truncate with char count if > 80 chars
791
+ - Dicts: Inline if small, indented if medium, collapsed if large
792
+ - Lists: Inline if simple, indented if fits, collapsed if large
793
+ - Functions: Show as helper description
794
+
795
+ Args:
796
+ value: Any Python value to format
797
+
798
+ Returns:
799
+ Rich-formatted string for display in REPL table
800
+ """
801
+
802
+ # None
803
+ if value is None:
804
+ return "[dim]None[/dim]"
805
+
806
+ # Booleans
807
+ elif isinstance(value, bool):
808
+ return f"[cyan]{value}[/cyan]"
809
+
810
+ # Numbers
811
+ elif isinstance(value, (int, float)):
812
+ return f"[cyan]{value}[/cyan]"
813
+
814
+ # Strings
815
+ elif isinstance(value, str):
816
+ return self._format_string_value(value)
817
+
818
+ # Dictionaries
819
+ elif isinstance(value, dict):
820
+ return self._format_dict_value(value)
821
+
822
+ # Lists
823
+ elif isinstance(value, list):
824
+ return self._format_list_value(value)
825
+
826
+ # Functions (like pp helper)
827
+ elif callable(value):
828
+ return f"[dim]<function>[/dim] [dim italic]- helper for pretty printing[/dim italic]"
829
+
830
+ # Other types - just show string representation
831
+ else:
832
+ str_repr = str(value)
833
+ if len(str_repr) <= 100:
834
+ return f"[white]{str_repr}[/white]"
835
+ else:
836
+ return f"[white]{str_repr[:100]}...[/white] [dim]({len(str_repr)} chars)[/dim]"
837
+
838
+ def _format_string_value(self, s: str) -> str:
839
+ """Format string values with truncation and char count.
840
+
841
+ Args:
842
+ s: String to format
843
+
844
+ Returns:
845
+ Short strings (≤80 chars): repr() with green color
846
+ Long strings (>80 chars): Truncated with "..." and char count
847
+ """
848
+ # Short strings - show as-is
849
+ if len(s) <= 80:
850
+ return f"[green]{repr(s)}[/green]"
851
+
852
+ # Long strings - truncate and show char count
853
+ truncated = repr(s[:80])[:-1] + "...'" # Remove closing quote, add ellipsis
854
+ return f"[green]{truncated}[/green]\n [dim]({len(s)} chars)[/dim]"
855
+
856
+ def _format_dict_value(self, d: dict) -> str:
857
+ """Format dict values using pprint for clean output.
858
+
859
+ Args:
860
+ d: Dictionary to format
861
+
862
+ Returns:
863
+ Empty dict: "{{}}" in dim
864
+ Small dict (≤3 keys, fits inline): Compact cyan format
865
+ Medium dict (≤5 lines): Multi-line with indentation
866
+ Large dict: Collapsed summary with key count and pp() hint
867
+ """
868
+ if not d:
869
+ return "[dim]{{}}[/dim]"
870
+
871
+ # Use pprint for nice formatting
872
+ pp = pformat(d, width=60, depth=2, compact=True)
873
+ lines = pp.split('\n')
874
+
875
+ # Small dict - show inline
876
+ if len(d) <= 3 and len(lines) == 1 and len(pp) <= 60:
877
+ return f"[cyan]{pp}[/cyan]"
878
+
879
+ # Medium dict - show with indentation
880
+ if len(lines) <= 5:
881
+ formatted_lines = [lines[0]]
882
+ for line in lines[1:]:
883
+ formatted_lines.append(f" {line}")
884
+ return f"[cyan]{chr(10).join(formatted_lines)}[/cyan]"
885
+
886
+ # Large dict - collapse with summary
887
+ return f"[dim cyan]{{... {len(d)} keys}}[/dim cyan] [dim]- type: pp(var_name)[/dim]"
888
+
889
+ def _format_list_value(self, lst: list) -> str:
890
+ """Format list values using pprint for clean output.
891
+
892
+ Args:
893
+ lst: List to format
894
+
895
+ Returns:
896
+ Empty list: "[]" in dim
897
+ Simple string list (≤5 items, fits inline): Compact format
898
+ Medium list (≤5 lines): Multi-line with indentation
899
+ Large list: Collapsed summary with item count and pp() hint
900
+ """
901
+ if not lst:
902
+ return "[dim][][/dim]"
903
+
904
+ # Simple list of strings - show inline
905
+ if all(isinstance(item, str) for item in lst) and len(lst) <= 5:
906
+ compact = "[" + ", ".join(f'"{s}"' for s in lst) + "]"
907
+ if len(compact) <= 60:
908
+ return f"[cyan]{compact}[/cyan]"
909
+
910
+ # Use pprint for nice formatting
911
+ pp = pformat(lst, width=60, depth=2, compact=True)
912
+ lines = pp.split('\n')
913
+
914
+ # If fits in a few lines, show it
915
+ if len(lines) <= 5:
916
+ formatted_lines = [lines[0]]
917
+ for line in lines[1:]:
918
+ formatted_lines.append(f" {line}")
919
+ result = chr(10).join(formatted_lines)
920
+ return f"[cyan]{result}[/cyan]"
921
+
922
+ # Large - show summary with hint
923
+ return f"[dim cyan][... {len(lst)} items][/dim cyan] [dim]- type: pp(var_name)[/dim]"
924
+
925
+ def _format_value_preview(self, value: Any) -> str:
926
+ """Format a value for compact preview display.
927
+
928
+ Used for showing values in constrained spaces like next action previews.
929
+
930
+ Args:
931
+ value: Any value to format
932
+
933
+ Returns:
934
+ Truncated string representation (max 30 chars)
935
+ """
936
+ if isinstance(value, str):
937
+ return f"'{value[:30]}...'" if len(value) > 30 else f"'{value}'"
938
+ elif isinstance(value, (dict, list)):
939
+ val_str = str(value)
940
+ return f"{val_str[:30]}..." if len(val_str) > 30 else val_str
941
+ else:
942
+ return str(value)
943
+
944
+ def _display_modifications(self, modifications: Dict[str, Any]) -> None:
945
+ """Display what was modified during REPL session.
946
+
947
+ Shows each modified variable with its new value,
948
+ formatted appropriately for display.
949
+
950
+ Args:
951
+ modifications: Dict of variable_name -> new_value pairs
952
+ """
953
+ self.console.print("\n[bold green]✅ Modifications Applied:[/bold green]\n")
954
+
955
+ for key, value in modifications.items():
956
+ # Format the value for display
957
+ if isinstance(value, str):
958
+ formatted = f"'{value}'" if len(value) <= 50 else f"'{value[:50]}...'"
959
+ elif isinstance(value, dict):
960
+ formatted = json.dumps(value, indent=2)[:100]
961
+ else:
962
+ formatted = str(value)
963
+
964
+ self.console.print(f" [yellow]{key}[/yellow] = [cyan]{formatted}[/cyan]")
965
+
966
+ self.console.print()
967
+
968
+ def display_explanation(self, explanation: str, context: BreakpointContext) -> None:
969
+ """Display AI explanation of why a tool was chosen.
970
+
971
+ Args:
972
+ explanation: The explanation text from the AI
973
+ context: Breakpoint context for displaying relevant info
974
+ """
975
+ self.console.print("\n")
976
+
977
+ panel = Panel(
978
+ explanation,
979
+ title=f"[bold cyan]🤔 Why {context.tool_name}?[/bold cyan]",
980
+ border_style="cyan",
981
+ padding=(1, 2)
982
+ )
983
+
984
+ self.console.print(panel)
985
+ self.console.print()
986
+
987
+ input("[dim]Press Enter to return to menu...[/dim]")
988
+
989
+ def display_execution_analysis(self, analysis) -> None:
990
+ """Display post-execution analysis with improvement suggestions.
991
+
992
+ Args:
993
+ analysis: ExecutionAnalysis object with structured results
994
+ """
995
+ self.console.print("\n")
996
+ self.console.print("[bold cyan]═══ 📊 Execution Analysis ═══[/bold cyan]\n")
997
+
998
+ # Task completion status
999
+ status_emoji = "✅" if analysis.task_completed else "❌"
1000
+ self.console.print(f"{status_emoji} [bold]Task Completed:[/bold] {analysis.task_completed}")
1001
+ self.console.print(f" {analysis.completion_explanation}\n")
1002
+
1003
+ # Overall quality
1004
+ quality_colors = {
1005
+ "excellent": "green",
1006
+ "good": "cyan",
1007
+ "fair": "yellow",
1008
+ "poor": "red"
1009
+ }
1010
+ quality_color = quality_colors.get(analysis.overall_quality, "white")
1011
+ self.console.print(f"[bold]Quality:[/bold] [{quality_color}]{analysis.overall_quality.upper()}[/{quality_color}]\n")
1012
+
1013
+ # Problems identified
1014
+ if analysis.problems_identified:
1015
+ self.console.print("[bold red]⚠️ Problems Identified:[/bold red]")
1016
+ for i, problem in enumerate(analysis.problems_identified, 1):
1017
+ self.console.print(f" {i}. {problem}")
1018
+ self.console.print()
1019
+
1020
+ # System prompt suggestions
1021
+ if analysis.system_prompt_suggestions:
1022
+ panel = Panel(
1023
+ "\n".join(f"• {suggestion}" for suggestion in analysis.system_prompt_suggestions),
1024
+ title="[bold green]💡 System Prompt Suggestions[/bold green]",
1025
+ border_style="green",
1026
+ padding=(1, 2)
1027
+ )
1028
+ self.console.print(panel)
1029
+ self.console.print()
1030
+
1031
+ # Key insights
1032
+ if analysis.key_insights:
1033
+ self.console.print("[bold magenta]🎯 Key Insights:[/bold magenta]")
1034
+ for i, insight in enumerate(analysis.key_insights, 1):
1035
+ self.console.print(f" {i}. {insight}")
1036
+ self.console.print()
1037
+
1038
+ self.console.print("[dim]" + "═" * 60 + "[/dim]\n")
1039
+ input("[dim]Press Enter to continue...[/dim]")