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,763 @@
1
+ """
2
+ Rich Text Rendering System for Omnimancer Terminal Interface.
3
+
4
+ This module provides comprehensive rich text rendering capabilities for the
5
+ approval interface and other terminal outputs, with color support, syntax
6
+ highlighting, and responsive layouts.
7
+ """
8
+
9
+ import os
10
+ import shutil
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import Any, Dict, List, Optional, Tuple, Union
14
+
15
+ from rich.box import ROUNDED, Box
16
+ from rich.columns import Columns
17
+ from rich.console import Console
18
+ from rich.layout import Layout
19
+ from rich.panel import Panel
20
+ from rich.progress import BarColumn, Progress, TextColumn, TimeRemainingColumn
21
+ from rich.rule import Rule
22
+ from rich.syntax import Syntax
23
+ from rich.table import Table
24
+ from rich.text import Text
25
+ from rich.theme import Theme
26
+
27
+
28
+ class RiskLevel(Enum):
29
+ """Risk levels for operations with associated colors."""
30
+
31
+ NONE = "none"
32
+ LOW = "low"
33
+ MEDIUM = "medium"
34
+ HIGH = "high"
35
+ CRITICAL = "critical"
36
+
37
+
38
+ class OperationType(Enum):
39
+ """Types of operations for color coding."""
40
+
41
+ FILE_READ = "file_read"
42
+ FILE_WRITE = "file_write"
43
+ FILE_DELETE = "file_delete"
44
+ COMMAND_EXECUTE = "command_execute"
45
+ WEB_REQUEST = "web_request"
46
+ SYSTEM_CHANGE = "system_change"
47
+ DATA_ACCESS = "data_access"
48
+
49
+
50
+ @dataclass
51
+ class ColorScheme:
52
+ """Color scheme configuration for different UI elements."""
53
+
54
+ # Risk level colors
55
+ risk_none: str = "dim white"
56
+ risk_low: str = "green"
57
+ risk_medium: str = "yellow"
58
+ risk_high: str = "bright_red"
59
+ risk_critical: str = "bold red on white"
60
+
61
+ # Operation type colors
62
+ op_read: str = "cyan"
63
+ op_write: str = "yellow"
64
+ op_delete: str = "red"
65
+ op_execute: str = "magenta"
66
+ op_web: str = "blue"
67
+ op_system: str = "bright_red"
68
+
69
+ # Diff colors
70
+ diff_added: str = "green"
71
+ diff_removed: str = "red"
72
+ diff_modified: str = "yellow"
73
+ diff_context: str = "dim white"
74
+ diff_header: str = "bold cyan"
75
+ diff_line_number: str = "dim blue"
76
+
77
+ # UI element colors
78
+ header: str = "bold cyan"
79
+ subheader: str = "cyan"
80
+ label: str = "bright_blue"
81
+ value: str = "white"
82
+ success: str = "bold green"
83
+ error: str = "bold red"
84
+ warning: str = "yellow"
85
+ info: str = "blue"
86
+ dim: str = "dim white"
87
+
88
+ # Interactive element colors
89
+ shortcut_key: str = "bold yellow"
90
+ shortcut_desc: str = "white"
91
+ selected: str = "reverse"
92
+ focused: str = "bold cyan"
93
+
94
+
95
+ @dataclass
96
+ class TerminalCapabilities:
97
+ """Terminal capabilities detection and configuration."""
98
+
99
+ width: int = 80
100
+ height: int = 24
101
+ supports_color: bool = True
102
+ supports_unicode: bool = True
103
+ supports_emoji: bool = True
104
+ color_depth: int = 256 # 8, 16, 256, or 16777216 (true color)
105
+
106
+ @classmethod
107
+ def detect(cls) -> "TerminalCapabilities":
108
+ """Detect current terminal capabilities."""
109
+ # Get terminal dimensions
110
+ size = shutil.get_terminal_size((80, 24))
111
+
112
+ # Detect color support
113
+ supports_color = (
114
+ os.environ.get("COLORTERM") in ("truecolor", "24bit")
115
+ or os.environ.get("TERM", "").endswith("256color")
116
+ or "color" in os.environ.get("TERM", "").lower()
117
+ or os.environ.get("TERM", "").startswith("xterm")
118
+ )
119
+
120
+ # Detect Unicode support
121
+ supports_unicode = True
122
+ try:
123
+ "✓".encode(
124
+ os.environ.get("LANG", "ascii").split(".")[1]
125
+ if "." in os.environ.get("LANG", "")
126
+ else "utf-8"
127
+ )
128
+ except (UnicodeEncodeError, LookupError):
129
+ supports_unicode = False
130
+
131
+ # Detect emoji support (simplified)
132
+ supports_emoji = (
133
+ supports_unicode and os.name != "nt"
134
+ ) # Windows terminal emoji support is limited
135
+
136
+ # Detect color depth
137
+ color_depth = 8
138
+ if os.environ.get("COLORTERM") in ("truecolor", "24bit"):
139
+ color_depth = 16777216
140
+ elif "256color" in os.environ.get("TERM", ""):
141
+ color_depth = 256
142
+ elif supports_color:
143
+ color_depth = 16
144
+
145
+ return cls(
146
+ width=size.columns,
147
+ height=size.lines,
148
+ supports_color=supports_color,
149
+ supports_unicode=supports_unicode,
150
+ supports_emoji=supports_emoji,
151
+ color_depth=color_depth,
152
+ )
153
+
154
+
155
+ class RichTextRenderer:
156
+ """
157
+ Rich text rendering system for terminal output.
158
+
159
+ Provides comprehensive formatting, color coding, and layout capabilities
160
+ for the approval interface and other terminal displays.
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ console: Optional[Console] = None,
166
+ color_scheme: Optional[ColorScheme] = None,
167
+ auto_detect: bool = True,
168
+ ):
169
+ """
170
+ Initialize the rich text renderer.
171
+
172
+ Args:
173
+ console: Rich console instance (creates new if not provided)
174
+ color_scheme: Color scheme to use (uses default if not provided)
175
+ auto_detect: Whether to auto-detect terminal capabilities
176
+ """
177
+ self.console = console or Console()
178
+ self.color_scheme = color_scheme or ColorScheme()
179
+
180
+ # Detect terminal capabilities
181
+ if auto_detect:
182
+ self.capabilities = TerminalCapabilities.detect()
183
+ else:
184
+ self.capabilities = TerminalCapabilities()
185
+
186
+ # Create theme from color scheme
187
+ self.theme = self._create_theme()
188
+
189
+ # Apply theme to console
190
+ self.console = Console(theme=self.theme)
191
+
192
+ # Layout cache for responsive designs
193
+ self._layout_cache: Dict[str, Layout] = {}
194
+
195
+ def _create_theme(self) -> Theme:
196
+ """Create Rich theme from color scheme."""
197
+ theme_dict = {
198
+ # Risk levels
199
+ "risk.none": self.color_scheme.risk_none,
200
+ "risk.low": self.color_scheme.risk_low,
201
+ "risk.medium": self.color_scheme.risk_medium,
202
+ "risk.high": self.color_scheme.risk_high,
203
+ "risk.critical": self.color_scheme.risk_critical,
204
+ # Operations
205
+ "op.read": self.color_scheme.op_read,
206
+ "op.write": self.color_scheme.op_write,
207
+ "op.delete": self.color_scheme.op_delete,
208
+ "op.execute": self.color_scheme.op_execute,
209
+ "op.web": self.color_scheme.op_web,
210
+ "op.system": self.color_scheme.op_system,
211
+ # Diff
212
+ "diff.added": self.color_scheme.diff_added,
213
+ "diff.removed": self.color_scheme.diff_removed,
214
+ "diff.modified": self.color_scheme.diff_modified,
215
+ "diff.context": self.color_scheme.diff_context,
216
+ "diff.header": self.color_scheme.diff_header,
217
+ "diff.line_number": self.color_scheme.diff_line_number,
218
+ # UI elements
219
+ "header": self.color_scheme.header,
220
+ "subheader": self.color_scheme.subheader,
221
+ "label": self.color_scheme.label,
222
+ "value": self.color_scheme.value,
223
+ "success": self.color_scheme.success,
224
+ "error": self.color_scheme.error,
225
+ "warning": self.color_scheme.warning,
226
+ "info": self.color_scheme.info,
227
+ "dim": self.color_scheme.dim,
228
+ # Interactive
229
+ "shortcut.key": self.color_scheme.shortcut_key,
230
+ "shortcut.desc": self.color_scheme.shortcut_desc,
231
+ "selected": self.color_scheme.selected,
232
+ "focused": self.color_scheme.focused,
233
+ }
234
+
235
+ return Theme(theme_dict)
236
+
237
+ def get_risk_color(self, risk_level: Union[RiskLevel, str, int, None]) -> str:
238
+ """
239
+ Get color style for risk level.
240
+
241
+ Args:
242
+ risk_level: Risk level (enum, string, or numeric 0-10)
243
+
244
+ Returns:
245
+ Style string for the risk level
246
+ """
247
+ if risk_level is None:
248
+ return "risk.none"
249
+
250
+ if isinstance(risk_level, int):
251
+ # Convert numeric risk to level
252
+ if risk_level <= 2:
253
+ risk_level = RiskLevel.LOW
254
+ elif risk_level <= 4:
255
+ risk_level = RiskLevel.MEDIUM
256
+ elif risk_level <= 7:
257
+ risk_level = RiskLevel.HIGH
258
+ else:
259
+ risk_level = RiskLevel.CRITICAL
260
+ elif isinstance(risk_level, str):
261
+ try:
262
+ risk_level = RiskLevel(risk_level.lower())
263
+ except ValueError:
264
+ risk_level = RiskLevel.NONE
265
+
266
+ risk_styles = {
267
+ RiskLevel.NONE: "risk.none",
268
+ RiskLevel.LOW: "risk.low",
269
+ RiskLevel.MEDIUM: "risk.medium",
270
+ RiskLevel.HIGH: "risk.high",
271
+ RiskLevel.CRITICAL: "risk.critical",
272
+ }
273
+
274
+ return risk_styles.get(risk_level, "risk.none")
275
+
276
+ def get_operation_color(self, operation_type: Union[OperationType, str]) -> str:
277
+ """
278
+ Get color style for operation type.
279
+
280
+ Args:
281
+ operation_type: Type of operation
282
+
283
+ Returns:
284
+ Style string for the operation type
285
+ """
286
+ if isinstance(operation_type, str):
287
+ # Map string to operation type
288
+ op_mapping = {
289
+ "read": OperationType.FILE_READ,
290
+ "write": OperationType.FILE_WRITE,
291
+ "delete": OperationType.FILE_DELETE,
292
+ "execute": OperationType.COMMAND_EXECUTE,
293
+ "web": OperationType.WEB_REQUEST,
294
+ "system": OperationType.SYSTEM_CHANGE,
295
+ }
296
+
297
+ for key, value in op_mapping.items():
298
+ if key in operation_type.lower():
299
+ operation_type = value
300
+ break
301
+ else:
302
+ operation_type = OperationType.FILE_READ # Default
303
+
304
+ op_styles = {
305
+ OperationType.FILE_READ: "op.read",
306
+ OperationType.FILE_WRITE: "op.write",
307
+ OperationType.FILE_DELETE: "op.delete",
308
+ OperationType.COMMAND_EXECUTE: "op.execute",
309
+ OperationType.WEB_REQUEST: "op.web",
310
+ OperationType.SYSTEM_CHANGE: "op.system",
311
+ OperationType.DATA_ACCESS: "op.read",
312
+ }
313
+
314
+ return op_styles.get(operation_type, "op.read")
315
+
316
+ def render_code_block(
317
+ self,
318
+ code: str,
319
+ language: Optional[str] = None,
320
+ theme: str = "monokai",
321
+ line_numbers: bool = True,
322
+ highlight_lines: Optional[List[int]] = None,
323
+ ) -> Syntax:
324
+ """
325
+ Render a code block with syntax highlighting.
326
+
327
+ Args:
328
+ code: Code content to render
329
+ language: Programming language for syntax highlighting
330
+ theme: Pygments theme to use
331
+ line_numbers: Whether to show line numbers
332
+ highlight_lines: Lines to highlight
333
+
334
+ Returns:
335
+ Syntax object for rendering
336
+ """
337
+ if not language:
338
+ # Try to auto-detect language
339
+ if "import " in code or "def " in code or "class " in code:
340
+ language = "python"
341
+ elif "function" in code or "const " in code or "var " in code:
342
+ language = "javascript"
343
+ elif "{" in code and "}" in code and ":" in code:
344
+ language = "json"
345
+ else:
346
+ language = "text"
347
+
348
+ syntax = Syntax(
349
+ code,
350
+ language,
351
+ theme=theme,
352
+ line_numbers=line_numbers,
353
+ highlight_lines=set(highlight_lines) if highlight_lines else None,
354
+ word_wrap=True,
355
+ )
356
+
357
+ return syntax
358
+
359
+ def render_table(
360
+ self,
361
+ headers: List[str],
362
+ rows: List[List[Any]],
363
+ title: Optional[str] = None,
364
+ box_style: Box = ROUNDED,
365
+ header_style: str = "header",
366
+ show_footer: bool = False,
367
+ ) -> Table:
368
+ """
369
+ Render a formatted table.
370
+
371
+ Args:
372
+ headers: Column headers
373
+ rows: Table data rows
374
+ title: Optional table title
375
+ box_style: Box drawing style
376
+ header_style: Style for headers
377
+ show_footer: Whether to show footer
378
+
379
+ Returns:
380
+ Table object for rendering
381
+ """
382
+ table = Table(
383
+ title=title,
384
+ box=box_style,
385
+ header_style=header_style,
386
+ show_footer=show_footer,
387
+ width=(
388
+ None if self.capabilities.width > 120 else self.capabilities.width - 2
389
+ ),
390
+ )
391
+
392
+ # Add columns
393
+ for header in headers:
394
+ table.add_column(header, justify="left")
395
+
396
+ # Add rows
397
+ for row in rows:
398
+ # Convert all values to strings and apply appropriate styles
399
+ styled_row = []
400
+ for i, value in enumerate(row):
401
+ if isinstance(value, bool):
402
+ styled_value = Text(
403
+ "✓" if value else "✗",
404
+ style="success" if value else "error",
405
+ )
406
+ elif (
407
+ isinstance(value, (int, float)) and i > 0
408
+ ): # Numeric columns (not first)
409
+ styled_value = Text(str(value), style="value")
410
+ else:
411
+ styled_value = str(value)
412
+ styled_row.append(styled_value)
413
+
414
+ table.add_row(*styled_row)
415
+
416
+ return table
417
+
418
+ def render_panel(
419
+ self,
420
+ content: Union[str, Text, Any],
421
+ title: Optional[str] = None,
422
+ subtitle: Optional[str] = None,
423
+ box_style: Box = ROUNDED,
424
+ border_style: Optional[str] = None,
425
+ padding: Tuple[int, int] = (0, 1),
426
+ expand: bool = True,
427
+ ) -> Panel:
428
+ """
429
+ Render a panel with content.
430
+
431
+ Args:
432
+ content: Content to display in panel
433
+ title: Panel title
434
+ subtitle: Panel subtitle
435
+ box_style: Box drawing style
436
+ border_style: Border color/style
437
+ padding: Padding (vertical, horizontal)
438
+ expand: Whether to expand to full width
439
+
440
+ Returns:
441
+ Panel object for rendering
442
+ """
443
+ # Apply responsive width
444
+ width = None
445
+ if not expand and self.capabilities.width < 100:
446
+ width = self.capabilities.width - 4
447
+
448
+ panel = Panel(
449
+ content,
450
+ title=title,
451
+ subtitle=subtitle,
452
+ box=box_style,
453
+ border_style=border_style or "dim",
454
+ padding=padding,
455
+ expand=expand,
456
+ width=width,
457
+ )
458
+
459
+ return panel
460
+
461
+ def render_risk_indicator(
462
+ self,
463
+ risk_level: Union[RiskLevel, int],
464
+ show_label: bool = True,
465
+ show_bar: bool = True,
466
+ ) -> Union[Text, Columns]:
467
+ """
468
+ Render a risk level indicator.
469
+
470
+ Args:
471
+ risk_level: Risk level to display
472
+ show_label: Whether to show text label
473
+ show_bar: Whether to show visual bar
474
+
475
+ Returns:
476
+ Formatted risk indicator
477
+ """
478
+ if isinstance(risk_level, int):
479
+ numeric_risk = risk_level
480
+ # Convert to RiskLevel
481
+ if risk_level <= 2:
482
+ risk_enum = RiskLevel.LOW
483
+ elif risk_level <= 4:
484
+ risk_enum = RiskLevel.MEDIUM
485
+ elif risk_level <= 7:
486
+ risk_enum = RiskLevel.HIGH
487
+ else:
488
+ risk_enum = RiskLevel.CRITICAL
489
+ else:
490
+ risk_enum = risk_level
491
+ # Estimate numeric value
492
+ risk_values = {
493
+ RiskLevel.NONE: 0,
494
+ RiskLevel.LOW: 2,
495
+ RiskLevel.MEDIUM: 5,
496
+ RiskLevel.HIGH: 7,
497
+ RiskLevel.CRITICAL: 9,
498
+ }
499
+ numeric_risk = risk_values.get(risk_enum, 0)
500
+
501
+ elements = []
502
+
503
+ if show_label:
504
+ risk_text = Text()
505
+ risk_text.append("Risk: ", style="label")
506
+ risk_text.append(
507
+ risk_enum.value.upper(), style=self.get_risk_color(risk_enum)
508
+ )
509
+ risk_text.append(f" ({numeric_risk}/10)", style="dim")
510
+ elements.append(risk_text)
511
+
512
+ if show_bar:
513
+ # Create visual risk bar
514
+ bar_width = min(20, self.capabilities.width // 4)
515
+ filled = int((numeric_risk / 10) * bar_width)
516
+ empty = bar_width - filled
517
+
518
+ bar = Text()
519
+ bar.append("[", style="dim")
520
+
521
+ # Gradient coloring for filled portion
522
+ for i in range(filled):
523
+ progress = (i + 1) / bar_width
524
+ if progress < 0.3:
525
+ style = "green"
526
+ elif progress < 0.6:
527
+ style = "yellow"
528
+ else:
529
+ style = "red"
530
+ bar.append("█", style=style)
531
+
532
+ bar.append("░" * empty, style="dim")
533
+ bar.append("]", style="dim")
534
+ elements.append(bar)
535
+
536
+ if len(elements) == 1:
537
+ return elements[0]
538
+ else:
539
+ return Columns(elements, padding=1)
540
+
541
+ def render_progress_bar(
542
+ self,
543
+ task_description: str,
544
+ total: Optional[int] = None,
545
+ completed: Optional[int] = None,
546
+ ) -> Progress:
547
+ """
548
+ Create a progress bar for operations.
549
+
550
+ Args:
551
+ task_description: Description of the task
552
+ total: Total number of steps
553
+ completed: Number of completed steps
554
+
555
+ Returns:
556
+ Progress bar object
557
+ """
558
+ progress = Progress(
559
+ TextColumn("[progress.description]{task.description}"),
560
+ BarColumn(),
561
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
562
+ TimeRemainingColumn(),
563
+ console=self.console,
564
+ transient=True,
565
+ )
566
+
567
+ task = progress.add_task(task_description, total=total)
568
+
569
+ if completed is not None and total is not None:
570
+ progress.update(task, completed=completed)
571
+
572
+ return progress
573
+
574
+ def create_responsive_layout(
575
+ self, sections: Dict[str, Any], vertical_threshold: int = 100
576
+ ) -> Union[Columns, Layout]:
577
+ """
578
+ Create a responsive layout that adapts to terminal width.
579
+
580
+ Args:
581
+ sections: Dictionary of section name to content
582
+ vertical_threshold: Width below which to use vertical layout
583
+
584
+ Returns:
585
+ Layout object appropriate for terminal width
586
+ """
587
+ if self.capabilities.width < vertical_threshold:
588
+ # Vertical layout for narrow terminals
589
+ layout = Layout()
590
+ for i, (name, content) in enumerate(sections.items()):
591
+ if i > 0:
592
+ layout.add_split(Layout(Rule(style="dim")))
593
+ layout.add_split(Layout(content, name=name))
594
+ return layout
595
+ else:
596
+ # Horizontal layout for wide terminals
597
+ columns = []
598
+ for name, content in sections.items():
599
+ columns.append(content)
600
+ return Columns(columns, equal=True, padding=1)
601
+
602
+ def format_shortcut(self, key: str, description: str, enabled: bool = True) -> Text:
603
+ """
604
+ Format a keyboard shortcut with description.
605
+
606
+ Args:
607
+ key: Keyboard shortcut key(s)
608
+ description: Description of the action
609
+ enabled: Whether the shortcut is currently enabled
610
+
611
+ Returns:
612
+ Formatted shortcut text
613
+ """
614
+ text = Text()
615
+
616
+ if enabled:
617
+ text.append(f"[{key}]", style="shortcut.key")
618
+ text.append(f" {description}", style="shortcut.desc")
619
+ else:
620
+ text.append(f"[{key}]", style="dim")
621
+ text.append(f" {description}", style="dim")
622
+
623
+ return text
624
+
625
+ def render_shortcuts_help(
626
+ self, shortcuts: Dict[str, str], columns: int = 2
627
+ ) -> Union[Table, Columns]:
628
+ """
629
+ Render a help panel for keyboard shortcuts.
630
+
631
+ Args:
632
+ shortcuts: Dictionary of key to description
633
+ columns: Number of columns for display
634
+
635
+ Returns:
636
+ Formatted shortcuts help
637
+ """
638
+ if self.capabilities.width < 60:
639
+ # Single column for narrow terminals
640
+ table = Table(show_header=False, box=None, padding=0)
641
+ table.add_column("Shortcut", style="shortcut.key")
642
+ table.add_column("Action", style="shortcut.desc")
643
+
644
+ for key, desc in shortcuts.items():
645
+ table.add_row(f"[{key}]", desc)
646
+
647
+ return table
648
+ else:
649
+ # Multi-column layout
650
+ shortcut_texts = []
651
+ for key, desc in shortcuts.items():
652
+ shortcut_texts.append(self.format_shortcut(key, desc))
653
+
654
+ # Split into columns
655
+ items_per_col = len(shortcut_texts) // columns + (
656
+ 1 if len(shortcut_texts) % columns else 0
657
+ )
658
+ columns_list = []
659
+
660
+ for i in range(columns):
661
+ start = i * items_per_col
662
+ end = min(start + items_per_col, len(shortcut_texts))
663
+ if start < len(shortcut_texts):
664
+ col_items = Text("\n").join(shortcut_texts[start:end])
665
+ columns_list.append(col_items)
666
+
667
+ return Columns(columns_list, equal=True, padding=2)
668
+
669
+ def apply_diff_highlighting(
670
+ self, diff_text: str, file_type: Optional[str] = None
671
+ ) -> Text:
672
+ """
673
+ Apply syntax highlighting to diff text.
674
+
675
+ Args:
676
+ diff_text: Diff content to highlight
677
+ file_type: File type for syntax highlighting
678
+
679
+ Returns:
680
+ Highlighted diff text
681
+ """
682
+ result = Text()
683
+
684
+ for line in diff_text.split("\n"):
685
+ if line.startswith("+++") or line.startswith("---"):
686
+ # File headers
687
+ result.append(line + "\n", style="diff.header")
688
+ elif line.startswith("@@"):
689
+ # Hunk headers
690
+ result.append(line + "\n", style="diff.line_number")
691
+ elif line.startswith("+"):
692
+ # Added lines
693
+ result.append(line + "\n", style="diff.added")
694
+ elif line.startswith("-"):
695
+ # Removed lines
696
+ result.append(line + "\n", style="diff.removed")
697
+ else:
698
+ # Context lines
699
+ result.append(line + "\n", style="diff.context")
700
+
701
+ return result
702
+
703
+ def render_with_fallback(self, rich_content: Any, plain_text: str) -> None:
704
+ """
705
+ Render content with fallback for limited terminals.
706
+
707
+ Args:
708
+ rich_content: Rich formatted content
709
+ plain_text: Plain text fallback
710
+ """
711
+ if self.capabilities.supports_color and self.capabilities.supports_unicode:
712
+ self.console.print(rich_content)
713
+ else:
714
+ # Fallback to plain text
715
+ print(plain_text)
716
+
717
+ def get_terminal_width(self) -> int:
718
+ """Get current terminal width."""
719
+ return self.capabilities.width
720
+
721
+ def get_terminal_height(self) -> int:
722
+ """Get current terminal height."""
723
+ return self.capabilities.height
724
+
725
+ def supports_emoji(self) -> bool:
726
+ """Check if terminal supports emoji."""
727
+ return self.capabilities.supports_emoji
728
+
729
+ def supports_color(self) -> bool:
730
+ """Check if terminal supports color."""
731
+ return self.capabilities.supports_color
732
+
733
+
734
+ # Utility functions for easy use
735
+
736
+
737
+ def create_renderer(auto_detect: bool = True) -> RichTextRenderer:
738
+ """Create a rich text renderer with auto-detection."""
739
+ return RichTextRenderer(auto_detect=auto_detect)
740
+
741
+
742
+ def render_risk_badge(
743
+ risk_level: Union[int, str], renderer: Optional[RichTextRenderer] = None
744
+ ) -> Text:
745
+ """Quick function to render a risk badge."""
746
+ renderer = renderer or create_renderer()
747
+ return renderer.render_risk_indicator(risk_level, show_bar=False)
748
+
749
+
750
+ def render_operation_badge(
751
+ operation_type: str, renderer: Optional[RichTextRenderer] = None
752
+ ) -> Text:
753
+ """Quick function to render an operation type badge."""
754
+ renderer = renderer or create_renderer()
755
+
756
+ text = Text()
757
+ text.append("⚡ ", style=renderer.get_operation_color(operation_type))
758
+ text.append(
759
+ operation_type.replace("_", " ").title(),
760
+ style=renderer.get_operation_color(operation_type),
761
+ )
762
+
763
+ return text