kollabor 0.4.9__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 (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
@@ -0,0 +1,946 @@
1
+ """Raw input processing system for Kollabor CLI - extracted from InputHandler."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+ import time
7
+ from typing import Optional, Dict, Any, Callable
8
+
9
+ # Platform-specific imports for input handling
10
+ IS_WINDOWS = sys.platform == "win32"
11
+
12
+ if IS_WINDOWS:
13
+ import msvcrt
14
+ else:
15
+ import select
16
+
17
+ from ..events import EventType
18
+ from ..events.models import CommandMode
19
+ from .key_parser import KeyParser, KeyPress, KeyType as KeyTypeEnum
20
+ from .buffer_manager import BufferManager
21
+ from .input_errors import InputErrorHandler, ErrorType, ErrorSeverity
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class RawInputProcessor:
27
+ """Handles raw terminal input processing, key parsing, and buffer management.
28
+
29
+ This component is responsible for:
30
+ - Main input loop with select() polling
31
+ - Raw data reading and chunking
32
+ - Escape sequence detection
33
+ - Character parsing and key press handling
34
+ - Display updates and cursor management
35
+ - Paste detection integration
36
+ - Basic event processing (Enter, Escape)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ event_bus,
42
+ renderer,
43
+ config,
44
+ buffer_manager: BufferManager,
45
+ key_parser: KeyParser,
46
+ error_handler: InputErrorHandler,
47
+ ) -> None:
48
+ """Initialize the raw input processor.
49
+
50
+ Args:
51
+ event_bus: Event bus for emitting input events.
52
+ renderer: Terminal renderer for updating input display.
53
+ config: Configuration manager for input settings.
54
+ buffer_manager: Buffer manager for text operations.
55
+ key_parser: Key parser for handling escape sequences.
56
+ error_handler: Error handler for input errors.
57
+ """
58
+ self.event_bus = event_bus
59
+ self.renderer = renderer
60
+ self.config = config
61
+ self.buffer_manager = buffer_manager
62
+ self.key_parser = key_parser
63
+ self.error_handler = error_handler
64
+
65
+ # Control flags
66
+ self.running = False
67
+ self.rendering_paused = (
68
+ False # Flag to pause rendering during special effects
69
+ )
70
+
71
+ # Load configurable parameters
72
+ self.polling_delay = config.get("input.polling_delay", 0.01)
73
+ self.error_delay = config.get("input.error_delay", 0.1)
74
+
75
+ # Paste detection configuration
76
+ self.paste_detection_enabled = False
77
+
78
+ # State tracking
79
+ self._last_cursor_pos = 0
80
+
81
+ # Simple paste detection state
82
+ self._paste_buffer = []
83
+ self._last_char_time = 0
84
+ # GENIUS PASTE SYSTEM - immediate synchronous storage
85
+ self._paste_bucket = {} # {paste_id: actual_content}
86
+ self._paste_counter = 0 # Counter for paste numbering
87
+ self._current_paste_id = None # Currently building paste ID
88
+ self._last_paste_time = 0 # Last chunk timestamp
89
+
90
+ # Callbacks for delegation back to InputHandler
91
+ self.on_command_mode_keypress: Optional[Callable] = None
92
+ self.on_prevent_default_check: Optional[Callable] = None
93
+ self.get_command_mode: Optional[Callable] = None
94
+ self.on_status_view_previous: Optional[Callable] = None
95
+ self.on_status_view_next: Optional[Callable] = None
96
+
97
+ logger.info("Raw input processor initialized")
98
+
99
+ def set_callbacks(
100
+ self,
101
+ on_command_mode_keypress: Callable,
102
+ on_prevent_default_check: Callable,
103
+ get_command_mode: Callable,
104
+ on_status_view_previous: Callable,
105
+ on_status_view_next: Callable,
106
+ ) -> None:
107
+ """Set callbacks for delegation back to InputHandler."""
108
+ self.on_command_mode_keypress = on_command_mode_keypress
109
+ self.on_prevent_default_check = on_prevent_default_check
110
+ self.get_command_mode = get_command_mode
111
+ self.on_status_view_previous = on_status_view_previous
112
+ self.on_status_view_next = on_status_view_next
113
+
114
+ async def start_input_loop(self) -> None:
115
+ """Start the input processing loop."""
116
+ self.running = True
117
+ await self._input_loop()
118
+
119
+ def stop_input_loop(self) -> None:
120
+ """Stop the input processing loop."""
121
+ self.running = False
122
+
123
+ def pause_rendering(self):
124
+ """Pause all UI rendering for special effects."""
125
+ self.rendering_paused = True
126
+ logger.debug("Input rendering paused")
127
+
128
+ def resume_rendering(self):
129
+ """Resume normal UI rendering."""
130
+ self.rendering_paused = False
131
+ logger.debug("Input rendering resumed")
132
+
133
+ async def _input_loop(self) -> None:
134
+ """Main input processing loop with enhanced error handling."""
135
+ while self.running:
136
+ try:
137
+ # Platform-specific input checking
138
+ has_input = await self._check_input_available()
139
+
140
+ if has_input:
141
+ # Read input data
142
+ chunk = await self._read_input_chunk()
143
+
144
+ if not chunk:
145
+ await asyncio.sleep(self.polling_delay)
146
+ continue
147
+
148
+ # Check if this is an escape sequence (arrow keys, etc.)
149
+ def is_escape_sequence(text: str) -> bool:
150
+ """Check if input is an escape sequence that should bypass paste detection."""
151
+ if not text:
152
+ return False
153
+ # Common escape sequences start with ESC (\x1b)
154
+ if text.startswith("\x1b"):
155
+ return True
156
+ return False
157
+
158
+ # If we got multiple characters, check if it's an escape sequence first
159
+ if len(chunk) > 10 and not is_escape_sequence(chunk):
160
+
161
+ import time
162
+
163
+ current_time = time.time()
164
+
165
+ # Check if this continues the current paste (within 100ms)
166
+ if (
167
+ self._current_paste_id
168
+ and self._last_paste_time > 0
169
+ and (current_time - self._last_paste_time) < 0.1
170
+ ):
171
+
172
+ # Merge with existing paste
173
+ self._paste_bucket[self._current_paste_id] += chunk
174
+ self._last_paste_time = current_time
175
+
176
+ # Update the placeholder to show new size
177
+ await self._update_paste_placeholder()
178
+ else:
179
+ # New paste - store immediately
180
+ self._paste_counter += 1
181
+ self._current_paste_id = f"PASTE_{self._paste_counter}"
182
+ self._paste_bucket[self._current_paste_id] = chunk
183
+ self._last_paste_time = current_time
184
+
185
+ # Create placeholder immediately
186
+ await self._create_paste_placeholder(
187
+ self._current_paste_id
188
+ )
189
+ elif is_escape_sequence(chunk):
190
+ # Escape sequence - process character by character to allow key parser to handle it
191
+ logger.debug(
192
+ f"Processing escape sequence character-by-character: {repr(chunk)}"
193
+ )
194
+ for char in chunk:
195
+ await self._process_character(char)
196
+ else:
197
+ # Normal input (single or multi-character) - process each character individually
198
+ logger.info(
199
+ f"🔤 Processing normal input character-by-character: {repr(chunk)}"
200
+ )
201
+ # await self._process_character(chunk)
202
+ for char in chunk:
203
+ await self._process_character(char)
204
+ else:
205
+ # No input available - check for standalone ESC key
206
+ esc_key = self.key_parser.check_for_standalone_escape()
207
+ if esc_key and self.on_command_mode_keypress:
208
+ logger.info("DETECTED STANDALONE ESC KEY!")
209
+ # CRITICAL FIX: Route escape to command mode handler if in modal mode
210
+ command_mode = (
211
+ self.get_command_mode()
212
+ if self.get_command_mode
213
+ else CommandMode.NORMAL
214
+ )
215
+ if command_mode == CommandMode.MODAL:
216
+ await self.on_command_mode_keypress(esc_key)
217
+ else:
218
+ await self._handle_key_press(esc_key)
219
+
220
+ await asyncio.sleep(self.polling_delay)
221
+
222
+ except KeyboardInterrupt:
223
+ logger.info("Ctrl+C received")
224
+ raise
225
+ except OSError as e:
226
+ await self.error_handler.handle_error(
227
+ ErrorType.IO_ERROR,
228
+ f"I/O error in input loop: {e}",
229
+ ErrorSeverity.HIGH,
230
+ {"buffer_manager": self.buffer_manager},
231
+ )
232
+ await asyncio.sleep(self.error_delay)
233
+ except Exception as e:
234
+ await self.error_handler.handle_error(
235
+ ErrorType.SYSTEM_ERROR,
236
+ f"Unexpected error in input loop: {e}",
237
+ ErrorSeverity.MEDIUM,
238
+ {"buffer_manager": self.buffer_manager},
239
+ )
240
+ await asyncio.sleep(self.error_delay)
241
+
242
+ async def _check_input_available(self) -> bool:
243
+ """Check if input is available (cross-platform).
244
+
245
+ Returns:
246
+ True if input is available, False otherwise.
247
+ """
248
+ if IS_WINDOWS:
249
+ # Windows: Use msvcrt.kbhit() to check for available input
250
+ return msvcrt.kbhit()
251
+ else:
252
+ # Unix: Use select with timeout
253
+ return bool(select.select([sys.stdin], [], [], self.polling_delay)[0])
254
+
255
+ async def _read_input_chunk(self) -> str:
256
+ """Read available input data (cross-platform).
257
+
258
+ Returns:
259
+ Decoded input string, or empty string if no input.
260
+ """
261
+ import os
262
+
263
+ if IS_WINDOWS:
264
+ # Windows: Read all available characters using msvcrt
265
+ chunk = b""
266
+ while msvcrt.kbhit():
267
+ char = msvcrt.getch()
268
+ char_code = char[0] if isinstance(char, bytes) else ord(char)
269
+
270
+ # Handle Windows extended keys (arrow keys, function keys, etc.)
271
+ # Extended keys are prefixed with 0x00 or 0xE0 (224)
272
+ if char_code in (0, 224):
273
+ # Read the actual key code
274
+ ext_char = msvcrt.getch()
275
+ ext_code = ext_char[0] if isinstance(ext_char, bytes) else ord(ext_char)
276
+ # Map Windows extended key codes to ANSI escape sequences
277
+ win_key_map = {
278
+ 72: b"\x1b[A", # ArrowUp
279
+ 80: b"\x1b[B", # ArrowDown
280
+ 75: b"\x1b[D", # ArrowLeft
281
+ 77: b"\x1b[C", # ArrowRight
282
+ 71: b"\x1b[H", # Home
283
+ 79: b"\x1b[F", # End
284
+ 73: b"\x1b[5~", # PageUp
285
+ 81: b"\x1b[6~", # PageDown
286
+ 82: b"\x1b[2~", # Insert
287
+ 83: b"\x1b[3~", # Delete
288
+ 59: b"\x1bOP", # F1
289
+ 60: b"\x1bOQ", # F2
290
+ 61: b"\x1bOR", # F3
291
+ 62: b"\x1bOS", # F4
292
+ 63: b"\x1b[15~", # F5
293
+ 64: b"\x1b[17~", # F6
294
+ 65: b"\x1b[18~", # F7
295
+ 66: b"\x1b[19~", # F8
296
+ 67: b"\x1b[20~", # F9
297
+ 68: b"\x1b[21~", # F10
298
+ 133: b"\x1b[23~", # F11
299
+ 134: b"\x1b[24~", # F12
300
+ }
301
+ if ext_code in win_key_map:
302
+ chunk += win_key_map[ext_code]
303
+ else:
304
+ logger.debug(f"Unknown Windows extended key: {ext_code}")
305
+ else:
306
+ chunk += char
307
+
308
+ # Small delay to allow for more input
309
+ await asyncio.sleep(0.001)
310
+ # Check if there's more data immediately available
311
+ if not msvcrt.kbhit():
312
+ break
313
+ return chunk.decode("utf-8", errors="ignore") if chunk else ""
314
+ else:
315
+ # Unix: Read ALL available data using os.read
316
+ chunk = b""
317
+ while True:
318
+ try:
319
+ # Read in 8KB chunks
320
+ more_data = os.read(0, 8192)
321
+ if not more_data:
322
+ break
323
+ chunk += more_data
324
+ # Check if more data is available (longer timeout for escape sequences)
325
+ if not select.select([sys.stdin], [], [], 0.02)[0]:
326
+ break # No more data waiting
327
+ except OSError:
328
+ break # No more data available
329
+ return chunk.decode("utf-8", errors="ignore") if chunk else ""
330
+
331
+ async def _process_character(self, char: str) -> None:
332
+ """Process a single character input.
333
+
334
+ Args:
335
+ char: Character received from terminal.
336
+ """
337
+ try:
338
+ current_time = time.time()
339
+
340
+ # NOTE: Slash command detection moved to key press level for proper buffer handling
341
+
342
+ # Simple paste detection - skip normal processing if character is part of paste
343
+ if self.paste_detection_enabled:
344
+ paste_handled = await self._simple_paste_detection(
345
+ char, current_time
346
+ )
347
+ if paste_handled:
348
+ return # Character consumed by paste detection, skip normal processing
349
+
350
+ # Parse character into structured key press (this handles escape sequences)
351
+ key_press = self.key_parser.parse_char(char)
352
+ if not key_press:
353
+ # For modal mode, add timeout-based standalone escape detection
354
+ command_mode = (
355
+ self.get_command_mode()
356
+ if self.get_command_mode
357
+ else CommandMode.NORMAL
358
+ )
359
+ if (
360
+ command_mode == CommandMode.MODAL
361
+ and self.on_command_mode_keypress
362
+ ):
363
+ # Schedule a delayed check for standalone escape (100ms delay)
364
+ async def delayed_escape_check():
365
+ await asyncio.sleep(0.1)
366
+ standalone_escape = (
367
+ self.key_parser.check_for_standalone_escape()
368
+ )
369
+ if standalone_escape:
370
+ await self.on_command_mode_keypress(standalone_escape)
371
+
372
+ asyncio.create_task(delayed_escape_check())
373
+ return # Incomplete escape sequence - wait for more characters
374
+
375
+ # Check for slash command mode handling AFTER parsing (so arrow keys work)
376
+ command_mode = (
377
+ self.get_command_mode()
378
+ if self.get_command_mode
379
+ else CommandMode.NORMAL
380
+ )
381
+ if command_mode != CommandMode.NORMAL and self.on_command_mode_keypress:
382
+ logger.info(
383
+ f"🎯 Processing key '{key_press.name}' in command mode: {command_mode}"
384
+ )
385
+ handled = await self.on_command_mode_keypress(key_press)
386
+ if handled:
387
+ # CRITICAL FIX: Update display after command mode processing
388
+ await self._update_display()
389
+ return
390
+
391
+ # Emit key press event for plugins
392
+ key_result = await self.event_bus.emit_with_hooks(
393
+ EventType.KEY_PRESS,
394
+ {
395
+ "key": key_press.name,
396
+ "char_code": key_press.code,
397
+ "key_type": key_press.type.value,
398
+ "modifiers": key_press.modifiers,
399
+ },
400
+ "input",
401
+ )
402
+
403
+ # Check if any plugin handled this key
404
+ prevent_default = self._check_prevent_default(key_result)
405
+
406
+ # Process key if not prevented by plugins
407
+ if not prevent_default:
408
+ await self._handle_key_press(key_press)
409
+
410
+ # Update renderer
411
+ await self._update_display()
412
+
413
+ except Exception as e:
414
+ await self.error_handler.handle_error(
415
+ ErrorType.PARSING_ERROR,
416
+ f"Error processing character: {e}",
417
+ ErrorSeverity.MEDIUM,
418
+ {"char": repr(char), "buffer_manager": self.buffer_manager},
419
+ )
420
+
421
+ def _check_prevent_default(self, key_result: Dict[str, Any]) -> bool:
422
+ """Check if plugins want to prevent default key handling.
423
+
424
+ Args:
425
+ key_result: Result from key press event.
426
+
427
+ Returns:
428
+ True if default handling should be prevented.
429
+ """
430
+ if self.on_prevent_default_check:
431
+ return self.on_prevent_default_check(key_result)
432
+
433
+ # Fallback implementation
434
+ if "main" in key_result:
435
+ for hook_result in key_result["main"].values():
436
+ if isinstance(hook_result, dict) and hook_result.get(
437
+ "prevent_default"
438
+ ):
439
+ return True
440
+ return False
441
+
442
+ async def _handle_key_press(self, key_press: KeyPress) -> None:
443
+ """Handle a parsed key press.
444
+
445
+ Args:
446
+ key_press: Parsed key press event.
447
+ """
448
+ # Process key press
449
+ try:
450
+ # Log all key presses for debugging
451
+ logger.info(
452
+ f"🔍 Key press: name='{key_press.name}', "
453
+ f"char='{key_press.char}', code={key_press.code}, "
454
+ f"type={key_press.type}, "
455
+ f"modifiers={getattr(key_press, 'modifiers', None)}"
456
+ )
457
+
458
+ # CRITICAL FIX: Command mode input routing - handle ALL command modes
459
+ command_mode = (
460
+ self.get_command_mode()
461
+ if self.get_command_mode
462
+ else CommandMode.NORMAL
463
+ )
464
+ if command_mode != CommandMode.NORMAL and self.on_command_mode_keypress:
465
+ logger.info(
466
+ f"🎯 Command mode active ({command_mode}) - routing input to command handler: {key_press.name}"
467
+ )
468
+ handled = await self.on_command_mode_keypress(key_press)
469
+ if handled:
470
+ # CRITICAL FIX: Update display after command mode processing
471
+ await self._update_display()
472
+ return
473
+
474
+ # LEGACY: Modal input isolation - kept for backward compatibility
475
+ if command_mode == CommandMode.MODAL and self.on_command_mode_keypress:
476
+ logger.info(
477
+ f"🎯 Modal mode active - routing ALL input to modal handler: {key_press.name}"
478
+ )
479
+ await self.on_command_mode_keypress(key_press)
480
+ return
481
+
482
+ # Handle control keys
483
+ if self.key_parser.is_control_key(key_press, "Ctrl+C"):
484
+ logger.info("Ctrl+C received")
485
+ raise KeyboardInterrupt
486
+
487
+ elif self.key_parser.is_control_key(key_press, "Enter"):
488
+ await self._handle_enter()
489
+
490
+ elif self.key_parser.is_control_key(key_press, "Backspace"):
491
+ self.buffer_manager.delete_char()
492
+
493
+ elif key_press.name == "Escape":
494
+ await self._handle_escape()
495
+
496
+ elif key_press.name == "Delete":
497
+ self.buffer_manager.delete_forward()
498
+
499
+ # Handle arrow keys for cursor movement and history
500
+ elif key_press.name == "ArrowLeft":
501
+ moved = self.buffer_manager.move_cursor("left")
502
+ if moved:
503
+ logger.debug(
504
+ f"Arrow Left: cursor moved to position {self.buffer_manager.cursor_position}"
505
+ )
506
+ await self._update_display(force_render=True)
507
+
508
+ elif key_press.name == "ArrowRight":
509
+ moved = self.buffer_manager.move_cursor("right")
510
+ if moved:
511
+ logger.debug(
512
+ f"Arrow Right: cursor moved to position {self.buffer_manager.cursor_position}"
513
+ )
514
+ await self._update_display(force_render=True)
515
+
516
+ elif key_press.name == "ArrowUp":
517
+ self.buffer_manager.navigate_history("up")
518
+ await self._update_display(force_render=True)
519
+
520
+ elif key_press.name == "ArrowDown":
521
+ self.buffer_manager.navigate_history("down")
522
+ await self._update_display(force_render=True)
523
+
524
+ # Handle Home/End keys
525
+ elif key_press.name == "Home":
526
+ self.buffer_manager.move_to_start()
527
+ await self._update_display(force_render=True)
528
+
529
+ elif key_press.name == "End":
530
+ self.buffer_manager.move_to_end()
531
+ await self._update_display(force_render=True)
532
+
533
+ # Handle Option+comma/period keys for status view navigation
534
+ elif key_press.char == "≤": # Option+comma
535
+ logger.info(
536
+ "🔑 Option+Comma (≤) detected - switching to previous status view"
537
+ )
538
+ if self.on_status_view_previous:
539
+ await self.on_status_view_previous()
540
+
541
+ elif key_press.char == "≥": # Option+period
542
+ logger.info(
543
+ "🔑 Option+Period (≥) detected - switching to next status view"
544
+ )
545
+ if self.on_status_view_next:
546
+ await self.on_status_view_next()
547
+
548
+ # Handle Cmd key combinations (mapped to Ctrl sequences on macOS)
549
+ elif self.key_parser.is_control_key(key_press, "Ctrl+A"):
550
+ logger.info("🔑 Ctrl+A (Cmd+Left) - moving cursor to start")
551
+ self.buffer_manager.move_to_start()
552
+ await self._update_display(force_render=True)
553
+
554
+ elif self.key_parser.is_control_key(key_press, "Ctrl+E"):
555
+ logger.info("🔑 Ctrl+E (Cmd+Right) - moving cursor to end")
556
+ self.buffer_manager.move_to_end()
557
+ await self._update_display(force_render=True)
558
+
559
+ elif self.key_parser.is_control_key(key_press, "Ctrl+U"):
560
+ logger.info("🔑 Ctrl+U (Cmd+Backspace) - clearing line")
561
+ self.buffer_manager.clear()
562
+ await self._update_display(force_render=True)
563
+
564
+ # Handle printable characters
565
+ elif self.key_parser.is_printable_char(key_press):
566
+ # Normal character processing
567
+ success = self.buffer_manager.insert_char(key_press.char)
568
+ if not success:
569
+ await self.error_handler.handle_error(
570
+ ErrorType.BUFFER_ERROR,
571
+ "Failed to insert character - buffer limit reached",
572
+ ErrorSeverity.LOW,
573
+ {
574
+ "char": key_press.char,
575
+ "buffer_manager": self.buffer_manager,
576
+ },
577
+ )
578
+ else:
579
+ # Check for slash command initiation AFTER character is in buffer
580
+ command_mode = (
581
+ self.get_command_mode()
582
+ if self.get_command_mode
583
+ else CommandMode.NORMAL
584
+ )
585
+ if (
586
+ key_press.char == "/"
587
+ and len(self.buffer_manager.content) == 1
588
+ and command_mode == CommandMode.NORMAL
589
+ ):
590
+ # Slash was just inserted and buffer only contains slash - enter command mode
591
+ if self.on_command_mode_keypress:
592
+ # Create a proper key press for slash
593
+ await self.on_command_mode_keypress(key_press)
594
+ # CRITICAL FIX: Update display after slash command initiation
595
+ await self._update_display()
596
+ return
597
+
598
+ # Handle other special keys (F1-F12, etc.)
599
+ elif key_press.type == KeyTypeEnum.EXTENDED:
600
+ logger.debug(f"Extended key pressed: {key_press.name}")
601
+ # Could emit special events for function keys, etc.
602
+
603
+ except Exception as e:
604
+ await self.error_handler.handle_error(
605
+ ErrorType.EVENT_ERROR,
606
+ f"Error handling key press: {e}",
607
+ ErrorSeverity.MEDIUM,
608
+ {
609
+ "key_press": key_press,
610
+ "buffer_manager": self.buffer_manager,
611
+ },
612
+ )
613
+
614
+ async def _update_display(self, force_render: bool = False) -> None:
615
+ """Update the terminal display with current buffer state."""
616
+ try:
617
+ # Skip rendering if paused (during special effects like Matrix)
618
+ if self.rendering_paused and not force_render:
619
+ return
620
+
621
+ buffer_content, cursor_pos = self.buffer_manager.get_display_info()
622
+
623
+ # Update renderer with buffer content and cursor position
624
+ self.renderer.input_buffer = buffer_content
625
+ self.renderer.cursor_position = cursor_pos
626
+
627
+ # Force immediate rendering if requested (needed for paste operations)
628
+ if force_render:
629
+ try:
630
+ if hasattr(
631
+ self.renderer, "render_active_area"
632
+ ) and asyncio.iscoroutinefunction(
633
+ self.renderer.render_active_area
634
+ ):
635
+ await self.renderer.render_active_area()
636
+ elif hasattr(
637
+ self.renderer, "render_input"
638
+ ) and asyncio.iscoroutinefunction(self.renderer.render_input):
639
+ await self.renderer.render_input()
640
+ elif hasattr(self.renderer, "render_active_area"):
641
+ self.renderer.render_active_area()
642
+ elif hasattr(self.renderer, "render_input"):
643
+ self.renderer.render_input()
644
+ except Exception as e:
645
+ logger.debug(f"Force render failed: {e}")
646
+ # Continue without forced render
647
+
648
+ # Only update cursor if position changed
649
+ if cursor_pos != self._last_cursor_pos:
650
+ # Could implement cursor positioning in renderer
651
+ self._last_cursor_pos = cursor_pos
652
+
653
+ except Exception as e:
654
+ await self.error_handler.handle_error(
655
+ ErrorType.SYSTEM_ERROR,
656
+ f"Error updating display: {e}",
657
+ ErrorSeverity.LOW,
658
+ {"buffer_manager": self.buffer_manager},
659
+ )
660
+
661
+ async def _handle_enter(self) -> None:
662
+ """Handle Enter key press with enhanced validation."""
663
+ try:
664
+ if self.buffer_manager.is_empty:
665
+ return
666
+
667
+ # Validate input before processing
668
+ validation_errors = self.buffer_manager.validate_content()
669
+ if validation_errors:
670
+ for error in validation_errors:
671
+ logger.warning(f"Input validation warning: {error}")
672
+
673
+ # Get message and clear buffer
674
+ message = self.buffer_manager.get_content_and_clear()
675
+
676
+ # GENIUS PASTE BUCKET: Immediate expansion - no waiting needed!
677
+ logger.debug(f"GENIUS SUBMIT: Original message: '{message}'")
678
+ logger.debug(
679
+ f"GENIUS SUBMIT: Paste bucket contains: {list(self._paste_bucket.keys())}"
680
+ )
681
+
682
+ expanded_message = self._expand_paste_placeholders(message)
683
+ logger.debug(
684
+ f"GENIUS SUBMIT: Final expanded: '{expanded_message[:100]}...' ({len(expanded_message)} chars)"
685
+ )
686
+
687
+ # Add to history (with expanded content)
688
+ self.buffer_manager.add_to_history(expanded_message)
689
+
690
+ # Update renderer
691
+ self.renderer.input_buffer = ""
692
+ self.renderer.clear_active_area()
693
+
694
+ # Emit user input event (with expanded content!)
695
+ await self.event_bus.emit_with_hooks(
696
+ EventType.USER_INPUT,
697
+ {
698
+ "message": expanded_message,
699
+ "validation_errors": validation_errors,
700
+ },
701
+ "user",
702
+ )
703
+
704
+ logger.debug(
705
+ f"Processed user input: {message[:100]}..."
706
+ if len(message) > 100
707
+ else f"Processed user input: {message}"
708
+ )
709
+
710
+ except Exception as e:
711
+ await self.error_handler.handle_error(
712
+ ErrorType.EVENT_ERROR,
713
+ f"Error handling Enter key: {e}",
714
+ ErrorSeverity.HIGH,
715
+ {"buffer_manager": self.buffer_manager},
716
+ )
717
+
718
+ async def _handle_escape(self) -> None:
719
+ """Handle Escape key press for request cancellation."""
720
+ try:
721
+ logger.info("_handle_escape called - emitting CANCEL_REQUEST event")
722
+
723
+ # Emit cancellation event
724
+ result = await self.event_bus.emit_with_hooks(
725
+ EventType.CANCEL_REQUEST,
726
+ {"reason": "user_escape", "source": "input_handler"},
727
+ "input",
728
+ )
729
+
730
+ logger.info(
731
+ f"ESC key pressed - cancellation request sent, result: {result}"
732
+ )
733
+
734
+ except Exception as e:
735
+ await self.error_handler.handle_error(
736
+ ErrorType.EVENT_ERROR,
737
+ f"Error handling Escape key: {e}",
738
+ ErrorSeverity.MEDIUM,
739
+ {"buffer_manager": self.buffer_manager},
740
+ )
741
+
742
+ def _expand_paste_placeholders(self, message: str) -> str:
743
+ """Expand paste placeholders with actual content from paste bucket.
744
+
745
+ Your brilliant idea: Replace [⚡ Pasted #N ...] with actual pasted content!
746
+ """
747
+ logger.debug(f"PASTE DEBUG: Expanding message: '{message}'")
748
+ logger.debug(
749
+ f"PASTE DEBUG: Paste bucket contains: {list(self._paste_bucket.keys())}"
750
+ )
751
+
752
+ expanded = message
753
+
754
+ # Find and replace each paste placeholder
755
+ import re
756
+
757
+ for paste_id, content in self._paste_bucket.items():
758
+ # Extract paste number from paste_id (PASTE_1 -> 1)
759
+ paste_num = paste_id.split("_")[1]
760
+
761
+ # Pattern to match: [Pasted #N X lines, Y chars]
762
+ pattern = rf"\[Pasted #{paste_num} \d+ lines?, \d+ chars\]"
763
+
764
+ logger.debug(f"PASTE DEBUG: Looking for pattern: {pattern}")
765
+ logger.debug(
766
+ f"PASTE DEBUG: Will replace with content: '{content[:50]}...'"
767
+ )
768
+
769
+ # Replace with actual content
770
+ matches = re.findall(pattern, expanded)
771
+ logger.debug(f"PASTE DEBUG: Found {len(matches)} matches")
772
+
773
+ expanded = re.sub(pattern, content, expanded)
774
+
775
+ logger.debug(f"PASTE DEBUG: Final expanded message: '{expanded[:100]}...'")
776
+ logger.info(
777
+ f"Paste expansion: {len(self._paste_bucket)} placeholders expanded"
778
+ )
779
+
780
+ # Clear paste bucket after expansion (one-time use)
781
+ self._paste_bucket.clear()
782
+
783
+ return expanded
784
+
785
+ async def _create_paste_placeholder(self, paste_id: str) -> None:
786
+ """Create placeholder for paste - GENIUS IMMEDIATE VERSION."""
787
+ content = self._paste_bucket[paste_id]
788
+
789
+ # Create elegant placeholder for user to see
790
+ line_count = content.count("\n") + 1 if "\n" in content else 1
791
+ char_count = len(content)
792
+ paste_num = paste_id.split("_")[1] # Extract number from PASTE_1
793
+ placeholder = f"[Pasted #{paste_num} {line_count} lines, {char_count} chars]"
794
+
795
+ # Insert placeholder into buffer (what user sees)
796
+ for char in placeholder:
797
+ self.buffer_manager.insert_char(char)
798
+
799
+ logger.info(
800
+ f"GENIUS: Created placeholder for {char_count} chars as {paste_id}"
801
+ )
802
+
803
+ # Update display once at the end
804
+ await self._update_display(force_render=True)
805
+
806
+ async def _update_paste_placeholder(self) -> None:
807
+ """Update existing placeholder when paste grows - GENIUS VERSION."""
808
+ # For now, just log - updating existing placeholder is complex
809
+ # The merge approach usually works fast enough that this isn't needed
810
+ content = self._paste_bucket[self._current_paste_id]
811
+ logger.info(
812
+ f"GENIUS: Updated {self._current_paste_id} to {len(content)} chars"
813
+ )
814
+
815
+ async def _simple_paste_detection(self, char: str, current_time: float) -> bool:
816
+ """Simple, reliable paste detection using timing only.
817
+
818
+ Returns:
819
+ True if character was consumed by paste detection, False otherwise.
820
+ """
821
+ # Check cooldown to prevent overlapping paste detections
822
+ if (
823
+ hasattr(self, "_paste_cooldown")
824
+ and self._paste_cooldown > 0
825
+ and (current_time - self._paste_cooldown) < 1.0
826
+ ):
827
+ # Still in cooldown period, skip paste detection
828
+ self._last_char_time = current_time
829
+ return False
830
+
831
+ # Check if we have a pending paste buffer that timed out
832
+ if self._paste_buffer and self._last_char_time > 0:
833
+ paste_timeout_ms = getattr(self, "_paste_timeout_ms", 50)
834
+ gap_ms = (current_time - self._last_char_time) * 1000
835
+
836
+ if gap_ms > paste_timeout_ms:
837
+ # Buffer timed out, process it
838
+ paste_min_chars = getattr(self, "paste_min_chars", 5)
839
+ if len(self._paste_buffer) >= paste_min_chars:
840
+ self._process_simple_paste_sync()
841
+ if not hasattr(self, "_paste_cooldown"):
842
+ self._paste_cooldown = 0
843
+ self._paste_cooldown = current_time # Set cooldown
844
+ else:
845
+ # Too few chars, process them as individual keystrokes
846
+ self._flush_paste_buffer_as_keystrokes_sync()
847
+ self._paste_buffer = []
848
+
849
+ # Now handle the current character
850
+ if self._last_char_time > 0:
851
+ paste_threshold_ms = getattr(self, "paste_threshold_ms", 20)
852
+ gap_ms = (current_time - self._last_char_time) * 1000
853
+
854
+ # If character arrived quickly, start/continue paste buffer
855
+ if gap_ms < paste_threshold_ms:
856
+ self._paste_buffer.append(char)
857
+ self._last_char_time = current_time
858
+ return True # Character consumed by paste buffer
859
+
860
+ # Character not part of paste, process normally
861
+ self._last_char_time = current_time
862
+ return False
863
+
864
+ def _flush_paste_buffer_as_keystrokes_sync(self) -> None:
865
+ """Process paste buffer contents as individual keystrokes (sync version)."""
866
+ logger.debug(
867
+ f"Flushing {len(self._paste_buffer)} chars as individual keystrokes"
868
+ )
869
+
870
+ # Just add characters to buffer without async processing
871
+ for char in self._paste_buffer:
872
+ if char.isprintable() or char in [" ", "\t"]:
873
+ self.buffer_manager.insert_char(char)
874
+
875
+ def _process_simple_paste_sync(self) -> None:
876
+ """Process detected paste content (sync version with inline indicator)."""
877
+ if not self._paste_buffer:
878
+ return
879
+
880
+ # Get the content and clean any terminal markers
881
+ content = "".join(self._paste_buffer)
882
+
883
+ # Clean bracketed paste markers if present
884
+ if content.startswith("[200~"):
885
+ content = content[5:]
886
+ if content.endswith("01~"):
887
+ content = content[:-3]
888
+ elif content.endswith("[201~"):
889
+ content = content[:-6]
890
+
891
+ # Count lines
892
+ line_count = content.count("\n") + 1
893
+ char_count = len(content)
894
+
895
+ # Increment paste counter
896
+ self._paste_counter += 1
897
+
898
+ # Create inline paste indicator exactly as user requested
899
+ indicator = f"[⚡ Pasted #{self._paste_counter} {line_count} lines]"
900
+
901
+ # Insert the indicator into the buffer at current position
902
+ try:
903
+ for char in indicator:
904
+ self.buffer_manager.insert_char(char)
905
+ logger.info(
906
+ f"Paste #{self._paste_counter}: {char_count} chars, {line_count} lines"
907
+ )
908
+ except Exception as e:
909
+ logger.error(f"Paste processing error: {e}")
910
+
911
+ # Clear paste buffer
912
+ self._paste_buffer = []
913
+
914
+ def get_status(self) -> Dict[str, Any]:
915
+ """Get current raw input processor status for debugging.
916
+
917
+ Returns:
918
+ Dictionary containing status information.
919
+ """
920
+ return {
921
+ "running": self.running,
922
+ "rendering_paused": self.rendering_paused,
923
+ "paste_detection_enabled": self.paste_detection_enabled,
924
+ "paste_bucket_size": len(self._paste_bucket),
925
+ "paste_counter": self._paste_counter,
926
+ "parser_state": {
927
+ "in_escape_sequence": self.key_parser._in_escape_sequence,
928
+ "escape_buffer": self.key_parser._escape_buffer,
929
+ },
930
+ }
931
+
932
+ async def cleanup(self) -> None:
933
+ """Perform cleanup operations."""
934
+ try:
935
+ # Reset parser state
936
+ self.key_parser._reset_escape_state()
937
+
938
+ # Clear paste state
939
+ self._paste_buffer = []
940
+ self._paste_bucket.clear()
941
+ self._current_paste_id = None
942
+
943
+ logger.debug("Raw input processor cleanup completed")
944
+
945
+ except Exception as e:
946
+ logger.error(f"Error during raw input processor cleanup: {e}")