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,658 @@
1
+ """
2
+ Complete File Modification Workflow with User Approval.
3
+
4
+ This module provides the end-to-end workflow for file modifications,
5
+ integrating all UI components and approval mechanisms into a cohesive system.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+ from enum import Enum
13
+ from typing import Any, Callable, Dict, List, Optional
14
+
15
+ from rich.console import Console
16
+ from rich.progress import Progress
17
+
18
+ from ...cli.approval_formatter import CLIApprovalFormatter
19
+ from ...cli.approval_prompt import (
20
+ ApprovalDecision,
21
+ ApprovalDecisionType,
22
+ CLIApprovalPrompt,
23
+ )
24
+ from ..security.approval_workflow import (
25
+ RiskLevel,
26
+ )
27
+ from .approval_manager import EnhancedApprovalManager
28
+ from .file_content_display import (
29
+ FileDisplayConfig,
30
+ UnifiedFileContentDisplay,
31
+ )
32
+ from .file_system_manager import FileSystemManager
33
+ from .proposed_changes_integration import (
34
+ ChangeDisplayMode,
35
+ ChangeSet,
36
+ ProposedChangesIntegration,
37
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class WorkflowState(Enum):
43
+ """States of the file modification workflow."""
44
+
45
+ INITIALIZED = "initialized"
46
+ DISPLAYING_CHANGES = "displaying_changes"
47
+ AWAITING_APPROVAL = "awaiting_approval"
48
+ PROCESSING_APPROVAL = "processing_approval"
49
+ APPLYING_CHANGES = "applying_changes"
50
+ COMPLETED = "completed"
51
+ CANCELLED = "cancelled"
52
+ ERROR = "error"
53
+
54
+
55
+ class WorkflowResult(Enum):
56
+ """Possible results of the workflow."""
57
+
58
+ APPROVED_AND_APPLIED = "approved_and_applied"
59
+ APPROVED_NOT_APPLIED = "approved_not_applied"
60
+ DENIED = "denied"
61
+ CANCELLED = "cancelled"
62
+ ERROR = "error"
63
+ TIMEOUT = "timeout"
64
+
65
+
66
+ @dataclass
67
+ class WorkflowConfig:
68
+ """Configuration for the file modification workflow."""
69
+
70
+ # Display settings
71
+ default_display_mode: ChangeDisplayMode = ChangeDisplayMode.UNIFIED
72
+ show_file_stats: bool = True
73
+ syntax_highlighting: bool = True
74
+
75
+ # Approval settings
76
+ require_confirmation: bool = True
77
+ auto_apply_approved: bool = True
78
+ remember_decisions: bool = True
79
+ approval_timeout_seconds: int = 300 # 5 minutes
80
+
81
+ # Risk thresholds
82
+ auto_approve_risk_threshold: RiskLevel = RiskLevel.LOW
83
+ require_explicit_approval: RiskLevel = RiskLevel.HIGH
84
+
85
+ # Batch settings
86
+ batch_approval_threshold: int = 5 # Show batch interface for 5+ changes
87
+ allow_selective_approval: bool = True
88
+
89
+ # Performance settings
90
+ max_concurrent_operations: int = 5 # Maximum concurrent file operations
91
+
92
+ # Persistence
93
+ save_workflow_history: bool = True
94
+ history_retention_days: int = 30
95
+
96
+
97
+ @dataclass
98
+ class WorkflowContext:
99
+ """Context information for the workflow."""
100
+
101
+ operation_id: str
102
+ operation_type: str
103
+ initiated_by: str = "user"
104
+ initiated_at: datetime = field(default_factory=datetime.now)
105
+ metadata: Dict[str, Any] = field(default_factory=dict)
106
+
107
+ # State tracking
108
+ current_state: WorkflowState = WorkflowState.INITIALIZED
109
+ state_history: List[Dict[str, Any]] = field(default_factory=list)
110
+
111
+ # Results
112
+ final_result: Optional[WorkflowResult] = None
113
+ user_decisions: List[ApprovalDecision] = field(default_factory=list)
114
+ applied_changes: List[str] = field(default_factory=list)
115
+ failed_changes: List[tuple] = field(default_factory=list)
116
+
117
+
118
+ class FileModificationWorkflow:
119
+ """
120
+ Complete file modification workflow with integrated user approval.
121
+
122
+ Orchestrates the entire process from displaying changes to applying
123
+ approved modifications, with comprehensive error handling and state management.
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ file_system_manager: Optional[FileSystemManager] = None,
129
+ approval_manager: Optional[EnhancedApprovalManager] = None,
130
+ console: Optional[Console] = None,
131
+ config: Optional[WorkflowConfig] = None,
132
+ ):
133
+ """
134
+ Initialize the file modification workflow.
135
+
136
+ Args:
137
+ file_system_manager: File system manager instance
138
+ approval_manager: Approval manager instance
139
+ console: Rich console for output
140
+ config: Workflow configuration
141
+ """
142
+ self.console = console or Console()
143
+ self.config = config or WorkflowConfig()
144
+ self.file_system_manager = file_system_manager or FileSystemManager()
145
+ self.approval_manager = approval_manager or EnhancedApprovalManager()
146
+
147
+ # Initialize UI components
148
+ self.unified_display = UnifiedFileContentDisplay(
149
+ console=self.console,
150
+ config=FileDisplayConfig(
151
+ syntax_highlighting=self.config.syntax_highlighting,
152
+ show_file_stats=self.config.show_file_stats,
153
+ ),
154
+ )
155
+
156
+ self.changes_integration = ProposedChangesIntegration(
157
+ file_system_manager=self.file_system_manager,
158
+ approval_manager=self.approval_manager,
159
+ console=self.console,
160
+ )
161
+
162
+ self.approval_prompt = CLIApprovalPrompt(
163
+ console=self.console,
164
+ default_timeout_seconds=self.config.approval_timeout_seconds,
165
+ )
166
+
167
+ self.approval_formatter = CLIApprovalFormatter(console=self.console)
168
+
169
+ # Workflow tracking
170
+ self.active_workflows: Dict[str, WorkflowContext] = {}
171
+ self.workflow_history: List[WorkflowContext] = []
172
+
173
+ # Callbacks
174
+ self.on_state_change: Optional[Callable] = None
175
+ self.on_approval_decision: Optional[Callable] = None
176
+ self.on_workflow_complete: Optional[Callable] = None
177
+
178
+ async def execute_file_modification_workflow(
179
+ self,
180
+ operation_id: str,
181
+ file_paths: Optional[List[str]] = None,
182
+ context_metadata: Optional[Dict[str, Any]] = None,
183
+ ) -> WorkflowContext:
184
+ """
185
+ Execute the complete file modification workflow.
186
+
187
+ Args:
188
+ operation_id: ID of the operation to process
189
+ file_paths: Optional list of specific files to process
190
+ context_metadata: Additional context information
191
+
192
+ Returns:
193
+ WorkflowContext containing the complete workflow state and results
194
+ """
195
+ # Initialize workflow context
196
+ workflow_context = WorkflowContext(
197
+ operation_id=operation_id,
198
+ operation_type="file_modification",
199
+ metadata=context_metadata or {},
200
+ )
201
+
202
+ self.active_workflows[operation_id] = workflow_context
203
+
204
+ try:
205
+ # Step 1: Fetch proposed changes
206
+ await self._transition_state(
207
+ workflow_context, WorkflowState.DISPLAYING_CHANGES
208
+ )
209
+ change_set = await self.changes_integration.fetch_proposed_changes(
210
+ operation_id, file_paths
211
+ )
212
+
213
+ if not change_set.changes:
214
+ workflow_context.final_result = WorkflowResult.CANCELLED
215
+ await self._transition_state(workflow_context, WorkflowState.COMPLETED)
216
+ self.console.print("[yellow]No changes found to process.[/yellow]")
217
+ return workflow_context
218
+
219
+ # Step 2: Display changes and get approval
220
+ await self._transition_state(
221
+ workflow_context, WorkflowState.AWAITING_APPROVAL
222
+ )
223
+ approval_result = await self._handle_change_approval(
224
+ workflow_context, change_set
225
+ )
226
+
227
+ if not approval_result.get("approved", False):
228
+ workflow_context.final_result = WorkflowResult.DENIED
229
+ await self._transition_state(workflow_context, WorkflowState.COMPLETED)
230
+ return workflow_context
231
+
232
+ # Step 3: Apply approved changes
233
+ if self.config.auto_apply_approved:
234
+ await self._transition_state(
235
+ workflow_context, WorkflowState.APPLYING_CHANGES
236
+ )
237
+ apply_result = await self._apply_approved_changes(
238
+ workflow_context, change_set, approval_result
239
+ )
240
+
241
+ if apply_result["success"]:
242
+ workflow_context.final_result = WorkflowResult.APPROVED_AND_APPLIED
243
+ else:
244
+ workflow_context.final_result = WorkflowResult.APPROVED_NOT_APPLIED
245
+ else:
246
+ workflow_context.final_result = WorkflowResult.APPROVED_NOT_APPLIED
247
+
248
+ await self._transition_state(workflow_context, WorkflowState.COMPLETED)
249
+
250
+ except asyncio.TimeoutError:
251
+ workflow_context.final_result = WorkflowResult.TIMEOUT
252
+ await self._transition_state(workflow_context, WorkflowState.ERROR)
253
+ self.console.print("[red]Workflow timed out waiting for user input.[/red]")
254
+
255
+ except Exception as e:
256
+ logger.error(f"Error in workflow {operation_id}: {e}")
257
+ workflow_context.final_result = WorkflowResult.ERROR
258
+ await self._transition_state(workflow_context, WorkflowState.ERROR)
259
+ self.console.print(f"[red]Workflow error: {str(e)}[/red]")
260
+
261
+ finally:
262
+ # Clean up and store history
263
+ await self._finalize_workflow(workflow_context)
264
+
265
+ return workflow_context
266
+
267
+ async def execute_workflow(self, changeset: ChangeSet) -> WorkflowContext:
268
+ """
269
+ Execute workflow for a given ChangeSet.
270
+
271
+ Args:
272
+ changeset: The ChangeSet to process
273
+
274
+ Returns:
275
+ WorkflowContext containing the complete workflow state and results
276
+ """
277
+ operation_id = changeset.id
278
+
279
+ # Initialize workflow context
280
+ workflow_context = WorkflowContext(
281
+ operation_id=operation_id,
282
+ operation_type="file_modification",
283
+ metadata={},
284
+ )
285
+
286
+ self.active_workflows[operation_id] = workflow_context
287
+
288
+ try:
289
+ # Step 1: Display changes (already have them in changeset)
290
+ await self._transition_state(
291
+ workflow_context, WorkflowState.DISPLAYING_CHANGES
292
+ )
293
+
294
+ # Step 2: Handle approval
295
+ await self._transition_state(
296
+ workflow_context, WorkflowState.AWAITING_APPROVAL
297
+ )
298
+ approval_result = await self._handle_change_approval(
299
+ workflow_context, changeset
300
+ )
301
+
302
+ if not approval_result.get("approved", False):
303
+ workflow_context.current_state = WorkflowState.CANCELLED
304
+ return workflow_context
305
+
306
+ # Step 3: Apply changes
307
+ await self._transition_state(
308
+ workflow_context, WorkflowState.APPLYING_CHANGES
309
+ )
310
+ apply_result = await self._apply_approved_changes(
311
+ workflow_context, changeset, approval_result
312
+ )
313
+
314
+ # Step 4: Complete
315
+ if apply_result.get("success", False):
316
+ await self._transition_state(workflow_context, WorkflowState.COMPLETED)
317
+ else:
318
+ await self._transition_state(workflow_context, WorkflowState.ERROR)
319
+ workflow_context.failed_changes = apply_result.get("failed_changes", [])
320
+
321
+ except Exception as e:
322
+ workflow_context.current_state = WorkflowState.ERROR
323
+ workflow_context.failed_changes.append((str(e), "Exception"))
324
+ finally:
325
+ # Clean up and store history
326
+ await self._finalize_workflow(workflow_context)
327
+
328
+ return workflow_context
329
+
330
+ async def execute_single_file_workflow(
331
+ self,
332
+ file_path: str,
333
+ operation_type: str,
334
+ current_content: Optional[str] = None,
335
+ new_content: Optional[str] = None,
336
+ metadata: Optional[Dict[str, Any]] = None,
337
+ ) -> WorkflowContext:
338
+ """
339
+ Execute workflow for a single file modification.
340
+
341
+ Args:
342
+ file_path: Path to the file
343
+ operation_type: Type of operation (create, modify, delete)
344
+ current_content: Current file content (for modify operations)
345
+ new_content: New content to write (for create/modify operations)
346
+ metadata: Additional metadata
347
+
348
+ Returns:
349
+ WorkflowContext with results
350
+ """
351
+ operation_id = f"single-file-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
352
+
353
+ workflow_context = WorkflowContext(
354
+ operation_id=operation_id,
355
+ operation_type=f"single_file_{operation_type}",
356
+ metadata=metadata or {},
357
+ )
358
+
359
+ try:
360
+ await self._transition_state(
361
+ workflow_context, WorkflowState.DISPLAYING_CHANGES
362
+ )
363
+
364
+ # Display based on operation type
365
+ current_model = workflow_context.metadata.get(
366
+ "current_model", "Omnimancer AI"
367
+ )
368
+ if operation_type == "create":
369
+ display_result = await self.unified_display.display_file_creation(
370
+ file_path,
371
+ new_content or "",
372
+ {"interactive": True, "current_model": current_model},
373
+ )
374
+ elif operation_type == "modify":
375
+ display_result = await self.unified_display.display_file_modification(
376
+ file_path,
377
+ current_content or "",
378
+ new_content or "",
379
+ {"interactive": True, "current_model": current_model},
380
+ )
381
+ elif operation_type == "delete":
382
+ display_result = await self.unified_display.display_file_deletion(
383
+ file_path,
384
+ current_content or "",
385
+ {"interactive": True, "current_model": current_model},
386
+ )
387
+ else:
388
+ raise ValueError(f"Unknown operation type: {operation_type}")
389
+
390
+ # Process approval decision
391
+ if display_result.get("approved", False):
392
+ workflow_context.final_result = WorkflowResult.APPROVED_AND_APPLIED
393
+
394
+ # Apply the change (bypass autonomous approval since we already got approval)
395
+ if operation_type == "create" and new_content is not None:
396
+ # Call original write_file directly to bypass autonomous approval wrapper
397
+ write_method = getattr(
398
+ self.file_system_manager,
399
+ "_original_write_file",
400
+ self.file_system_manager.write_file,
401
+ )
402
+ await write_method(
403
+ path=file_path,
404
+ content=new_content,
405
+ read_before_write=False, # Skip read_before_write since we already got user approval
406
+ )
407
+ elif operation_type == "modify" and new_content is not None:
408
+ # Call original write_file directly to bypass autonomous approval wrapper
409
+ write_method = getattr(
410
+ self.file_system_manager,
411
+ "_original_write_file",
412
+ self.file_system_manager.write_file,
413
+ )
414
+ await write_method(
415
+ path=file_path,
416
+ content=new_content,
417
+ read_before_write=False, # Skip read_before_write since we already got user approval
418
+ )
419
+ elif operation_type == "delete":
420
+ await self.file_system_manager.delete_file(file_path)
421
+
422
+ workflow_context.applied_changes.append(file_path)
423
+ else:
424
+ workflow_context.final_result = WorkflowResult.DENIED
425
+
426
+ await self._transition_state(workflow_context, WorkflowState.COMPLETED)
427
+
428
+ except Exception as e:
429
+ logger.error(f"Error in single file workflow: {e}")
430
+ workflow_context.final_result = WorkflowResult.ERROR
431
+ await self._transition_state(workflow_context, WorkflowState.ERROR)
432
+
433
+ return workflow_context
434
+
435
+ async def get_workflow_status(self, operation_id: str) -> Optional[Dict[str, Any]]:
436
+ """
437
+ Get current status of a workflow.
438
+
439
+ Args:
440
+ operation_id: ID of the workflow to check
441
+
442
+ Returns:
443
+ Status dictionary or None if workflow not found
444
+ """
445
+ workflow = self.active_workflows.get(operation_id)
446
+ if not workflow:
447
+ return None
448
+
449
+ return {
450
+ "operation_id": workflow.operation_id,
451
+ "current_state": workflow.current_state.value,
452
+ "initiated_at": workflow.initiated_at.isoformat(),
453
+ "elapsed_seconds": (datetime.now() - workflow.initiated_at).total_seconds(),
454
+ "decisions_count": len(workflow.user_decisions),
455
+ "applied_changes": workflow.applied_changes,
456
+ "failed_changes": workflow.failed_changes,
457
+ }
458
+
459
+ async def cancel_workflow(
460
+ self, operation_id: str, reason: str = "User cancelled"
461
+ ) -> bool:
462
+ """
463
+ Cancel an active workflow.
464
+
465
+ Args:
466
+ operation_id: ID of the workflow to cancel
467
+ reason: Reason for cancellation
468
+
469
+ Returns:
470
+ True if successfully cancelled, False if workflow not found
471
+ """
472
+ workflow = self.active_workflows.get(operation_id)
473
+ if not workflow:
474
+ return False
475
+
476
+ workflow.final_result = WorkflowResult.CANCELLED
477
+ workflow.metadata["cancellation_reason"] = reason
478
+
479
+ await self._transition_state(workflow, WorkflowState.CANCELLED)
480
+ await self._finalize_workflow(workflow)
481
+
482
+ return True
483
+
484
+ async def _handle_change_approval(
485
+ self, workflow_context: WorkflowContext, change_set: ChangeSet
486
+ ) -> Dict[str, Any]:
487
+ """Handle the approval process for changes."""
488
+ # Determine if we should use batch interface
489
+ use_batch_interface = (
490
+ len(change_set.changes) >= self.config.batch_approval_threshold
491
+ )
492
+
493
+ if use_batch_interface:
494
+ return await self.changes_integration.display_proposed_changes(
495
+ change_set,
496
+ display_mode=self.config.default_display_mode,
497
+ interactive=True,
498
+ )
499
+ else:
500
+ # Handle individual approval for each change
501
+ approved_changes = []
502
+
503
+ for i, change in enumerate(change_set.changes):
504
+ self.console.print(
505
+ f"\n[bold]Change {i+1} of {len(change_set.changes)}[/bold]"
506
+ )
507
+
508
+ # Create single-change set for display
509
+ single_change_set = ChangeSet(
510
+ id=f"{change_set.id}-{i}",
511
+ description=f"Change for {change.file_path}",
512
+ changes=[change],
513
+ )
514
+
515
+ result = await self.changes_integration.display_proposed_changes(
516
+ single_change_set,
517
+ display_mode=self.config.default_display_mode,
518
+ interactive=True,
519
+ )
520
+
521
+ if result.get("approved", False):
522
+ approved_changes.append(i)
523
+
524
+ # Record decision
525
+ decision = ApprovalDecision(
526
+ decision=(
527
+ ApprovalDecisionType.APPROVED
528
+ if result.get("approved")
529
+ else ApprovalDecisionType.DENIED
530
+ ),
531
+ user_notes=result.get("reason", ""),
532
+ )
533
+ workflow_context.user_decisions.append(decision)
534
+
535
+ return {
536
+ "approved": len(approved_changes) > 0,
537
+ "selected_changes": approved_changes,
538
+ }
539
+
540
+ async def _apply_approved_changes(
541
+ self,
542
+ workflow_context: WorkflowContext,
543
+ change_set: ChangeSet,
544
+ approval_result: Dict[str, Any],
545
+ ) -> Dict[str, Any]:
546
+ """Apply the approved changes."""
547
+ selected_changes = approval_result.get("selected_changes")
548
+
549
+ # Show progress for multiple changes
550
+ if len(change_set.changes) > 1:
551
+ with Progress(console=self.console) as progress:
552
+ task = progress.add_task(
553
+ "Applying changes...", total=len(change_set.changes)
554
+ )
555
+
556
+ apply_result = await self.changes_integration.apply_proposed_changes(
557
+ change_set.id, selected_changes
558
+ )
559
+
560
+ progress.update(task, completed=len(change_set.changes))
561
+ else:
562
+ apply_result = await self.changes_integration.apply_proposed_changes(
563
+ change_set.id, selected_changes
564
+ )
565
+
566
+ # Update workflow context
567
+ workflow_context.applied_changes.extend(apply_result.get("applied", []))
568
+ workflow_context.failed_changes.extend(apply_result.get("failed", []))
569
+
570
+ # Display results
571
+ if apply_result["success"]:
572
+ self.console.print(
573
+ f"[green]✓ Successfully applied {len(apply_result.get('applied', []))} changes[/green]"
574
+ )
575
+ else:
576
+ failed_count = len(apply_result.get("failed", []))
577
+ self.console.print(f"[red]✗ {failed_count} changes failed to apply[/red]")
578
+
579
+ for file_path, error in apply_result.get("failed", []):
580
+ self.console.print(f" [red]• {file_path}: {error}[/red]")
581
+
582
+ return apply_result
583
+
584
+ async def _transition_state(
585
+ self, workflow_context: WorkflowContext, new_state: WorkflowState
586
+ ):
587
+ """Transition workflow to a new state."""
588
+ old_state = workflow_context.current_state
589
+ workflow_context.current_state = new_state
590
+
591
+ # Record state change
592
+ workflow_context.state_history.append(
593
+ {
594
+ "from_state": old_state.value,
595
+ "to_state": new_state.value,
596
+ "timestamp": datetime.now().isoformat(),
597
+ }
598
+ )
599
+
600
+ # Call callback if registered
601
+ if self.on_state_change:
602
+ await self.on_state_change(workflow_context, old_state, new_state)
603
+
604
+ async def _finalize_workflow(self, workflow_context: WorkflowContext):
605
+ """Finalize workflow and clean up."""
606
+ # Remove from active workflows
607
+ if workflow_context.operation_id in self.active_workflows:
608
+ del self.active_workflows[workflow_context.operation_id]
609
+
610
+ # Add to history if configured
611
+ if self.config.save_workflow_history:
612
+ self.workflow_history.append(workflow_context)
613
+
614
+ # Clean old history
615
+ cutoff_date = datetime.now() - timedelta(
616
+ days=self.config.history_retention_days
617
+ )
618
+ self.workflow_history = [
619
+ w for w in self.workflow_history if w.initiated_at > cutoff_date
620
+ ]
621
+
622
+ # Call completion callback
623
+ if self.on_workflow_complete:
624
+ await self.on_workflow_complete(workflow_context)
625
+
626
+ def get_workflow_statistics(self) -> Dict[str, Any]:
627
+ """Get statistics about workflow usage."""
628
+ total_workflows = len(self.workflow_history)
629
+ if total_workflows == 0:
630
+ return {"total_workflows": 0}
631
+
632
+ # Count results
633
+ result_counts = {}
634
+ for workflow in self.workflow_history:
635
+ result = workflow.final_result.value if workflow.final_result else "unknown"
636
+ result_counts[result] = result_counts.get(result, 0) + 1
637
+
638
+ # Calculate success rate
639
+ successful = result_counts.get("approved_and_applied", 0)
640
+ success_rate = (
641
+ (successful / total_workflows) * 100 if total_workflows > 0 else 0
642
+ )
643
+
644
+ return {
645
+ "total_workflows": total_workflows,
646
+ "active_workflows": len(self.active_workflows),
647
+ "result_distribution": result_counts,
648
+ "success_rate": success_rate,
649
+ "average_changes_per_workflow": (
650
+ sum(
651
+ len(w.applied_changes) + len(w.failed_changes)
652
+ for w in self.workflow_history
653
+ )
654
+ / total_workflows
655
+ if total_workflows > 0
656
+ else 0
657
+ ),
658
+ }