omnimancer-cli 0.1.0__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 (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1048 @@
1
+ """
2
+ Handler for agent persona CLI commands.
3
+
4
+ This module provides the command handling logic for managing agent personas
5
+ through the CLI interface.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ from rich.box import ROUNDED
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+ from ...core.agent.agent_switcher import (
16
+ SwitchState,
17
+ get_agent_switcher,
18
+ )
19
+ from ...core.agent.persona import (
20
+ PersonaStatus,
21
+ get_persona_manager,
22
+ )
23
+ from ..commands import Command
24
+
25
+
26
+ class AgentPersonaHandler:
27
+ """Handles agent persona-related CLI commands."""
28
+
29
+ def __init__(self, console: Optional[Console] = None):
30
+ """
31
+ Initialize the handler.
32
+
33
+ Args:
34
+ console: Rich console for output
35
+ """
36
+ self.console = console or Console()
37
+ self.persona_manager = get_persona_manager()
38
+ self.agent_switcher = get_agent_switcher(self.persona_manager)
39
+
40
+ async def handle_agent_command(self, command: Command) -> None:
41
+ """
42
+ Main entry point for handling agent commands.
43
+
44
+ Args:
45
+ command: The command to handle
46
+ """
47
+ args = command.parameters.get("args", [])
48
+
49
+ if not args:
50
+ # Default to 'list' if no subcommand
51
+ await self.handle_list_personas()
52
+ return
53
+
54
+ subcommand = args[0].lower()
55
+
56
+ # Route to appropriate handler
57
+ if subcommand == "list":
58
+ await self.handle_list_personas()
59
+ elif subcommand == "use":
60
+ if len(args) < 2:
61
+ self._show_error("Usage: /agent use <persona_name>")
62
+ return
63
+ await self.handle_use_persona(args[1])
64
+ elif subcommand == "current":
65
+ await self.handle_current_persona()
66
+ elif subcommand == "info":
67
+ if len(args) < 2:
68
+ self._show_error("Usage: /agent info <persona_name>")
69
+ return
70
+ await self.handle_persona_info(args[1])
71
+ elif subcommand == "status":
72
+ # Show detailed status including switch state
73
+ await self.handle_persona_status()
74
+ elif subcommand == "history":
75
+ # Show switch history
76
+ await self.handle_switch_history()
77
+ elif subcommand == "recommend":
78
+ user_query = " ".join(args[1:]) if len(args) > 1 else None
79
+ await self.handle_recommend_persona(user_query)
80
+ elif subcommand == "compare":
81
+ await self.handle_compare_personas()
82
+ elif subcommand == "preview":
83
+ if len(args) < 2:
84
+ self._show_error("Usage: /agent preview <persona_name>")
85
+ return
86
+ await self.handle_preview_persona(args[1])
87
+ elif subcommand == "discover":
88
+ await self.handle_discover_personas()
89
+ elif subcommand == "help":
90
+ self._show_agent_help()
91
+ else:
92
+ self._show_error(f"Unknown agent subcommand: {subcommand}")
93
+ self._show_agent_help()
94
+
95
+ async def handle_list_personas(self) -> None:
96
+ """List all available agent personas."""
97
+ personas = self.persona_manager.get_all_personas()
98
+
99
+ if not personas:
100
+ self._show_warning("No agent personas available.")
101
+ return
102
+
103
+ # Create a table for personas
104
+ table = Table(
105
+ title="🤖 Available Agent Personas",
106
+ box=ROUNDED,
107
+ show_header=True,
108
+ header_style="bold cyan",
109
+ )
110
+
111
+ table.add_column("Icon", style="cyan", justify="center")
112
+ table.add_column("ID", style="green")
113
+ table.add_column("Name", style="bold white")
114
+ table.add_column("Category", style="yellow")
115
+ table.add_column("Status", justify="center")
116
+ table.add_column("Description", style="dim")
117
+
118
+ # Sort personas by category and name
119
+ sorted_personas = sorted(
120
+ personas.values(), key=lambda p: (p.category.value, p.name)
121
+ )
122
+
123
+ active_persona = self.persona_manager.active_persona
124
+
125
+ for persona in sorted_personas:
126
+ # Format status with color
127
+ status = self._format_status(persona.status)
128
+
129
+ # Highlight active persona
130
+ if active_persona and persona.id == active_persona.id:
131
+ status = "[bold green]● ACTIVE[/bold green]"
132
+ name_style = "[bold cyan]" + persona.name + "[/bold cyan]"
133
+ else:
134
+ name_style = persona.name
135
+
136
+ table.add_row(
137
+ persona.icon,
138
+ persona.id,
139
+ name_style,
140
+ persona.category.value.title(),
141
+ status,
142
+ (
143
+ persona.description[:50] + "..."
144
+ if len(persona.description) > 50
145
+ else persona.description
146
+ ),
147
+ )
148
+
149
+ self.console.print(table)
150
+
151
+ # Show usage hint
152
+ self.console.print(
153
+ "\n[dim]Use '/agent use <persona_id>' to switch to a persona[/dim]"
154
+ )
155
+ self.console.print(
156
+ "[dim]Use '/agent info <persona_id>' for detailed information[/dim]"
157
+ )
158
+
159
+ async def handle_use_persona(self, persona_id: str) -> None:
160
+ """
161
+ Switch to a specified persona.
162
+
163
+ Args:
164
+ persona_id: ID of the persona to switch to
165
+ """
166
+ # Check if persona exists
167
+ persona = self.persona_manager.get_persona(persona_id.lower())
168
+ if not persona:
169
+ self._show_error(f"Persona '{persona_id}' not found.")
170
+ self._suggest_personas(persona_id)
171
+ return
172
+
173
+ # Check if already active
174
+ if self.persona_manager.active_persona == persona:
175
+ self._show_info(f"Already using {persona.name} ({persona.icon})")
176
+ return
177
+
178
+ # Show switching message
179
+ self.console.print(
180
+ f"\n[yellow]Switching to {persona.name} ({persona.icon})...[/yellow]"
181
+ )
182
+
183
+ # Perform the switch
184
+ success, message = self.agent_switcher.switch_persona(
185
+ persona_id.lower(), reason="User requested switch via CLI"
186
+ )
187
+
188
+ if success:
189
+ self._show_success(f"✓ {message}")
190
+
191
+ # Show persona capabilities
192
+ if persona.capabilities:
193
+ caps_text = ", ".join(
194
+ [
195
+ cap.value.replace("_", " ").title()
196
+ for cap in persona.capabilities
197
+ ]
198
+ )
199
+ self.console.print(f"[dim]Capabilities: {caps_text}[/dim]")
200
+ else:
201
+ self._show_error(f"Failed to switch persona: {message}")
202
+
203
+ async def handle_current_persona(self) -> None:
204
+ """Show the currently active persona."""
205
+ active_persona = self.persona_manager.active_persona
206
+
207
+ if not active_persona:
208
+ self._show_info("No agent persona is currently active.")
209
+ self.console.print(
210
+ "[dim]Use '/agent use <persona_id>' to activate a persona[/dim]"
211
+ )
212
+ return
213
+
214
+ # Create a panel with current persona info
215
+ info_lines = [
216
+ f"[bold cyan]{active_persona.icon} {active_persona.name}[/bold cyan]",
217
+ f"[yellow]ID:[/yellow] {active_persona.id}",
218
+ f"[yellow]Category:[/yellow] {active_persona.category.value.title()}",
219
+ f"[yellow]Status:[/yellow] {self._format_status(active_persona.status)}",
220
+ "",
221
+ f"[dim]{active_persona.description}[/dim]",
222
+ ]
223
+
224
+ if active_persona.capabilities:
225
+ caps_text = ", ".join(
226
+ [
227
+ cap.value.replace("_", " ").title()
228
+ for cap in active_persona.capabilities
229
+ ]
230
+ )
231
+ info_lines.append("")
232
+ info_lines.append(f"[yellow]Capabilities:[/yellow] {caps_text}")
233
+
234
+ # Add usage stats if available
235
+ if active_persona.metadata:
236
+ info_lines.append("")
237
+ info_lines.append(
238
+ f"[dim]Usage Count: {active_persona.metadata.usage_count}[/dim]"
239
+ )
240
+ if active_persona.metadata.last_used:
241
+ info_lines.append(
242
+ f"[dim]Last Used: {active_persona.metadata.last_used.strftime('%Y-%m-%d %H:%M')}[/dim]"
243
+ )
244
+
245
+ panel = Panel(
246
+ "\n".join(info_lines),
247
+ title="Current Agent Persona",
248
+ border_style="green",
249
+ )
250
+
251
+ self.console.print(panel)
252
+
253
+ async def handle_persona_info(self, persona_id: str) -> None:
254
+ """
255
+ Show detailed information about a persona.
256
+
257
+ Args:
258
+ persona_id: ID of the persona
259
+ """
260
+ persona = self.persona_manager.get_persona(persona_id.lower())
261
+
262
+ if not persona:
263
+ self._show_error(f"Persona '{persona_id}' not found.")
264
+ self._suggest_personas(persona_id)
265
+ return
266
+
267
+ # Create detailed info panel
268
+ info_sections = []
269
+
270
+ # Basic info section
271
+ basic_info = [
272
+ f"[bold cyan]{persona.icon} {persona.name}[/bold cyan]",
273
+ f"[yellow]ID:[/yellow] {persona.id}",
274
+ f"[yellow]Category:[/yellow] {persona.category.value.title()}",
275
+ f"[yellow]Status:[/yellow] {self._format_status(persona.status)}",
276
+ "",
277
+ f"{persona.description}",
278
+ ]
279
+ info_sections.append(Panel("\n".join(basic_info), title="Basic Information"))
280
+
281
+ # Capabilities section
282
+ if persona.capabilities:
283
+ cap_list = []
284
+ for cap in sorted(persona.capabilities, key=lambda c: c.value):
285
+ cap_name = cap.value.replace("_", " ").title()
286
+ cap_list.append(f"• {cap_name}")
287
+
288
+ info_sections.append(Panel("\n".join(cap_list), title="Capabilities"))
289
+
290
+ # Configuration section
291
+ if persona.configuration:
292
+ config_info = [
293
+ f"[yellow]Template:[/yellow] {persona.configuration.template_id}",
294
+ f"[yellow]Primary Provider:[/yellow] {persona.configuration.primary_provider}",
295
+ f"[yellow]Tools Enabled:[/yellow] {'Yes' if persona.configuration.tools_enabled else 'No'}",
296
+ f"[yellow]Web Search:[/yellow] {'Yes' if persona.configuration.web_search_enabled else 'No'}",
297
+ f"[yellow]File Operations:[/yellow] {'Yes' if persona.configuration.file_operations_enabled else 'No'}",
298
+ f"[yellow]Approval Required:[/yellow] {'Yes' if persona.configuration.approval_required else 'No'}",
299
+ ]
300
+
301
+ if (
302
+ hasattr(persona.configuration, "fallback_providers")
303
+ and persona.configuration.fallback_providers
304
+ ):
305
+ fallback_list = persona.configuration.fallback_providers
306
+ if isinstance(fallback_list, (list, tuple)):
307
+ config_info.append(
308
+ f"[yellow]Fallback Providers:[/yellow] {', '.join(fallback_list)}"
309
+ )
310
+ else:
311
+ config_info.append(
312
+ f"[yellow]Fallback Providers:[/yellow] {str(fallback_list)}"
313
+ )
314
+
315
+ info_sections.append(Panel("\n".join(config_info), title="Configuration"))
316
+
317
+ # Metadata section
318
+ if persona.metadata:
319
+ meta_info = [
320
+ f"[yellow]Version:[/yellow] {persona.metadata.version}",
321
+ f"[yellow]Author:[/yellow] {persona.metadata.author}",
322
+ f"[yellow]Usage Count:[/yellow] {persona.metadata.usage_count}",
323
+ f"[yellow]Built-in:[/yellow] {'Yes' if persona.metadata.is_builtin else 'No'}",
324
+ ]
325
+
326
+ if persona.metadata.created_at:
327
+ meta_info.append(
328
+ f"[yellow]Created:[/yellow] {persona.metadata.created_at.strftime('%Y-%m-%d %H:%M')}"
329
+ )
330
+
331
+ if persona.metadata.last_used:
332
+ meta_info.append(
333
+ f"[yellow]Last Used:[/yellow] {persona.metadata.last_used.strftime('%Y-%m-%d %H:%M')}"
334
+ )
335
+
336
+ if persona.metadata.tags:
337
+ meta_info.append(
338
+ f"[yellow]Tags:[/yellow] {', '.join(persona.metadata.tags)}"
339
+ )
340
+
341
+ info_sections.append(Panel("\n".join(meta_info), title="Metadata"))
342
+
343
+ # Display all sections
344
+ for section in info_sections:
345
+ self.console.print(section)
346
+
347
+ # Show activation hint if not active
348
+ if self.persona_manager.active_persona != persona:
349
+ self.console.print(
350
+ f"\n[dim]Use '/agent use {persona.id}' to activate this persona[/dim]"
351
+ )
352
+
353
+ async def handle_persona_status(self) -> None:
354
+ """Show detailed agent persona status."""
355
+ # Current persona
356
+ active_persona = self.persona_manager.active_persona
357
+
358
+ # Switch state
359
+ switch_state = self.agent_switcher.get_current_state()
360
+
361
+ # Session state info
362
+ session_state = self.agent_switcher.current_session_state
363
+
364
+ # Create status sections
365
+ status_lines = []
366
+
367
+ # Active persona section
368
+ status_lines.append("[bold]Active Persona:[/bold]")
369
+ if active_persona:
370
+ status_lines.append(
371
+ f" {active_persona.icon} {active_persona.name} ({active_persona.id})"
372
+ )
373
+ status_lines.append(
374
+ f" Status: {self._format_status(active_persona.status)}"
375
+ )
376
+ else:
377
+ status_lines.append(" [dim]No persona active[/dim]")
378
+
379
+ status_lines.append("")
380
+
381
+ # Switch state section
382
+ status_lines.append("[bold]Switch State:[/bold]")
383
+ status_lines.append(f" {self._format_switch_state(switch_state)}")
384
+
385
+ status_lines.append("")
386
+
387
+ # Session state section
388
+ status_lines.append("[bold]Session State:[/bold]")
389
+ if session_state:
390
+ status_lines.append(
391
+ f" Session ID: {session_state.session_id or '[dim]None[/dim]'}"
392
+ )
393
+ status_lines.append(
394
+ f" History Items: {len(session_state.conversation_history)}"
395
+ )
396
+ status_lines.append(f" Active Tools: {len(session_state.active_tools)}")
397
+ status_lines.append(
398
+ f" Active Operations: {len(session_state.active_operations)}"
399
+ )
400
+ if session_state.last_modified:
401
+ status_lines.append(
402
+ f" Last Modified: {session_state.last_modified.strftime('%H:%M:%S')}"
403
+ )
404
+ else:
405
+ status_lines.append(" [dim]No session state[/dim]")
406
+
407
+ status_lines.append("")
408
+
409
+ # Manager stats
410
+ stats = self.persona_manager.get_stats()
411
+ status_lines.append("[bold]Manager Statistics:[/bold]")
412
+ status_lines.append(f" Total Personas: {stats['total_personas']}")
413
+ status_lines.append(f" Built-in: {stats['builtin_personas']}")
414
+ status_lines.append(f" Custom: {stats['custom_personas']}")
415
+ status_lines.append(f" Available: {stats['available_personas']}")
416
+
417
+ panel = Panel(
418
+ "\n".join(status_lines),
419
+ title="🤖 Agent Persona Status",
420
+ border_style="blue",
421
+ )
422
+
423
+ self.console.print(panel)
424
+
425
+ async def handle_switch_history(self) -> None:
426
+ """Show persona switch history."""
427
+ history = self.agent_switcher.get_switch_history()
428
+
429
+ if not history:
430
+ self._show_info("No persona switch history available.")
431
+ return
432
+
433
+ # Create history table
434
+ table = Table(
435
+ title="Persona Switch History",
436
+ box=ROUNDED,
437
+ show_header=True,
438
+ header_style="bold cyan",
439
+ )
440
+
441
+ table.add_column("#", style="dim", width=3)
442
+ table.add_column("Time", style="cyan")
443
+ table.add_column("From", style="yellow")
444
+ table.add_column("To", style="green")
445
+ table.add_column("Reason", style="dim")
446
+ table.add_column("Status", justify="center")
447
+
448
+ for idx, context in enumerate(history[-10:], 1): # Show last 10 switches
449
+ from_name = (
450
+ context.from_persona.name if context.from_persona else "[dim]None[/dim]"
451
+ )
452
+ to_name = context.to_persona.name
453
+ time_str = context.timestamp.strftime("%H:%M:%S")
454
+
455
+ # Determine status
456
+ if context.error_message:
457
+ status = "[red]✗ Failed[/red]"
458
+ else:
459
+ status = "[green]✓ Success[/green]"
460
+
461
+ reason = (
462
+ context.switch_reason[:30] + "..."
463
+ if len(context.switch_reason) > 30
464
+ else context.switch_reason
465
+ )
466
+
467
+ table.add_row(
468
+ str(idx),
469
+ time_str,
470
+ from_name,
471
+ to_name,
472
+ reason or "[dim]Manual[/dim]",
473
+ status,
474
+ )
475
+
476
+ self.console.print(table)
477
+
478
+ def _show_agent_help(self) -> None:
479
+ """Show help for agent commands."""
480
+ help_text = """
481
+ [bold cyan]Agent Persona Commands:[/bold cyan]
482
+
483
+ [yellow]Basic Commands:[/yellow]
484
+ /agent list - List all available agent personas
485
+ /agent use <persona> - Switch to a specific persona
486
+ /agent current - Show the currently active persona
487
+ /agent info <persona> - Show detailed information about a persona
488
+
489
+ [yellow]Discovery & Selection:[/yellow]
490
+ /agent discover - Interactive persona discovery guide
491
+ /agent recommend [query] - Get persona recommendations for a task
492
+ /agent compare - Compare capabilities of all personas
493
+ /agent preview <persona> - Preview persona without switching
494
+
495
+ [yellow]Advanced Commands:[/yellow]
496
+ /agent status - Show detailed agent system status
497
+ /agent history - Show persona switch history
498
+
499
+ [yellow]Available Personas:[/yellow]
500
+ • coding - Optimized for software development
501
+ • research - Configured for research with web search
502
+ • creative - Set up for creative writing
503
+ • performance - Optimized for speed and efficiency
504
+ • general - Balanced general-purpose configuration
505
+
506
+ [yellow]Examples:[/yellow]
507
+ /agent discover - Start interactive discovery
508
+ /agent recommend "debug python" - Get recommendations for debugging
509
+ /agent compare - See capability comparison table
510
+ /agent preview coding - Preview coding persona behavior
511
+ /agent use coding - Switch to coding persona
512
+ """
513
+ panel = Panel(
514
+ help_text.strip(), title="Agent Persona Help", border_style="cyan"
515
+ )
516
+ self.console.print(panel)
517
+
518
+ def _format_status(self, status: PersonaStatus) -> str:
519
+ """Format persona status with color."""
520
+ status_colors = {
521
+ PersonaStatus.AVAILABLE: "[green]Available[/green]",
522
+ PersonaStatus.ACTIVE: "[bold green]Active[/bold green]",
523
+ PersonaStatus.DISABLED: "[dim]Disabled[/dim]",
524
+ PersonaStatus.ERROR: "[red]Error[/red]",
525
+ PersonaStatus.LOADING: "[yellow]Loading...[/yellow]",
526
+ }
527
+ return status_colors.get(status, str(status.value))
528
+
529
+ def _format_switch_state(self, state: SwitchState) -> str:
530
+ """Format switch state with color."""
531
+ state_colors = {
532
+ SwitchState.IDLE: "[green]Idle[/green]",
533
+ SwitchState.PREPARING: "[yellow]Preparing[/yellow]",
534
+ SwitchState.SWITCHING: "[yellow]Switching[/yellow]",
535
+ SwitchState.VALIDATING: "[yellow]Validating[/yellow]",
536
+ SwitchState.COMPLETE: "[green]Complete[/green]",
537
+ SwitchState.ERROR: "[red]Error[/red]",
538
+ SwitchState.ROLLBACK: "[red]Rollback[/red]",
539
+ }
540
+ return state_colors.get(state, str(state.value))
541
+
542
+ def _suggest_personas(self, typed: str) -> None:
543
+ """Suggest similar persona names."""
544
+ personas = self.persona_manager.get_all_personas()
545
+ suggestions = []
546
+
547
+ typed_lower = typed.lower()
548
+ for pid, persona in personas.items():
549
+ if typed_lower in pid or typed_lower in persona.name.lower():
550
+ suggestions.append(f"{pid} ({persona.name})")
551
+
552
+ if suggestions:
553
+ self.console.print("[dim]Did you mean one of these?[/dim]")
554
+ for suggestion in suggestions[:3]:
555
+ self.console.print(f" • {suggestion}")
556
+
557
+ def _show_info(self, message: str) -> None:
558
+ """Show info message."""
559
+ self.console.print(f"[cyan]ℹ {message}[/cyan]")
560
+
561
+ def _show_success(self, message: str) -> None:
562
+ """Show success message."""
563
+ self.console.print(f"[green]{message}[/green]")
564
+
565
+ def _show_warning(self, message: str) -> None:
566
+ """Show warning message."""
567
+ self.console.print(f"[yellow]⚠ {message}[/yellow]")
568
+
569
+ def _show_error(self, message: str) -> None:
570
+ """Show error message."""
571
+ self.console.print(f"[red]✗ {message}[/red]")
572
+
573
+ async def handle_recommend_persona(self, user_query: Optional[str] = None) -> None:
574
+ """
575
+ Recommend a persona based on user query or task.
576
+
577
+ Args:
578
+ user_query: Optional user query to base recommendations on
579
+ """
580
+ self.console.print("\n[bold cyan]🎯 Persona Recommendations[/bold cyan]")
581
+
582
+ personas = self.persona_manager.get_all_personas()
583
+ if not personas:
584
+ self._show_warning("No personas available for recommendations.")
585
+ return
586
+
587
+ # If no query provided, show general recommendations
588
+ if not user_query:
589
+ self.console.print(
590
+ "\n[dim]Here are some general persona recommendations based on common tasks:[/dim]\n"
591
+ )
592
+
593
+ # Show recommended personas for common tasks
594
+ recommendations = [
595
+ ("💻 For software development and coding tasks", "coding"),
596
+ ("🔍 For research and information gathering", "research"),
597
+ ("✍️ For creative writing and content creation", "creative"),
598
+ ("⚡ For quick responses and simple tasks", "performance"),
599
+ ("🎯 For balanced general-purpose work", "general"),
600
+ ]
601
+
602
+ for task_desc, persona_id in recommendations:
603
+ persona = personas.get(persona_id)
604
+ if persona:
605
+ self.console.print(f"{task_desc}")
606
+ self.console.print(
607
+ f" → Use: [cyan]/agent use {persona_id}[/cyan] ({persona.name})"
608
+ )
609
+ self.console.print("")
610
+
611
+ else:
612
+ # Analyze query and recommend appropriate persona
613
+ self.console.print(f'[dim]Based on your query: "{user_query}"[/dim]\n')
614
+
615
+ query_lower = user_query.lower()
616
+ recommendations = []
617
+
618
+ # Simple keyword-based matching
619
+ if any(
620
+ word in query_lower
621
+ for word in [
622
+ "code",
623
+ "program",
624
+ "debug",
625
+ "develop",
626
+ "build",
627
+ "fix",
628
+ ]
629
+ ):
630
+ if "coding" in personas:
631
+ recommendations.append(
632
+ (
633
+ personas["coding"],
634
+ "Optimized for programming and development tasks",
635
+ )
636
+ )
637
+
638
+ if any(
639
+ word in query_lower
640
+ for word in [
641
+ "research",
642
+ "find",
643
+ "search",
644
+ "learn",
645
+ "study",
646
+ "investigate",
647
+ ]
648
+ ):
649
+ if "research" in personas:
650
+ recommendations.append(
651
+ (
652
+ personas["research"],
653
+ "Enhanced with web search capabilities",
654
+ )
655
+ )
656
+
657
+ if any(
658
+ word in query_lower
659
+ for word in [
660
+ "write",
661
+ "creative",
662
+ "story",
663
+ "content",
664
+ "blog",
665
+ "article",
666
+ ]
667
+ ):
668
+ if "creative" in personas:
669
+ recommendations.append(
670
+ (
671
+ personas["creative"],
672
+ "Specialized for creative writing tasks",
673
+ )
674
+ )
675
+
676
+ if any(
677
+ word in query_lower
678
+ for word in ["fast", "quick", "speed", "urgent", "simple"]
679
+ ):
680
+ if "performance" in personas:
681
+ recommendations.append(
682
+ (
683
+ personas["performance"],
684
+ "Optimized for speed and efficiency",
685
+ )
686
+ )
687
+
688
+ # If no specific matches, recommend general
689
+ if not recommendations and "general" in personas:
690
+ recommendations.append(
691
+ (personas["general"], "Balanced for general-purpose tasks")
692
+ )
693
+
694
+ if recommendations:
695
+ for persona, reason in recommendations[:3]: # Show top 3
696
+ self.console.print(
697
+ f"[green]✓[/green] [bold]{persona.icon} {persona.name}[/bold]"
698
+ )
699
+ self.console.print(f" {reason}")
700
+ self.console.print(f" Use: [cyan]/agent use {persona.id}[/cyan]")
701
+ self.console.print("")
702
+ else:
703
+ self.console.print(
704
+ "[dim]No specific recommendations found. Try /agent discover for interactive selection.[/dim]"
705
+ )
706
+
707
+ # Show additional help
708
+ self.console.print(
709
+ "[dim]💡 Tip: Use '/agent compare' to see capability differences[/dim]"
710
+ )
711
+ self.console.print(
712
+ "[dim]💡 Tip: Use '/agent preview <persona>' to test without switching[/dim]"
713
+ )
714
+
715
+ async def handle_compare_personas(self) -> None:
716
+ """Show capability comparison matrix for all personas."""
717
+ personas = self.persona_manager.get_all_personas()
718
+ if not personas:
719
+ self._show_warning("No personas available for comparison.")
720
+ return
721
+
722
+ # Create comparison table
723
+ table = Table(
724
+ title="🔀 Persona Capability Comparison",
725
+ box=ROUNDED,
726
+ show_header=True,
727
+ header_style="bold cyan",
728
+ )
729
+
730
+ table.add_column("Persona", style="bold white", width=12)
731
+ table.add_column("Category", style="yellow", width=12)
732
+ table.add_column("Tools", justify="center", width=8)
733
+ table.add_column("Web Search", justify="center", width=10)
734
+ table.add_column("File Ops", justify="center", width=8)
735
+ table.add_column("Speed", justify="center", width=8)
736
+ table.add_column("Best For", style="dim", width=25)
737
+
738
+ # Sort personas by category and name
739
+ sorted_personas = sorted(
740
+ personas.values(), key=lambda p: (p.category.value, p.name)
741
+ )
742
+
743
+ for persona in sorted_personas:
744
+ # Get capabilities from configuration
745
+ config = persona.configuration
746
+ tools_enabled = "✅" if (config and config.tools_enabled) else "❌"
747
+ web_search = (
748
+ "✅"
749
+ if (config and getattr(config, "web_search_enabled", False))
750
+ else "❌"
751
+ )
752
+ file_ops = (
753
+ "✅"
754
+ if (config and getattr(config, "file_operations_enabled", False))
755
+ else "❌"
756
+ )
757
+
758
+ # Determine speed based on persona type
759
+ speed_rating = (
760
+ "⚡"
761
+ if persona.id == "performance"
762
+ else "🐢" if persona.id == "research" else "⚖️"
763
+ )
764
+
765
+ # Best use case
766
+ use_cases = {
767
+ "coding": "Software development",
768
+ "research": "Research & analysis",
769
+ "creative": "Writing & content",
770
+ "performance": "Quick tasks",
771
+ "general": "Balanced workflows",
772
+ }
773
+ best_for = use_cases.get(persona.id, persona.description[:25])
774
+
775
+ table.add_row(
776
+ f"{persona.icon} {persona.name}",
777
+ persona.category.value.title(),
778
+ tools_enabled,
779
+ web_search,
780
+ file_ops,
781
+ speed_rating,
782
+ best_for,
783
+ )
784
+
785
+ self.console.print(table)
786
+
787
+ # Legend
788
+ legend_lines = [
789
+ "[bold]Legend:[/bold]",
790
+ "✅ = Enabled/Available ❌ = Disabled/Unavailable",
791
+ "⚡ = Fast ⚖️ = Balanced 🐢 = Thorough",
792
+ "",
793
+ "[dim]💡 Use '/agent info <persona>' for detailed information[/dim]",
794
+ "[dim]💡 Use '/agent use <persona>' to switch to a persona[/dim]",
795
+ ]
796
+
797
+ legend_panel = Panel(
798
+ "\n".join(legend_lines), title="Usage Guide", border_style="dim"
799
+ )
800
+
801
+ self.console.print("\n")
802
+ self.console.print(legend_panel)
803
+
804
+ async def handle_preview_persona(self, persona_id: str) -> None:
805
+ """
806
+ Preview a persona without switching to it.
807
+
808
+ Args:
809
+ persona_id: ID of the persona to preview
810
+ """
811
+ persona = self.persona_manager.get_persona(persona_id.lower())
812
+ if not persona:
813
+ self._show_error(f"Persona '{persona_id}' not found.")
814
+ self._suggest_personas(persona_id)
815
+ return
816
+
817
+ # Show preview header
818
+ self.console.print(
819
+ f"\n[bold cyan]👁️ Preview: {persona.icon} {persona.name}[/bold cyan]"
820
+ )
821
+ self.console.print(
822
+ f"[dim]This is how '{persona.name}' would behave if activated[/dim]\n"
823
+ )
824
+
825
+ # Show configuration preview
826
+ config_lines = [
827
+ f"[bold]Configuration Preview[/bold]",
828
+ f"• Template: {persona.configuration.template_id if persona.configuration else 'None'}",
829
+ f"• Provider: {persona.configuration.primary_provider if persona.configuration else 'None'}",
830
+ ]
831
+
832
+ if persona.configuration:
833
+ config_lines.extend(
834
+ [
835
+ f"• Tools: {'✅ Enabled' if persona.configuration.tools_enabled else '❌ Disabled'}",
836
+ f"• Web Search: {'✅ Enabled' if getattr(persona.configuration, 'web_search_enabled', False) else '❌ Disabled'}",
837
+ f"• File Operations: {'✅ Enabled' if getattr(persona.configuration, 'file_operations_enabled', False) else '❌ Disabled'}",
838
+ f"• Approval Required: {'✅ Yes' if getattr(persona.configuration, 'approval_required', True) else '❌ No'}",
839
+ ]
840
+ )
841
+
842
+ config_panel = Panel(
843
+ "\n".join(config_lines), title="Configuration", border_style="blue"
844
+ )
845
+
846
+ # Show capabilities preview
847
+ if persona.capabilities:
848
+ caps_text = ", ".join(
849
+ [cap.value.replace("_", " ").title() for cap in persona.capabilities]
850
+ )
851
+ caps_panel = Panel(caps_text, title="Capabilities", border_style="green")
852
+ else:
853
+ caps_panel = Panel(
854
+ "[dim]No specific capabilities defined[/dim]",
855
+ title="Capabilities",
856
+ border_style="dim",
857
+ )
858
+
859
+ # Show behavioral description
860
+ behavior_lines = [
861
+ persona.description,
862
+ "",
863
+ "[bold]Expected Behavior:[/bold]",
864
+ ]
865
+
866
+ # Add behavior predictions based on persona type
867
+ behavior_predictions = {
868
+ "coding": [
869
+ "• Will focus on code quality and best practices",
870
+ "• Provides detailed technical explanations",
871
+ "• Suggests testing and documentation",
872
+ "• Uses development tools and file operations",
873
+ ],
874
+ "research": [
875
+ "• Will search for current information online",
876
+ "• Provides comprehensive analysis",
877
+ "• Cites sources and references",
878
+ "• Takes more time for thorough responses",
879
+ ],
880
+ "creative": [
881
+ "• Focuses on engaging and original content",
882
+ "• Uses varied writing styles and techniques",
883
+ "• Emphasizes creativity over strict accuracy",
884
+ "• May suggest multiple creative approaches",
885
+ ],
886
+ "performance": [
887
+ "• Provides quick, concise responses",
888
+ "• Prioritizes speed over depth",
889
+ "• Uses minimal tools to avoid delays",
890
+ "• Best for simple, straightforward tasks",
891
+ ],
892
+ "general": [
893
+ "• Provides balanced responses",
894
+ "• Adapts approach to task requirements",
895
+ "• Uses moderate depth and detail",
896
+ "• Good all-around performance",
897
+ ],
898
+ }
899
+
900
+ if persona.id in behavior_predictions:
901
+ behavior_lines.extend(behavior_predictions[persona.id])
902
+ else:
903
+ behavior_lines.append(
904
+ "• Behavior will depend on configured template and settings"
905
+ )
906
+
907
+ behavior_panel = Panel(
908
+ "\n".join(behavior_lines),
909
+ title="Behavioral Preview",
910
+ border_style="yellow",
911
+ )
912
+
913
+ # Display all panels
914
+ self.console.print(config_panel)
915
+ self.console.print(caps_panel)
916
+ self.console.print(behavior_panel)
917
+
918
+ # Show action options
919
+ action_lines = [
920
+ f"[bold]Ready to switch?[/bold]",
921
+ f"• [cyan]/agent use {persona.id}[/cyan] - Switch to this persona",
922
+ f"• [cyan]/agent compare[/cyan] - Compare with other personas",
923
+ f"• [cyan]/agent info {persona.id}[/cyan] - View detailed information",
924
+ ]
925
+
926
+ action_panel = Panel(
927
+ "\n".join(action_lines), title="Next Steps", border_style="cyan"
928
+ )
929
+
930
+ self.console.print(action_panel)
931
+
932
+ async def handle_discover_personas(self) -> None:
933
+ """Provide interactive persona discovery interface."""
934
+ self.console.print("\n[bold cyan]🧭 Persona Discovery[/bold cyan]")
935
+
936
+ personas = self.persona_manager.get_all_personas()
937
+ if not personas:
938
+ self._show_warning("No personas available for discovery.")
939
+ return
940
+
941
+ # Show discovery intro
942
+ intro_lines = [
943
+ "[bold]Welcome to Persona Discovery![/bold]",
944
+ "",
945
+ "This guide will help you find the perfect agent persona for your needs.",
946
+ "Each persona is optimized for different types of tasks and workflows.",
947
+ "",
948
+ "[dim]💡 Tip: You can always switch personas later with '/agent use <name>'[/dim]",
949
+ ]
950
+
951
+ intro_panel = Panel(
952
+ "\n".join(intro_lines),
953
+ title="Getting Started",
954
+ border_style="cyan",
955
+ )
956
+
957
+ self.console.print(intro_panel)
958
+
959
+ # Group personas by category
960
+ categories = {}
961
+ for persona in personas.values():
962
+ cat = persona.category.value
963
+ if cat not in categories:
964
+ categories[cat] = []
965
+ categories[cat].append(persona)
966
+
967
+ # Show personas by category
968
+ for category, persona_list in categories.items():
969
+ category_title = f"📂 {category.title()} Personas"
970
+ self.console.print(f"\n[bold yellow]{category_title}[/bold yellow]")
971
+
972
+ for persona in sorted(persona_list, key=lambda p: p.name):
973
+ # Create persona card
974
+ card_lines = [
975
+ f"[bold]{persona.icon} {persona.name}[/bold]",
976
+ persona.description,
977
+ "",
978
+ ]
979
+
980
+ # Add key features
981
+ features = []
982
+ if persona.configuration:
983
+ if persona.configuration.tools_enabled:
984
+ features.append("🛠️ Tools enabled")
985
+ if getattr(persona.configuration, "web_search_enabled", False):
986
+ features.append("🌐 Web search")
987
+ if getattr(persona.configuration, "file_operations_enabled", False):
988
+ features.append("📁 File operations")
989
+
990
+ if features:
991
+ card_lines.extend(
992
+ [
993
+ "[dim]Key features:[/dim]",
994
+ "[dim]" + " • ".join(features) + "[/dim]",
995
+ "",
996
+ ]
997
+ )
998
+
999
+ # Add usage stats
1000
+ if persona.metadata and persona.metadata.usage_count > 0:
1001
+ card_lines.append(
1002
+ f"[dim]Used {persona.metadata.usage_count} times[/dim]"
1003
+ )
1004
+
1005
+ # Add quick actions
1006
+ card_lines.extend(
1007
+ [
1008
+ "",
1009
+ f"[cyan]Preview:[/cyan] /agent preview {persona.id}",
1010
+ f"[green]Use:[/green] /agent use {persona.id}",
1011
+ ]
1012
+ )
1013
+
1014
+ # Show card
1015
+ persona_panel = Panel(
1016
+ "\n".join(card_lines),
1017
+ title=f"{persona.id}",
1018
+ border_style=(
1019
+ "blue"
1020
+ if persona == self.persona_manager.active_persona
1021
+ else "dim"
1022
+ ),
1023
+ width=50,
1024
+ )
1025
+
1026
+ self.console.print(persona_panel)
1027
+
1028
+ # Show discovery footer
1029
+ footer_lines = [
1030
+ "[bold]Need help choosing?[/bold]",
1031
+ "",
1032
+ '• [cyan]/agent recommend "<your task>"[/cyan] - Get recommendations for a specific task',
1033
+ "• [cyan]/agent compare[/cyan] - See side-by-side capability comparison",
1034
+ "• [cyan]/agent info <persona>[/cyan] - Get detailed information about a persona",
1035
+ "• [cyan]/agent preview <persona>[/cyan] - Preview how a persona would behave",
1036
+ "",
1037
+ "[bold]Popular starting choices:[/bold]",
1038
+ "• [green]coding[/green] - For developers and programmers",
1039
+ "• [green]research[/green] - For information gathering and analysis",
1040
+ "• [green]general[/green] - For balanced, general-purpose work",
1041
+ ]
1042
+
1043
+ footer_panel = Panel(
1044
+ "\n".join(footer_lines), title="Next Steps", border_style="green"
1045
+ )
1046
+
1047
+ self.console.print("\n")
1048
+ self.console.print(footer_panel)