pomera-ai-commander 0.1.0 → 1.2.1

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 (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,619 +1,619 @@
1
- """
2
- Statistics Update Manager for Pomera AI Commander.
3
-
4
- This module provides visibility-aware statistics update coordination to optimize
5
- performance by skipping updates for hidden tabs, inactive components, and minimized windows.
6
-
7
- Requirements addressed:
8
- - 3.1: Skip statistics updates for inactive tabs
9
- - 3.2: Pause statistics updates when application window is minimized
10
- - 3.3: Skip calculations when statistics bars are not visible
11
- - 3.4: Reduce update frequency when user is idle
12
- """
13
-
14
- import time
15
- import threading
16
- import tkinter as tk
17
- from typing import Dict, Optional, Callable, Any, List, Set
18
- from dataclasses import dataclass, field
19
- from enum import Enum
20
- from collections import deque
21
- import weakref
22
-
23
-
24
- class UpdatePriority(Enum):
25
- """Priority levels for statistics updates."""
26
- IMMEDIATE = 0 # Update immediately (user just switched to this tab)
27
- HIGH = 1 # Update soon (active tab, user typing)
28
- NORMAL = 2 # Update when convenient (visible but not active)
29
- LOW = 3 # Update when idle (background tab)
30
- DEFERRED = 4 # Update only when explicitly requested
31
-
32
-
33
- class VisibilityState(Enum):
34
- """Visibility states for components."""
35
- VISIBLE_ACTIVE = "visible_active" # Currently visible and active
36
- VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
37
- HIDDEN = "hidden" # Not visible (hidden tab)
38
- MINIMIZED = "minimized" # Window is minimized
39
- UNKNOWN = "unknown" # State not yet determined
40
-
41
-
42
- @dataclass
43
- class UpdateRequest:
44
- """Request for a statistics update."""
45
- widget_id: str
46
- priority: UpdatePriority
47
- timestamp: float = field(default_factory=time.time)
48
- content_hash: str = ""
49
- visibility_state: VisibilityState = VisibilityState.UNKNOWN
50
- callback: Optional[Callable] = None
51
-
52
- def __post_init__(self):
53
- if not self.timestamp:
54
- self.timestamp = time.time()
55
-
56
- @property
57
- def age_seconds(self) -> float:
58
- """Get the age of this request in seconds."""
59
- return time.time() - self.timestamp
60
-
61
- @property
62
- def is_expired(self) -> bool:
63
- """Check if this request has expired (older than 5 seconds)."""
64
- return self.age_seconds > 5.0
65
-
66
- def should_batch_with(self, other: 'UpdateRequest') -> bool:
67
- """Check if this request can be batched with another."""
68
- # Can batch if same widget and similar priority
69
- if self.widget_id != other.widget_id:
70
- return False
71
-
72
- # Can batch if priorities are within 1 level
73
- return abs(self.priority.value - other.priority.value) <= 1
74
-
75
-
76
- @dataclass
77
- class ComponentInfo:
78
- """Information about a component being monitored."""
79
- component_id: str
80
- widget_ref: weakref.ref
81
- visibility_state: VisibilityState = VisibilityState.UNKNOWN
82
- last_update_time: float = 0.0
83
- update_count: int = 0
84
- skip_count: int = 0
85
-
86
- @property
87
- def widget(self):
88
- """Get the actual widget from the weak reference."""
89
- return self.widget_ref() if self.widget_ref else None
90
-
91
- @property
92
- def is_valid(self) -> bool:
93
- """Check if the widget still exists."""
94
- return self.widget is not None
95
-
96
- @property
97
- def time_since_last_update(self) -> float:
98
- """Get time since last update in seconds."""
99
- return time.time() - self.last_update_time if self.last_update_time > 0 else float('inf')
100
-
101
-
102
- class StatisticsUpdateManager:
103
- """
104
- Manages statistics updates with visibility awareness and performance optimization.
105
-
106
- This class coordinates statistics updates across multiple widgets, implementing
107
- visibility-aware updates that skip hidden tabs and inactive components, automatic
108
- pause during window minimization, and priority-based update queuing.
109
- """
110
-
111
- def __init__(self):
112
- """Initialize the statistics update manager."""
113
- # Component registry
114
- self.components: Dict[str, ComponentInfo] = {}
115
-
116
- # Update queue organized by priority
117
- self.update_queues: Dict[UpdatePriority, deque] = {
118
- priority: deque() for priority in UpdatePriority
119
- }
120
-
121
- # Global state
122
- self.paused = False
123
- self.window_minimized = False
124
- self.user_idle = False
125
- self.last_user_activity = time.time()
126
-
127
- # Configuration
128
- self.idle_threshold_seconds = 5.0
129
- self.min_update_interval_ms = 100 # Minimum time between updates for same widget
130
-
131
- # Statistics
132
- self.stats = {
133
- 'updates_requested': 0,
134
- 'updates_executed': 0,
135
- 'updates_skipped_visibility': 0,
136
- 'updates_skipped_paused': 0,
137
- 'updates_batched': 0
138
- }
139
-
140
- # Thread safety
141
- self.lock = threading.RLock()
142
-
143
- # Tkinter root reference
144
- self._tk_root: Optional[tk.Tk] = None
145
-
146
- # Processing state
147
- self._processing_queue = False
148
- self._queue_process_after_id: Optional[str] = None
149
-
150
- def set_tk_root(self, root: tk.Tk):
151
- """Set the Tkinter root for scheduling callbacks."""
152
- self._tk_root = root
153
-
154
- # Bind window state events
155
- try:
156
- root.bind("<Unmap>", self._on_window_minimized)
157
- root.bind("<Map>", self._on_window_restored)
158
- except tk.TclError:
159
- pass # Binding might fail in some cases
160
-
161
- def register_component(self, component_id: str, widget: tk.Widget,
162
- initial_visibility: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
163
- """
164
- Register a component for visibility-aware updates.
165
-
166
- Args:
167
- component_id: Unique identifier for the component
168
- widget: The widget to monitor
169
- initial_visibility: Initial visibility state
170
- """
171
- with self.lock:
172
- self.components[component_id] = ComponentInfo(
173
- component_id=component_id,
174
- widget_ref=weakref.ref(widget),
175
- visibility_state=initial_visibility,
176
- last_update_time=0.0
177
- )
178
-
179
- def unregister_component(self, component_id: str) -> None:
180
- """
181
- Unregister a component and remove any pending updates.
182
-
183
- Args:
184
- component_id: Identifier of the component to unregister
185
- """
186
- with self.lock:
187
- # Remove from components
188
- self.components.pop(component_id, None)
189
-
190
- # Remove from all update queues
191
- for queue in self.update_queues.values():
192
- # Filter out requests for this component
193
- filtered = deque([req for req in queue if req.widget_id != component_id])
194
- queue.clear()
195
- queue.extend(filtered)
196
-
197
- def set_visibility_state(self, component_id: str, state: VisibilityState) -> None:
198
- """
199
- Set the visibility state for a component.
200
-
201
- Args:
202
- component_id: Component identifier
203
- state: New visibility state
204
- """
205
- with self.lock:
206
- component = self.components.get(component_id)
207
- if component:
208
- old_state = component.visibility_state
209
- component.visibility_state = state
210
-
211
- # If component became visible and active, process any pending updates immediately
212
- if (old_state != VisibilityState.VISIBLE_ACTIVE and
213
- state == VisibilityState.VISIBLE_ACTIVE):
214
- self._promote_pending_updates(component_id)
215
-
216
- def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
217
- """
218
- Get the current visibility state of a component.
219
-
220
- Args:
221
- component_id: Component identifier
222
-
223
- Returns:
224
- Current visibility state or None if component not found
225
- """
226
- with self.lock:
227
- component = self.components.get(component_id)
228
- return component.visibility_state if component else None
229
-
230
- def request_update(self, widget_id: str, priority: UpdatePriority = UpdatePriority.NORMAL,
231
- callback: Optional[Callable] = None, content_hash: str = "") -> None:
232
- """
233
- Request a statistics update for a widget.
234
-
235
- Args:
236
- widget_id: Widget identifier
237
- priority: Update priority level
238
- callback: Optional callback to execute for the update
239
- content_hash: Optional hash of content for deduplication
240
- """
241
- with self.lock:
242
- self.stats['updates_requested'] += 1
243
-
244
- # Check if component is registered
245
- component = self.components.get(widget_id)
246
- if not component or not component.is_valid:
247
- return
248
-
249
- # Check if we should skip this update
250
- if self._should_skip_update(component, priority):
251
- self.stats['updates_skipped_visibility'] += 1
252
- component.skip_count += 1
253
- return
254
-
255
- # Check if paused
256
- if self.paused or self.window_minimized:
257
- # Only process IMMEDIATE priority updates when paused
258
- if priority != UpdatePriority.IMMEDIATE:
259
- self.stats['updates_skipped_paused'] += 1
260
- return
261
-
262
- # Create update request
263
- request = UpdateRequest(
264
- widget_id=widget_id,
265
- priority=priority,
266
- content_hash=content_hash,
267
- visibility_state=component.visibility_state,
268
- callback=callback
269
- )
270
-
271
- # Add to appropriate queue
272
- self.update_queues[priority].append(request)
273
-
274
- # Schedule queue processing
275
- self._schedule_queue_processing()
276
-
277
- def _should_skip_update(self, component: ComponentInfo, priority: UpdatePriority) -> bool:
278
- """
279
- Determine if an update should be skipped based on visibility and timing.
280
-
281
- Args:
282
- component: Component information
283
- priority: Update priority
284
-
285
- Returns:
286
- True if update should be skipped
287
- """
288
- # Never skip IMMEDIATE priority
289
- if priority == UpdatePriority.IMMEDIATE:
290
- return False
291
-
292
- # Skip if component is hidden
293
- if component.visibility_state == VisibilityState.HIDDEN:
294
- return True
295
-
296
- # Skip if minimized (unless IMMEDIATE)
297
- if component.visibility_state == VisibilityState.MINIMIZED:
298
- return True
299
-
300
- # Check minimum update interval
301
- if component.time_since_last_update < (self.min_update_interval_ms / 1000.0):
302
- # Too soon since last update
303
- return True
304
-
305
- # Skip low priority updates for inactive components
306
- if (component.visibility_state == VisibilityState.VISIBLE_INACTIVE and
307
- priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]):
308
- return True
309
-
310
- return False
311
-
312
- def _promote_pending_updates(self, component_id: str) -> None:
313
- """
314
- Promote pending updates for a component to higher priority.
315
-
316
- Called when a component becomes visible and active.
317
-
318
- Args:
319
- component_id: Component identifier
320
- """
321
- # Move LOW and DEFERRED updates to NORMAL priority
322
- for low_priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]:
323
- queue = self.update_queues[low_priority]
324
- promoted = []
325
- remaining = deque()
326
-
327
- for request in queue:
328
- if request.widget_id == component_id:
329
- # Promote to NORMAL priority
330
- request.priority = UpdatePriority.NORMAL
331
- promoted.append(request)
332
- else:
333
- remaining.append(request)
334
-
335
- # Update queue
336
- queue.clear()
337
- queue.extend(remaining)
338
-
339
- # Add promoted requests to NORMAL queue
340
- self.update_queues[UpdatePriority.NORMAL].extend(promoted)
341
-
342
- def _schedule_queue_processing(self) -> None:
343
- """Schedule processing of the update queue."""
344
- if self._processing_queue or not self._tk_root:
345
- return
346
-
347
- # Cancel any existing scheduled processing
348
- if self._queue_process_after_id:
349
- try:
350
- self._tk_root.after_cancel(self._queue_process_after_id)
351
- except tk.TclError:
352
- pass
353
-
354
- # Schedule new processing
355
- self._queue_process_after_id = self._tk_root.after(10, self._process_update_queue)
356
-
357
- def _process_update_queue(self) -> None:
358
- """Process pending update requests from the queue."""
359
- if self._processing_queue:
360
- return
361
-
362
- self._processing_queue = True
363
-
364
- try:
365
- with self.lock:
366
- # Process queues in priority order
367
- for priority in UpdatePriority:
368
- queue = self.update_queues[priority]
369
-
370
- # Process up to 5 requests per priority level per cycle
371
- batch_size = 5 if priority in [UpdatePriority.IMMEDIATE, UpdatePriority.HIGH] else 3
372
- processed = 0
373
-
374
- while queue and processed < batch_size:
375
- request = queue.popleft()
376
-
377
- # Skip expired requests
378
- if request.is_expired:
379
- continue
380
-
381
- # Execute the update
382
- self._execute_update(request)
383
- processed += 1
384
-
385
- # Check if there are more requests to process
386
- has_pending = any(len(queue) > 0 for queue in self.update_queues.values())
387
-
388
- if has_pending:
389
- # Schedule next processing cycle
390
- self._queue_process_after_id = None
391
- self._schedule_queue_processing()
392
-
393
- finally:
394
- self._processing_queue = False
395
-
396
- def _execute_update(self, request: UpdateRequest) -> None:
397
- """
398
- Execute a statistics update request.
399
-
400
- Args:
401
- request: Update request to execute
402
- """
403
- component = self.components.get(request.widget_id)
404
- if not component or not component.is_valid:
405
- return
406
-
407
- # Double-check visibility before executing
408
- if self._should_skip_update(component, request.priority):
409
- self.stats['updates_skipped_visibility'] += 1
410
- component.skip_count += 1
411
- return
412
-
413
- try:
414
- # Execute callback if provided
415
- if request.callback:
416
- request.callback(request.widget_id)
417
-
418
- # Update component info
419
- component.last_update_time = time.time()
420
- component.update_count += 1
421
-
422
- self.stats['updates_executed'] += 1
423
-
424
- except Exception as e:
425
- # Log error but don't crash
426
- print(f"Error executing statistics update for {request.widget_id}: {e}")
427
-
428
- def pause_updates(self, paused: bool = True) -> None:
429
- """
430
- Pause or resume all statistics updates.
431
-
432
- Args:
433
- paused: True to pause, False to resume
434
- """
435
- with self.lock:
436
- self.paused = paused
437
-
438
- if not paused:
439
- # Resume - process any pending high-priority updates
440
- self._schedule_queue_processing()
441
-
442
- def set_user_idle(self, idle: bool) -> None:
443
- """
444
- Set the user idle state.
445
-
446
- Args:
447
- idle: True if user is idle, False if active
448
- """
449
- with self.lock:
450
- self.user_idle = idle
451
- if not idle:
452
- self.last_user_activity = time.time()
453
-
454
- def mark_user_activity(self) -> None:
455
- """Mark that user activity has occurred."""
456
- self.set_user_idle(False)
457
-
458
- def _on_window_minimized(self, event=None) -> None:
459
- """Handle window minimization event."""
460
- with self.lock:
461
- self.window_minimized = True
462
-
463
- # Update all components to minimized state
464
- for component in self.components.values():
465
- if component.visibility_state != VisibilityState.HIDDEN:
466
- component.visibility_state = VisibilityState.MINIMIZED
467
-
468
- def _on_window_restored(self, event=None) -> None:
469
- """Handle window restoration event."""
470
- with self.lock:
471
- self.window_minimized = False
472
-
473
- # Restore component visibility states (will need to be updated by app)
474
- # For now, just mark as unknown so they get re-evaluated
475
- for component in self.components.values():
476
- if component.visibility_state == VisibilityState.MINIMIZED:
477
- component.visibility_state = VisibilityState.UNKNOWN
478
-
479
- # Resume processing
480
- self._schedule_queue_processing()
481
-
482
- def get_update_queue_status(self) -> Dict[str, Any]:
483
- """
484
- Get the current status of update queues.
485
-
486
- Returns:
487
- Dictionary with queue status information
488
- """
489
- with self.lock:
490
- queue_sizes = {
491
- priority.name: len(queue)
492
- for priority, queue in self.update_queues.items()
493
- }
494
-
495
- return {
496
- 'queue_sizes': queue_sizes,
497
- 'total_pending': sum(queue_sizes.values()),
498
- 'paused': self.paused,
499
- 'window_minimized': self.window_minimized,
500
- 'user_idle': self.user_idle,
501
- 'processing': self._processing_queue,
502
- 'registered_components': len(self.components),
503
- 'statistics': self.stats.copy()
504
- }
505
-
506
- def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
507
- """
508
- Get information about a registered component.
509
-
510
- Args:
511
- component_id: Component identifier
512
-
513
- Returns:
514
- Dictionary with component information or None if not found
515
- """
516
- with self.lock:
517
- component = self.components.get(component_id)
518
- if not component:
519
- return None
520
-
521
- return {
522
- 'component_id': component.component_id,
523
- 'visibility_state': component.visibility_state.value,
524
- 'is_valid': component.is_valid,
525
- 'last_update_time': component.last_update_time,
526
- 'time_since_last_update': component.time_since_last_update,
527
- 'update_count': component.update_count,
528
- 'skip_count': component.skip_count,
529
- 'skip_rate': (component.skip_count / max(1, component.update_count + component.skip_count)) * 100
530
- }
531
-
532
- def get_statistics(self) -> Dict[str, Any]:
533
- """
534
- Get statistics about update management.
535
-
536
- Returns:
537
- Dictionary with statistics
538
- """
539
- with self.lock:
540
- stats = self.stats.copy()
541
-
542
- # Calculate derived statistics
543
- total_requests = stats['updates_requested']
544
- if total_requests > 0:
545
- stats['execution_rate'] = (stats['updates_executed'] / total_requests) * 100
546
- stats['skip_rate'] = (
547
- (stats['updates_skipped_visibility'] + stats['updates_skipped_paused']) /
548
- total_requests
549
- ) * 100
550
- else:
551
- stats['execution_rate'] = 0.0
552
- stats['skip_rate'] = 0.0
553
-
554
- # Add component statistics
555
- stats['total_components'] = len(self.components)
556
- stats['valid_components'] = sum(1 for c in self.components.values() if c.is_valid)
557
-
558
- # Add visibility breakdown
559
- visibility_counts = {}
560
- for component in self.components.values():
561
- state = component.visibility_state.value
562
- visibility_counts[state] = visibility_counts.get(state, 0) + 1
563
- stats['visibility_breakdown'] = visibility_counts
564
-
565
- return stats
566
-
567
- def cleanup_invalid_components(self) -> int:
568
- """
569
- Clean up components whose widgets have been destroyed.
570
-
571
- Returns:
572
- Number of components cleaned up
573
- """
574
- with self.lock:
575
- invalid_ids = [
576
- comp_id for comp_id, comp in self.components.items()
577
- if not comp.is_valid
578
- ]
579
-
580
- for comp_id in invalid_ids:
581
- self.unregister_component(comp_id)
582
-
583
- return len(invalid_ids)
584
-
585
- def clear_all_queues(self) -> None:
586
- """Clear all pending update requests."""
587
- with self.lock:
588
- for queue in self.update_queues.values():
589
- queue.clear()
590
-
591
- def force_update_all_visible(self) -> None:
592
- """Force immediate update of all visible components."""
593
- with self.lock:
594
- for component_id, component in self.components.items():
595
- if component.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
596
- VisibilityState.VISIBLE_INACTIVE]:
597
- self.request_update(component_id, UpdatePriority.IMMEDIATE)
598
-
599
-
600
- # Global instance for easy access
601
- _global_update_manager: Optional[StatisticsUpdateManager] = None
602
-
603
-
604
- def get_statistics_update_manager() -> StatisticsUpdateManager:
605
- """Get the global statistics update manager instance."""
606
- global _global_update_manager
607
- if _global_update_manager is None:
608
- _global_update_manager = StatisticsUpdateManager()
609
- return _global_update_manager
610
-
611
-
612
- def create_statistics_update_manager() -> StatisticsUpdateManager:
613
- """
614
- Create a new statistics update manager instance.
615
-
616
- Returns:
617
- New StatisticsUpdateManager instance
618
- """
619
- return StatisticsUpdateManager()
1
+ """
2
+ Statistics Update Manager for Pomera AI Commander.
3
+
4
+ This module provides visibility-aware statistics update coordination to optimize
5
+ performance by skipping updates for hidden tabs, inactive components, and minimized windows.
6
+
7
+ Requirements addressed:
8
+ - 3.1: Skip statistics updates for inactive tabs
9
+ - 3.2: Pause statistics updates when application window is minimized
10
+ - 3.3: Skip calculations when statistics bars are not visible
11
+ - 3.4: Reduce update frequency when user is idle
12
+ """
13
+
14
+ import time
15
+ import threading
16
+ import tkinter as tk
17
+ from typing import Dict, Optional, Callable, Any, List, Set
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from collections import deque
21
+ import weakref
22
+
23
+
24
+ class UpdatePriority(Enum):
25
+ """Priority levels for statistics updates."""
26
+ IMMEDIATE = 0 # Update immediately (user just switched to this tab)
27
+ HIGH = 1 # Update soon (active tab, user typing)
28
+ NORMAL = 2 # Update when convenient (visible but not active)
29
+ LOW = 3 # Update when idle (background tab)
30
+ DEFERRED = 4 # Update only when explicitly requested
31
+
32
+
33
+ class VisibilityState(Enum):
34
+ """Visibility states for components."""
35
+ VISIBLE_ACTIVE = "visible_active" # Currently visible and active
36
+ VISIBLE_INACTIVE = "visible_inactive" # Visible but not active
37
+ HIDDEN = "hidden" # Not visible (hidden tab)
38
+ MINIMIZED = "minimized" # Window is minimized
39
+ UNKNOWN = "unknown" # State not yet determined
40
+
41
+
42
+ @dataclass
43
+ class UpdateRequest:
44
+ """Request for a statistics update."""
45
+ widget_id: str
46
+ priority: UpdatePriority
47
+ timestamp: float = field(default_factory=time.time)
48
+ content_hash: str = ""
49
+ visibility_state: VisibilityState = VisibilityState.UNKNOWN
50
+ callback: Optional[Callable] = None
51
+
52
+ def __post_init__(self):
53
+ if not self.timestamp:
54
+ self.timestamp = time.time()
55
+
56
+ @property
57
+ def age_seconds(self) -> float:
58
+ """Get the age of this request in seconds."""
59
+ return time.time() - self.timestamp
60
+
61
+ @property
62
+ def is_expired(self) -> bool:
63
+ """Check if this request has expired (older than 5 seconds)."""
64
+ return self.age_seconds > 5.0
65
+
66
+ def should_batch_with(self, other: 'UpdateRequest') -> bool:
67
+ """Check if this request can be batched with another."""
68
+ # Can batch if same widget and similar priority
69
+ if self.widget_id != other.widget_id:
70
+ return False
71
+
72
+ # Can batch if priorities are within 1 level
73
+ return abs(self.priority.value - other.priority.value) <= 1
74
+
75
+
76
+ @dataclass
77
+ class ComponentInfo:
78
+ """Information about a component being monitored."""
79
+ component_id: str
80
+ widget_ref: weakref.ref
81
+ visibility_state: VisibilityState = VisibilityState.UNKNOWN
82
+ last_update_time: float = 0.0
83
+ update_count: int = 0
84
+ skip_count: int = 0
85
+
86
+ @property
87
+ def widget(self):
88
+ """Get the actual widget from the weak reference."""
89
+ return self.widget_ref() if self.widget_ref else None
90
+
91
+ @property
92
+ def is_valid(self) -> bool:
93
+ """Check if the widget still exists."""
94
+ return self.widget is not None
95
+
96
+ @property
97
+ def time_since_last_update(self) -> float:
98
+ """Get time since last update in seconds."""
99
+ return time.time() - self.last_update_time if self.last_update_time > 0 else float('inf')
100
+
101
+
102
+ class StatisticsUpdateManager:
103
+ """
104
+ Manages statistics updates with visibility awareness and performance optimization.
105
+
106
+ This class coordinates statistics updates across multiple widgets, implementing
107
+ visibility-aware updates that skip hidden tabs and inactive components, automatic
108
+ pause during window minimization, and priority-based update queuing.
109
+ """
110
+
111
+ def __init__(self):
112
+ """Initialize the statistics update manager."""
113
+ # Component registry
114
+ self.components: Dict[str, ComponentInfo] = {}
115
+
116
+ # Update queue organized by priority
117
+ self.update_queues: Dict[UpdatePriority, deque] = {
118
+ priority: deque() for priority in UpdatePriority
119
+ }
120
+
121
+ # Global state
122
+ self.paused = False
123
+ self.window_minimized = False
124
+ self.user_idle = False
125
+ self.last_user_activity = time.time()
126
+
127
+ # Configuration
128
+ self.idle_threshold_seconds = 5.0
129
+ self.min_update_interval_ms = 100 # Minimum time between updates for same widget
130
+
131
+ # Statistics
132
+ self.stats = {
133
+ 'updates_requested': 0,
134
+ 'updates_executed': 0,
135
+ 'updates_skipped_visibility': 0,
136
+ 'updates_skipped_paused': 0,
137
+ 'updates_batched': 0
138
+ }
139
+
140
+ # Thread safety
141
+ self.lock = threading.RLock()
142
+
143
+ # Tkinter root reference
144
+ self._tk_root: Optional[tk.Tk] = None
145
+
146
+ # Processing state
147
+ self._processing_queue = False
148
+ self._queue_process_after_id: Optional[str] = None
149
+
150
+ def set_tk_root(self, root: tk.Tk):
151
+ """Set the Tkinter root for scheduling callbacks."""
152
+ self._tk_root = root
153
+
154
+ # Bind window state events
155
+ try:
156
+ root.bind("<Unmap>", self._on_window_minimized)
157
+ root.bind("<Map>", self._on_window_restored)
158
+ except tk.TclError:
159
+ pass # Binding might fail in some cases
160
+
161
+ def register_component(self, component_id: str, widget: tk.Widget,
162
+ initial_visibility: VisibilityState = VisibilityState.VISIBLE_ACTIVE) -> None:
163
+ """
164
+ Register a component for visibility-aware updates.
165
+
166
+ Args:
167
+ component_id: Unique identifier for the component
168
+ widget: The widget to monitor
169
+ initial_visibility: Initial visibility state
170
+ """
171
+ with self.lock:
172
+ self.components[component_id] = ComponentInfo(
173
+ component_id=component_id,
174
+ widget_ref=weakref.ref(widget),
175
+ visibility_state=initial_visibility,
176
+ last_update_time=0.0
177
+ )
178
+
179
+ def unregister_component(self, component_id: str) -> None:
180
+ """
181
+ Unregister a component and remove any pending updates.
182
+
183
+ Args:
184
+ component_id: Identifier of the component to unregister
185
+ """
186
+ with self.lock:
187
+ # Remove from components
188
+ self.components.pop(component_id, None)
189
+
190
+ # Remove from all update queues
191
+ for queue in self.update_queues.values():
192
+ # Filter out requests for this component
193
+ filtered = deque([req for req in queue if req.widget_id != component_id])
194
+ queue.clear()
195
+ queue.extend(filtered)
196
+
197
+ def set_visibility_state(self, component_id: str, state: VisibilityState) -> None:
198
+ """
199
+ Set the visibility state for a component.
200
+
201
+ Args:
202
+ component_id: Component identifier
203
+ state: New visibility state
204
+ """
205
+ with self.lock:
206
+ component = self.components.get(component_id)
207
+ if component:
208
+ old_state = component.visibility_state
209
+ component.visibility_state = state
210
+
211
+ # If component became visible and active, process any pending updates immediately
212
+ if (old_state != VisibilityState.VISIBLE_ACTIVE and
213
+ state == VisibilityState.VISIBLE_ACTIVE):
214
+ self._promote_pending_updates(component_id)
215
+
216
+ def get_visibility_state(self, component_id: str) -> Optional[VisibilityState]:
217
+ """
218
+ Get the current visibility state of a component.
219
+
220
+ Args:
221
+ component_id: Component identifier
222
+
223
+ Returns:
224
+ Current visibility state or None if component not found
225
+ """
226
+ with self.lock:
227
+ component = self.components.get(component_id)
228
+ return component.visibility_state if component else None
229
+
230
+ def request_update(self, widget_id: str, priority: UpdatePriority = UpdatePriority.NORMAL,
231
+ callback: Optional[Callable] = None, content_hash: str = "") -> None:
232
+ """
233
+ Request a statistics update for a widget.
234
+
235
+ Args:
236
+ widget_id: Widget identifier
237
+ priority: Update priority level
238
+ callback: Optional callback to execute for the update
239
+ content_hash: Optional hash of content for deduplication
240
+ """
241
+ with self.lock:
242
+ self.stats['updates_requested'] += 1
243
+
244
+ # Check if component is registered
245
+ component = self.components.get(widget_id)
246
+ if not component or not component.is_valid:
247
+ return
248
+
249
+ # Check if we should skip this update
250
+ if self._should_skip_update(component, priority):
251
+ self.stats['updates_skipped_visibility'] += 1
252
+ component.skip_count += 1
253
+ return
254
+
255
+ # Check if paused
256
+ if self.paused or self.window_minimized:
257
+ # Only process IMMEDIATE priority updates when paused
258
+ if priority != UpdatePriority.IMMEDIATE:
259
+ self.stats['updates_skipped_paused'] += 1
260
+ return
261
+
262
+ # Create update request
263
+ request = UpdateRequest(
264
+ widget_id=widget_id,
265
+ priority=priority,
266
+ content_hash=content_hash,
267
+ visibility_state=component.visibility_state,
268
+ callback=callback
269
+ )
270
+
271
+ # Add to appropriate queue
272
+ self.update_queues[priority].append(request)
273
+
274
+ # Schedule queue processing
275
+ self._schedule_queue_processing()
276
+
277
+ def _should_skip_update(self, component: ComponentInfo, priority: UpdatePriority) -> bool:
278
+ """
279
+ Determine if an update should be skipped based on visibility and timing.
280
+
281
+ Args:
282
+ component: Component information
283
+ priority: Update priority
284
+
285
+ Returns:
286
+ True if update should be skipped
287
+ """
288
+ # Never skip IMMEDIATE priority
289
+ if priority == UpdatePriority.IMMEDIATE:
290
+ return False
291
+
292
+ # Skip if component is hidden
293
+ if component.visibility_state == VisibilityState.HIDDEN:
294
+ return True
295
+
296
+ # Skip if minimized (unless IMMEDIATE)
297
+ if component.visibility_state == VisibilityState.MINIMIZED:
298
+ return True
299
+
300
+ # Check minimum update interval
301
+ if component.time_since_last_update < (self.min_update_interval_ms / 1000.0):
302
+ # Too soon since last update
303
+ return True
304
+
305
+ # Skip low priority updates for inactive components
306
+ if (component.visibility_state == VisibilityState.VISIBLE_INACTIVE and
307
+ priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]):
308
+ return True
309
+
310
+ return False
311
+
312
+ def _promote_pending_updates(self, component_id: str) -> None:
313
+ """
314
+ Promote pending updates for a component to higher priority.
315
+
316
+ Called when a component becomes visible and active.
317
+
318
+ Args:
319
+ component_id: Component identifier
320
+ """
321
+ # Move LOW and DEFERRED updates to NORMAL priority
322
+ for low_priority in [UpdatePriority.LOW, UpdatePriority.DEFERRED]:
323
+ queue = self.update_queues[low_priority]
324
+ promoted = []
325
+ remaining = deque()
326
+
327
+ for request in queue:
328
+ if request.widget_id == component_id:
329
+ # Promote to NORMAL priority
330
+ request.priority = UpdatePriority.NORMAL
331
+ promoted.append(request)
332
+ else:
333
+ remaining.append(request)
334
+
335
+ # Update queue
336
+ queue.clear()
337
+ queue.extend(remaining)
338
+
339
+ # Add promoted requests to NORMAL queue
340
+ self.update_queues[UpdatePriority.NORMAL].extend(promoted)
341
+
342
+ def _schedule_queue_processing(self) -> None:
343
+ """Schedule processing of the update queue."""
344
+ if self._processing_queue or not self._tk_root:
345
+ return
346
+
347
+ # Cancel any existing scheduled processing
348
+ if self._queue_process_after_id:
349
+ try:
350
+ self._tk_root.after_cancel(self._queue_process_after_id)
351
+ except tk.TclError:
352
+ pass
353
+
354
+ # Schedule new processing
355
+ self._queue_process_after_id = self._tk_root.after(10, self._process_update_queue)
356
+
357
+ def _process_update_queue(self) -> None:
358
+ """Process pending update requests from the queue."""
359
+ if self._processing_queue:
360
+ return
361
+
362
+ self._processing_queue = True
363
+
364
+ try:
365
+ with self.lock:
366
+ # Process queues in priority order
367
+ for priority in UpdatePriority:
368
+ queue = self.update_queues[priority]
369
+
370
+ # Process up to 5 requests per priority level per cycle
371
+ batch_size = 5 if priority in [UpdatePriority.IMMEDIATE, UpdatePriority.HIGH] else 3
372
+ processed = 0
373
+
374
+ while queue and processed < batch_size:
375
+ request = queue.popleft()
376
+
377
+ # Skip expired requests
378
+ if request.is_expired:
379
+ continue
380
+
381
+ # Execute the update
382
+ self._execute_update(request)
383
+ processed += 1
384
+
385
+ # Check if there are more requests to process
386
+ has_pending = any(len(queue) > 0 for queue in self.update_queues.values())
387
+
388
+ if has_pending:
389
+ # Schedule next processing cycle
390
+ self._queue_process_after_id = None
391
+ self._schedule_queue_processing()
392
+
393
+ finally:
394
+ self._processing_queue = False
395
+
396
+ def _execute_update(self, request: UpdateRequest) -> None:
397
+ """
398
+ Execute a statistics update request.
399
+
400
+ Args:
401
+ request: Update request to execute
402
+ """
403
+ component = self.components.get(request.widget_id)
404
+ if not component or not component.is_valid:
405
+ return
406
+
407
+ # Double-check visibility before executing
408
+ if self._should_skip_update(component, request.priority):
409
+ self.stats['updates_skipped_visibility'] += 1
410
+ component.skip_count += 1
411
+ return
412
+
413
+ try:
414
+ # Execute callback if provided
415
+ if request.callback:
416
+ request.callback(request.widget_id)
417
+
418
+ # Update component info
419
+ component.last_update_time = time.time()
420
+ component.update_count += 1
421
+
422
+ self.stats['updates_executed'] += 1
423
+
424
+ except Exception as e:
425
+ # Log error but don't crash
426
+ print(f"Error executing statistics update for {request.widget_id}: {e}")
427
+
428
+ def pause_updates(self, paused: bool = True) -> None:
429
+ """
430
+ Pause or resume all statistics updates.
431
+
432
+ Args:
433
+ paused: True to pause, False to resume
434
+ """
435
+ with self.lock:
436
+ self.paused = paused
437
+
438
+ if not paused:
439
+ # Resume - process any pending high-priority updates
440
+ self._schedule_queue_processing()
441
+
442
+ def set_user_idle(self, idle: bool) -> None:
443
+ """
444
+ Set the user idle state.
445
+
446
+ Args:
447
+ idle: True if user is idle, False if active
448
+ """
449
+ with self.lock:
450
+ self.user_idle = idle
451
+ if not idle:
452
+ self.last_user_activity = time.time()
453
+
454
+ def mark_user_activity(self) -> None:
455
+ """Mark that user activity has occurred."""
456
+ self.set_user_idle(False)
457
+
458
+ def _on_window_minimized(self, event=None) -> None:
459
+ """Handle window minimization event."""
460
+ with self.lock:
461
+ self.window_minimized = True
462
+
463
+ # Update all components to minimized state
464
+ for component in self.components.values():
465
+ if component.visibility_state != VisibilityState.HIDDEN:
466
+ component.visibility_state = VisibilityState.MINIMIZED
467
+
468
+ def _on_window_restored(self, event=None) -> None:
469
+ """Handle window restoration event."""
470
+ with self.lock:
471
+ self.window_minimized = False
472
+
473
+ # Restore component visibility states (will need to be updated by app)
474
+ # For now, just mark as unknown so they get re-evaluated
475
+ for component in self.components.values():
476
+ if component.visibility_state == VisibilityState.MINIMIZED:
477
+ component.visibility_state = VisibilityState.UNKNOWN
478
+
479
+ # Resume processing
480
+ self._schedule_queue_processing()
481
+
482
+ def get_update_queue_status(self) -> Dict[str, Any]:
483
+ """
484
+ Get the current status of update queues.
485
+
486
+ Returns:
487
+ Dictionary with queue status information
488
+ """
489
+ with self.lock:
490
+ queue_sizes = {
491
+ priority.name: len(queue)
492
+ for priority, queue in self.update_queues.items()
493
+ }
494
+
495
+ return {
496
+ 'queue_sizes': queue_sizes,
497
+ 'total_pending': sum(queue_sizes.values()),
498
+ 'paused': self.paused,
499
+ 'window_minimized': self.window_minimized,
500
+ 'user_idle': self.user_idle,
501
+ 'processing': self._processing_queue,
502
+ 'registered_components': len(self.components),
503
+ 'statistics': self.stats.copy()
504
+ }
505
+
506
+ def get_component_info(self, component_id: str) -> Optional[Dict[str, Any]]:
507
+ """
508
+ Get information about a registered component.
509
+
510
+ Args:
511
+ component_id: Component identifier
512
+
513
+ Returns:
514
+ Dictionary with component information or None if not found
515
+ """
516
+ with self.lock:
517
+ component = self.components.get(component_id)
518
+ if not component:
519
+ return None
520
+
521
+ return {
522
+ 'component_id': component.component_id,
523
+ 'visibility_state': component.visibility_state.value,
524
+ 'is_valid': component.is_valid,
525
+ 'last_update_time': component.last_update_time,
526
+ 'time_since_last_update': component.time_since_last_update,
527
+ 'update_count': component.update_count,
528
+ 'skip_count': component.skip_count,
529
+ 'skip_rate': (component.skip_count / max(1, component.update_count + component.skip_count)) * 100
530
+ }
531
+
532
+ def get_statistics(self) -> Dict[str, Any]:
533
+ """
534
+ Get statistics about update management.
535
+
536
+ Returns:
537
+ Dictionary with statistics
538
+ """
539
+ with self.lock:
540
+ stats = self.stats.copy()
541
+
542
+ # Calculate derived statistics
543
+ total_requests = stats['updates_requested']
544
+ if total_requests > 0:
545
+ stats['execution_rate'] = (stats['updates_executed'] / total_requests) * 100
546
+ stats['skip_rate'] = (
547
+ (stats['updates_skipped_visibility'] + stats['updates_skipped_paused']) /
548
+ total_requests
549
+ ) * 100
550
+ else:
551
+ stats['execution_rate'] = 0.0
552
+ stats['skip_rate'] = 0.0
553
+
554
+ # Add component statistics
555
+ stats['total_components'] = len(self.components)
556
+ stats['valid_components'] = sum(1 for c in self.components.values() if c.is_valid)
557
+
558
+ # Add visibility breakdown
559
+ visibility_counts = {}
560
+ for component in self.components.values():
561
+ state = component.visibility_state.value
562
+ visibility_counts[state] = visibility_counts.get(state, 0) + 1
563
+ stats['visibility_breakdown'] = visibility_counts
564
+
565
+ return stats
566
+
567
+ def cleanup_invalid_components(self) -> int:
568
+ """
569
+ Clean up components whose widgets have been destroyed.
570
+
571
+ Returns:
572
+ Number of components cleaned up
573
+ """
574
+ with self.lock:
575
+ invalid_ids = [
576
+ comp_id for comp_id, comp in self.components.items()
577
+ if not comp.is_valid
578
+ ]
579
+
580
+ for comp_id in invalid_ids:
581
+ self.unregister_component(comp_id)
582
+
583
+ return len(invalid_ids)
584
+
585
+ def clear_all_queues(self) -> None:
586
+ """Clear all pending update requests."""
587
+ with self.lock:
588
+ for queue in self.update_queues.values():
589
+ queue.clear()
590
+
591
+ def force_update_all_visible(self) -> None:
592
+ """Force immediate update of all visible components."""
593
+ with self.lock:
594
+ for component_id, component in self.components.items():
595
+ if component.visibility_state in [VisibilityState.VISIBLE_ACTIVE,
596
+ VisibilityState.VISIBLE_INACTIVE]:
597
+ self.request_update(component_id, UpdatePriority.IMMEDIATE)
598
+
599
+
600
+ # Global instance for easy access
601
+ _global_update_manager: Optional[StatisticsUpdateManager] = None
602
+
603
+
604
+ def get_statistics_update_manager() -> StatisticsUpdateManager:
605
+ """Get the global statistics update manager instance."""
606
+ global _global_update_manager
607
+ if _global_update_manager is None:
608
+ _global_update_manager = StatisticsUpdateManager()
609
+ return _global_update_manager
610
+
611
+
612
+ def create_statistics_update_manager() -> StatisticsUpdateManager:
613
+ """
614
+ Create a new statistics update manager instance.
615
+
616
+ Returns:
617
+ New StatisticsUpdateManager instance
618
+ """
619
+ return StatisticsUpdateManager()