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,339 @@
|
|
|
1
|
+
"""Paste processing component for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
Responsible for detecting, storing, and expanding pasted content.
|
|
4
|
+
Implements a dual paste detection system:
|
|
5
|
+
1. PRIMARY (chunk-based): Detects large chunks >10 chars, always active
|
|
6
|
+
2. SECONDARY (timing-based): Detects rapid typing, currently disabled
|
|
7
|
+
|
|
8
|
+
The PRIMARY system is handled in InputLoopManager (chunk detection).
|
|
9
|
+
This component handles placeholder creation, storage, and expansion.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from typing import Dict, Any, Optional, Callable, Awaitable
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PasteProcessor:
|
|
20
|
+
"""Processes paste detection, placeholder creation, and content expansion.
|
|
21
|
+
|
|
22
|
+
This component manages the "genius paste system" which:
|
|
23
|
+
1. Stores pasted content immediately in a bucket
|
|
24
|
+
2. Shows a placeholder to the user: [Pasted #N X lines, Y chars]
|
|
25
|
+
3. Expands placeholders with actual content on submit
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
buffer_manager: Buffer manager for inserting characters.
|
|
29
|
+
display_callback: Async callback to update display after paste operations.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
buffer_manager: Any,
|
|
35
|
+
display_callback: Optional[Callable[..., Awaitable[None]]] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize the paste processor.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
buffer_manager: Buffer manager instance for character insertion.
|
|
41
|
+
display_callback: Optional async callback for display updates.
|
|
42
|
+
"""
|
|
43
|
+
self.buffer_manager = buffer_manager
|
|
44
|
+
self._display_callback = display_callback
|
|
45
|
+
|
|
46
|
+
# PRIMARY paste system state (chunk-based, always active)
|
|
47
|
+
self._paste_bucket: Dict[str, str] = {} # {paste_id: actual_content}
|
|
48
|
+
self._paste_counter = 0 # Counter for paste numbering
|
|
49
|
+
self._current_paste_id: Optional[str] = None # Currently building paste ID
|
|
50
|
+
self._last_paste_time = 0.0 # Last chunk timestamp
|
|
51
|
+
|
|
52
|
+
# SECONDARY paste system state (timing-based, disabled by default)
|
|
53
|
+
self.paste_detection_enabled = False # Only enables SECONDARY system
|
|
54
|
+
self._paste_buffer: list = []
|
|
55
|
+
self._last_char_time = 0.0
|
|
56
|
+
self._paste_cooldown = 0.0
|
|
57
|
+
# These would need to be configured if secondary system is enabled:
|
|
58
|
+
self._paste_timeout_ms = 100.0 # Timeout for paste buffer
|
|
59
|
+
self.paste_threshold_ms = 50.0 # Threshold for rapid typing detection
|
|
60
|
+
self.paste_min_chars = 5 # Minimum chars to consider as paste
|
|
61
|
+
|
|
62
|
+
logger.debug("PasteProcessor initialized")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def paste_bucket(self) -> Dict[str, str]:
|
|
66
|
+
"""Get the paste bucket (read-only access for external checks)."""
|
|
67
|
+
return self._paste_bucket
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def current_paste_id(self) -> Optional[str]:
|
|
71
|
+
"""Get the current paste ID being built."""
|
|
72
|
+
return self._current_paste_id
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def last_paste_time(self) -> float:
|
|
76
|
+
"""Get the last paste timestamp."""
|
|
77
|
+
return self._last_paste_time
|
|
78
|
+
|
|
79
|
+
def expand_paste_placeholders(self, message: str) -> str:
|
|
80
|
+
"""Expand paste placeholders with actual content from paste bucket.
|
|
81
|
+
|
|
82
|
+
Replaces [Pasted #N X lines, Y chars] with actual pasted content.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
message: Message containing paste placeholders.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Message with placeholders expanded to actual content.
|
|
89
|
+
"""
|
|
90
|
+
logger.debug(f"PASTE DEBUG: Expanding message: '{message}'")
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"PASTE DEBUG: Paste bucket contains: {list(self._paste_bucket.keys())}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expanded = message
|
|
96
|
+
|
|
97
|
+
# Find and replace each paste placeholder
|
|
98
|
+
for paste_id, content in self._paste_bucket.items():
|
|
99
|
+
# Extract paste number from paste_id (PASTE_1 -> 1)
|
|
100
|
+
paste_num = paste_id.split("_")[1]
|
|
101
|
+
|
|
102
|
+
# Pattern to match: [Pasted #N X lines, Y chars]
|
|
103
|
+
pattern = rf"\[Pasted #{paste_num} \d+ lines?, \d+ chars\]"
|
|
104
|
+
|
|
105
|
+
logger.debug(f"PASTE DEBUG: Looking for pattern: {pattern}")
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"PASTE DEBUG: Will replace with content: '{content[:50]}...'"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Replace with actual content
|
|
111
|
+
matches = re.findall(pattern, expanded)
|
|
112
|
+
logger.debug(f"PASTE DEBUG: Found {len(matches)} matches")
|
|
113
|
+
|
|
114
|
+
# Use lambda to treat content as literal text, not a replacement template
|
|
115
|
+
# (avoids backslashes being interpreted as regex backreferences)
|
|
116
|
+
expanded = re.sub(pattern, lambda m: content, expanded)
|
|
117
|
+
|
|
118
|
+
logger.debug(f"PASTE DEBUG: Final expanded message: '{expanded[:100]}...'")
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Paste expansion: {len(self._paste_bucket)} placeholders expanded"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Clear paste bucket after expansion (one-time use)
|
|
124
|
+
self._paste_bucket.clear()
|
|
125
|
+
|
|
126
|
+
return expanded
|
|
127
|
+
|
|
128
|
+
async def create_paste_placeholder(self, paste_id: str) -> None:
|
|
129
|
+
"""Create placeholder for paste - GENIUS IMMEDIATE VERSION.
|
|
130
|
+
|
|
131
|
+
Creates an elegant placeholder that the user sees in the input buffer,
|
|
132
|
+
while the actual content is stored in the paste bucket.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
paste_id: The ID of the paste (e.g., "PASTE_1").
|
|
136
|
+
"""
|
|
137
|
+
content = self._paste_bucket[paste_id]
|
|
138
|
+
|
|
139
|
+
# Create elegant placeholder for user to see
|
|
140
|
+
line_count = content.count("\n") + 1 if "\n" in content else 1
|
|
141
|
+
char_count = len(content)
|
|
142
|
+
paste_num = paste_id.split("_")[1] # Extract number from PASTE_1
|
|
143
|
+
placeholder = f"[Pasted #{paste_num} {line_count} lines, {char_count} chars]"
|
|
144
|
+
|
|
145
|
+
# Insert placeholder into buffer (what user sees)
|
|
146
|
+
for char in placeholder:
|
|
147
|
+
self.buffer_manager.insert_char(char)
|
|
148
|
+
|
|
149
|
+
logger.info(
|
|
150
|
+
f"GENIUS: Created placeholder for {char_count} chars as {paste_id}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Update display once at the end
|
|
154
|
+
if self._display_callback:
|
|
155
|
+
await self._display_callback(force_render=True)
|
|
156
|
+
|
|
157
|
+
async def update_paste_placeholder(self) -> None:
|
|
158
|
+
"""Update existing placeholder when paste grows - GENIUS VERSION.
|
|
159
|
+
|
|
160
|
+
For now, just logs - updating existing placeholder is complex.
|
|
161
|
+
The merge approach usually works fast enough that this isn't needed.
|
|
162
|
+
"""
|
|
163
|
+
content = self._paste_bucket[self._current_paste_id]
|
|
164
|
+
logger.info(
|
|
165
|
+
f"GENIUS: Updated {self._current_paste_id} to {len(content)} chars"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def simple_paste_detection(self, char: str, current_time: float) -> bool:
|
|
169
|
+
"""Simple, reliable paste detection using timing only.
|
|
170
|
+
|
|
171
|
+
This is the SECONDARY paste detection system (disabled by default).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
char: The character to process.
|
|
175
|
+
current_time: Current timestamp.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if character was consumed by paste detection, False otherwise.
|
|
179
|
+
"""
|
|
180
|
+
# Check cooldown to prevent overlapping paste detections
|
|
181
|
+
if self._paste_cooldown > 0 and (current_time - self._paste_cooldown) < 1.0:
|
|
182
|
+
# Still in cooldown period, skip paste detection
|
|
183
|
+
self._last_char_time = current_time
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Check if we have a pending paste buffer that timed out
|
|
187
|
+
if self._paste_buffer and self._last_char_time > 0:
|
|
188
|
+
gap_ms = (current_time - self._last_char_time) * 1000
|
|
189
|
+
|
|
190
|
+
if gap_ms > self._paste_timeout_ms:
|
|
191
|
+
# Buffer timed out, process it
|
|
192
|
+
if len(self._paste_buffer) >= self.paste_min_chars:
|
|
193
|
+
self._process_simple_paste_sync()
|
|
194
|
+
self._paste_cooldown = current_time # Set cooldown
|
|
195
|
+
else:
|
|
196
|
+
# Too few chars, process them as individual keystrokes
|
|
197
|
+
self._flush_paste_buffer_as_keystrokes_sync()
|
|
198
|
+
self._paste_buffer = []
|
|
199
|
+
|
|
200
|
+
# Now handle the current character
|
|
201
|
+
if self._last_char_time > 0:
|
|
202
|
+
gap_ms = (current_time - self._last_char_time) * 1000
|
|
203
|
+
|
|
204
|
+
# If character arrived quickly, start/continue paste buffer
|
|
205
|
+
if gap_ms < self.paste_threshold_ms:
|
|
206
|
+
self._paste_buffer.append(char)
|
|
207
|
+
self._last_char_time = current_time
|
|
208
|
+
return True # Character consumed by paste buffer
|
|
209
|
+
|
|
210
|
+
# Character not part of paste, process normally
|
|
211
|
+
self._last_char_time = current_time
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def _flush_paste_buffer_as_keystrokes_sync(self) -> None:
|
|
215
|
+
"""Process paste buffer contents as individual keystrokes (sync version)."""
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"Flushing {len(self._paste_buffer)} chars as individual keystrokes"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Just add characters to buffer without async processing
|
|
221
|
+
for char in self._paste_buffer:
|
|
222
|
+
if char.isprintable() or char in [" ", "\t"]:
|
|
223
|
+
self.buffer_manager.insert_char(char)
|
|
224
|
+
|
|
225
|
+
def _process_simple_paste_sync(self) -> None:
|
|
226
|
+
"""Process detected paste content (sync version with inline indicator)."""
|
|
227
|
+
if not self._paste_buffer:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Get the content and clean any terminal markers
|
|
231
|
+
content = "".join(self._paste_buffer)
|
|
232
|
+
|
|
233
|
+
# Clean bracketed paste markers if present
|
|
234
|
+
if content.startswith("[200~"):
|
|
235
|
+
content = content[5:]
|
|
236
|
+
if content.endswith("01~"):
|
|
237
|
+
content = content[:-3]
|
|
238
|
+
elif content.endswith("[201~"):
|
|
239
|
+
content = content[:-6]
|
|
240
|
+
|
|
241
|
+
# Count lines
|
|
242
|
+
line_count = content.count("\n") + 1
|
|
243
|
+
char_count = len(content)
|
|
244
|
+
|
|
245
|
+
# Increment paste counter
|
|
246
|
+
self._paste_counter += 1
|
|
247
|
+
|
|
248
|
+
# Create inline paste indicator exactly as user requested
|
|
249
|
+
indicator = f"[Pasted #{self._paste_counter} {line_count} lines]"
|
|
250
|
+
|
|
251
|
+
# Insert the indicator into the buffer at current position
|
|
252
|
+
try:
|
|
253
|
+
for char in indicator:
|
|
254
|
+
self.buffer_manager.insert_char(char)
|
|
255
|
+
logger.info(
|
|
256
|
+
f"Paste #{self._paste_counter}: {char_count} chars, {line_count} lines"
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.error(f"Paste processing error: {e}")
|
|
260
|
+
|
|
261
|
+
# Clear paste buffer
|
|
262
|
+
self._paste_buffer = []
|
|
263
|
+
|
|
264
|
+
async def flush_paste_buffer_as_keystrokes(self) -> None:
|
|
265
|
+
"""Process paste buffer contents as individual keystrokes."""
|
|
266
|
+
self._flush_paste_buffer_as_keystrokes_sync()
|
|
267
|
+
|
|
268
|
+
async def process_simple_paste(self) -> None:
|
|
269
|
+
"""Process detected paste content."""
|
|
270
|
+
self._process_simple_paste_sync()
|
|
271
|
+
if self._display_callback:
|
|
272
|
+
await self._display_callback(force_render=True)
|
|
273
|
+
|
|
274
|
+
# Methods for InputLoopManager to manage paste state during chunk detection
|
|
275
|
+
|
|
276
|
+
def _normalize_line_endings(self, text: str) -> str:
|
|
277
|
+
"""Normalize line endings to Unix style (LF only).
|
|
278
|
+
|
|
279
|
+
Converts Windows (CRLF) and old Mac (CR) line endings to Unix (LF).
|
|
280
|
+
This prevents display issues where CR causes lines to overwrite each other.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
text: Text with potentially mixed line endings.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Text with normalized line endings.
|
|
287
|
+
"""
|
|
288
|
+
# First convert CRLF to LF, then convert remaining CR to LF
|
|
289
|
+
return text.replace('\r\n', '\n').replace('\r', '\n')
|
|
290
|
+
|
|
291
|
+
def start_new_paste(self, chunk: str, current_time: float) -> str:
|
|
292
|
+
"""Start a new paste with the given chunk.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
chunk: The pasted content.
|
|
296
|
+
current_time: Current timestamp.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The paste ID for this paste.
|
|
300
|
+
"""
|
|
301
|
+
self._paste_counter += 1
|
|
302
|
+
self._current_paste_id = f"PASTE_{self._paste_counter}"
|
|
303
|
+
# Normalize line endings to prevent display issues
|
|
304
|
+
self._paste_bucket[self._current_paste_id] = self._normalize_line_endings(chunk)
|
|
305
|
+
self._last_paste_time = current_time
|
|
306
|
+
logger.debug(f"Started new paste: {self._current_paste_id}")
|
|
307
|
+
return self._current_paste_id
|
|
308
|
+
|
|
309
|
+
def append_to_current_paste(self, chunk: str, current_time: float) -> None:
|
|
310
|
+
"""Append content to the current paste being built.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
chunk: Additional content to append.
|
|
314
|
+
current_time: Current timestamp.
|
|
315
|
+
"""
|
|
316
|
+
if self._current_paste_id and self._current_paste_id in self._paste_bucket:
|
|
317
|
+
# Normalize line endings to prevent display issues
|
|
318
|
+
self._paste_bucket[self._current_paste_id] += self._normalize_line_endings(chunk)
|
|
319
|
+
self._last_paste_time = current_time
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"Appended to {self._current_paste_id}: "
|
|
322
|
+
f"now {len(self._paste_bucket[self._current_paste_id])} chars"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def should_merge_paste(self, current_time: float, threshold: float = 0.1) -> bool:
|
|
326
|
+
"""Check if a new chunk should merge with current paste.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
current_time: Current timestamp.
|
|
330
|
+
threshold: Time threshold in seconds for merging (default 0.1s).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
True if the chunk should merge with current paste.
|
|
334
|
+
"""
|
|
335
|
+
return (
|
|
336
|
+
self._current_paste_id is not None
|
|
337
|
+
and self._last_paste_time > 0
|
|
338
|
+
and (current_time - self._last_paste_time) < threshold
|
|
339
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Status modal rendering component.
|
|
2
|
+
|
|
3
|
+
Responsible for generating formatted lines for status modal display.
|
|
4
|
+
This is a pure rendering component with no state management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import List, Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StatusModalRenderer:
|
|
14
|
+
"""Renders status modal content with borders and styling.
|
|
15
|
+
|
|
16
|
+
This component handles the visual presentation of status modals,
|
|
17
|
+
including box borders, content formatting, and optional styling
|
|
18
|
+
when the enhanced input plugin is available.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
renderer: Terminal renderer for accessing terminal state.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, renderer: Any) -> None:
|
|
25
|
+
"""Initialize the status modal renderer.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
renderer: Terminal renderer instance for accessing terminal state.
|
|
29
|
+
"""
|
|
30
|
+
self.renderer = renderer
|
|
31
|
+
|
|
32
|
+
def generate_status_modal_lines(self, ui_config: Any) -> List[str]:
|
|
33
|
+
"""Generate formatted lines for status modal display using visual effects.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
ui_config: UI configuration for the status modal.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of formatted lines for display.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
# Get dynamic terminal width
|
|
43
|
+
terminal_width = getattr(self.renderer.terminal_state, "width", 80)
|
|
44
|
+
# Reserve space for borders and padding (| content | = 4 chars total)
|
|
45
|
+
content_width = terminal_width - 6 # Leave 6 for borders/padding
|
|
46
|
+
max_line_length = content_width - 4 # Additional safety margin
|
|
47
|
+
|
|
48
|
+
content_lines = []
|
|
49
|
+
|
|
50
|
+
# Modal content based on config (no duplicate headers)
|
|
51
|
+
modal_config = ui_config.modal_config or {}
|
|
52
|
+
|
|
53
|
+
if "sections" in modal_config:
|
|
54
|
+
for section in modal_config["sections"]:
|
|
55
|
+
# Skip section title since it's redundant with modal title
|
|
56
|
+
# Display commands directly
|
|
57
|
+
commands = section.get("commands", [])
|
|
58
|
+
for cmd in commands:
|
|
59
|
+
name = cmd.get("name", "")
|
|
60
|
+
description = cmd.get("description", "")
|
|
61
|
+
|
|
62
|
+
# Format command line with better alignment, using dynamic width
|
|
63
|
+
cmd_line = f"{name:<28} {description}"
|
|
64
|
+
if len(cmd_line) > max_line_length:
|
|
65
|
+
cmd_line = cmd_line[: max_line_length - 3] + "..."
|
|
66
|
+
|
|
67
|
+
content_lines.append(cmd_line)
|
|
68
|
+
|
|
69
|
+
# Add spacing before footer
|
|
70
|
+
content_lines.append("")
|
|
71
|
+
|
|
72
|
+
# Modal footer with special styling marker
|
|
73
|
+
footer = modal_config.get(
|
|
74
|
+
"footer",
|
|
75
|
+
"Press Esc to close . Use /help <command> for detailed help",
|
|
76
|
+
)
|
|
77
|
+
content_lines.append(f"__FOOTER__{footer}")
|
|
78
|
+
|
|
79
|
+
# Clean content lines for box rendering (no ANSI codes)
|
|
80
|
+
clean_content = []
|
|
81
|
+
for line in content_lines:
|
|
82
|
+
if line.startswith("__FOOTER__"):
|
|
83
|
+
footer_text = line.replace("__FOOTER__", "")
|
|
84
|
+
clean_content.append(footer_text)
|
|
85
|
+
else:
|
|
86
|
+
clean_content.append(line)
|
|
87
|
+
|
|
88
|
+
# Use BoxRenderer from enhanced input plugin if available
|
|
89
|
+
try:
|
|
90
|
+
from ...plugins.enhanced_input.box_renderer import BoxRenderer
|
|
91
|
+
from ...plugins.enhanced_input.box_styles import BoxStyleRegistry
|
|
92
|
+
from ...plugins.enhanced_input.color_engine import ColorEngine
|
|
93
|
+
from ...plugins.enhanced_input.geometry import GeometryCalculator
|
|
94
|
+
from ...plugins.enhanced_input.text_processor import TextProcessor
|
|
95
|
+
|
|
96
|
+
# Initialize components
|
|
97
|
+
style_registry = BoxStyleRegistry()
|
|
98
|
+
color_engine = ColorEngine()
|
|
99
|
+
geometry = GeometryCalculator()
|
|
100
|
+
text_processor = TextProcessor()
|
|
101
|
+
box_renderer = BoxRenderer(
|
|
102
|
+
style_registry, color_engine, geometry, text_processor
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Render with clean rounded style first, using dynamic width
|
|
106
|
+
bordered_lines = box_renderer.render_box(
|
|
107
|
+
clean_content, content_width, "rounded"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Add title to top border
|
|
111
|
+
title = ui_config.title or "Status Modal"
|
|
112
|
+
if bordered_lines:
|
|
113
|
+
_ = bordered_lines[0]
|
|
114
|
+
# Create title border: ╭─ Title ─────...─╮
|
|
115
|
+
title_text = f"─ {title} "
|
|
116
|
+
remaining_width = max(
|
|
117
|
+
0, content_width - 2 - len(title_text)
|
|
118
|
+
) # content_width - 2 border chars - title length
|
|
119
|
+
titled_border = f"╭{title_text}{'─' * remaining_width}╮"
|
|
120
|
+
bordered_lines[0] = titled_border
|
|
121
|
+
|
|
122
|
+
# Apply styling to content lines after border rendering
|
|
123
|
+
styled_lines = []
|
|
124
|
+
for i, line in enumerate(bordered_lines):
|
|
125
|
+
if i == 0 or i == len(bordered_lines) - 1:
|
|
126
|
+
# Border lines - keep as is
|
|
127
|
+
styled_lines.append(line)
|
|
128
|
+
elif line.strip() and "│" in line:
|
|
129
|
+
# Content lines with borders
|
|
130
|
+
if any(
|
|
131
|
+
footer in line for footer in ["Press Esc", "Use /help"]
|
|
132
|
+
):
|
|
133
|
+
# Footer line - apply cyan
|
|
134
|
+
styled_line = line.replace("│", "│\033[2;36m", 1)
|
|
135
|
+
styled_line = styled_line.replace("│", "\033[0m│", -1)
|
|
136
|
+
styled_lines.append(styled_line)
|
|
137
|
+
elif line.strip() != "│" + " " * 76 + "│": # Not empty line
|
|
138
|
+
# Command line - apply dim
|
|
139
|
+
styled_line = line.replace("│", "│\033[2m", 1)
|
|
140
|
+
styled_line = styled_line.replace("│", "\033[0m│", -1)
|
|
141
|
+
styled_lines.append(styled_line)
|
|
142
|
+
else:
|
|
143
|
+
# Empty line
|
|
144
|
+
styled_lines.append(line)
|
|
145
|
+
else:
|
|
146
|
+
styled_lines.append(line)
|
|
147
|
+
|
|
148
|
+
return styled_lines
|
|
149
|
+
|
|
150
|
+
except ImportError:
|
|
151
|
+
# Fallback to simple manual borders if enhanced input not available
|
|
152
|
+
return self._create_simple_bordered_content(clean_content)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Error generating status modal lines: {e}")
|
|
156
|
+
return [f"Error displaying status modal: {e}"]
|
|
157
|
+
|
|
158
|
+
def _create_simple_bordered_content(self, content_lines: List[str]) -> List[str]:
|
|
159
|
+
"""Create simple bordered content as fallback.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
content_lines: Content lines to border.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Lines with simple borders.
|
|
166
|
+
"""
|
|
167
|
+
# Get dynamic terminal width
|
|
168
|
+
terminal_width = getattr(self.renderer.terminal_state, "width", 80)
|
|
169
|
+
# Reserve space for borders and padding
|
|
170
|
+
width = terminal_width - 6 # Leave 6 for borders/padding
|
|
171
|
+
lines = []
|
|
172
|
+
|
|
173
|
+
# Simple top border
|
|
174
|
+
lines.append("╭" + "─" * (width + 2) + "╮")
|
|
175
|
+
|
|
176
|
+
# Content with side borders
|
|
177
|
+
for line in content_lines:
|
|
178
|
+
padded_line = f"{line:<{width}}"
|
|
179
|
+
lines.append(f"│ {padded_line} │")
|
|
180
|
+
|
|
181
|
+
# Simple bottom border
|
|
182
|
+
lines.append("╰" + "─" * (width + 2) + "╯")
|
|
183
|
+
|
|
184
|
+
return lines
|
core/io/input_errors.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
"""Error handling and recovery mechanisms for input processing.
|
|
1
|
+
"""Error handling and recovery mechanisms for input processing.
|
|
2
|
+
|
|
3
|
+
This module provides centralized error handling, error recovery strategies,
|
|
4
|
+
and error storm detection for the input processing subsystem.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
import asyncio
|
|
4
8
|
import logging
|