kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Input loop manager for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
This module handles the main input processing loop, platform-specific I/O,
|
|
4
|
+
and coordinates between raw terminal input and character processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .paste_processor import PasteProcessor
|
|
13
|
+
from ..input_errors import InputErrorHandler, ErrorType, ErrorSeverity
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..key_parser import KeyParser, KeyPress
|
|
17
|
+
from ...events.models import CommandMode
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Platform-specific imports
|
|
22
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
23
|
+
|
|
24
|
+
if IS_WINDOWS:
|
|
25
|
+
import msvcrt
|
|
26
|
+
else:
|
|
27
|
+
import select
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputLoopManager:
|
|
31
|
+
"""Manages the main input processing loop.
|
|
32
|
+
|
|
33
|
+
Responsibilities:
|
|
34
|
+
- Main input loop execution
|
|
35
|
+
- Platform-specific input checking (Windows/Unix)
|
|
36
|
+
- Chunk reading and routing
|
|
37
|
+
- Start/stop lifecycle
|
|
38
|
+
- Error handling and recovery
|
|
39
|
+
- Windows extended key mapping (arrow keys, F1-F12, etc.)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Windows extended key code mapping to ANSI escape sequences
|
|
43
|
+
WIN_KEY_MAP = {
|
|
44
|
+
72: b"\x1b[A", # ArrowUp
|
|
45
|
+
80: b"\x1b[B", # ArrowDown
|
|
46
|
+
75: b"\x1b[D", # ArrowLeft
|
|
47
|
+
77: b"\x1b[C", # ArrowRight
|
|
48
|
+
71: b"\x1b[H", # Home
|
|
49
|
+
79: b"\x1b[F", # End
|
|
50
|
+
73: b"\x1b[5~", # PageUp
|
|
51
|
+
81: b"\x1b[6~", # PageDown
|
|
52
|
+
82: b"\x1b[2~", # Insert
|
|
53
|
+
83: b"\x1b[3~", # Delete
|
|
54
|
+
59: b"\x1bOP", # F1
|
|
55
|
+
60: b"\x1bOQ", # F2
|
|
56
|
+
61: b"\x1bOR", # F3
|
|
57
|
+
62: b"\x1bOS", # F4
|
|
58
|
+
63: b"\x1b[15~", # F5
|
|
59
|
+
64: b"\x1b[17~", # F6
|
|
60
|
+
65: b"\x1b[18~", # F7
|
|
61
|
+
66: b"\x1b[19~", # F8
|
|
62
|
+
67: b"\x1b[20~", # F9
|
|
63
|
+
68: b"\x1b[21~", # F10
|
|
64
|
+
133: b"\x1b[23~", # F11
|
|
65
|
+
134: b"\x1b[24~", # F12
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
renderer: Any,
|
|
71
|
+
key_parser: "KeyParser",
|
|
72
|
+
error_handler: InputErrorHandler,
|
|
73
|
+
paste_processor: PasteProcessor,
|
|
74
|
+
config: Any,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Initialize the input loop manager.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
renderer: Terminal renderer for raw mode control.
|
|
80
|
+
key_parser: Key parser for escape sequence handling.
|
|
81
|
+
error_handler: Error handler for error recovery.
|
|
82
|
+
paste_processor: Paste processor for paste detection.
|
|
83
|
+
config: Configuration manager for timing settings.
|
|
84
|
+
"""
|
|
85
|
+
self.renderer = renderer
|
|
86
|
+
self.key_parser = key_parser
|
|
87
|
+
self.error_handler = error_handler
|
|
88
|
+
self.paste_processor = paste_processor
|
|
89
|
+
self.config = config
|
|
90
|
+
|
|
91
|
+
# State
|
|
92
|
+
self.running = False
|
|
93
|
+
|
|
94
|
+
# Config values
|
|
95
|
+
self.polling_delay = config.get("input.polling_delay", 0.01)
|
|
96
|
+
self.error_delay = config.get("input.error_delay", 0.1)
|
|
97
|
+
|
|
98
|
+
# Callbacks (set by parent)
|
|
99
|
+
self._process_character_callback: Optional[Callable] = None
|
|
100
|
+
self._handle_key_press_callback: Optional[Callable] = None
|
|
101
|
+
self._handle_command_mode_keypress_callback: Optional[Callable] = None
|
|
102
|
+
self._handle_live_modal_keypress_callback: Optional[Callable] = None
|
|
103
|
+
self._register_hooks_callback: Optional[Callable] = None
|
|
104
|
+
self._get_command_mode_callback: Optional[Callable] = None
|
|
105
|
+
|
|
106
|
+
# Reference to buffer_manager for cleanup error context
|
|
107
|
+
self._buffer_manager: Optional[Any] = None
|
|
108
|
+
|
|
109
|
+
def set_callbacks(
|
|
110
|
+
self,
|
|
111
|
+
process_character: Callable,
|
|
112
|
+
handle_key_press: Callable,
|
|
113
|
+
handle_command_mode_keypress: Callable,
|
|
114
|
+
handle_live_modal_keypress: Callable,
|
|
115
|
+
register_hooks: Callable,
|
|
116
|
+
get_command_mode: Callable,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Set callback functions for input processing.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
process_character: Callback for processing individual characters.
|
|
122
|
+
handle_key_press: Callback for handling parsed key presses.
|
|
123
|
+
handle_command_mode_keypress: Callback for command mode key handling.
|
|
124
|
+
handle_live_modal_keypress: Callback for live modal key handling.
|
|
125
|
+
register_hooks: Callback to register all hooks.
|
|
126
|
+
get_command_mode: Callback to get current command mode.
|
|
127
|
+
"""
|
|
128
|
+
self._process_character_callback = process_character
|
|
129
|
+
self._handle_key_press_callback = handle_key_press
|
|
130
|
+
self._handle_command_mode_keypress_callback = handle_command_mode_keypress
|
|
131
|
+
self._handle_live_modal_keypress_callback = handle_live_modal_keypress
|
|
132
|
+
self._register_hooks_callback = register_hooks
|
|
133
|
+
self._get_command_mode_callback = get_command_mode
|
|
134
|
+
|
|
135
|
+
def set_buffer_manager(self, buffer_manager: Any) -> None:
|
|
136
|
+
"""Set buffer manager reference for error context.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
buffer_manager: Buffer manager instance.
|
|
140
|
+
"""
|
|
141
|
+
self._buffer_manager = buffer_manager
|
|
142
|
+
|
|
143
|
+
async def start(self) -> None:
|
|
144
|
+
"""Start the input handling loop."""
|
|
145
|
+
self.running = True
|
|
146
|
+
self.renderer.enter_raw_mode()
|
|
147
|
+
|
|
148
|
+
# Check if raw mode worked
|
|
149
|
+
if (
|
|
150
|
+
getattr(
|
|
151
|
+
self.renderer.terminal_state.current_mode,
|
|
152
|
+
"value",
|
|
153
|
+
self.renderer.terminal_state.current_mode,
|
|
154
|
+
)
|
|
155
|
+
!= "raw"
|
|
156
|
+
):
|
|
157
|
+
logger.warning("Raw mode failed - using fallback ESC detection")
|
|
158
|
+
|
|
159
|
+
# Register all hooks via callback
|
|
160
|
+
if self._register_hooks_callback:
|
|
161
|
+
await self._register_hooks_callback()
|
|
162
|
+
|
|
163
|
+
logger.info("Input handler started")
|
|
164
|
+
await self._input_loop()
|
|
165
|
+
|
|
166
|
+
async def stop(self) -> None:
|
|
167
|
+
"""Stop the input handling loop with cleanup."""
|
|
168
|
+
self.running = False
|
|
169
|
+
await self.cleanup()
|
|
170
|
+
self.renderer.exit_raw_mode()
|
|
171
|
+
logger.info("Input handler stopped")
|
|
172
|
+
|
|
173
|
+
async def _input_loop(self) -> None:
|
|
174
|
+
"""Main input processing loop with enhanced error handling."""
|
|
175
|
+
from ...events.models import CommandMode
|
|
176
|
+
|
|
177
|
+
while self.running:
|
|
178
|
+
try:
|
|
179
|
+
# Platform-specific input checking
|
|
180
|
+
has_input = await self._check_input_available()
|
|
181
|
+
|
|
182
|
+
if has_input:
|
|
183
|
+
# Read input data
|
|
184
|
+
chunk = await self._read_input_chunk()
|
|
185
|
+
|
|
186
|
+
if not chunk:
|
|
187
|
+
await asyncio.sleep(self.polling_delay)
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Check if this is an escape sequence (arrow keys, etc.)
|
|
191
|
+
if self._is_escape_sequence(chunk):
|
|
192
|
+
# Escape sequence - process character by character
|
|
193
|
+
logger.debug(
|
|
194
|
+
f"Processing escape sequence "
|
|
195
|
+
f"character-by-character: {repr(chunk)}"
|
|
196
|
+
)
|
|
197
|
+
for char in chunk:
|
|
198
|
+
if self._process_character_callback:
|
|
199
|
+
await self._process_character_callback(char)
|
|
200
|
+
elif len(chunk) > 10 and not self._is_in_modal_mode():
|
|
201
|
+
# PRIMARY PASTE DETECTION:
|
|
202
|
+
# Large chunk detection (ONLY when not in modal mode)
|
|
203
|
+
# Skip paste detection in modals to allow normal pasting into form fields
|
|
204
|
+
await self._handle_paste_chunk(chunk)
|
|
205
|
+
else:
|
|
206
|
+
# Normal input - process each character individually
|
|
207
|
+
# This also handles paste in modal mode (chars go to form fields)
|
|
208
|
+
logger.info(
|
|
209
|
+
f"Processing normal input "
|
|
210
|
+
f"character-by-character: {repr(chunk)}"
|
|
211
|
+
)
|
|
212
|
+
for char in chunk:
|
|
213
|
+
if self._process_character_callback:
|
|
214
|
+
await self._process_character_callback(char)
|
|
215
|
+
else:
|
|
216
|
+
# No input available - check for standalone ESC key
|
|
217
|
+
esc_key = self.key_parser.check_for_standalone_escape()
|
|
218
|
+
if esc_key:
|
|
219
|
+
logger.info("DETECTED STANDALONE ESC KEY!")
|
|
220
|
+
await self._route_escape_key(esc_key)
|
|
221
|
+
|
|
222
|
+
await asyncio.sleep(self.polling_delay)
|
|
223
|
+
|
|
224
|
+
except KeyboardInterrupt:
|
|
225
|
+
logger.info("Ctrl+C received")
|
|
226
|
+
raise
|
|
227
|
+
except OSError as e:
|
|
228
|
+
await self.error_handler.handle_error(
|
|
229
|
+
ErrorType.IO_ERROR,
|
|
230
|
+
f"I/O error in input loop: {e}",
|
|
231
|
+
ErrorSeverity.HIGH,
|
|
232
|
+
{"buffer_manager": self._buffer_manager},
|
|
233
|
+
)
|
|
234
|
+
await asyncio.sleep(self.error_delay)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
await self.error_handler.handle_error(
|
|
237
|
+
ErrorType.SYSTEM_ERROR,
|
|
238
|
+
f"Unexpected error in input loop: {e}",
|
|
239
|
+
ErrorSeverity.MEDIUM,
|
|
240
|
+
{"buffer_manager": self._buffer_manager},
|
|
241
|
+
)
|
|
242
|
+
await asyncio.sleep(self.error_delay)
|
|
243
|
+
|
|
244
|
+
async def _route_escape_key(self, esc_key: "KeyPress") -> None:
|
|
245
|
+
"""Route escape key to correct handler based on mode.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
esc_key: The parsed escape key press.
|
|
249
|
+
"""
|
|
250
|
+
from ...events.models import CommandMode
|
|
251
|
+
|
|
252
|
+
command_mode = None
|
|
253
|
+
if self._get_command_mode_callback:
|
|
254
|
+
command_mode = self._get_command_mode_callback()
|
|
255
|
+
|
|
256
|
+
if command_mode in (CommandMode.MODAL, CommandMode.STATUS_MODAL):
|
|
257
|
+
if self._handle_command_mode_keypress_callback:
|
|
258
|
+
await self._handle_command_mode_keypress_callback(esc_key)
|
|
259
|
+
elif command_mode == CommandMode.LIVE_MODAL:
|
|
260
|
+
if self._handle_live_modal_keypress_callback:
|
|
261
|
+
await self._handle_live_modal_keypress_callback(esc_key)
|
|
262
|
+
else:
|
|
263
|
+
if self._handle_key_press_callback:
|
|
264
|
+
await self._handle_key_press_callback(esc_key)
|
|
265
|
+
|
|
266
|
+
async def _handle_paste_chunk(self, chunk: str) -> None:
|
|
267
|
+
"""Handle a large chunk as pasted content.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
chunk: The input chunk to process as paste.
|
|
271
|
+
"""
|
|
272
|
+
import time
|
|
273
|
+
|
|
274
|
+
current_time = time.time()
|
|
275
|
+
|
|
276
|
+
# Check if this continues the current paste (within 100ms)
|
|
277
|
+
if self.paste_processor.should_merge_paste(current_time, threshold=0.1):
|
|
278
|
+
# Merge with existing paste
|
|
279
|
+
self.paste_processor.append_to_current_paste(chunk, current_time)
|
|
280
|
+
# Update the placeholder to show new size
|
|
281
|
+
await self.paste_processor.update_paste_placeholder()
|
|
282
|
+
else:
|
|
283
|
+
# New paste - store immediately
|
|
284
|
+
paste_id = self.paste_processor.start_new_paste(chunk, current_time)
|
|
285
|
+
# Create placeholder immediately
|
|
286
|
+
await self.paste_processor.create_paste_placeholder(paste_id)
|
|
287
|
+
|
|
288
|
+
def _is_escape_sequence(self, text: str) -> bool:
|
|
289
|
+
"""Check if input is an escape sequence that should bypass paste detection.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
text: Input text to check.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if text is an escape sequence, False otherwise.
|
|
296
|
+
"""
|
|
297
|
+
if not text:
|
|
298
|
+
return False
|
|
299
|
+
# Common escape sequences start with ESC (\x1b)
|
|
300
|
+
return text.startswith("\x1b")
|
|
301
|
+
|
|
302
|
+
def _is_in_modal_mode(self) -> bool:
|
|
303
|
+
"""Check if we're currently in a modal/fullscreen mode.
|
|
304
|
+
|
|
305
|
+
Paste detection is disabled in modal mode to allow normal
|
|
306
|
+
pasting into form fields and text inputs.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if in modal mode, False otherwise.
|
|
310
|
+
"""
|
|
311
|
+
from ...events.models import CommandMode
|
|
312
|
+
|
|
313
|
+
if not self._get_command_mode_callback:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
command_mode = self._get_command_mode_callback()
|
|
317
|
+
# Disable paste detection for all modal types
|
|
318
|
+
return command_mode in (
|
|
319
|
+
CommandMode.MODAL,
|
|
320
|
+
CommandMode.STATUS_MODAL,
|
|
321
|
+
CommandMode.LIVE_MODAL,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
async def _check_input_available(self) -> bool:
|
|
325
|
+
"""Check if input is available (cross-platform).
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
True if input is available, False otherwise.
|
|
329
|
+
"""
|
|
330
|
+
if IS_WINDOWS:
|
|
331
|
+
# Windows: Use msvcrt.kbhit() to check for available input
|
|
332
|
+
return msvcrt.kbhit()
|
|
333
|
+
else:
|
|
334
|
+
# Unix: Use select with timeout
|
|
335
|
+
return bool(select.select([sys.stdin], [], [], self.polling_delay)[0])
|
|
336
|
+
|
|
337
|
+
async def _read_input_chunk(self) -> str:
|
|
338
|
+
"""Read available input data (cross-platform).
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Decoded input string, or empty string if no input.
|
|
342
|
+
"""
|
|
343
|
+
import os
|
|
344
|
+
|
|
345
|
+
if IS_WINDOWS:
|
|
346
|
+
return await self._read_windows_input()
|
|
347
|
+
else:
|
|
348
|
+
return await self._read_unix_input()
|
|
349
|
+
|
|
350
|
+
async def _read_windows_input(self) -> str:
|
|
351
|
+
"""Read input on Windows platform.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Decoded input string.
|
|
355
|
+
"""
|
|
356
|
+
chunk = b""
|
|
357
|
+
while msvcrt.kbhit():
|
|
358
|
+
char = msvcrt.getch()
|
|
359
|
+
char_code = char[0] if isinstance(char, bytes) else ord(char)
|
|
360
|
+
|
|
361
|
+
# Handle Windows extended keys (arrow keys, function keys, etc.)
|
|
362
|
+
# Extended keys are prefixed with 0x00 or 0xE0 (224)
|
|
363
|
+
if char_code in (0, 224):
|
|
364
|
+
# Read the actual key code
|
|
365
|
+
ext_char = msvcrt.getch()
|
|
366
|
+
ext_code = ext_char[0] if isinstance(ext_char, bytes) else ord(ext_char)
|
|
367
|
+
# Map Windows extended key codes to ANSI escape sequences
|
|
368
|
+
if ext_code in self.WIN_KEY_MAP:
|
|
369
|
+
chunk += self.WIN_KEY_MAP[ext_code]
|
|
370
|
+
else:
|
|
371
|
+
logger.debug(f"Unknown Windows extended key: {ext_code}")
|
|
372
|
+
else:
|
|
373
|
+
chunk += char
|
|
374
|
+
|
|
375
|
+
# Small delay to allow for more input
|
|
376
|
+
await asyncio.sleep(0.001)
|
|
377
|
+
# Check if there's more data immediately available
|
|
378
|
+
if not msvcrt.kbhit():
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
382
|
+
|
|
383
|
+
async def _read_unix_input(self) -> str:
|
|
384
|
+
"""Read input on Unix platform.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Decoded input string.
|
|
388
|
+
"""
|
|
389
|
+
import os
|
|
390
|
+
|
|
391
|
+
chunk = b""
|
|
392
|
+
while True:
|
|
393
|
+
try:
|
|
394
|
+
# Read in 8KB chunks
|
|
395
|
+
more_data = os.read(0, 8192)
|
|
396
|
+
if not more_data:
|
|
397
|
+
break
|
|
398
|
+
chunk += more_data
|
|
399
|
+
# Check if more data is immediately available
|
|
400
|
+
if not select.select([sys.stdin], [], [], 0.001)[0]:
|
|
401
|
+
break # No more data waiting
|
|
402
|
+
except OSError:
|
|
403
|
+
break # No more data available
|
|
404
|
+
|
|
405
|
+
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
406
|
+
|
|
407
|
+
async def cleanup(self) -> None:
|
|
408
|
+
"""Perform cleanup operations."""
|
|
409
|
+
try:
|
|
410
|
+
# Clear old errors
|
|
411
|
+
cleared_errors = self.error_handler.clear_old_errors()
|
|
412
|
+
if cleared_errors > 0:
|
|
413
|
+
logger.info(f"Cleaned up {cleared_errors} old errors")
|
|
414
|
+
|
|
415
|
+
# Reset parser state
|
|
416
|
+
self.key_parser._reset_escape_state()
|
|
417
|
+
|
|
418
|
+
logger.debug("Input handler cleanup completed")
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Error during cleanup: {e}")
|