pomera-ai-commander 0.1.0

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. package/LICENSE +21 -0
  2. package/README.md +680 -0
  3. package/bin/pomera-ai-commander.js +62 -0
  4. package/core/__init__.py +66 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  7. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  8. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  9. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  10. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  11. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  12. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  13. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  14. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  15. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  16. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  17. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  18. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  19. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  20. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  21. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  22. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  23. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  24. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  25. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  26. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  27. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  28. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  29. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  30. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  31. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  32. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  33. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  34. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  35. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  36. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  37. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  38. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  39. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  40. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  41. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  42. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  43. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  44. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  45. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  46. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  47. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  48. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  49. package/core/app_context.py +482 -0
  50. package/core/async_text_processor.py +422 -0
  51. package/core/backup_manager.py +656 -0
  52. package/core/backup_recovery_manager.py +1034 -0
  53. package/core/content_hash_cache.py +509 -0
  54. package/core/context_menu.py +313 -0
  55. package/core/data_validator.py +1067 -0
  56. package/core/database_connection_manager.py +745 -0
  57. package/core/database_curl_settings_manager.py +609 -0
  58. package/core/database_promera_ai_settings_manager.py +447 -0
  59. package/core/database_schema.py +412 -0
  60. package/core/database_schema_manager.py +396 -0
  61. package/core/database_settings_manager.py +1508 -0
  62. package/core/database_settings_manager_interface.py +457 -0
  63. package/core/dialog_manager.py +735 -0
  64. package/core/efficient_line_numbers.py +511 -0
  65. package/core/error_handler.py +747 -0
  66. package/core/error_service.py +431 -0
  67. package/core/event_consolidator.py +512 -0
  68. package/core/mcp/__init__.py +43 -0
  69. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  70. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  71. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  72. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  73. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  74. package/core/mcp/protocol.py +288 -0
  75. package/core/mcp/schema.py +251 -0
  76. package/core/mcp/server_stdio.py +299 -0
  77. package/core/mcp/tool_registry.py +2345 -0
  78. package/core/memory_efficient_text_widget.py +712 -0
  79. package/core/migration_manager.py +915 -0
  80. package/core/migration_test_suite.py +1086 -0
  81. package/core/migration_validator.py +1144 -0
  82. package/core/optimized_find_replace.py +715 -0
  83. package/core/optimized_pattern_engine.py +424 -0
  84. package/core/optimized_search_highlighter.py +553 -0
  85. package/core/performance_monitor.py +675 -0
  86. package/core/persistence_manager.py +713 -0
  87. package/core/progressive_stats_calculator.py +632 -0
  88. package/core/regex_pattern_cache.py +530 -0
  89. package/core/regex_pattern_library.py +351 -0
  90. package/core/search_operation_manager.py +435 -0
  91. package/core/settings_defaults_registry.py +1087 -0
  92. package/core/settings_integrity_validator.py +1112 -0
  93. package/core/settings_serializer.py +558 -0
  94. package/core/settings_validator.py +1824 -0
  95. package/core/smart_stats_calculator.py +710 -0
  96. package/core/statistics_update_manager.py +619 -0
  97. package/core/stats_config_manager.py +858 -0
  98. package/core/streaming_text_handler.py +723 -0
  99. package/core/task_scheduler.py +596 -0
  100. package/core/update_pattern_library.py +169 -0
  101. package/core/visibility_monitor.py +596 -0
  102. package/core/widget_cache.py +498 -0
  103. package/mcp.json +61 -0
  104. package/package.json +57 -0
  105. package/pomera.py +7483 -0
  106. package/pomera_mcp_server.py +144 -0
  107. package/tools/__init__.py +5 -0
  108. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  110. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  111. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  112. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  113. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  114. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  115. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  116. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  117. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  118. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  119. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  120. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  121. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  122. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  123. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  124. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  125. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  126. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  127. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  128. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  129. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  130. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  131. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  132. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  133. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  134. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  135. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  136. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  137. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  138. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  139. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  140. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  141. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  142. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  143. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  144. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  145. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  146. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  147. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  148. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
  151. package/tools/ai_tools.py +2892 -0
  152. package/tools/ascii_art_generator.py +353 -0
  153. package/tools/base64_tools.py +184 -0
  154. package/tools/base_tool.py +511 -0
  155. package/tools/case_tool.py +309 -0
  156. package/tools/column_tools.py +396 -0
  157. package/tools/cron_tool.py +885 -0
  158. package/tools/curl_history.py +601 -0
  159. package/tools/curl_processor.py +1208 -0
  160. package/tools/curl_settings.py +503 -0
  161. package/tools/curl_tool.py +5467 -0
  162. package/tools/diff_viewer.py +1072 -0
  163. package/tools/email_extraction_tool.py +249 -0
  164. package/tools/email_header_analyzer.py +426 -0
  165. package/tools/extraction_tools.py +250 -0
  166. package/tools/find_replace.py +1751 -0
  167. package/tools/folder_file_reporter.py +1463 -0
  168. package/tools/folder_file_reporter_adapter.py +480 -0
  169. package/tools/generator_tools.py +1217 -0
  170. package/tools/hash_generator.py +256 -0
  171. package/tools/html_tool.py +657 -0
  172. package/tools/huggingface_helper.py +449 -0
  173. package/tools/jsonxml_tool.py +730 -0
  174. package/tools/line_tools.py +419 -0
  175. package/tools/list_comparator.py +720 -0
  176. package/tools/markdown_tools.py +562 -0
  177. package/tools/mcp_widget.py +1417 -0
  178. package/tools/notes_widget.py +973 -0
  179. package/tools/number_base_converter.py +373 -0
  180. package/tools/regex_extractor.py +572 -0
  181. package/tools/slug_generator.py +311 -0
  182. package/tools/sorter_tools.py +459 -0
  183. package/tools/string_escape_tool.py +393 -0
  184. package/tools/text_statistics_tool.py +366 -0
  185. package/tools/text_wrapper.py +431 -0
  186. package/tools/timestamp_converter.py +422 -0
  187. package/tools/tool_loader.py +710 -0
  188. package/tools/translator_tools.py +523 -0
  189. package/tools/url_link_extractor.py +262 -0
  190. package/tools/url_parser.py +205 -0
  191. package/tools/whitespace_tools.py +356 -0
  192. package/tools/word_frequency_counter.py +147 -0
