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,614 @@
1
+ """
2
+ Interactive Input Handler for Omnimancer Agent Approval System.
3
+
4
+ This module provides keyboard input handling, navigation controls, and
5
+ interactive features for the approval dialog system.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import sys
11
+ import termios
12
+ import tty
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import Any, Callable, Dict, List, Optional
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class InputMode(Enum):
21
+ """Input modes for the interactive handler."""
22
+
23
+ NORMAL = "normal"
24
+ NAVIGATION = "navigation"
25
+ SEARCH = "search"
26
+ HELP = "help"
27
+
28
+
29
+ class KeyAction(Enum):
30
+ """Available key actions."""
31
+
32
+ APPROVE = "approve"
33
+ DENY = "deny"
34
+ QUIT = "quit"
35
+ HELP = "help"
36
+ NAVIGATE_UP = "navigate_up"
37
+ NAVIGATE_DOWN = "navigate_down"
38
+ NAVIGATE_LEFT = "navigate_left"
39
+ NAVIGATE_RIGHT = "navigate_right"
40
+ PAGE_UP = "page_up"
41
+ PAGE_DOWN = "page_down"
42
+ HOME = "home"
43
+ END = "end"
44
+ TOGGLE_DIFF = "toggle_diff"
45
+ TOGGLE_DETAILS = "toggle_details"
46
+ ZOOM_IN = "zoom_in"
47
+ ZOOM_OUT = "zoom_out"
48
+ SEARCH = "search"
49
+ COPY = "copy"
50
+ EXPORT = "export"
51
+
52
+
53
+ @dataclass
54
+ class KeyBinding:
55
+ """Represents a key binding configuration."""
56
+
57
+ key: str
58
+ action: KeyAction
59
+ description: str
60
+ mode: InputMode = InputMode.NORMAL
61
+ requires_modifier: bool = False
62
+ modifier: Optional[str] = None
63
+
64
+
65
+ @dataclass
66
+ class InputState:
67
+ """Current state of input handling."""
68
+
69
+ mode: InputMode = InputMode.NORMAL
70
+ current_section: str = "diff"
71
+ scroll_position: int = 0
72
+ zoom_level: float = 1.0
73
+ search_query: str = ""
74
+ help_visible: bool = False
75
+ diff_expanded: bool = False
76
+ details_expanded: bool = False
77
+ last_input: Optional[str] = None
78
+ input_history: List[str] = field(default_factory=list)
79
+
80
+
81
+ class InteractiveInputHandler:
82
+ """
83
+ Handles keyboard input and interactive controls for approval dialogs.
84
+
85
+ Provides comprehensive keyboard shortcuts, navigation controls,
86
+ and interactive features for the approval interface.
87
+ """
88
+
89
+ def __init__(self, timeout_seconds: Optional[int] = None):
90
+ """
91
+ Initialize interactive input handler.
92
+
93
+ Args:
94
+ timeout_seconds: Optional timeout for input operations
95
+ """
96
+ self.timeout_seconds = timeout_seconds
97
+ self.state = InputState()
98
+
99
+ # Default key bindings
100
+ self.key_bindings: Dict[str, KeyBinding] = {
101
+ # Approval actions
102
+ "y": KeyBinding("y", KeyAction.APPROVE, "Approve operation"),
103
+ "Y": KeyBinding("Y", KeyAction.APPROVE, "Approve operation"),
104
+ "n": KeyBinding("n", KeyAction.DENY, "Deny operation"),
105
+ "N": KeyBinding("N", KeyAction.DENY, "Deny operation"),
106
+ "q": KeyBinding("q", KeyAction.QUIT, "Quit/Cancel"),
107
+ "Q": KeyBinding("Q", KeyAction.QUIT, "Quit/Cancel"),
108
+ # Navigation
109
+ "k": KeyBinding("k", KeyAction.NAVIGATE_UP, "Navigate up"),
110
+ "j": KeyBinding("j", KeyAction.NAVIGATE_DOWN, "Navigate down"),
111
+ "h": KeyBinding("h", KeyAction.NAVIGATE_LEFT, "Navigate left"),
112
+ "l": KeyBinding("l", KeyAction.NAVIGATE_RIGHT, "Navigate right"),
113
+ "\x1b[A": KeyBinding("\x1b[A", KeyAction.NAVIGATE_UP, "Up arrow"),
114
+ "\x1b[B": KeyBinding("\x1b[B", KeyAction.NAVIGATE_DOWN, "Down arrow"),
115
+ "\x1b[C": KeyBinding("\x1b[C", KeyAction.NAVIGATE_RIGHT, "Right arrow"),
116
+ "\x1b[D": KeyBinding("\x1b[D", KeyAction.NAVIGATE_LEFT, "Left arrow"),
117
+ # Page navigation
118
+ "\x1b[5~": KeyBinding("\x1b[5~", KeyAction.PAGE_UP, "Page up"),
119
+ "\x1b[6~": KeyBinding("\x1b[6~", KeyAction.PAGE_DOWN, "Page down"),
120
+ "\x1b[H": KeyBinding("\x1b[H", KeyAction.HOME, "Home"),
121
+ "\x1b[F": KeyBinding("\x1b[F", KeyAction.END, "End"),
122
+ # Display controls
123
+ "d": KeyBinding("d", KeyAction.TOGGLE_DIFF, "Toggle diff view"),
124
+ "D": KeyBinding("D", KeyAction.TOGGLE_DETAILS, "Toggle details view"),
125
+ "+": KeyBinding("+", KeyAction.ZOOM_IN, "Zoom in"),
126
+ "=": KeyBinding("=", KeyAction.ZOOM_IN, "Zoom in"),
127
+ "-": KeyBinding("-", KeyAction.ZOOM_OUT, "Zoom out"),
128
+ "_": KeyBinding("_", KeyAction.ZOOM_OUT, "Zoom out"),
129
+ # Utility functions
130
+ "?": KeyBinding("?", KeyAction.HELP, "Show help"),
131
+ "/": KeyBinding("/", KeyAction.SEARCH, "Search"),
132
+ "c": KeyBinding("c", KeyAction.COPY, "Copy content"),
133
+ "e": KeyBinding("e", KeyAction.EXPORT, "Export details"),
134
+ }
135
+
136
+ # Action handlers
137
+ self.action_handlers: Dict[KeyAction, Callable] = {
138
+ KeyAction.APPROVE: self._handle_approve,
139
+ KeyAction.DENY: self._handle_deny,
140
+ KeyAction.QUIT: self._handle_quit,
141
+ KeyAction.HELP: self._handle_help,
142
+ KeyAction.NAVIGATE_UP: self._handle_navigate_up,
143
+ KeyAction.NAVIGATE_DOWN: self._handle_navigate_down,
144
+ KeyAction.NAVIGATE_LEFT: self._handle_navigate_left,
145
+ KeyAction.NAVIGATE_RIGHT: self._handle_navigate_right,
146
+ KeyAction.PAGE_UP: self._handle_page_up,
147
+ KeyAction.PAGE_DOWN: self._handle_page_down,
148
+ KeyAction.HOME: self._handle_home,
149
+ KeyAction.END: self._handle_end,
150
+ KeyAction.TOGGLE_DIFF: self._handle_toggle_diff,
151
+ KeyAction.TOGGLE_DETAILS: self._handle_toggle_details,
152
+ KeyAction.ZOOM_IN: self._handle_zoom_in,
153
+ KeyAction.ZOOM_OUT: self._handle_zoom_out,
154
+ KeyAction.SEARCH: self._handle_search,
155
+ KeyAction.COPY: self._handle_copy,
156
+ KeyAction.EXPORT: self._handle_export,
157
+ }
158
+
159
+ # Callbacks for external integration
160
+ self.approval_callback: Optional[Callable] = None
161
+ self.denial_callback: Optional[Callable] = None
162
+ self.quit_callback: Optional[Callable] = None
163
+ self.display_update_callback: Optional[Callable] = None
164
+
165
+ # Terminal state management
166
+ self.original_terminal_settings: Optional[List] = None
167
+ self.terminal_setup: bool = False
168
+
169
+ def set_callbacks(
170
+ self,
171
+ approval_callback: Optional[Callable] = None,
172
+ denial_callback: Optional[Callable] = None,
173
+ quit_callback: Optional[Callable] = None,
174
+ display_update_callback: Optional[Callable] = None,
175
+ ):
176
+ """Set callback functions for various actions."""
177
+ if approval_callback:
178
+ self.approval_callback = approval_callback
179
+ if denial_callback:
180
+ self.denial_callback = denial_callback
181
+ if quit_callback:
182
+ self.quit_callback = quit_callback
183
+ if display_update_callback:
184
+ self.display_update_callback = display_update_callback
185
+
186
+ def add_key_binding(
187
+ self,
188
+ key: str,
189
+ action: KeyAction,
190
+ description: str,
191
+ mode: InputMode = InputMode.NORMAL,
192
+ ):
193
+ """Add or modify a key binding."""
194
+ self.key_bindings[key] = KeyBinding(key, action, description, mode)
195
+
196
+ def remove_key_binding(self, key: str):
197
+ """Remove a key binding."""
198
+ if key in self.key_bindings:
199
+ del self.key_bindings[key]
200
+
201
+ def get_key_bindings_by_mode(self, mode: InputMode) -> Dict[str, KeyBinding]:
202
+ """Get key bindings for a specific mode."""
203
+ return {k: v for k, v in self.key_bindings.items() if v.mode == mode}
204
+
205
+ async def setup_terminal(self):
206
+ """Setup terminal for raw input mode."""
207
+ if self.terminal_setup:
208
+ return
209
+
210
+ try:
211
+ # Save original terminal settings
212
+ self.original_terminal_settings = termios.tcgetattr(sys.stdin)
213
+
214
+ # Set terminal to raw mode
215
+ tty.setraw(sys.stdin.fileno())
216
+ self.terminal_setup = True
217
+
218
+ except (termios.error, OSError) as e:
219
+ logger.warning(f"Could not setup terminal for raw input: {e}")
220
+ # Fall back to line-based input if raw mode fails
221
+
222
+ async def restore_terminal(self):
223
+ """Restore original terminal settings."""
224
+ if not self.terminal_setup or not self.original_terminal_settings:
225
+ return
226
+
227
+ try:
228
+ termios.tcsetattr(
229
+ sys.stdin, termios.TCSADRAIN, self.original_terminal_settings
230
+ )
231
+ self.terminal_setup = False
232
+ self.original_terminal_settings = None
233
+ except (termios.error, OSError) as e:
234
+ logger.warning(f"Could not restore terminal settings: {e}")
235
+
236
+ async def read_key(self) -> Optional[str]:
237
+ """
238
+ Read a single key from input.
239
+
240
+ Returns:
241
+ The key pressed, or None if timeout/error
242
+ """
243
+ try:
244
+ if not self.terminal_setup:
245
+ # Fallback to line input
246
+ if self.timeout_seconds:
247
+ try:
248
+ line = await asyncio.wait_for(
249
+ asyncio.get_event_loop().run_in_executor(None, input),
250
+ timeout=self.timeout_seconds,
251
+ )
252
+ return line.strip().lower()
253
+ except asyncio.TimeoutError:
254
+ return None
255
+ else:
256
+ return input().strip().lower()
257
+
258
+ # Raw character input
259
+ def _read_char():
260
+ char = sys.stdin.read(1)
261
+ # Handle escape sequences
262
+ if char == "\x1b":
263
+ char += sys.stdin.read(2) # Read next 2 chars for arrow keys
264
+ if char.endswith("["):
265
+ char += sys.stdin.read(1) # Read the final character
266
+ return char
267
+
268
+ if self.timeout_seconds:
269
+ try:
270
+ key = await asyncio.wait_for(
271
+ asyncio.get_event_loop().run_in_executor(None, _read_char),
272
+ timeout=self.timeout_seconds,
273
+ )
274
+ return key
275
+ except asyncio.TimeoutError:
276
+ return None
277
+ else:
278
+ return await asyncio.get_event_loop().run_in_executor(None, _read_char)
279
+
280
+ except (KeyboardInterrupt, EOFError):
281
+ return "q" # Treat as quit
282
+ except Exception as e:
283
+ logger.error(f"Error reading key input: {e}")
284
+ return None
285
+
286
+ async def handle_input_loop(self) -> Optional[KeyAction]:
287
+ """
288
+ Main input handling loop.
289
+
290
+ Returns:
291
+ The final action that should terminate the loop
292
+ """
293
+ await self.setup_terminal()
294
+
295
+ try:
296
+ while True:
297
+ key = await self.read_key()
298
+
299
+ if key is None:
300
+ # Timeout or error - return quit
301
+ return KeyAction.QUIT
302
+
303
+ # Handle the key
304
+ action = await self.handle_key(key)
305
+
306
+ # Check if this action should terminate the loop
307
+ if action in [
308
+ KeyAction.APPROVE,
309
+ KeyAction.DENY,
310
+ KeyAction.QUIT,
311
+ ]:
312
+ return action
313
+
314
+ # Update display if callback is available
315
+ if self.display_update_callback:
316
+ await self.display_update_callback(self.state)
317
+
318
+ finally:
319
+ await self.restore_terminal()
320
+
321
+ async def handle_key(self, key: str) -> Optional[KeyAction]:
322
+ """
323
+ Handle a single key press.
324
+
325
+ Args:
326
+ key: The key that was pressed
327
+
328
+ Returns:
329
+ The action that was triggered, if any
330
+ """
331
+ # Record input history
332
+ self.state.last_input = key
333
+ self.state.input_history.append(key)
334
+
335
+ # Keep history limited
336
+ if len(self.state.input_history) > 100:
337
+ self.state.input_history = self.state.input_history[-100:]
338
+
339
+ # Look up key binding
340
+ binding = self.key_bindings.get(key)
341
+
342
+ if not binding:
343
+ # Unknown key - ignore or show help
344
+ if key not in ["\r", "\n", " "]: # Ignore common non-action keys
345
+ logger.debug(f"Unknown key pressed: {repr(key)}")
346
+ return None
347
+
348
+ # Check if binding is valid for current mode
349
+ if binding.mode != InputMode.NORMAL and binding.mode != self.state.mode:
350
+ return None
351
+
352
+ # Get the action handler
353
+ handler = self.action_handlers.get(binding.action)
354
+
355
+ if not handler:
356
+ logger.warning(f"No handler for action: {binding.action}")
357
+ return None
358
+
359
+ # Execute the handler
360
+ try:
361
+ await handler()
362
+ return binding.action
363
+ except Exception as e:
364
+ logger.error(f"Error executing action {binding.action}: {e}")
365
+ return None
366
+
367
+ # Action handlers
368
+ async def _handle_approve(self) -> bool:
369
+ """Handle approval action."""
370
+ if self.approval_callback:
371
+ return await self.approval_callback()
372
+ return True
373
+
374
+ async def _handle_deny(self) -> bool:
375
+ """Handle denial action."""
376
+ if self.denial_callback:
377
+ return await self.denial_callback()
378
+ return True
379
+
380
+ async def _handle_quit(self) -> bool:
381
+ """Handle quit action."""
382
+ if self.quit_callback:
383
+ return await self.quit_callback()
384
+ return True
385
+
386
+ async def _handle_help(self) -> bool:
387
+ """Handle help display toggle."""
388
+ self.state.help_visible = not self.state.help_visible
389
+ self.state.mode = (
390
+ InputMode.HELP if self.state.help_visible else InputMode.NORMAL
391
+ )
392
+ return True
393
+
394
+ async def _handle_navigate_up(self) -> bool:
395
+ """Handle upward navigation."""
396
+ if self.state.scroll_position > 0:
397
+ self.state.scroll_position -= 1
398
+ return True
399
+
400
+ async def _handle_navigate_down(self) -> bool:
401
+ """Handle downward navigation."""
402
+ self.state.scroll_position += 1
403
+ return True
404
+
405
+ async def _handle_navigate_left(self) -> bool:
406
+ """Handle leftward navigation."""
407
+ sections = ["diff", "details", "operation", "risk"]
408
+ try:
409
+ current_idx = sections.index(self.state.current_section)
410
+ if current_idx > 0:
411
+ self.state.current_section = sections[current_idx - 1]
412
+ except ValueError:
413
+ self.state.current_section = sections[0]
414
+ return True
415
+
416
+ async def _handle_navigate_right(self) -> bool:
417
+ """Handle rightward navigation."""
418
+ sections = ["diff", "details", "operation", "risk"]
419
+ try:
420
+ current_idx = sections.index(self.state.current_section)
421
+ if current_idx < len(sections) - 1:
422
+ self.state.current_section = sections[current_idx + 1]
423
+ except ValueError:
424
+ self.state.current_section = sections[0]
425
+ return True
426
+
427
+ async def _handle_page_up(self) -> bool:
428
+ """Handle page up navigation."""
429
+ self.state.scroll_position = max(0, self.state.scroll_position - 10)
430
+ return True
431
+
432
+ async def _handle_page_down(self) -> bool:
433
+ """Handle page down navigation."""
434
+ self.state.scroll_position += 10
435
+ return True
436
+
437
+ async def _handle_home(self) -> bool:
438
+ """Handle home navigation."""
439
+ self.state.scroll_position = 0
440
+ return True
441
+
442
+ async def _handle_end(self) -> bool:
443
+ """Handle end navigation."""
444
+ # Would need access to content length to implement properly
445
+ self.state.scroll_position = 1000 # Large number as approximation
446
+ return True
447
+
448
+ async def _handle_toggle_diff(self) -> bool:
449
+ """Handle diff view toggle."""
450
+ self.state.diff_expanded = not self.state.diff_expanded
451
+ return True
452
+
453
+ async def _handle_toggle_details(self) -> bool:
454
+ """Handle details view toggle."""
455
+ self.state.details_expanded = not self.state.details_expanded
456
+ return True
457
+
458
+ async def _handle_zoom_in(self) -> bool:
459
+ """Handle zoom in."""
460
+ self.state.zoom_level = min(2.0, self.state.zoom_level + 0.1)
461
+ return True
462
+
463
+ async def _handle_zoom_out(self) -> bool:
464
+ """Handle zoom out."""
465
+ self.state.zoom_level = max(0.5, self.state.zoom_level - 0.1)
466
+ return True
467
+
468
+ async def _handle_search(self) -> bool:
469
+ """Handle search mode activation."""
470
+ self.state.mode = InputMode.SEARCH
471
+ # Would implement search input handling here
472
+ return True
473
+
474
+ async def _handle_copy(self) -> bool:
475
+ """Handle copy to clipboard."""
476
+ # Would implement clipboard functionality here
477
+ logger.info("Copy functionality not yet implemented")
478
+ return True
479
+
480
+ async def _handle_export(self) -> bool:
481
+ """Handle export functionality."""
482
+ # Would implement export functionality here
483
+ logger.info("Export functionality not yet implemented")
484
+ return True
485
+
486
+ def get_help_text(self) -> str:
487
+ """Generate help text for current mode."""
488
+ mode_bindings = self.get_key_bindings_by_mode(self.state.mode)
489
+
490
+ help_lines = [
491
+ f"=== Help - {self.state.mode.value.title()} Mode ===",
492
+ "",
493
+ ]
494
+
495
+ # Group bindings by category
496
+ categories = {
497
+ "Approval Actions": [
498
+ KeyAction.APPROVE,
499
+ KeyAction.DENY,
500
+ KeyAction.QUIT,
501
+ ],
502
+ "Navigation": [
503
+ KeyAction.NAVIGATE_UP,
504
+ KeyAction.NAVIGATE_DOWN,
505
+ KeyAction.NAVIGATE_LEFT,
506
+ KeyAction.NAVIGATE_RIGHT,
507
+ KeyAction.PAGE_UP,
508
+ KeyAction.PAGE_DOWN,
509
+ KeyAction.HOME,
510
+ KeyAction.END,
511
+ ],
512
+ "Display Control": [
513
+ KeyAction.TOGGLE_DIFF,
514
+ KeyAction.TOGGLE_DETAILS,
515
+ KeyAction.ZOOM_IN,
516
+ KeyAction.ZOOM_OUT,
517
+ ],
518
+ "Utility": [
519
+ KeyAction.HELP,
520
+ KeyAction.SEARCH,
521
+ KeyAction.COPY,
522
+ KeyAction.EXPORT,
523
+ ],
524
+ }
525
+
526
+ for category, actions in categories.items():
527
+ help_lines.append(f"{category}:")
528
+ for key, binding in mode_bindings.items():
529
+ if binding.action in actions:
530
+ help_lines.append(f" {key:<8} - {binding.description}")
531
+ help_lines.append("")
532
+
533
+ help_lines.extend(
534
+ ["Press '?' again to close help", "Press 'q' to quit the dialog"]
535
+ )
536
+
537
+ return "\n".join(help_lines)
538
+
539
+ def get_status_info(self) -> Dict[str, Any]:
540
+ """Get current input handler status information."""
541
+ return {
542
+ "mode": self.state.mode.value,
543
+ "current_section": self.state.current_section,
544
+ "scroll_position": self.state.scroll_position,
545
+ "zoom_level": self.state.zoom_level,
546
+ "help_visible": self.state.help_visible,
547
+ "diff_expanded": self.state.diff_expanded,
548
+ "details_expanded": self.state.details_expanded,
549
+ "last_input": self.state.last_input,
550
+ "input_count": len(self.state.input_history),
551
+ }
552
+
553
+
554
+ # Utility functions
555
+
556
+
557
+ def create_input_handler(
558
+ timeout_seconds: Optional[int] = None,
559
+ ) -> InteractiveInputHandler:
560
+ """Create an input handler with default configuration."""
561
+ return InteractiveInputHandler(timeout_seconds=timeout_seconds)
562
+
563
+
564
+ def create_custom_input_handler(
565
+ key_bindings: Optional[Dict[str, KeyBinding]] = None,
566
+ timeout_seconds: Optional[int] = None,
567
+ ) -> InteractiveInputHandler:
568
+ """Create an input handler with custom key bindings."""
569
+ handler = InteractiveInputHandler(timeout_seconds=timeout_seconds)
570
+
571
+ if key_bindings:
572
+ # Clear default bindings and add custom ones
573
+ handler.key_bindings.clear()
574
+ handler.key_bindings.update(key_bindings)
575
+
576
+ return handler
577
+
578
+
579
+ # Predefined key binding sets
580
+
581
+ MINIMAL_KEY_BINDINGS = {
582
+ "y": KeyBinding("y", KeyAction.APPROVE, "Approve"),
583
+ "n": KeyBinding("n", KeyAction.DENY, "Deny"),
584
+ "q": KeyBinding("q", KeyAction.QUIT, "Quit"),
585
+ "?": KeyBinding("?", KeyAction.HELP, "Help"),
586
+ }
587
+
588
+ VIM_STYLE_KEY_BINDINGS = {
589
+ "y": KeyBinding("y", KeyAction.APPROVE, "Approve (yes)"),
590
+ "n": KeyBinding("n", KeyAction.DENY, "Deny (no)"),
591
+ "q": KeyBinding("q", KeyAction.QUIT, "Quit"),
592
+ "j": KeyBinding("j", KeyAction.NAVIGATE_DOWN, "Down"),
593
+ "k": KeyBinding("k", KeyAction.NAVIGATE_UP, "Up"),
594
+ "h": KeyBinding("h", KeyAction.NAVIGATE_LEFT, "Left"),
595
+ "l": KeyBinding("l", KeyAction.NAVIGATE_RIGHT, "Right"),
596
+ "g": KeyBinding("g", KeyAction.HOME, "Go to top"),
597
+ "G": KeyBinding("G", KeyAction.END, "Go to bottom"),
598
+ "d": KeyBinding("d", KeyAction.TOGGLE_DIFF, "Toggle diff"),
599
+ "/": KeyBinding("/", KeyAction.SEARCH, "Search"),
600
+ "?": KeyBinding("?", KeyAction.HELP, "Help"),
601
+ }
602
+
603
+ ARROW_KEY_BINDINGS = {
604
+ "y": KeyBinding("y", KeyAction.APPROVE, "Approve"),
605
+ "n": KeyBinding("n", KeyAction.DENY, "Deny"),
606
+ "q": KeyBinding("q", KeyAction.QUIT, "Quit"),
607
+ "\x1b[A": KeyBinding("\x1b[A", KeyAction.NAVIGATE_UP, "Up arrow"),
608
+ "\x1b[B": KeyBinding("\x1b[B", KeyAction.NAVIGATE_DOWN, "Down arrow"),
609
+ "\x1b[C": KeyBinding("\x1b[C", KeyAction.NAVIGATE_RIGHT, "Right arrow"),
610
+ "\x1b[D": KeyBinding("\x1b[D", KeyAction.NAVIGATE_LEFT, "Left arrow"),
611
+ "\x1b[5~": KeyBinding("\x1b[5~", KeyAction.PAGE_UP, "Page Up"),
612
+ "\x1b[6~": KeyBinding("\x1b[6~", KeyAction.PAGE_DOWN, "Page Down"),
613
+ "?": KeyBinding("?", KeyAction.HELP, "Help"),
614
+ }