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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured Approval Dialog Layout for Omnimancer Agent Operations.
|
|
3
|
+
|
|
4
|
+
This module provides the main approval dialog interface that combines
|
|
5
|
+
rich text rendering, diff display, and interactive controls for
|
|
6
|
+
operation approval workflows.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Dict, Optional, Union
|
|
14
|
+
|
|
15
|
+
from rich.align import Align
|
|
16
|
+
from rich.box import MINIMAL, ROUNDED, SIMPLE
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.layout import Layout
|
|
19
|
+
from rich.live import Live
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.rule import Rule
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
|
|
25
|
+
from .approval_context import (
|
|
26
|
+
ApprovalContext,
|
|
27
|
+
ApprovalDecision,
|
|
28
|
+
OperationDetails,
|
|
29
|
+
)
|
|
30
|
+
from .diff_renderer import DiffType, EnhancedDiffRenderer, create_diff_renderer
|
|
31
|
+
from .input_handler import (
|
|
32
|
+
InputState,
|
|
33
|
+
InteractiveInputHandler,
|
|
34
|
+
KeyAction,
|
|
35
|
+
create_input_handler,
|
|
36
|
+
)
|
|
37
|
+
from .rich_renderer import (
|
|
38
|
+
RichTextRenderer,
|
|
39
|
+
RiskLevel,
|
|
40
|
+
create_renderer,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DialogState(Enum):
|
|
47
|
+
"""States of the approval dialog."""
|
|
48
|
+
|
|
49
|
+
INITIALIZING = "initializing"
|
|
50
|
+
DISPLAYING = "displaying"
|
|
51
|
+
WAITING_INPUT = "waiting_input"
|
|
52
|
+
PROCESSING = "processing"
|
|
53
|
+
COMPLETED = "completed"
|
|
54
|
+
CANCELLED = "cancelled"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DialogSection(Enum):
|
|
58
|
+
"""Sections of the approval dialog."""
|
|
59
|
+
|
|
60
|
+
HEADER = "header"
|
|
61
|
+
OPERATION = "operation"
|
|
62
|
+
DIFF = "diff"
|
|
63
|
+
RISK = "risk"
|
|
64
|
+
CONTROLS = "controls"
|
|
65
|
+
STATUS = "status"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class DialogOptions:
|
|
70
|
+
"""Configuration options for the approval dialog."""
|
|
71
|
+
|
|
72
|
+
# Display options
|
|
73
|
+
show_diff: bool = True
|
|
74
|
+
show_risk_assessment: bool = True
|
|
75
|
+
show_file_tree: bool = True
|
|
76
|
+
show_operation_details: bool = True
|
|
77
|
+
|
|
78
|
+
# Layout options
|
|
79
|
+
max_height: int = 40
|
|
80
|
+
max_width: int = 120
|
|
81
|
+
compact_mode: bool = False
|
|
82
|
+
|
|
83
|
+
# Interactive options
|
|
84
|
+
auto_scroll: bool = True
|
|
85
|
+
enable_shortcuts: bool = True
|
|
86
|
+
timeout_seconds: Optional[int] = None
|
|
87
|
+
|
|
88
|
+
# Rendering options
|
|
89
|
+
diff_type: DiffType = DiffType.UNIFIED
|
|
90
|
+
syntax_highlighting: bool = True
|
|
91
|
+
show_line_numbers: bool = True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ApprovalDialog:
|
|
95
|
+
"""
|
|
96
|
+
Structured approval dialog for agent operations.
|
|
97
|
+
|
|
98
|
+
Provides a comprehensive interface for reviewing and approving
|
|
99
|
+
agent operations with rich formatting, diff display, and
|
|
100
|
+
interactive controls.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
renderer: Optional[RichTextRenderer] = None,
|
|
106
|
+
diff_renderer: Optional[EnhancedDiffRenderer] = None,
|
|
107
|
+
console: Optional[Console] = None,
|
|
108
|
+
options: Optional[DialogOptions] = None,
|
|
109
|
+
input_handler: Optional[InteractiveInputHandler] = None,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Initialize the approval dialog.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
renderer: Rich text renderer
|
|
116
|
+
diff_renderer: Enhanced diff renderer
|
|
117
|
+
console: Rich console for output
|
|
118
|
+
options: Dialog configuration options
|
|
119
|
+
input_handler: Interactive input handler for keyboard controls
|
|
120
|
+
"""
|
|
121
|
+
self.renderer = renderer or create_renderer()
|
|
122
|
+
self.diff_renderer = diff_renderer or create_diff_renderer(self.renderer)
|
|
123
|
+
self.console = console or self.renderer.console
|
|
124
|
+
self.options = options or DialogOptions()
|
|
125
|
+
|
|
126
|
+
# Interactive input handling
|
|
127
|
+
self.input_handler = input_handler or create_input_handler(
|
|
128
|
+
timeout_seconds=self.options.timeout_seconds
|
|
129
|
+
)
|
|
130
|
+
self.input_handler.set_callbacks(
|
|
131
|
+
approval_callback=self._handle_user_approval,
|
|
132
|
+
denial_callback=self._handle_user_denial,
|
|
133
|
+
quit_callback=self._handle_user_quit,
|
|
134
|
+
display_update_callback=self._handle_display_update,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Dialog state
|
|
138
|
+
self.state = DialogState.INITIALIZING
|
|
139
|
+
self.current_context: Optional[ApprovalContext] = None
|
|
140
|
+
self.decision: Optional[ApprovalDecision] = None
|
|
141
|
+
|
|
142
|
+
# Layout components
|
|
143
|
+
self.layout = Layout()
|
|
144
|
+
self.sections: Dict[str, Layout] = {}
|
|
145
|
+
|
|
146
|
+
# Legacy shortcuts for compatibility
|
|
147
|
+
self.shortcuts: Dict[str, str] = {
|
|
148
|
+
"y": "Approve",
|
|
149
|
+
"n": "Deny",
|
|
150
|
+
"d": "Show Details",
|
|
151
|
+
"f": "Show Full Diff",
|
|
152
|
+
"s": "Show Summary",
|
|
153
|
+
"q": "Quit",
|
|
154
|
+
"h": "Help",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Initialize layout
|
|
158
|
+
self._setup_layout()
|
|
159
|
+
|
|
160
|
+
def _setup_layout(self) -> None:
|
|
161
|
+
"""Set up the dialog layout structure."""
|
|
162
|
+
# Calculate responsive dimensions
|
|
163
|
+
self.renderer.get_terminal_width()
|
|
164
|
+
term_height = self.renderer.get_terminal_height()
|
|
165
|
+
|
|
166
|
+
# Initialize sections dictionary
|
|
167
|
+
self.sections = {}
|
|
168
|
+
|
|
169
|
+
# Adjust for compact mode
|
|
170
|
+
if self.options.compact_mode or term_height < 25:
|
|
171
|
+
self.layout.split(
|
|
172
|
+
Layout(name="header", size=3),
|
|
173
|
+
Layout(name="main", ratio=1),
|
|
174
|
+
Layout(name="controls", size=4),
|
|
175
|
+
)
|
|
176
|
+
# Store basic sections
|
|
177
|
+
self.sections.update(
|
|
178
|
+
{
|
|
179
|
+
"header": self.layout["header"],
|
|
180
|
+
"main": self.layout["main"],
|
|
181
|
+
"controls": self.layout["controls"],
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
self.layout.split(
|
|
186
|
+
Layout(name="header", size=3),
|
|
187
|
+
Layout(name="operation", size=6),
|
|
188
|
+
Layout(name="main", ratio=1),
|
|
189
|
+
Layout(name="risk", size=5),
|
|
190
|
+
Layout(name="controls", size=4),
|
|
191
|
+
)
|
|
192
|
+
# Store all sections including operation and risk
|
|
193
|
+
self.sections.update(
|
|
194
|
+
{
|
|
195
|
+
"header": self.layout["header"],
|
|
196
|
+
"operation": self.layout["operation"],
|
|
197
|
+
"main": self.layout["main"],
|
|
198
|
+
"risk": self.layout["risk"],
|
|
199
|
+
"controls": self.layout["controls"],
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Set up main content area
|
|
204
|
+
if not self.options.compact_mode:
|
|
205
|
+
self.layout["main"].split_row(
|
|
206
|
+
Layout(name="diff", ratio=2), Layout(name="details", ratio=1)
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
# Single column for compact mode
|
|
210
|
+
self.layout["main"].split(
|
|
211
|
+
Layout(name="diff", ratio=2), Layout(name="details", ratio=1)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Add main content sections
|
|
215
|
+
self.sections.update(
|
|
216
|
+
{
|
|
217
|
+
"diff": self.layout["main"]["diff"],
|
|
218
|
+
"details": self.layout["main"]["details"],
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _render_header(self, context: ApprovalContext) -> Panel:
|
|
223
|
+
"""Render the dialog header."""
|
|
224
|
+
title = Text()
|
|
225
|
+
title.append("🔒 ", style="warning")
|
|
226
|
+
title.append("Operation Approval Required", style="header")
|
|
227
|
+
|
|
228
|
+
subtitle = Text()
|
|
229
|
+
subtitle.append(f"Agent: ", style="label")
|
|
230
|
+
subtitle.append(context.agent_name, style="value")
|
|
231
|
+
subtitle.append(" | ", style="dim")
|
|
232
|
+
subtitle.append(f"Time: ", style="label")
|
|
233
|
+
subtitle.append(context.timestamp.strftime("%H:%M:%S"), style="value")
|
|
234
|
+
|
|
235
|
+
return Panel(
|
|
236
|
+
Align.center(subtitle),
|
|
237
|
+
title=title,
|
|
238
|
+
border_style="cyan",
|
|
239
|
+
box=ROUNDED,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _render_operation_summary(self, operation: OperationDetails) -> Panel:
|
|
243
|
+
"""Render operation summary information."""
|
|
244
|
+
# Create summary table
|
|
245
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
246
|
+
table.add_column("Field", style="label", width=15)
|
|
247
|
+
table.add_column("Value", style="value")
|
|
248
|
+
|
|
249
|
+
# Add operation details
|
|
250
|
+
table.add_row("Operation", operation.operation_type)
|
|
251
|
+
table.add_row("Target", operation.target or "N/A")
|
|
252
|
+
table.add_row("Risk Level", self._format_risk_level(operation.risk_level))
|
|
253
|
+
|
|
254
|
+
if operation.estimated_time:
|
|
255
|
+
table.add_row("Est. Time", f"{operation.estimated_time}s")
|
|
256
|
+
|
|
257
|
+
if operation.files_affected:
|
|
258
|
+
table.add_row("Files", str(len(operation.files_affected)))
|
|
259
|
+
|
|
260
|
+
# Add description if available
|
|
261
|
+
if operation.description:
|
|
262
|
+
description = Text(operation.description)
|
|
263
|
+
if len(operation.description) > 60:
|
|
264
|
+
description = Text(operation.description[:57] + "...")
|
|
265
|
+
table.add_row("Description", description)
|
|
266
|
+
|
|
267
|
+
return Panel(
|
|
268
|
+
table,
|
|
269
|
+
title="📋 Operation Summary",
|
|
270
|
+
border_style="blue",
|
|
271
|
+
box=SIMPLE,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def _render_risk_assessment(self, operation: OperationDetails) -> Panel:
|
|
275
|
+
"""Render risk assessment panel."""
|
|
276
|
+
# Risk indicator
|
|
277
|
+
risk_indicator = self.renderer.render_risk_indicator(
|
|
278
|
+
operation.risk_level, show_label=True, show_bar=True
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Risk factors
|
|
282
|
+
factors = []
|
|
283
|
+
if operation.risk_factors:
|
|
284
|
+
for factor in operation.risk_factors[:3]: # Show top 3
|
|
285
|
+
factors.append(f"• {factor}")
|
|
286
|
+
|
|
287
|
+
if len(operation.risk_factors) > 3:
|
|
288
|
+
factors.append(f"• ... and {len(operation.risk_factors) - 3} more")
|
|
289
|
+
|
|
290
|
+
content = []
|
|
291
|
+
content.append(risk_indicator)
|
|
292
|
+
|
|
293
|
+
if factors:
|
|
294
|
+
content.append(Rule(style="dim"))
|
|
295
|
+
content.append(Text("\n".join(factors), style="warning"))
|
|
296
|
+
|
|
297
|
+
return Panel(
|
|
298
|
+
Align.left("\n".join([str(c) for c in content])),
|
|
299
|
+
title="⚠️ Risk Assessment",
|
|
300
|
+
border_style=self._get_risk_border_color(operation.risk_level),
|
|
301
|
+
box=ROUNDED,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _render_diff_panel(self, context: ApprovalContext) -> Panel:
|
|
305
|
+
"""Render file differences panel."""
|
|
306
|
+
if not context.diff_content:
|
|
307
|
+
return Panel(
|
|
308
|
+
Align.center("No file changes to display"),
|
|
309
|
+
title="📄 Changes",
|
|
310
|
+
border_style="dim",
|
|
311
|
+
box=SIMPLE,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Render diff using enhanced diff renderer
|
|
315
|
+
renderables = self.diff_renderer.render_diff_set(
|
|
316
|
+
context.diff_content,
|
|
317
|
+
diff_type=self.options.diff_type,
|
|
318
|
+
show_summary=not self.options.compact_mode,
|
|
319
|
+
show_file_tree=self.options.show_file_tree,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Combine renderables
|
|
323
|
+
if len(renderables) == 1:
|
|
324
|
+
content = renderables[0]
|
|
325
|
+
else:
|
|
326
|
+
# Join with rules between sections
|
|
327
|
+
combined = []
|
|
328
|
+
for i, renderable in enumerate(renderables):
|
|
329
|
+
if i > 0:
|
|
330
|
+
combined.append(Rule(style="dim"))
|
|
331
|
+
combined.append(renderable)
|
|
332
|
+
content = "\n".join([str(r) for r in combined])
|
|
333
|
+
|
|
334
|
+
return Panel(
|
|
335
|
+
content,
|
|
336
|
+
title="📄 File Changes",
|
|
337
|
+
border_style="yellow",
|
|
338
|
+
box=SIMPLE,
|
|
339
|
+
expand=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _render_operation_details(self, operation: OperationDetails) -> Panel:
|
|
343
|
+
"""Render detailed operation information."""
|
|
344
|
+
sections = []
|
|
345
|
+
|
|
346
|
+
# Command/Script details
|
|
347
|
+
if operation.command:
|
|
348
|
+
sections.append(self._render_command_details(operation))
|
|
349
|
+
|
|
350
|
+
# File operations
|
|
351
|
+
if operation.files_affected:
|
|
352
|
+
sections.append(self._render_file_details(operation))
|
|
353
|
+
|
|
354
|
+
# Environment/Context
|
|
355
|
+
if operation.environment_vars or operation.working_directory:
|
|
356
|
+
sections.append(self._render_environment_details(operation))
|
|
357
|
+
|
|
358
|
+
# Metadata
|
|
359
|
+
if operation.metadata:
|
|
360
|
+
sections.append(self._render_metadata(operation))
|
|
361
|
+
|
|
362
|
+
if not sections:
|
|
363
|
+
content = Align.center("No additional details available")
|
|
364
|
+
else:
|
|
365
|
+
content = "\n".join([str(s) for s in sections])
|
|
366
|
+
|
|
367
|
+
return Panel(
|
|
368
|
+
content,
|
|
369
|
+
title="🔍 Details",
|
|
370
|
+
border_style="cyan",
|
|
371
|
+
box=SIMPLE,
|
|
372
|
+
expand=True,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _render_command_details(self, operation: OperationDetails) -> Table:
|
|
376
|
+
"""Render command execution details."""
|
|
377
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
378
|
+
table.add_column("Property", style="label")
|
|
379
|
+
table.add_column("Value", style="value")
|
|
380
|
+
|
|
381
|
+
table.add_row("Command", operation.command)
|
|
382
|
+
if operation.arguments:
|
|
383
|
+
table.add_row("Arguments", " ".join(operation.arguments))
|
|
384
|
+
if operation.working_directory:
|
|
385
|
+
table.add_row("Working Dir", operation.working_directory)
|
|
386
|
+
|
|
387
|
+
return table
|
|
388
|
+
|
|
389
|
+
def _render_file_details(self, operation: OperationDetails) -> Table:
|
|
390
|
+
"""Render file operation details."""
|
|
391
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
392
|
+
table.add_column("File", style="value", width=30)
|
|
393
|
+
table.add_column("Operation", style="label")
|
|
394
|
+
table.add_column("Size", style="dim", justify="right")
|
|
395
|
+
|
|
396
|
+
for file_path in operation.files_affected[:5]: # Show first 5
|
|
397
|
+
# Determine operation type from file path context
|
|
398
|
+
op_type = "modify" # Default
|
|
399
|
+
size = "N/A"
|
|
400
|
+
|
|
401
|
+
table.add_row(str(file_path), op_type, size)
|
|
402
|
+
|
|
403
|
+
if len(operation.files_affected) > 5:
|
|
404
|
+
table.add_row(
|
|
405
|
+
f"... and {len(operation.files_affected) - 5} more files",
|
|
406
|
+
"",
|
|
407
|
+
"",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return table
|
|
411
|
+
|
|
412
|
+
def _render_environment_details(self, operation: OperationDetails) -> Table:
|
|
413
|
+
"""Render environment and context details."""
|
|
414
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
415
|
+
table.add_column("Variable", style="label")
|
|
416
|
+
table.add_column("Value", style="value")
|
|
417
|
+
|
|
418
|
+
if operation.working_directory:
|
|
419
|
+
table.add_row("PWD", operation.working_directory)
|
|
420
|
+
|
|
421
|
+
if operation.environment_vars:
|
|
422
|
+
for key, value in list(operation.environment_vars.items())[:3]:
|
|
423
|
+
# Mask sensitive values
|
|
424
|
+
if any(
|
|
425
|
+
sensitive in key.lower()
|
|
426
|
+
for sensitive in ["key", "token", "password", "secret"]
|
|
427
|
+
):
|
|
428
|
+
value = "***"
|
|
429
|
+
elif len(str(value)) > 40:
|
|
430
|
+
value = str(value)[:37] + "..."
|
|
431
|
+
|
|
432
|
+
table.add_row(key, str(value))
|
|
433
|
+
|
|
434
|
+
return table
|
|
435
|
+
|
|
436
|
+
def _render_metadata(self, operation: OperationDetails) -> Table:
|
|
437
|
+
"""Render operation metadata."""
|
|
438
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
439
|
+
table.add_column("Key", style="label")
|
|
440
|
+
table.add_column("Value", style="value")
|
|
441
|
+
|
|
442
|
+
for key, value in list(operation.metadata.items())[:3]:
|
|
443
|
+
if len(str(value)) > 40:
|
|
444
|
+
value = str(value)[:37] + "..."
|
|
445
|
+
table.add_row(key, str(value))
|
|
446
|
+
|
|
447
|
+
return table
|
|
448
|
+
|
|
449
|
+
def _render_controls(self) -> Panel:
|
|
450
|
+
"""Render control buttons and shortcuts."""
|
|
451
|
+
if self.options.enable_shortcuts:
|
|
452
|
+
shortcuts_help = self.renderer.render_shortcuts_help(
|
|
453
|
+
self.shortcuts,
|
|
454
|
+
columns=2 if self.renderer.get_terminal_width() > 80 else 1,
|
|
455
|
+
)
|
|
456
|
+
content = shortcuts_help
|
|
457
|
+
else:
|
|
458
|
+
# Simple button layout
|
|
459
|
+
content = Text("Use 'y' to approve, 'n' to deny, 'q' to quit")
|
|
460
|
+
|
|
461
|
+
return Panel(content, title="⌨️ Controls", border_style="green", box=SIMPLE)
|
|
462
|
+
|
|
463
|
+
def _format_risk_level(self, risk_level: Union[RiskLevel, int, str]) -> Text:
|
|
464
|
+
"""Format risk level with appropriate styling."""
|
|
465
|
+
if isinstance(risk_level, int):
|
|
466
|
+
risk_text = f"Level {risk_level}"
|
|
467
|
+
if risk_level <= 3:
|
|
468
|
+
style = "green"
|
|
469
|
+
elif risk_level <= 6:
|
|
470
|
+
style = "yellow"
|
|
471
|
+
else:
|
|
472
|
+
style = "red"
|
|
473
|
+
else:
|
|
474
|
+
risk_text = str(risk_level).upper()
|
|
475
|
+
style = self.renderer.get_risk_color(risk_level)
|
|
476
|
+
|
|
477
|
+
return Text(risk_text, style=style)
|
|
478
|
+
|
|
479
|
+
def _get_risk_border_color(self, risk_level: Union[RiskLevel, int, str]) -> str:
|
|
480
|
+
"""Get appropriate border color for risk level."""
|
|
481
|
+
if isinstance(risk_level, int):
|
|
482
|
+
if risk_level <= 3:
|
|
483
|
+
return "green"
|
|
484
|
+
elif risk_level <= 6:
|
|
485
|
+
return "yellow"
|
|
486
|
+
else:
|
|
487
|
+
return "red"
|
|
488
|
+
|
|
489
|
+
risk_colors = {
|
|
490
|
+
RiskLevel.LOW: "green",
|
|
491
|
+
RiskLevel.MEDIUM: "yellow",
|
|
492
|
+
RiskLevel.HIGH: "red",
|
|
493
|
+
RiskLevel.CRITICAL: "bold red",
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return risk_colors.get(risk_level, "yellow")
|
|
497
|
+
|
|
498
|
+
async def show_approval_dialog(self, context: ApprovalContext) -> ApprovalDecision:
|
|
499
|
+
"""
|
|
500
|
+
Display the approval dialog and wait for user decision.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
context: Approval context with operation details
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
User's approval decision
|
|
507
|
+
"""
|
|
508
|
+
try:
|
|
509
|
+
self.state = DialogState.DISPLAYING
|
|
510
|
+
self.current_context = context
|
|
511
|
+
|
|
512
|
+
# Render all dialog sections
|
|
513
|
+
self.sections["header"].update(self._render_header(context))
|
|
514
|
+
|
|
515
|
+
if not self.options.compact_mode and "operation" in self.sections:
|
|
516
|
+
self.sections["operation"].update(
|
|
517
|
+
self._render_operation_summary(context.operation_details)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
if not self.options.compact_mode and "risk" in self.sections:
|
|
521
|
+
self.sections["risk"].update(
|
|
522
|
+
self._render_risk_assessment(context.operation_details)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
self.sections["diff"].update(self._render_diff_panel(context))
|
|
526
|
+
self.sections["details"].update(
|
|
527
|
+
self._render_operation_details(context.operation_details)
|
|
528
|
+
)
|
|
529
|
+
self.sections["controls"].update(self._render_controls())
|
|
530
|
+
|
|
531
|
+
# Show the dialog with Live display
|
|
532
|
+
with Live(
|
|
533
|
+
self.layout,
|
|
534
|
+
console=self.console,
|
|
535
|
+
refresh_per_second=4,
|
|
536
|
+
auto_refresh=True,
|
|
537
|
+
) as live:
|
|
538
|
+
self.state = DialogState.WAITING_INPUT
|
|
539
|
+
self.live_display = live # Store for display updates
|
|
540
|
+
|
|
541
|
+
# Wait for user input
|
|
542
|
+
decision = await self._wait_for_decision()
|
|
543
|
+
|
|
544
|
+
self.state = DialogState.COMPLETED
|
|
545
|
+
self.decision = decision
|
|
546
|
+
|
|
547
|
+
return decision
|
|
548
|
+
|
|
549
|
+
except KeyboardInterrupt:
|
|
550
|
+
self.state = DialogState.CANCELLED
|
|
551
|
+
return ApprovalDecision(
|
|
552
|
+
approved=False,
|
|
553
|
+
reason="Operation cancelled by user",
|
|
554
|
+
timestamp=datetime.now(),
|
|
555
|
+
)
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error(f"Dialog error: {e}", exc_info=True)
|
|
558
|
+
self.state = DialogState.CANCELLED
|
|
559
|
+
return ApprovalDecision(
|
|
560
|
+
approved=False,
|
|
561
|
+
reason=f"Dialog error: {e}",
|
|
562
|
+
timestamp=datetime.now(),
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
async def _wait_for_decision(self) -> ApprovalDecision:
|
|
566
|
+
"""Wait for user input and return decision."""
|
|
567
|
+
try:
|
|
568
|
+
# Use the interactive input handler to get user decision
|
|
569
|
+
final_action = await self.input_handler.handle_input_loop()
|
|
570
|
+
|
|
571
|
+
# Create decision based on the action
|
|
572
|
+
if final_action == KeyAction.APPROVE:
|
|
573
|
+
return ApprovalDecision(
|
|
574
|
+
approved=True,
|
|
575
|
+
reason="Approved by user",
|
|
576
|
+
timestamp=datetime.now(),
|
|
577
|
+
additional_data={
|
|
578
|
+
"input_action": "approve",
|
|
579
|
+
"interactive": True,
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
elif final_action == KeyAction.DENY:
|
|
583
|
+
return ApprovalDecision(
|
|
584
|
+
approved=False,
|
|
585
|
+
reason="Denied by user",
|
|
586
|
+
timestamp=datetime.now(),
|
|
587
|
+
additional_data={
|
|
588
|
+
"input_action": "deny",
|
|
589
|
+
"interactive": True,
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
else: # QUIT or timeout
|
|
593
|
+
return ApprovalDecision(
|
|
594
|
+
approved=False,
|
|
595
|
+
reason="Operation cancelled or timed out",
|
|
596
|
+
timestamp=datetime.now(),
|
|
597
|
+
additional_data={
|
|
598
|
+
"input_action": "quit",
|
|
599
|
+
"interactive": True,
|
|
600
|
+
},
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
logger.error(f"Error in input handling: {e}")
|
|
605
|
+
return ApprovalDecision(
|
|
606
|
+
approved=False,
|
|
607
|
+
reason=f"Input handling error: {e}",
|
|
608
|
+
timestamp=datetime.now(),
|
|
609
|
+
additional_data={"input_error": str(e)},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def get_current_state(self) -> DialogState:
|
|
613
|
+
"""Get current dialog state."""
|
|
614
|
+
return self.state
|
|
615
|
+
|
|
616
|
+
def get_decision(self) -> Optional[ApprovalDecision]:
|
|
617
|
+
"""Get the approval decision if available."""
|
|
618
|
+
return self.decision
|
|
619
|
+
|
|
620
|
+
# Callback methods for input handler integration
|
|
621
|
+
|
|
622
|
+
async def _handle_user_approval(self) -> bool:
|
|
623
|
+
"""Handle user approval action."""
|
|
624
|
+
logger.info("User approved operation")
|
|
625
|
+
return True
|
|
626
|
+
|
|
627
|
+
async def _handle_user_denial(self) -> bool:
|
|
628
|
+
"""Handle user denial action."""
|
|
629
|
+
logger.info("User denied operation")
|
|
630
|
+
return True
|
|
631
|
+
|
|
632
|
+
async def _handle_user_quit(self) -> bool:
|
|
633
|
+
"""Handle user quit action."""
|
|
634
|
+
logger.info("User quit approval dialog")
|
|
635
|
+
return True
|
|
636
|
+
|
|
637
|
+
async def _handle_display_update(self, input_state: InputState) -> None:
|
|
638
|
+
"""Handle display updates based on input state changes."""
|
|
639
|
+
try:
|
|
640
|
+
# Update dialog based on input state
|
|
641
|
+
if hasattr(self, "live_display") and self.current_context:
|
|
642
|
+
|
|
643
|
+
# Update sections based on input state
|
|
644
|
+
if input_state.help_visible:
|
|
645
|
+
# Show help overlay
|
|
646
|
+
help_panel = Panel(
|
|
647
|
+
self.input_handler.get_help_text(),
|
|
648
|
+
title="🔍 Keyboard Shortcuts",
|
|
649
|
+
border_style="cyan",
|
|
650
|
+
box=ROUNDED,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# Replace main content with help
|
|
654
|
+
if "main" in self.sections:
|
|
655
|
+
self.sections["main"].update(help_panel)
|
|
656
|
+
|
|
657
|
+
else:
|
|
658
|
+
# Show normal content - update diff section if focused
|
|
659
|
+
if (
|
|
660
|
+
input_state.current_section == "diff"
|
|
661
|
+
and "diff" in self.sections
|
|
662
|
+
):
|
|
663
|
+
# Potentially update diff rendering based on expanded state
|
|
664
|
+
diff_panel = self._render_diff_panel(
|
|
665
|
+
self.current_context,
|
|
666
|
+
expanded=input_state.diff_expanded,
|
|
667
|
+
)
|
|
668
|
+
self.sections["diff"].update(diff_panel)
|
|
669
|
+
|
|
670
|
+
# Update details section if focused
|
|
671
|
+
elif (
|
|
672
|
+
input_state.current_section == "details"
|
|
673
|
+
and "details" in self.sections
|
|
674
|
+
):
|
|
675
|
+
details_panel = self._render_operation_details(
|
|
676
|
+
self.current_context.operation_details,
|
|
677
|
+
expanded=input_state.details_expanded,
|
|
678
|
+
)
|
|
679
|
+
self.sections["details"].update(details_panel)
|
|
680
|
+
|
|
681
|
+
# Update status/controls with current input state
|
|
682
|
+
if "controls" in self.sections:
|
|
683
|
+
controls_panel = self._render_controls_with_state(input_state)
|
|
684
|
+
self.sections["controls"].update(controls_panel)
|
|
685
|
+
|
|
686
|
+
# Refresh the display
|
|
687
|
+
self.live_display.refresh()
|
|
688
|
+
|
|
689
|
+
except Exception as e:
|
|
690
|
+
logger.error(f"Error updating display: {e}")
|
|
691
|
+
|
|
692
|
+
def _render_diff_panel(
|
|
693
|
+
self, context: ApprovalContext, expanded: bool = False
|
|
694
|
+
) -> Panel:
|
|
695
|
+
"""Render file differences panel with optional expansion."""
|
|
696
|
+
if not context.diff_content:
|
|
697
|
+
return Panel(
|
|
698
|
+
Align.center("No file changes to display"),
|
|
699
|
+
title="📄 Changes",
|
|
700
|
+
border_style="dim",
|
|
701
|
+
box=SIMPLE,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# Render diff using enhanced diff renderer
|
|
705
|
+
renderables = self.diff_renderer.render_diff_set(
|
|
706
|
+
context.diff_content,
|
|
707
|
+
diff_type=(DiffType.SIDE_BY_SIDE if expanded else self.options.diff_type),
|
|
708
|
+
show_summary=not self.options.compact_mode or expanded,
|
|
709
|
+
show_file_tree=self.options.show_file_tree,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Combine renderables
|
|
713
|
+
if len(renderables) == 1:
|
|
714
|
+
content = renderables[0]
|
|
715
|
+
else:
|
|
716
|
+
# Join with rules between sections
|
|
717
|
+
combined = []
|
|
718
|
+
for i, renderable in enumerate(renderables):
|
|
719
|
+
if i > 0:
|
|
720
|
+
combined.append(Rule(style="dim"))
|
|
721
|
+
combined.append(renderable)
|
|
722
|
+
content = "\n".join([str(r) for r in combined])
|
|
723
|
+
|
|
724
|
+
title = "📄 File Changes (Expanded)" if expanded else "📄 File Changes"
|
|
725
|
+
|
|
726
|
+
return Panel(
|
|
727
|
+
content,
|
|
728
|
+
title=title,
|
|
729
|
+
border_style="yellow" if expanded else "blue",
|
|
730
|
+
box=SIMPLE,
|
|
731
|
+
expand=True,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
def _render_operation_details(
|
|
735
|
+
self, operation: OperationDetails, expanded: bool = False
|
|
736
|
+
) -> Panel:
|
|
737
|
+
"""Render detailed operation information with optional expansion."""
|
|
738
|
+
sections = []
|
|
739
|
+
|
|
740
|
+
# Command/Script details
|
|
741
|
+
if operation.command:
|
|
742
|
+
sections.append(self._render_command_details(operation))
|
|
743
|
+
|
|
744
|
+
# File operations
|
|
745
|
+
if operation.files_affected:
|
|
746
|
+
sections.append(self._render_file_details(operation, show_all=expanded))
|
|
747
|
+
|
|
748
|
+
# Environment/Context
|
|
749
|
+
if operation.environment_vars or operation.working_directory:
|
|
750
|
+
sections.append(
|
|
751
|
+
self._render_environment_details(operation, show_all=expanded)
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Metadata
|
|
755
|
+
if operation.metadata:
|
|
756
|
+
sections.append(self._render_metadata(operation, show_all=expanded))
|
|
757
|
+
|
|
758
|
+
if not sections:
|
|
759
|
+
content = Align.center("No additional details available")
|
|
760
|
+
else:
|
|
761
|
+
content = "\n".join([str(s) for s in sections])
|
|
762
|
+
|
|
763
|
+
title = "🔍 Details (Expanded)" if expanded else "🔍 Details"
|
|
764
|
+
|
|
765
|
+
return Panel(
|
|
766
|
+
content,
|
|
767
|
+
title=title,
|
|
768
|
+
border_style="cyan" if expanded else "green",
|
|
769
|
+
box=SIMPLE,
|
|
770
|
+
expand=True,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
def _render_file_details(
|
|
774
|
+
self, operation: OperationDetails, show_all: bool = False
|
|
775
|
+
) -> Table:
|
|
776
|
+
"""Render file operation details with optional full listing."""
|
|
777
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
778
|
+
table.add_column("File", style="value", width=30)
|
|
779
|
+
table.add_column("Operation", style="label")
|
|
780
|
+
table.add_column("Size", style="dim", justify="right")
|
|
781
|
+
|
|
782
|
+
file_limit = len(operation.files_affected) if show_all else 5
|
|
783
|
+
|
|
784
|
+
for file_path in operation.files_affected[:file_limit]:
|
|
785
|
+
# Determine operation type from file path context
|
|
786
|
+
op_type = "modify" # Default
|
|
787
|
+
size = "N/A"
|
|
788
|
+
|
|
789
|
+
table.add_row(str(file_path), op_type, size)
|
|
790
|
+
|
|
791
|
+
if not show_all and len(operation.files_affected) > 5:
|
|
792
|
+
table.add_row(
|
|
793
|
+
f"... and {len(operation.files_affected) - 5} more files",
|
|
794
|
+
"",
|
|
795
|
+
"",
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
return table
|
|
799
|
+
|
|
800
|
+
def _render_environment_details(
|
|
801
|
+
self, operation: OperationDetails, show_all: bool = False
|
|
802
|
+
) -> Table:
|
|
803
|
+
"""Render environment and context details with optional full listing."""
|
|
804
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
805
|
+
table.add_column("Variable", style="label")
|
|
806
|
+
table.add_column("Value", style="value")
|
|
807
|
+
|
|
808
|
+
if operation.working_directory:
|
|
809
|
+
table.add_row("PWD", operation.working_directory)
|
|
810
|
+
|
|
811
|
+
if operation.environment_vars:
|
|
812
|
+
var_limit = len(operation.environment_vars) if show_all else 3
|
|
813
|
+
for key, value in list(operation.environment_vars.items())[:var_limit]:
|
|
814
|
+
# Mask sensitive values
|
|
815
|
+
if any(
|
|
816
|
+
sensitive in key.lower()
|
|
817
|
+
for sensitive in ["key", "token", "password", "secret"]
|
|
818
|
+
):
|
|
819
|
+
value = "***"
|
|
820
|
+
elif len(str(value)) > 40:
|
|
821
|
+
value = str(value)[:37] + "..."
|
|
822
|
+
|
|
823
|
+
table.add_row(key, str(value))
|
|
824
|
+
|
|
825
|
+
return table
|
|
826
|
+
|
|
827
|
+
def _render_metadata(
|
|
828
|
+
self, operation: OperationDetails, show_all: bool = False
|
|
829
|
+
) -> Table:
|
|
830
|
+
"""Render operation metadata with optional full listing."""
|
|
831
|
+
table = Table(show_header=True, box=MINIMAL)
|
|
832
|
+
table.add_column("Key", style="label")
|
|
833
|
+
table.add_column("Value", style="value")
|
|
834
|
+
|
|
835
|
+
item_limit = len(operation.metadata) if show_all else 3
|
|
836
|
+
for key, value in list(operation.metadata.items())[:item_limit]:
|
|
837
|
+
if len(str(value)) > 40:
|
|
838
|
+
value = str(value)[:37] + "..."
|
|
839
|
+
table.add_row(key, str(value))
|
|
840
|
+
|
|
841
|
+
return table
|
|
842
|
+
|
|
843
|
+
def _render_controls_with_state(self, input_state: InputState) -> Panel:
|
|
844
|
+
"""Render control buttons with current state information."""
|
|
845
|
+
if self.options.enable_shortcuts:
|
|
846
|
+
# Show shortcuts with current state
|
|
847
|
+
shortcuts_text = self.renderer.render_shortcuts_help(
|
|
848
|
+
self.shortcuts,
|
|
849
|
+
columns=2 if self.renderer.get_terminal_width() > 80 else 1,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Add state information
|
|
853
|
+
state_info = Text()
|
|
854
|
+
state_info.append(f"Mode: {input_state.mode.value} | ", style="dim")
|
|
855
|
+
state_info.append(f"Section: {input_state.current_section} | ", style="dim")
|
|
856
|
+
state_info.append(f"Scroll: {input_state.scroll_position}", style="dim")
|
|
857
|
+
|
|
858
|
+
content = f"{shortcuts_text}\n{state_info}"
|
|
859
|
+
else:
|
|
860
|
+
# Simple button layout with state
|
|
861
|
+
content = Text(
|
|
862
|
+
f"Use 'y' to approve, 'n' to deny, 'q' to quit | Mode: {input_state.mode.value}"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
return Panel(content, title="⌨️ Controls", border_style="green", box=SIMPLE)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# Utility functions
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def create_approval_dialog(
|
|
872
|
+
renderer: Optional[RichTextRenderer] = None,
|
|
873
|
+
options: Optional[DialogOptions] = None,
|
|
874
|
+
) -> ApprovalDialog:
|
|
875
|
+
"""Create an approval dialog with default configuration."""
|
|
876
|
+
return ApprovalDialog(renderer=renderer, options=options)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
async def show_quick_approval(
|
|
880
|
+
operation_type: str,
|
|
881
|
+
target: str,
|
|
882
|
+
risk_level: Union[RiskLevel, int] = RiskLevel.MEDIUM,
|
|
883
|
+
console: Optional[Console] = None,
|
|
884
|
+
) -> bool:
|
|
885
|
+
"""Quick approval dialog for simple operations."""
|
|
886
|
+
context = ApprovalContext(
|
|
887
|
+
agent_name="Omnimancer",
|
|
888
|
+
operation_details=OperationDetails(
|
|
889
|
+
operation_type=operation_type, target=target, risk_level=risk_level
|
|
890
|
+
),
|
|
891
|
+
timestamp=datetime.now(),
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# Use compact dialog for quick approval
|
|
895
|
+
options = DialogOptions(compact_mode=True, show_file_tree=False, timeout_seconds=30)
|
|
896
|
+
dialog = create_approval_dialog(options=options)
|
|
897
|
+
|
|
898
|
+
if console:
|
|
899
|
+
dialog.console = console
|
|
900
|
+
|
|
901
|
+
decision = await dialog.show_approval_dialog(context)
|
|
902
|
+
return decision.approved
|