@@ -0,0 +1,512 @@
1
+ """
2
+ Event Consolidation System for Pomera AI Commander.
3
+
4
+ This module provides intelligent event consolidation to replace multiple event handlers
5
+ with a single consolidated handler, implementing adaptive debouncing and event deduplication
6
+ to optimize statistics calculation performance.
7
+
8
+ Requirements addressed:
9
+ - 1.1: Consolidate multiple event triggers into single statistics update
10
+ - 1.2: Use intelligent debouncing to prevent excessive calculations
11
+ - 1.3: Increase debounce delays automatically for large content (>10,000 characters)
12
+ - 1.4: Prevent duplicate statistics calculations
13
+ """
14
+
15
+ import time
16
+ import threading
17
+ import tkinter as tk
18
+ from typing import Dict, Optional, Callable, Any, Set
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ import weakref
22
+
23
+
24
+ class EventType(Enum):
25
+ """Types of text widget events that trigger statistics updates."""
26
+ MODIFIED = "<<Modified>>"
27
+ KEY_RELEASE = "<KeyRelease>"
28
+ BUTTON_CLICK = "<Button-1>"
29
+ FOCUS_IN = "<FocusIn>"
30
+ FOCUS_OUT = "<FocusOut>"
31
+
32
+
33
+ class DebounceStrategy(Enum):
34
+ """Debouncing strategies for different content sizes and user behaviors."""
35
+ IMMEDIATE = "immediate" # No debouncing for very small changes
36
+ FAST = "fast" # 50ms for small content
37
+ NORMAL = "normal" # 300ms for medium content
38
+ SLOW = "slow" # 500ms for large content
39
+ ADAPTIVE = "adaptive" # Automatically adjust based on content size
40
+
41
+
42
+ @dataclass
43
+ class EventInfo:
44
+ """Information about a text widget event."""
45
+ widget_id: str
46
+ event_type: EventType
47
+ timestamp: float
48
+ content_size: int = 0
49
+ content_hash: str = ""
50
+
51
+ def __post_init__(self):
52
+ if not self.timestamp:
53
+ self.timestamp = time.time()
54
+
55
+
56
+ @dataclass
57
+ class DebounceConfig:
58
+ """Configuration for debouncing behavior."""
59
+ strategy: DebounceStrategy = DebounceStrategy.ADAPTIVE
60
+ immediate_threshold: int = 100 # Characters below which updates are immediate
61
+ fast_delay_ms: int = 50 # Delay for small content
62
+ normal_delay_ms: int = 300 # Delay for medium content
63
+ slow_delay_ms: int = 500 # Delay for large content
64
+ large_content_threshold: int = 10000 # Characters above which content is considered large
65
+ very_large_threshold: int = 100000 # Characters above which extra delays apply
66
+ max_delay_ms: int = 1000 # Maximum debounce delay
67
+
68
+ def get_delay_for_content_size(self, content_size: int) -> int:
69
+ """Get appropriate debounce delay based on content size."""
70
+ if self.strategy == DebounceStrategy.IMMEDIATE:
71
+ return 0
72
+ elif self.strategy == DebounceStrategy.FAST:
73
+ return self.fast_delay_ms
74
+ elif self.strategy == DebounceStrategy.NORMAL:
75
+ return self.normal_delay_ms
76
+ elif self.strategy == DebounceStrategy.SLOW:
77
+ return self.slow_delay_ms
78
+ elif self.strategy == DebounceStrategy.ADAPTIVE:
79
+ if content_size < self.immediate_threshold:
80
+ return 0
81
+ elif content_size < 1000:
82
+ return self.fast_delay_ms
83
+ elif content_size < self.large_content_threshold:
84
+ return self.normal_delay_ms
85
+ elif content_size < self.very_large_threshold:
86
+ return self.slow_delay_ms
87
+ else:
88
+ # Extra delay for very large content
89
+ return min(self.max_delay_ms, self.slow_delay_ms + 200)
90
+
91
+ return self.normal_delay_ms
92
+
93
+
94
+ @dataclass
95
+ class PendingUpdate:
96
+ """Information about a pending statistics update."""
97
+ widget_id: str
98
+ event_info: EventInfo
99
+ callback: Callable
100
+ after_id: Optional[str] = None
101
+ scheduled_time: float = field(default_factory=time.time)
102
+
103
+ @property
104
+ def is_expired(self) -> bool:
105
+ """Check if this update has been waiting too long."""
106
+ return (time.time() - self.scheduled_time) > 2.0 # 2 second timeout
107
+
108
+
109
+ class EventConsolidator:
110
+ """
111
+ Consolidates multiple text widget events into single statistics updates.
112
+
113
+ This class replaces multiple event handlers (<<Modified>>, <KeyRelease>, <Button-1>)
114
+ with a single consolidated handler that implements intelligent debouncing and
115
+ event deduplication to optimize performance.
116
+ """
117
+
118
+ def __init__(self, debounce_config: Optional[DebounceConfig] = None):
119
+ """
120
+ Initialize the event consolidator.
121
+
122
+ Args:
123
+ debounce_config: Configuration for debouncing behavior
124
+ """
125
+ self.debounce_config = debounce_config or DebounceConfig()
126
+
127
+ # Widget registry - use weak references to avoid memory leaks
128
+ self.registered_widgets: Dict[str, weakref.ref] = {}
129
+ self.widget_callbacks: Dict[str, Callable] = {}
130
+
131
+ # Event tracking for deduplication
132
+ self.recent_events: Dict[str, EventInfo] = {}
133
+ self.event_lock = threading.RLock()
134
+
135
+ # Pending updates tracking
136
+ self.pending_updates: Dict[str, PendingUpdate] = {}
137
+
138
+ # Statistics for monitoring
139
+ self.stats = {
140
+ 'events_received': 0,
141
+ 'events_deduplicated': 0,
142
+ 'updates_triggered': 0,
143
+ 'average_debounce_delay': 0.0
144
+ }
145
+
146
+ # Tkinter root reference for after() calls
147
+ self._tk_root: Optional[tk.Tk] = None
148
+
149
+ def set_tk_root(self, root: tk.Tk):
150
+ """Set the Tkinter root for scheduling callbacks."""
151
+ self._tk_root = root
152
+
153
+ def register_text_widget(self, widget_id: str, widget: tk.Text,
154
+ callback: Callable[[str], None]) -> None:
155
+ """
156
+ Register a text widget for consolidated event handling.
157
+
158
+ Args:
159
+ widget_id: Unique identifier for the widget
160
+ widget: The text widget to monitor
161
+ callback: Function to call when statistics should be updated
162
+ """
163
+ # Store weak reference to widget to avoid memory leaks
164
+ self.registered_widgets[widget_id] = weakref.ref(widget)
165
+ self.widget_callbacks[widget_id] = callback
166
+
167
+ # Bind single consolidated event handler
168
+ self._bind_consolidated_events(widget, widget_id)
169
+
170
+ def unregister_text_widget(self, widget_id: str) -> None:
171
+ """
172
+ Unregister a text widget and cancel any pending updates.
173
+
174
+ Args:
175
+ widget_id: Identifier of the widget to unregister
176
+ """
177
+ # Cancel pending updates
178
+ self.cancel_pending_updates(widget_id)
179
+
180
+ # Remove from registries
181
+ self.registered_widgets.pop(widget_id, None)
182
+ self.widget_callbacks.pop(widget_id, None)
183
+ self.recent_events.pop(widget_id, None)
184
+
185
+ def _bind_consolidated_events(self, widget: tk.Text, widget_id: str) -> None:
186
+ """
187
+ Bind consolidated event handler to replace multiple event bindings.
188
+
189
+ This replaces the multiple event bindings (<<Modified>>, <KeyRelease>, <Button-1>)
190
+ with a single handler per widget as required.
191
+ """
192
+ # Create consolidated event handler
193
+ def consolidated_handler(event=None):
194
+ event_type = self._determine_event_type(event)
195
+ self.handle_text_event(widget_id, event_type, event)
196
+ return None # Don't break event propagation
197
+
198
+ # Bind to all relevant events with single handler
199
+ widget.bind("<<Modified>>", consolidated_handler, add=True)
200
+ widget.bind("<KeyRelease>", consolidated_handler, add=True)
201
+ widget.bind("<Button-1>", consolidated_handler, add=True)
202
+
203
+ # Optional: Also handle focus events for better user experience
204
+ widget.bind("<FocusIn>", consolidated_handler, add=True)
205
+ widget.bind("<FocusOut>", consolidated_handler, add=True)
206
+
207
+ def _determine_event_type(self, event) -> EventType:
208
+ """Determine the type of event from the event object."""
209
+ if not event:
210
+ return EventType.MODIFIED
211
+
212
+ event_str = str(event.type) if hasattr(event, 'type') else str(event)
213
+
214
+ if "KeyRelease" in event_str or hasattr(event, 'keysym'):
215
+ return EventType.KEY_RELEASE
216
+ elif "Button" in event_str and hasattr(event, 'num'):
217
+ return EventType.BUTTON_CLICK
218
+ elif "FocusIn" in event_str:
219
+ return EventType.FOCUS_IN
220
+ elif "FocusOut" in event_str:
221
+ return EventType.FOCUS_OUT
222
+ else:
223
+ return EventType.MODIFIED
224
+
225
+ def handle_text_event(self, widget_id: str, event_type: EventType,
226
+ event=None) -> None:
227
+ """
228
+ Handle consolidated text widget events with intelligent deduplication.
229
+
230
+ Args:
231
+ widget_id: Identifier of the widget that triggered the event
232
+ event_type: Type of event that occurred
233
+ event: Original event object (optional)
234
+ """
235
+ with self.event_lock:
236
+ self.stats['events_received'] += 1
237
+
238
+ # Get widget reference
239
+ widget_ref = self.registered_widgets.get(widget_id)
240
+ if not widget_ref:
241
+ return
242
+
243
+ widget = widget_ref()
244
+ if not widget:
245
+ # Widget was garbage collected, clean up
246
+ self.unregister_text_widget(widget_id)
247
+ return
248
+
249
+ # Get current content for analysis
250
+ try:
251
+ content = widget.get("1.0", tk.END)
252
+ content_size = len(content.encode('utf-8'))
253
+ content_hash = self._generate_content_hash(content)
254
+ except tk.TclError:
255
+ # Widget might be destroyed
256
+ return
257
+
258
+ # Create event info
259
+ event_info = EventInfo(
260
+ widget_id=widget_id,
261
+ event_type=event_type,
262
+ timestamp=time.time(),
263
+ content_size=content_size,
264
+ content_hash=content_hash
265
+ )
266
+
267
+ # Check for duplicate events
268
+ if self._is_duplicate_event(widget_id, event_info):
269
+ self.stats['events_deduplicated'] += 1
270
+ return
271
+
272
+ # Store recent event for deduplication
273
+ self.recent_events[widget_id] = event_info
274
+
275
+ # Schedule update with appropriate debouncing
276
+ self._schedule_update(widget_id, event_info)
277
+
278
+ def _is_duplicate_event(self, widget_id: str, event_info: EventInfo) -> bool:
279
+ """
280
+ Check if this event is a duplicate that should be ignored.
281
+
282
+ Args:
283
+ widget_id: Widget identifier
284
+ event_info: Information about the current event
285
+
286
+ Returns:
287
+ True if this is a duplicate event that should be ignored
288
+ """
289
+ recent_event = self.recent_events.get(widget_id)
290
+ if not recent_event:
291
+ return False
292
+
293
+ # Check if content hasn't changed
294
+ if recent_event.content_hash == event_info.content_hash:
295
+ # Same content - check if enough time has passed
296
+ time_diff = event_info.timestamp - recent_event.timestamp
297
+ if time_diff < 0.1: # Less than 100ms
298
+ return True
299
+
300
+ # Check for rapid successive events of the same type
301
+ if (recent_event.event_type == event_info.event_type and
302
+ event_info.timestamp - recent_event.timestamp < 0.05): # Less than 50ms
303
+ return True
304
+
305
+ return False
306
+
307
+ def _schedule_update(self, widget_id: str, event_info: EventInfo) -> None:
308
+ """
309
+ Schedule a statistics update with appropriate debouncing.
310
+
311
+ Args:
312
+ widget_id: Widget identifier
313
+ event_info: Information about the event
314
+ """
315
+ # Cancel any existing pending update for this widget
316
+ self.cancel_pending_updates(widget_id)
317
+
318
+ # Get callback
319
+ callback = self.widget_callbacks.get(widget_id)
320
+ if not callback:
321
+ return
322
+
323
+ # Determine debounce delay
324
+ delay_ms = self.debounce_config.get_delay_for_content_size(event_info.content_size)
325
+
326
+ # Update statistics
327
+ self.stats['average_debounce_delay'] = (
328
+ (self.stats['average_debounce_delay'] * self.stats['updates_triggered'] + delay_ms) /
329
+ (self.stats['updates_triggered'] + 1)
330
+ )
331
+
332
+ if delay_ms == 0:
333
+ # Immediate update
334
+ self._execute_update(widget_id, callback)
335
+ else:
336
+ # Debounced update
337
+ if self._tk_root:
338
+ after_id = self._tk_root.after(delay_ms,
339
+ lambda: self._execute_update(widget_id, callback))
340
+
341
+ # Track pending update
342
+ self.pending_updates[widget_id] = PendingUpdate(
343
+ widget_id=widget_id,
344
+ event_info=event_info,
345
+ callback=callback,
346
+ after_id=after_id
347
+ )
348
+
349
+ def _execute_update(self, widget_id: str, callback: Callable) -> None:
350
+ """
351
+ Execute the statistics update callback.
352
+
353
+ Args:
354
+ widget_id: Widget identifier
355
+ callback: Callback function to execute
356
+ """
357
+ try:
358
+ # Remove from pending updates
359
+ self.pending_updates.pop(widget_id, None)
360
+
361
+ # Execute callback
362
+ callback(widget_id)
363
+
364
+ self.stats['updates_triggered'] += 1
365
+
366
+ except Exception as e:
367
+ # Log error if possible, but don't crash
368
+ print(f"Error executing update callback for {widget_id}: {e}")
369
+
370
+ def cancel_pending_updates(self, widget_id: str) -> None:
371
+ """
372
+ Cancel any pending updates for a specific widget.
373
+
374
+ Args:
375
+ widget_id: Widget identifier
376
+ """
377
+ pending = self.pending_updates.get(widget_id)
378
+ if pending and pending.after_id and self._tk_root:
379
+ try:
380
+ self._tk_root.after_cancel(pending.after_id)
381
+ except tk.TclError:
382
+ pass # after_id might be invalid
383
+
384
+ self.pending_updates.pop(widget_id, None)
385
+
386
+ def cancel_all_pending_updates(self) -> None:
387
+ """Cancel all pending updates."""
388
+ for widget_id in list(self.pending_updates.keys()):
389
+ self.cancel_pending_updates(widget_id)
390
+
391
+ def set_debounce_strategy(self, strategy: DebounceStrategy) -> None:
392
+ """
393
+ Set the debouncing strategy for all widgets.
394
+
395
+ Args:
396
+ strategy: New debouncing strategy to use
397
+ """
398
+ self.debounce_config.strategy = strategy
399
+
400
+ def set_debounce_config(self, config: DebounceConfig) -> None:
401
+ """
402
+ Set a new debounce configuration.
403
+
404
+ Args:
405
+ config: New debounce configuration
406
+ """
407
+ self.debounce_config = config
408
+
409
+ def get_statistics(self) -> Dict[str, Any]:
410
+ """
411
+ Get event consolidation statistics.
412
+
413
+ Returns:
414
+ Dictionary with statistics about event handling
415
+ """
416
+ with self.event_lock:
417
+ stats = self.stats.copy()
418
+ stats.update({
419
+ 'registered_widgets': len(self.registered_widgets),
420
+ 'pending_updates': len(self.pending_updates),
421
+ 'recent_events_tracked': len(self.recent_events),
422
+ 'deduplication_rate': (
423
+ self.stats['events_deduplicated'] / max(1, self.stats['events_received'])
424
+ ) * 100
425
+ })
426
+ return stats
427
+
428
+ def cleanup_expired_updates(self) -> None:
429
+ """Clean up any expired pending updates."""
430
+ expired_widgets = []
431
+
432
+ for widget_id, pending in self.pending_updates.items():
433
+ if pending.is_expired:
434
+ expired_widgets.append(widget_id)
435
+
436
+ for widget_id in expired_widgets:
437
+ self.cancel_pending_updates(widget_id)
438
+
439
+ def _generate_content_hash(self, content: str) -> str:
440
+ """Generate a simple hash for content comparison."""
441
+ # Simple hash based on length and first/last characters
442
+ if not content:
443
+ return "empty"
444
+
445
+ content_clean = content.strip()
446
+ if not content_clean:
447
+ return "whitespace"
448
+
449
+ # Create hash from length + first 10 + last 10 characters
450
+ first_part = content_clean[:10] if len(content_clean) >= 10 else content_clean
451
+ last_part = content_clean[-10:] if len(content_clean) >= 20 else ""
452
+
453
+ return f"{len(content_clean)}_{hash(first_part + last_part) % 10000}"
454
+
455
+ def get_widget_info(self, widget_id: str) -> Optional[Dict[str, Any]]:
456
+ """
457
+ Get information about a registered widget.
458
+
459
+ Args:
460
+ widget_id: Widget identifier
461
+
462
+ Returns:
463
+ Dictionary with widget information or None if not found
464
+ """
465
+ if widget_id not in self.registered_widgets:
466
+ return None
467
+
468
+ widget_ref = self.registered_widgets[widget_id]
469
+ widget = widget_ref() if widget_ref else None
470
+
471
+ recent_event = self.recent_events.get(widget_id)
472
+ pending_update = self.pending_updates.get(widget_id)
473
+
474
+ return {
475
+ 'widget_id': widget_id,
476
+ 'widget_exists': widget is not None,
477
+ 'has_callback': widget_id in self.widget_callbacks,
478
+ 'recent_event': {
479
+ 'timestamp': recent_event.timestamp if recent_event else None,
480
+ 'event_type': recent_event.event_type.value if recent_event else None,
481
+ 'content_size': recent_event.content_size if recent_event else 0
482
+ } if recent_event else None,
483
+ 'pending_update': {
484
+ 'scheduled_time': pending_update.scheduled_time,
485
+ 'is_expired': pending_update.is_expired
486
+ } if pending_update else None
487
+ }
488
+
489
+
490
+ # Global instance for easy access
491
+ _global_event_consolidator: Optional[EventConsolidator] = None
492
+
493
+
494
+ def get_event_consolidator() -> EventConsolidator:
495
+ """Get the global event consolidator instance."""
496
+ global _global_event_consolidator
497
+ if _global_event_consolidator is None:
498
+ _global_event_consolidator = EventConsolidator()
499
+ return _global_event_consolidator
500
+
501
+
502
+ def create_event_consolidator(debounce_config: Optional[DebounceConfig] = None) -> EventConsolidator:
503
+ """
504
+ Create a new event consolidator instance.
505
+
506
+ Args:
507
+ debounce_config: Optional debounce configuration
508
+
509
+ Returns:
510
+ New EventConsolidator instance
511
+ """
512
+ return EventConsolidator(debounce_config)
@@ -0,0 +1,43 @@
1
+ """
2
+ MCP (Model Context Protocol) Module for Pomera AI Commander
3
+
4
+ This module provides bidirectional MCP functionality:
5
+ 1. MCP Client - Connect to external MCP servers (filesystem, GitHub, etc.)
6
+ 2. MCP Server - Expose Pomera's text tools to external AI assistants
7
+
8
+ Submodules:
9
+ - schema: Data classes for MCP types (Tool, Resource, Message)
10
+ - protocol: JSON-RPC 2.0 message handling
11
+ - tool_registry: Maps Pomera tools to MCP tool definitions
12
+ - server_stdio: stdio transport for MCP server
13
+ - resource_provider: Exposes tab contents as MCP resources
14
+ """
15
+
16
+ from .schema import (
17
+ MCPMessage,
18
+ MCPTool,
19
+ MCPResource,
20
+ MCPError,
21
+ MCPErrorCode,
22
+ )
23
+
24
+ from .protocol import MCPProtocol
25
+
26
+ from .tool_registry import ToolRegistry, MCPToolAdapter
27
+
28
+ __all__ = [
29
+ # Schema
30
+ "MCPMessage",
31
+ "MCPTool",
32
+ "MCPResource",
33
+ "MCPError",
34
+ "MCPErrorCode",
35
+ # Protocol
36
+ "MCPProtocol",
37
+ # Tool Registry
38
+ "ToolRegistry",
39
+ "MCPToolAdapter",
40
+ ]
41
+
42
+ __version__ = "0.1.0"
43
+