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,498 +1,498 @@
1
- """
2
- Widget Cache - Caches tool widgets to avoid recreation on tool switching.
3
-
4
- Instead of destroying and recreating widgets on every tool switch,
5
- this cache hides/shows widgets as needed, improving performance.
6
-
7
- Author: Pomera AI Commander Team
8
- """
9
-
10
- import tkinter as tk
11
- from tkinter import ttk
12
- from typing import Dict, Optional, Callable, Any, Set, List
13
- import logging
14
- import weakref
15
- from dataclasses import dataclass, field
16
- from enum import Enum
17
-
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- class CacheStrategy(Enum):
23
- """Widget caching strategies."""
24
- ALWAYS = "always" # Always cache (default)
25
- ON_DEMAND = "on_demand" # Cache only when explicitly requested
26
- NEVER = "never" # Never cache (always recreate)
27
- LRU = "lru" # Least Recently Used eviction
28
-
29
-
30
- @dataclass
31
- class CachedWidget:
32
- """
33
- Information about a cached widget.
34
-
35
- Attributes:
36
- widget: The actual Tkinter widget
37
- tool_name: Name of the tool this widget belongs to
38
- created_at: Timestamp when widget was created
39
- last_shown: Timestamp when widget was last shown
40
- show_count: Number of times widget has been shown
41
- needs_refresh: Whether widget needs to be refreshed on next show
42
- """
43
- widget: tk.Widget
44
- tool_name: str
45
- created_at: float = 0.0
46
- last_shown: float = 0.0
47
- show_count: int = 0
48
- needs_refresh: bool = False
49
-
50
- def mark_shown(self) -> None:
51
- """Mark widget as shown."""
52
- import time
53
- self.last_shown = time.time()
54
- self.show_count += 1
55
-
56
-
57
- class WidgetCache:
58
- """
59
- Caches tool widgets to improve tool switching performance.
60
-
61
- Instead of destroying and recreating widgets on every tool switch,
62
- this cache hides/shows widgets as needed. This significantly improves
63
- the user experience when switching between tools frequently.
64
-
65
- Usage:
66
- cache = WidgetCache(parent_frame)
67
-
68
- # Register widget factories
69
- cache.register_factory("Case Tool", lambda: create_case_tool_widget())
70
- cache.register_factory("AI Tools", lambda: create_ai_tools_widget())
71
-
72
- # Switch to a tool (creates widget if not cached, shows it)
73
- cache.show("Case Tool")
74
-
75
- # Switch to another tool (hides Case Tool, shows AI Tools)
76
- cache.show("AI Tools")
77
-
78
- # Force refresh a widget on next show
79
- cache.invalidate("Case Tool")
80
- """
81
-
82
- def __init__(self,
83
- parent_frame: tk.Frame,
84
- strategy: CacheStrategy = CacheStrategy.ALWAYS,
85
- max_cached: int = 20,
86
- logger: Optional[logging.Logger] = None):
87
- """
88
- Initialize the widget cache.
89
-
90
- Args:
91
- parent_frame: The frame where tool widgets are displayed
92
- strategy: Caching strategy to use
93
- max_cached: Maximum number of widgets to cache (for LRU)
94
- logger: Logger instance
95
- """
96
- self.parent_frame = parent_frame
97
- self.strategy = strategy
98
- self.max_cached = max_cached
99
- self.logger = logger or logging.getLogger(__name__)
100
-
101
- # Widget storage
102
- self._cache: Dict[str, CachedWidget] = {}
103
- self._factories: Dict[str, Callable[[], tk.Widget]] = {}
104
- self._current_tool: Optional[str] = None
105
-
106
- # Tools that should never be cached
107
- self._never_cache: Set[str] = set()
108
-
109
- # Callbacks
110
- self._on_widget_created: Optional[Callable[[str, tk.Widget], None]] = None
111
- self._on_widget_shown: Optional[Callable[[str, tk.Widget], None]] = None
112
- self._on_widget_hidden: Optional[Callable[[str, tk.Widget], None]] = None
113
-
114
- # Statistics
115
- self._stats = {
116
- 'cache_hits': 0,
117
- 'cache_misses': 0,
118
- 'widgets_created': 0,
119
- 'widgets_destroyed': 0
120
- }
121
-
122
- def register_factory(self,
123
- tool_name: str,
124
- factory: Callable[[], tk.Widget],
125
- never_cache: bool = False) -> None:
126
- """
127
- Register a factory function for creating a tool's widget.
128
-
129
- Args:
130
- tool_name: Name of the tool
131
- factory: Function that creates and returns the widget
132
- never_cache: If True, widget will always be recreated
133
- """
134
- self._factories[tool_name] = factory
135
- if never_cache:
136
- self._never_cache.add(tool_name)
137
- self.logger.debug(f"Registered factory for: {tool_name}")
138
-
139
- def unregister_factory(self, tool_name: str) -> bool:
140
- """
141
- Unregister a factory function.
142
-
143
- Args:
144
- tool_name: Name of the tool
145
-
146
- Returns:
147
- True if factory was found and removed
148
- """
149
- if tool_name in self._factories:
150
- del self._factories[tool_name]
151
- self._never_cache.discard(tool_name)
152
- return True
153
- return False
154
-
155
- def get_or_create(self, tool_name: str) -> Optional[tk.Widget]:
156
- """
157
- Get a cached widget or create it if not cached.
158
-
159
- Args:
160
- tool_name: Name of the tool
161
-
162
- Returns:
163
- The tool's widget, or None if no factory registered
164
- """
165
- # Check if we should never cache this tool
166
- should_cache = (
167
- self.strategy != CacheStrategy.NEVER and
168
- tool_name not in self._never_cache
169
- )
170
-
171
- # Return cached widget if available and valid
172
- if tool_name in self._cache and should_cache:
173
- cached = self._cache[tool_name]
174
- if not cached.needs_refresh:
175
- self._stats['cache_hits'] += 1
176
- self.logger.debug(f"Cache hit for: {tool_name}")
177
- return cached.widget
178
- else:
179
- # Widget needs refresh, destroy and recreate
180
- self._destroy_widget(tool_name)
181
-
182
- self._stats['cache_misses'] += 1
183
-
184
- # Check if we have a factory
185
- if tool_name not in self._factories:
186
- self.logger.warning(f"No factory registered for tool: {tool_name}")
187
- return None
188
-
189
- # Enforce max cache size (LRU eviction)
190
- if self.strategy == CacheStrategy.LRU and len(self._cache) >= self.max_cached:
191
- self._evict_lru()
192
-
193
- # Create new widget
194
- try:
195
- widget = self._factories[tool_name]()
196
-
197
- if should_cache:
198
- import time
199
- self._cache[tool_name] = CachedWidget(
200
- widget=widget,
201
- tool_name=tool_name,
202
- created_at=time.time()
203
- )
204
-
205
- self._stats['widgets_created'] += 1
206
- self.logger.debug(f"Created widget for: {tool_name}")
207
-
208
- # Callback
209
- if self._on_widget_created:
210
- self._on_widget_created(tool_name, widget)
211
-
212
- return widget
213
-
214
- except Exception as e:
215
- self.logger.error(f"Failed to create widget for {tool_name}: {e}")
216
- return None
217
-
218
- def show(self, tool_name: str) -> bool:
219
- """
220
- Show the widget for the specified tool, hiding others.
221
-
222
- Args:
223
- tool_name: Name of the tool to show
224
-
225
- Returns:
226
- True if successful, False otherwise
227
- """
228
- # Hide current widget
229
- if self._current_tool and self._current_tool != tool_name:
230
- self._hide_current()
231
-
232
- # Get or create the new widget
233
- widget = self.get_or_create(tool_name)
234
- if widget is None:
235
- return False
236
-
237
- # Show the new widget
238
- try:
239
- widget.pack(fill=tk.BOTH, expand=True)
240
- self._current_tool = tool_name
241
-
242
- # Update cached widget stats
243
- if tool_name in self._cache:
244
- self._cache[tool_name].mark_shown()
245
-
246
- # Callback
247
- if self._on_widget_shown:
248
- self._on_widget_shown(tool_name, widget)
249
-
250
- self.logger.debug(f"Showing widget: {tool_name}")
251
- return True
252
-
253
- except tk.TclError as e:
254
- self.logger.error(f"Failed to show widget {tool_name}: {e}")
255
- # Widget might be destroyed, remove from cache
256
- self._cache.pop(tool_name, None)
257
- return False
258
-
259
- def hide(self, tool_name: str) -> bool:
260
- """
261
- Hide a specific tool's widget.
262
-
263
- Args:
264
- tool_name: Name of the tool to hide
265
-
266
- Returns:
267
- True if widget was hidden
268
- """
269
- if tool_name not in self._cache:
270
- return False
271
-
272
- try:
273
- self._cache[tool_name].widget.pack_forget()
274
-
275
- if self._current_tool == tool_name:
276
- self._current_tool = None
277
-
278
- # Callback
279
- if self._on_widget_hidden:
280
- self._on_widget_hidden(tool_name, self._cache[tool_name].widget)
281
-
282
- return True
283
-
284
- except tk.TclError:
285
- # Widget might be destroyed
286
- self._cache.pop(tool_name, None)
287
- return False
288
-
289
- def _hide_current(self) -> None:
290
- """Hide the currently shown widget."""
291
- if self._current_tool and self._current_tool in self._cache:
292
- try:
293
- self._cache[self._current_tool].widget.pack_forget()
294
-
295
- if self._on_widget_hidden:
296
- self._on_widget_hidden(
297
- self._current_tool,
298
- self._cache[self._current_tool].widget
299
- )
300
- except tk.TclError:
301
- # Widget might be destroyed
302
- self._cache.pop(self._current_tool, None)
303
-
304
- def invalidate(self, tool_name: str) -> None:
305
- """
306
- Mark a widget for refresh (will be recreated on next show).
307
-
308
- Args:
309
- tool_name: Name of the tool to invalidate
310
- """
311
- if tool_name in self._cache:
312
- self._cache[tool_name].needs_refresh = True
313
- self.logger.debug(f"Invalidated cache for: {tool_name}")
314
-
315
- def destroy(self, tool_name: str) -> bool:
316
- """
317
- Destroy a cached widget immediately.
318
-
319
- Args:
320
- tool_name: Name of the tool
321
-
322
- Returns:
323
- True if widget was found and destroyed
324
- """
325
- return self._destroy_widget(tool_name)
326
-
327
- def _destroy_widget(self, tool_name: str) -> bool:
328
- """Internal method to destroy a widget."""
329
- if tool_name not in self._cache:
330
- return False
331
-
332
- try:
333
- self._cache[tool_name].widget.destroy()
334
- except tk.TclError:
335
- pass # Already destroyed
336
-
337
- del self._cache[tool_name]
338
- self._stats['widgets_destroyed'] += 1
339
-
340
- if self._current_tool == tool_name:
341
- self._current_tool = None
342
-
343
- self.logger.debug(f"Destroyed widget: {tool_name}")
344
- return True
345
-
346
- def _evict_lru(self) -> None:
347
- """Evict the least recently used widget."""
348
- if not self._cache:
349
- return
350
-
351
- # Don't evict current widget
352
- candidates = [
353
- (name, cached) for name, cached in self._cache.items()
354
- if name != self._current_tool
355
- ]
356
-
357
- if not candidates:
358
- return
359
-
360
- # Find LRU
361
- lru_name, _ = min(candidates, key=lambda x: x[1].last_shown)
362
- self._destroy_widget(lru_name)
363
- self.logger.debug(f"Evicted LRU widget: {lru_name}")
364
-
365
- def clear(self) -> None:
366
- """Clear all cached widgets."""
367
- for tool_name in list(self._cache.keys()):
368
- self._destroy_widget(tool_name)
369
- self._current_tool = None
370
- self.logger.debug("Cleared all cached widgets")
371
-
372
- def refresh_all(self) -> None:
373
- """Mark all widgets for refresh."""
374
- for cached in self._cache.values():
375
- cached.needs_refresh = True
376
- self.logger.debug("Marked all widgets for refresh")
377
-
378
- def is_cached(self, tool_name: str) -> bool:
379
- """Check if a tool's widget is cached."""
380
- return tool_name in self._cache
381
-
382
- def get_widget(self, tool_name: str) -> Optional[tk.Widget]:
383
- """
384
- Get a cached widget without creating it.
385
-
386
- Args:
387
- tool_name: Name of the tool
388
-
389
- Returns:
390
- The widget if cached, None otherwise
391
- """
392
- if tool_name in self._cache:
393
- return self._cache[tool_name].widget
394
- return None
395
-
396
- @property
397
- def cached_tools(self) -> List[str]:
398
- """Get list of currently cached tool names."""
399
- return list(self._cache.keys())
400
-
401
- @property
402
- def current_tool(self) -> Optional[str]:
403
- """Get the currently displayed tool name."""
404
- return self._current_tool
405
-
406
- @property
407
- def factory_count(self) -> int:
408
- """Get number of registered factories."""
409
- return len(self._factories)
410
-
411
- def get_stats(self) -> Dict[str, Any]:
412
- """Get cache statistics."""
413
- total_requests = self._stats['cache_hits'] + self._stats['cache_misses']
414
- hit_rate = (
415
- self._stats['cache_hits'] / total_requests * 100
416
- if total_requests > 0 else 0
417
- )
418
-
419
- return {
420
- **self._stats,
421
- 'cached_widgets': len(self._cache),
422
- 'registered_factories': len(self._factories),
423
- 'current_tool': self._current_tool,
424
- 'hit_rate_percent': round(hit_rate, 2),
425
- 'strategy': self.strategy.value
426
- }
427
-
428
- def get_cache_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
429
- """
430
- Get information about a cached widget.
431
-
432
- Args:
433
- tool_name: Name of the tool
434
-
435
- Returns:
436
- Dictionary with cache info, or None if not cached
437
- """
438
- if tool_name not in self._cache:
439
- return None
440
-
441
- cached = self._cache[tool_name]
442
- return {
443
- 'tool_name': cached.tool_name,
444
- 'created_at': cached.created_at,
445
- 'last_shown': cached.last_shown,
446
- 'show_count': cached.show_count,
447
- 'needs_refresh': cached.needs_refresh
448
- }
449
-
450
- # Callback setters
451
- def set_on_widget_created(self, callback: Callable[[str, tk.Widget], None]) -> None:
452
- """Set callback for when a widget is created."""
453
- self._on_widget_created = callback
454
-
455
- def set_on_widget_shown(self, callback: Callable[[str, tk.Widget], None]) -> None:
456
- """Set callback for when a widget is shown."""
457
- self._on_widget_shown = callback
458
-
459
- def set_on_widget_hidden(self, callback: Callable[[str, tk.Widget], None]) -> None:
460
- """Set callback for when a widget is hidden."""
461
- self._on_widget_hidden = callback
462
-
463
-
464
- # Global instance
465
- _widget_cache: Optional[WidgetCache] = None
466
-
467
-
468
- def get_widget_cache() -> Optional[WidgetCache]:
469
- """Get the global widget cache instance."""
470
- return _widget_cache
471
-
472
-
473
- def init_widget_cache(parent_frame: tk.Frame,
474
- strategy: CacheStrategy = CacheStrategy.ALWAYS,
475
- max_cached: int = 20) -> WidgetCache:
476
- """
477
- Initialize the global widget cache.
478
-
479
- Args:
480
- parent_frame: Parent frame for widgets
481
- strategy: Caching strategy
482
- max_cached: Maximum cached widgets (for LRU)
483
-
484
- Returns:
485
- Initialized WidgetCache
486
- """
487
- global _widget_cache
488
- _widget_cache = WidgetCache(parent_frame, strategy, max_cached)
489
- return _widget_cache
490
-
491
-
492
- def shutdown_widget_cache() -> None:
493
- """Shutdown the global widget cache."""
494
- global _widget_cache
495
- if _widget_cache is not None:
496
- _widget_cache.clear()
497
- _widget_cache = None
498
-
1
+ """
2
+ Widget Cache - Caches tool widgets to avoid recreation on tool switching.
3
+
4
+ Instead of destroying and recreating widgets on every tool switch,
5
+ this cache hides/shows widgets as needed, improving performance.
6
+
7
+ Author: Pomera AI Commander Team
8
+ """
9
+
10
+ import tkinter as tk
11
+ from tkinter import ttk
12
+ from typing import Dict, Optional, Callable, Any, Set, List
13
+ import logging
14
+ import weakref
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class CacheStrategy(Enum):
23
+ """Widget caching strategies."""
24
+ ALWAYS = "always" # Always cache (default)
25
+ ON_DEMAND = "on_demand" # Cache only when explicitly requested
26
+ NEVER = "never" # Never cache (always recreate)
27
+ LRU = "lru" # Least Recently Used eviction
28
+
29
+
30
+ @dataclass
31
+ class CachedWidget:
32
+ """
33
+ Information about a cached widget.
34
+
35
+ Attributes:
36
+ widget: The actual Tkinter widget
37
+ tool_name: Name of the tool this widget belongs to
38
+ created_at: Timestamp when widget was created
39
+ last_shown: Timestamp when widget was last shown
40
+ show_count: Number of times widget has been shown
41
+ needs_refresh: Whether widget needs to be refreshed on next show
42
+ """
43
+ widget: tk.Widget
44
+ tool_name: str
45
+ created_at: float = 0.0
46
+ last_shown: float = 0.0
47
+ show_count: int = 0
48
+ needs_refresh: bool = False
49
+
50
+ def mark_shown(self) -> None:
51
+ """Mark widget as shown."""
52
+ import time
53
+ self.last_shown = time.time()
54
+ self.show_count += 1
55
+
56
+
57
+ class WidgetCache:
58
+ """
59
+ Caches tool widgets to improve tool switching performance.
60
+
61
+ Instead of destroying and recreating widgets on every tool switch,
62
+ this cache hides/shows widgets as needed. This significantly improves
63
+ the user experience when switching between tools frequently.
64
+
65
+ Usage:
66
+ cache = WidgetCache(parent_frame)
67
+
68
+ # Register widget factories
69
+ cache.register_factory("Case Tool", lambda: create_case_tool_widget())
70
+ cache.register_factory("AI Tools", lambda: create_ai_tools_widget())
71
+
72
+ # Switch to a tool (creates widget if not cached, shows it)
73
+ cache.show("Case Tool")
74
+
75
+ # Switch to another tool (hides Case Tool, shows AI Tools)
76
+ cache.show("AI Tools")
77
+
78
+ # Force refresh a widget on next show
79
+ cache.invalidate("Case Tool")
80
+ """
81
+
82
+ def __init__(self,
83
+ parent_frame: tk.Frame,
84
+ strategy: CacheStrategy = CacheStrategy.ALWAYS,
85
+ max_cached: int = 20,
86
+ logger: Optional[logging.Logger] = None):
87
+ """
88
+ Initialize the widget cache.
89
+
90
+ Args:
91
+ parent_frame: The frame where tool widgets are displayed
92
+ strategy: Caching strategy to use
93
+ max_cached: Maximum number of widgets to cache (for LRU)
94
+ logger: Logger instance
95
+ """
96
+ self.parent_frame = parent_frame
97
+ self.strategy = strategy
98
+ self.max_cached = max_cached
99
+ self.logger = logger or logging.getLogger(__name__)
100
+
101
+ # Widget storage
102
+ self._cache: Dict[str, CachedWidget] = {}
103
+ self._factories: Dict[str, Callable[[], tk.Widget]] = {}
104
+ self._current_tool: Optional[str] = None
105
+
106
+ # Tools that should never be cached
107
+ self._never_cache: Set[str] = set()
108
+
109
+ # Callbacks
110
+ self._on_widget_created: Optional[Callable[[str, tk.Widget], None]] = None
111
+ self._on_widget_shown: Optional[Callable[[str, tk.Widget], None]] = None
112
+ self._on_widget_hidden: Optional[Callable[[str, tk.Widget], None]] = None
113
+
114
+ # Statistics
115
+ self._stats = {
116
+ 'cache_hits': 0,
117
+ 'cache_misses': 0,
118
+ 'widgets_created': 0,
119
+ 'widgets_destroyed': 0
120
+ }
121
+
122
+ def register_factory(self,
123
+ tool_name: str,
124
+ factory: Callable[[], tk.Widget],
125
+ never_cache: bool = False) -> None:
126
+ """
127
+ Register a factory function for creating a tool's widget.
128
+
129
+ Args:
130
+ tool_name: Name of the tool
131
+ factory: Function that creates and returns the widget
132
+ never_cache: If True, widget will always be recreated
133
+ """
134
+ self._factories[tool_name] = factory
135
+ if never_cache:
136
+ self._never_cache.add(tool_name)
137
+ self.logger.debug(f"Registered factory for: {tool_name}")
138
+
139
+ def unregister_factory(self, tool_name: str) -> bool:
140
+ """
141
+ Unregister a factory function.
142
+
143
+ Args:
144
+ tool_name: Name of the tool
145
+
146
+ Returns:
147
+ True if factory was found and removed
148
+ """
149
+ if tool_name in self._factories:
150
+ del self._factories[tool_name]
151
+ self._never_cache.discard(tool_name)
152
+ return True
153
+ return False
154
+
155
+ def get_or_create(self, tool_name: str) -> Optional[tk.Widget]:
156
+ """
157
+ Get a cached widget or create it if not cached.
158
+
159
+ Args:
160
+ tool_name: Name of the tool
161
+
162
+ Returns:
163
+ The tool's widget, or None if no factory registered
164
+ """
165
+ # Check if we should never cache this tool
166
+ should_cache = (
167
+ self.strategy != CacheStrategy.NEVER and
168
+ tool_name not in self._never_cache
169
+ )
170
+
171
+ # Return cached widget if available and valid
172
+ if tool_name in self._cache and should_cache:
173
+ cached = self._cache[tool_name]
174
+ if not cached.needs_refresh:
175
+ self._stats['cache_hits'] += 1
176
+ self.logger.debug(f"Cache hit for: {tool_name}")
177
+ return cached.widget
178
+ else:
179
+ # Widget needs refresh, destroy and recreate
180
+ self._destroy_widget(tool_name)
181
+
182
+ self._stats['cache_misses'] += 1
183
+
184
+ # Check if we have a factory
185
+ if tool_name not in self._factories:
186
+ self.logger.warning(f"No factory registered for tool: {tool_name}")
187
+ return None
188
+
189
+ # Enforce max cache size (LRU eviction)
190
+ if self.strategy == CacheStrategy.LRU and len(self._cache) >= self.max_cached:
191
+ self._evict_lru()
192
+
193
+ # Create new widget
194
+ try:
195
+ widget = self._factories[tool_name]()
196
+
197
+ if should_cache:
198
+ import time
199
+ self._cache[tool_name] = CachedWidget(
200
+ widget=widget,
201
+ tool_name=tool_name,
202
+ created_at=time.time()
203
+ )
204
+
205
+ self._stats['widgets_created'] += 1
206
+ self.logger.debug(f"Created widget for: {tool_name}")
207
+
208
+ # Callback
209
+ if self._on_widget_created:
210
+ self._on_widget_created(tool_name, widget)
211
+
212
+ return widget
213
+
214
+ except Exception as e:
215
+ self.logger.error(f"Failed to create widget for {tool_name}: {e}")
216
+ return None
217
+
218
+ def show(self, tool_name: str) -> bool:
219
+ """
220
+ Show the widget for the specified tool, hiding others.
221
+
222
+ Args:
223
+ tool_name: Name of the tool to show
224
+
225
+ Returns:
226
+ True if successful, False otherwise
227
+ """
228
+ # Hide current widget
229
+ if self._current_tool and self._current_tool != tool_name:
230
+ self._hide_current()
231
+
232
+ # Get or create the new widget
233
+ widget = self.get_or_create(tool_name)
234
+ if widget is None:
235
+ return False
236
+
237
+ # Show the new widget
238
+ try:
239
+ widget.pack(fill=tk.BOTH, expand=True)
240
+ self._current_tool = tool_name
241
+
242
+ # Update cached widget stats
243
+ if tool_name in self._cache:
244
+ self._cache[tool_name].mark_shown()
245
+
246
+ # Callback
247
+ if self._on_widget_shown:
248
+ self._on_widget_shown(tool_name, widget)
249
+
250
+ self.logger.debug(f"Showing widget: {tool_name}")
251
+ return True
252
+
253
+ except tk.TclError as e:
254
+ self.logger.error(f"Failed to show widget {tool_name}: {e}")
255
+ # Widget might be destroyed, remove from cache
256
+ self._cache.pop(tool_name, None)
257
+ return False
258
+
259
+ def hide(self, tool_name: str) -> bool:
260
+ """
261
+ Hide a specific tool's widget.
262
+
263
+ Args:
264
+ tool_name: Name of the tool to hide
265
+
266
+ Returns:
267
+ True if widget was hidden
268
+ """
269
+ if tool_name not in self._cache:
270
+ return False
271
+
272
+ try:
273
+ self._cache[tool_name].widget.pack_forget()
274
+
275
+ if self._current_tool == tool_name:
276
+ self._current_tool = None
277
+
278
+ # Callback
279
+ if self._on_widget_hidden:
280
+ self._on_widget_hidden(tool_name, self._cache[tool_name].widget)
281
+
282
+ return True
283
+
284
+ except tk.TclError:
285
+ # Widget might be destroyed
286
+ self._cache.pop(tool_name, None)
287
+ return False
288
+
289
+ def _hide_current(self) -> None:
290
+ """Hide the currently shown widget."""
291
+ if self._current_tool and self._current_tool in self._cache:
292
+ try:
293
+ self._cache[self._current_tool].widget.pack_forget()
294
+
295
+ if self._on_widget_hidden:
296
+ self._on_widget_hidden(
297
+ self._current_tool,
298
+ self._cache[self._current_tool].widget
299
+ )
300
+ except tk.TclError:
301
+ # Widget might be destroyed
302
+ self._cache.pop(self._current_tool, None)
303
+
304
+ def invalidate(self, tool_name: str) -> None:
305
+ """
306
+ Mark a widget for refresh (will be recreated on next show).
307
+
308
+ Args:
309
+ tool_name: Name of the tool to invalidate
310
+ """
311
+ if tool_name in self._cache:
312
+ self._cache[tool_name].needs_refresh = True
313
+ self.logger.debug(f"Invalidated cache for: {tool_name}")
314
+
315
+ def destroy(self, tool_name: str) -> bool:
316
+ """
317
+ Destroy a cached widget immediately.
318
+
319
+ Args:
320
+ tool_name: Name of the tool
321
+
322
+ Returns:
323
+ True if widget was found and destroyed
324
+ """
325
+ return self._destroy_widget(tool_name)
326
+
327
+ def _destroy_widget(self, tool_name: str) -> bool:
328
+ """Internal method to destroy a widget."""
329
+ if tool_name not in self._cache:
330
+ return False
331
+
332
+ try:
333
+ self._cache[tool_name].widget.destroy()
334
+ except tk.TclError:
335
+ pass # Already destroyed
336
+
337
+ del self._cache[tool_name]
338
+ self._stats['widgets_destroyed'] += 1
339
+
340
+ if self._current_tool == tool_name:
341
+ self._current_tool = None
342
+
343
+ self.logger.debug(f"Destroyed widget: {tool_name}")
344
+ return True
345
+
346
+ def _evict_lru(self) -> None:
347
+ """Evict the least recently used widget."""
348
+ if not self._cache:
349
+ return
350
+
351
+ # Don't evict current widget
352
+ candidates = [
353
+ (name, cached) for name, cached in self._cache.items()
354
+ if name != self._current_tool
355
+ ]
356
+
357
+ if not candidates:
358
+ return
359
+
360
+ # Find LRU
361
+ lru_name, _ = min(candidates, key=lambda x: x[1].last_shown)
362
+ self._destroy_widget(lru_name)
363
+ self.logger.debug(f"Evicted LRU widget: {lru_name}")
364
+
365
+ def clear(self) -> None:
366
+ """Clear all cached widgets."""
367
+ for tool_name in list(self._cache.keys()):
368
+ self._destroy_widget(tool_name)
369
+ self._current_tool = None
370
+ self.logger.debug("Cleared all cached widgets")
371
+
372
+ def refresh_all(self) -> None:
373
+ """Mark all widgets for refresh."""
374
+ for cached in self._cache.values():
375
+ cached.needs_refresh = True
376
+ self.logger.debug("Marked all widgets for refresh")
377
+
378
+ def is_cached(self, tool_name: str) -> bool:
379
+ """Check if a tool's widget is cached."""
380
+ return tool_name in self._cache
381
+
382
+ def get_widget(self, tool_name: str) -> Optional[tk.Widget]:
383
+ """
384
+ Get a cached widget without creating it.
385
+
386
+ Args:
387
+ tool_name: Name of the tool
388
+
389
+ Returns:
390
+ The widget if cached, None otherwise
391
+ """
392
+ if tool_name in self._cache:
393
+ return self._cache[tool_name].widget
394
+ return None
395
+
396
+ @property
397
+ def cached_tools(self) -> List[str]:
398
+ """Get list of currently cached tool names."""
399
+ return list(self._cache.keys())
400
+
401
+ @property
402
+ def current_tool(self) -> Optional[str]:
403
+ """Get the currently displayed tool name."""
404
+ return self._current_tool
405
+
406
+ @property
407
+ def factory_count(self) -> int:
408
+ """Get number of registered factories."""
409
+ return len(self._factories)
410
+
411
+ def get_stats(self) -> Dict[str, Any]:
412
+ """Get cache statistics."""
413
+ total_requests = self._stats['cache_hits'] + self._stats['cache_misses']
414
+ hit_rate = (
415
+ self._stats['cache_hits'] / total_requests * 100
416
+ if total_requests > 0 else 0
417
+ )
418
+
419
+ return {
420
+ **self._stats,
421
+ 'cached_widgets': len(self._cache),
422
+ 'registered_factories': len(self._factories),
423
+ 'current_tool': self._current_tool,
424
+ 'hit_rate_percent': round(hit_rate, 2),
425
+ 'strategy': self.strategy.value
426
+ }
427
+
428
+ def get_cache_info(self, tool_name: str) -> Optional[Dict[str, Any]]:
429
+ """
430
+ Get information about a cached widget.
431
+
432
+ Args:
433
+ tool_name: Name of the tool
434
+
435
+ Returns:
436
+ Dictionary with cache info, or None if not cached
437
+ """
438
+ if tool_name not in self._cache:
439
+ return None
440
+
441
+ cached = self._cache[tool_name]
442
+ return {
443
+ 'tool_name': cached.tool_name,
444
+ 'created_at': cached.created_at,
445
+ 'last_shown': cached.last_shown,
446
+ 'show_count': cached.show_count,
447
+ 'needs_refresh': cached.needs_refresh
448
+ }
449
+
450
+ # Callback setters
451
+ def set_on_widget_created(self, callback: Callable[[str, tk.Widget], None]) -> None:
452
+ """Set callback for when a widget is created."""
453
+ self._on_widget_created = callback
454
+
455
+ def set_on_widget_shown(self, callback: Callable[[str, tk.Widget], None]) -> None:
456
+ """Set callback for when a widget is shown."""
457
+ self._on_widget_shown = callback
458
+
459
+ def set_on_widget_hidden(self, callback: Callable[[str, tk.Widget], None]) -> None:
460
+ """Set callback for when a widget is hidden."""
461
+ self._on_widget_hidden = callback
462
+
463
+
464
+ # Global instance
465
+ _widget_cache: Optional[WidgetCache] = None
466
+
467
+
468
+ def get_widget_cache() -> Optional[WidgetCache]:
469
+ """Get the global widget cache instance."""
470
+ return _widget_cache
471
+
472
+
473
+ def init_widget_cache(parent_frame: tk.Frame,
474
+ strategy: CacheStrategy = CacheStrategy.ALWAYS,
475
+ max_cached: int = 20) -> WidgetCache:
476
+ """
477
+ Initialize the global widget cache.
478
+
479
+ Args:
480
+ parent_frame: Parent frame for widgets
481
+ strategy: Caching strategy
482
+ max_cached: Maximum cached widgets (for LRU)
483
+
484
+ Returns:
485
+ Initialized WidgetCache
486
+ """
487
+ global _widget_cache
488
+ _widget_cache = WidgetCache(parent_frame, strategy, max_cached)
489
+ return _widget_cache
490
+
491
+
492
+ def shutdown_widget_cache() -> None:
493
+ """Shutdown the global widget cache."""
494
+ global _widget_cache
495
+ if _widget_cache is not None:
496
+ _widget_cache.clear()
497
+ _widget_cache = None
498
+