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.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {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