pomera-ai-commander 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +680 -0
  3. package/bin/pomera-ai-commander.js +62 -0
  4. package/core/__init__.py +66 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  7. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  8. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  9. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  10. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  11. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  12. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  13. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  14. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  15. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  16. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  17. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  18. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  19. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  20. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  21. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  22. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  23. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  24. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  25. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  26. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  27. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  28. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  29. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  30. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  31. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  32. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  33. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  34. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  35. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  36. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  37. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  38. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  39. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  40. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  41. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  42. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  43. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  44. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  45. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  46. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  47. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  48. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  49. package/core/app_context.py +482 -0
  50. package/core/async_text_processor.py +422 -0
  51. package/core/backup_manager.py +656 -0
  52. package/core/backup_recovery_manager.py +1034 -0
  53. package/core/content_hash_cache.py +509 -0
  54. package/core/context_menu.py +313 -0
  55. package/core/data_validator.py +1067 -0
  56. package/core/database_connection_manager.py +745 -0
  57. package/core/database_curl_settings_manager.py +609 -0
  58. package/core/database_promera_ai_settings_manager.py +447 -0
  59. package/core/database_schema.py +412 -0
  60. package/core/database_schema_manager.py +396 -0
  61. package/core/database_settings_manager.py +1508 -0
  62. package/core/database_settings_manager_interface.py +457 -0
  63. package/core/dialog_manager.py +735 -0
  64. package/core/efficient_line_numbers.py +511 -0
  65. package/core/error_handler.py +747 -0
  66. package/core/error_service.py +431 -0
  67. package/core/event_consolidator.py +512 -0
  68. package/core/mcp/__init__.py +43 -0
  69. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  70. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  71. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  72. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  73. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  74. package/core/mcp/protocol.py +288 -0
  75. package/core/mcp/schema.py +251 -0
  76. package/core/mcp/server_stdio.py +299 -0
  77. package/core/mcp/tool_registry.py +2345 -0
  78. package/core/memory_efficient_text_widget.py +712 -0
  79. package/core/migration_manager.py +915 -0
  80. package/core/migration_test_suite.py +1086 -0
  81. package/core/migration_validator.py +1144 -0
  82. package/core/optimized_find_replace.py +715 -0
  83. package/core/optimized_pattern_engine.py +424 -0
  84. package/core/optimized_search_highlighter.py +553 -0
  85. package/core/performance_monitor.py +675 -0
  86. package/core/persistence_manager.py +713 -0
  87. package/core/progressive_stats_calculator.py +632 -0
  88. package/core/regex_pattern_cache.py +530 -0
  89. package/core/regex_pattern_library.py +351 -0
  90. package/core/search_operation_manager.py +435 -0
  91. package/core/settings_defaults_registry.py +1087 -0
  92. package/core/settings_integrity_validator.py +1112 -0
  93. package/core/settings_serializer.py +558 -0
  94. package/core/settings_validator.py +1824 -0
  95. package/core/smart_stats_calculator.py +710 -0
  96. package/core/statistics_update_manager.py +619 -0
  97. package/core/stats_config_manager.py +858 -0
  98. package/core/streaming_text_handler.py +723 -0
  99. package/core/task_scheduler.py +596 -0
  100. package/core/update_pattern_library.py +169 -0
  101. package/core/visibility_monitor.py +596 -0
  102. package/core/widget_cache.py +498 -0
  103. package/mcp.json +61 -0
  104. package/package.json +57 -0
  105. package/pomera.py +7483 -0
  106. package/pomera_mcp_server.py +144 -0
  107. package/tools/__init__.py +5 -0
  108. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  110. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  111. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  112. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  113. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  114. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  115. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  116. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  117. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  118. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  119. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  120. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  121. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  122. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  123. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  124. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  125. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  126. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  127. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  128. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  129. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  130. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  131. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  132. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  133. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  134. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  135. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  136. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  137. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  138. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  139. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  140. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  141. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  142. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  143. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  144. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  145. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  146. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  147. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  148. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
  151. package/tools/ai_tools.py +2892 -0
  152. package/tools/ascii_art_generator.py +353 -0
  153. package/tools/base64_tools.py +184 -0
  154. package/tools/base_tool.py +511 -0
  155. package/tools/case_tool.py +309 -0
  156. package/tools/column_tools.py +396 -0
  157. package/tools/cron_tool.py +885 -0
  158. package/tools/curl_history.py +601 -0
  159. package/tools/curl_processor.py +1208 -0
  160. package/tools/curl_settings.py +503 -0
  161. package/tools/curl_tool.py +5467 -0
  162. package/tools/diff_viewer.py +1072 -0
  163. package/tools/email_extraction_tool.py +249 -0
  164. package/tools/email_header_analyzer.py +426 -0
  165. package/tools/extraction_tools.py +250 -0
  166. package/tools/find_replace.py +1751 -0
  167. package/tools/folder_file_reporter.py +1463 -0
  168. package/tools/folder_file_reporter_adapter.py +480 -0
  169. package/tools/generator_tools.py +1217 -0
  170. package/tools/hash_generator.py +256 -0
  171. package/tools/html_tool.py +657 -0
  172. package/tools/huggingface_helper.py +449 -0
  173. package/tools/jsonxml_tool.py +730 -0
  174. package/tools/line_tools.py +419 -0
  175. package/tools/list_comparator.py +720 -0
  176. package/tools/markdown_tools.py +562 -0
  177. package/tools/mcp_widget.py +1417 -0
  178. package/tools/notes_widget.py +973 -0
  179. package/tools/number_base_converter.py +373 -0
  180. package/tools/regex_extractor.py +572 -0
  181. package/tools/slug_generator.py +311 -0
  182. package/tools/sorter_tools.py +459 -0
  183. package/tools/string_escape_tool.py +393 -0
  184. package/tools/text_statistics_tool.py +366 -0
  185. package/tools/text_wrapper.py +431 -0
  186. package/tools/timestamp_converter.py +422 -0
  187. package/tools/tool_loader.py +710 -0
  188. package/tools/translator_tools.py +523 -0
  189. package/tools/url_link_extractor.py +262 -0
  190. package/tools/url_parser.py +205 -0
  191. package/tools/whitespace_tools.py +356 -0
  192. package/tools/word_frequency_counter.py +147 -0
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Search operation manager with cancellation, timeout handling, and resource management.
4
+ """
5
+
6
+ import tkinter as tk
7
+ import threading
8
+ import time
9
+ import weakref
10
+ from typing import Dict, List, Optional, Callable, Any, Set
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ import uuid
14
+
15
+ class CancellationReason(Enum):
16
+ """Reasons for operation cancellation."""
17
+ USER_REQUESTED = "user_requested"
18
+ TIMEOUT = "timeout"
19
+ RESOURCE_LIMIT = "resource_limit"
20
+ WIDGET_DESTROYED = "widget_destroyed"
21
+ SYSTEM_SHUTDOWN = "system_shutdown"
22
+ ERROR = "error"
23
+
24
+ class OperationStatus(Enum):
25
+ """Status of search operations."""
26
+ PENDING = "pending"
27
+ RUNNING = "running"
28
+ COMPLETED = "completed"
29
+ CANCELLED = "cancelled"
30
+ FAILED = "failed"
31
+
32
+ @dataclass
33
+ class OperationTimeout:
34
+ """Timeout configuration for operations."""
35
+ search_timeout: float = 30.0 # seconds
36
+ highlight_timeout: float = 60.0 # seconds
37
+ replace_timeout: float = 120.0 # seconds
38
+ preview_timeout: float = 15.0 # seconds
39
+
40
+ @dataclass
41
+ class ResourceLimits:
42
+ """Resource limits for operations."""
43
+ max_concurrent_operations: int = 5
44
+ max_operations_per_widget: int = 3
45
+ max_memory_mb: float = 100.0
46
+ max_matches_per_operation: int = 10000
47
+
48
+
49
+
50
+ @dataclass
51
+ class ManagedOperation:
52
+ """Represents a managed search operation."""
53
+ operation_id: str
54
+ operation_type: str
55
+ text_widget_ref: Optional[weakref.ref] = None
56
+ status: OperationStatus = OperationStatus.PENDING
57
+ cancellation_reason: Optional[CancellationReason] = None
58
+
59
+ # Operation parameters
60
+ pattern: str = ""
61
+ replacement: str = ""
62
+ case_sensitive: bool = True
63
+ whole_words: bool = False
64
+ use_regex: bool = False
65
+
66
+ # Control
67
+ cancel_event: threading.Event = field(default_factory=threading.Event)
68
+ completion_event: threading.Event = field(default_factory=threading.Event)
69
+
70
+ # Callbacks
71
+ progress_callback: Optional[Callable] = None
72
+ completion_callback: Optional[Callable] = None
73
+ error_callback: Optional[Callable] = None
74
+
75
+ # Results
76
+ results: Dict[str, Any] = field(default_factory=dict)
77
+ error_message: Optional[str] = None
78
+
79
+ def is_cancelled(self) -> bool:
80
+ """Check if operation is cancelled."""
81
+ return self.cancel_event.is_set()
82
+
83
+ def cancel(self, reason: CancellationReason):
84
+ """Cancel the operation with given reason."""
85
+ self.cancellation_reason = reason
86
+ self.status = OperationStatus.CANCELLED
87
+ self.cancel_event.set()
88
+ self.completion_event.set()
89
+
90
+ def complete(self, results: Optional[Dict[str, Any]] = None):
91
+ """Mark operation as completed."""
92
+ if results:
93
+ self.results.update(results)
94
+ self.status = OperationStatus.COMPLETED
95
+ self.completion_event.set()
96
+
97
+ def fail(self, error_message: str):
98
+ """Mark operation as failed."""
99
+ self.error_message = error_message
100
+ self.status = OperationStatus.FAILED
101
+ self.completion_event.set()
102
+
103
+ def get_text_widget(self) -> Optional[tk.Text]:
104
+ """Get the text widget if it still exists."""
105
+ return self.text_widget_ref() if self.text_widget_ref else None
106
+
107
+ class SearchOperationManager:
108
+ """
109
+ Manages search operations with cancellation, timeout handling,
110
+ and resource management for optimal performance.
111
+ """
112
+
113
+ def __init__(self,
114
+ timeouts: Optional[OperationTimeout] = None,
115
+ limits: Optional[ResourceLimits] = None):
116
+
117
+ self.timeouts = timeouts or OperationTimeout()
118
+ self.limits = limits or ResourceLimits()
119
+
120
+ # Operation tracking
121
+ self.operations: Dict[str, ManagedOperation] = {}
122
+ self.operations_lock = threading.RLock()
123
+
124
+ # Widget-specific operation tracking
125
+ self.widget_operations: Dict[int, List[str]] = {} # widget_id -> operation_ids
126
+
127
+ # Timeout monitoring
128
+ self.timeout_monitor_thread = None
129
+ self.shutdown_event = threading.Event()
130
+ self._start_timeout_monitor()
131
+
132
+ def _start_timeout_monitor(self):
133
+ """Start the timeout monitoring thread."""
134
+ if self.timeout_monitor_thread is None or not self.timeout_monitor_thread.is_alive():
135
+ self.timeout_monitor_thread = threading.Thread(
136
+ target=self._timeout_monitor_loop,
137
+ daemon=True,
138
+ name="SearchOperationTimeout"
139
+ )
140
+ self.timeout_monitor_thread.start()
141
+
142
+ def _timeout_monitor_loop(self):
143
+ """Monitor operations for timeouts."""
144
+ while not self.shutdown_event.is_set():
145
+ try:
146
+ current_time = time.time()
147
+ operations_to_cancel = []
148
+
149
+ with self.operations_lock:
150
+ for op_id, operation in self.operations.items():
151
+ if operation.status not in [OperationStatus.RUNNING, OperationStatus.PENDING]:
152
+ continue
153
+
154
+ # Check timeout based on operation type
155
+ timeout = self._get_timeout_for_operation(operation.operation_type)
156
+ if current_time - operation.metrics.start_time > timeout:
157
+ operations_to_cancel.append((op_id, operation))
158
+
159
+ # Cancel timed out operations
160
+ for op_id, operation in operations_to_cancel:
161
+ self._cancel_operation_internal(operation, CancellationReason.TIMEOUT)
162
+
163
+ # Sleep for a short interval
164
+ time.sleep(1.0)
165
+
166
+ except Exception as e:
167
+ print(f"Error in timeout monitor: {e}")
168
+ time.sleep(1.0)
169
+
170
+ def _get_timeout_for_operation(self, operation_type: str) -> float:
171
+ """Get timeout value for operation type."""
172
+ timeout_map = {
173
+ 'search': self.timeouts.search_timeout,
174
+ 'highlight': self.timeouts.highlight_timeout,
175
+ 'replace': self.timeouts.replace_timeout,
176
+ 'preview': self.timeouts.preview_timeout
177
+ }
178
+ return timeout_map.get(operation_type, self.timeouts.search_timeout)
179
+
180
+ def create_operation(self,
181
+ operation_type: str,
182
+ text_widget: tk.Text,
183
+ pattern: str,
184
+ replacement: str = "",
185
+ case_sensitive: bool = True,
186
+ whole_words: bool = False,
187
+ use_regex: bool = False,
188
+ progress_callback: Optional[Callable] = None,
189
+ completion_callback: Optional[Callable] = None,
190
+ error_callback: Optional[Callable] = None) -> Optional[str]:
191
+ """
192
+ Create a new managed search operation.
193
+
194
+ Returns:
195
+ Operation ID if created successfully, None if rejected due to limits
196
+ """
197
+ with self.operations_lock:
198
+ # Check resource limits
199
+ if not self._can_create_operation(text_widget):
200
+ return None
201
+
202
+ # Generate unique operation ID
203
+ operation_id = str(uuid.uuid4())
204
+
205
+ # Create operation
206
+ operation = ManagedOperation(
207
+ operation_id=operation_id,
208
+ operation_type=operation_type,
209
+ text_widget_ref=weakref.ref(text_widget),
210
+ pattern=pattern,
211
+ replacement=replacement,
212
+ case_sensitive=case_sensitive,
213
+ whole_words=whole_words,
214
+ use_regex=use_regex,
215
+ progress_callback=progress_callback,
216
+ completion_callback=completion_callback,
217
+ error_callback=error_callback
218
+ )
219
+
220
+ # Track operation
221
+ self.operations[operation_id] = operation
222
+
223
+ # Track by widget
224
+ widget_id = id(text_widget)
225
+ if widget_id not in self.widget_operations:
226
+ self.widget_operations[widget_id] = []
227
+ self.widget_operations[widget_id].append(operation_id)
228
+
229
+ return operation_id
230
+
231
+ def _can_create_operation(self, text_widget: tk.Text) -> bool:
232
+ """Check if a new operation can be created based on resource limits."""
233
+ # Check total concurrent operations
234
+ active_count = sum(1 for op in self.operations.values()
235
+ if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING])
236
+
237
+ if active_count >= self.limits.max_concurrent_operations:
238
+ return False
239
+
240
+ # Check operations per widget
241
+ widget_id = id(text_widget)
242
+ if widget_id in self.widget_operations:
243
+ widget_active_count = sum(1 for op_id in self.widget_operations[widget_id]
244
+ if op_id in self.operations and
245
+ self.operations[op_id].status in [OperationStatus.PENDING, OperationStatus.RUNNING])
246
+
247
+ if widget_active_count >= self.limits.max_operations_per_widget:
248
+ return False
249
+
250
+ return True
251
+
252
+ def start_operation(self, operation_id: str) -> bool:
253
+ """Start a pending operation."""
254
+ with self.operations_lock:
255
+ if operation_id not in self.operations:
256
+ return False
257
+
258
+ operation = self.operations[operation_id]
259
+ if operation.status != OperationStatus.PENDING:
260
+ return False
261
+
262
+ # Check if widget still exists
263
+ if operation.get_text_widget() is None:
264
+ self._cancel_operation_internal(operation, CancellationReason.WIDGET_DESTROYED)
265
+ return False
266
+
267
+ operation.status = OperationStatus.RUNNING
268
+ return True
269
+
270
+ def cancel_operation(self, operation_id: str, reason: CancellationReason = CancellationReason.USER_REQUESTED) -> bool:
271
+ """Cancel a specific operation."""
272
+ with self.operations_lock:
273
+ if operation_id not in self.operations:
274
+ return False
275
+
276
+ operation = self.operations[operation_id]
277
+ self._cancel_operation_internal(operation, reason)
278
+ return True
279
+
280
+ def _cancel_operation_internal(self, operation: ManagedOperation, reason: CancellationReason):
281
+ """Internal method to cancel an operation."""
282
+ if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
283
+ return
284
+
285
+ operation.cancel(reason)
286
+
287
+ # Call error callback if provided
288
+ if operation.error_callback:
289
+ try:
290
+ operation.error_callback(operation, f"Operation cancelled: {reason.value}")
291
+ except Exception as e:
292
+ print(f"Error in operation error callback: {e}")
293
+
294
+ def cancel_widget_operations(self, text_widget: tk.Text, reason: CancellationReason = CancellationReason.USER_REQUESTED):
295
+ """Cancel all operations for a specific widget."""
296
+ widget_id = id(text_widget)
297
+
298
+ with self.operations_lock:
299
+ if widget_id not in self.widget_operations:
300
+ return
301
+
302
+ for operation_id in self.widget_operations[widget_id][:]: # Copy list to avoid modification during iteration
303
+ if operation_id in self.operations:
304
+ operation = self.operations[operation_id]
305
+ self._cancel_operation_internal(operation, reason)
306
+
307
+ def cancel_all_operations(self, reason: CancellationReason = CancellationReason.SYSTEM_SHUTDOWN):
308
+ """Cancel all active operations."""
309
+ with self.operations_lock:
310
+ for operation in list(self.operations.values()):
311
+ self._cancel_operation_internal(operation, reason)
312
+
313
+ def complete_operation(self, operation_id: str, results: Optional[Dict[str, Any]] = None) -> bool:
314
+ """Mark an operation as completed."""
315
+ with self.operations_lock:
316
+ if operation_id not in self.operations:
317
+ return False
318
+
319
+ operation = self.operations[operation_id]
320
+ if operation.status != OperationStatus.RUNNING:
321
+ return False
322
+
323
+ operation.complete(results)
324
+
325
+ # Call completion callback if provided
326
+ if operation.completion_callback:
327
+ try:
328
+ operation.completion_callback(operation)
329
+ except Exception as e:
330
+ print(f"Error in operation completion callback: {e}")
331
+
332
+ return True
333
+
334
+ def fail_operation(self, operation_id: str, error_message: str) -> bool:
335
+ """Mark an operation as failed."""
336
+ with self.operations_lock:
337
+ if operation_id not in self.operations:
338
+ return False
339
+
340
+ operation = self.operations[operation_id]
341
+ if operation.status not in [OperationStatus.PENDING, OperationStatus.RUNNING]:
342
+ return False
343
+
344
+ operation.fail(error_message)
345
+
346
+ # Call error callback if provided
347
+ if operation.error_callback:
348
+ try:
349
+ operation.error_callback(operation, error_message)
350
+ except Exception as e:
351
+ print(f"Error in operation error callback: {e}")
352
+
353
+ return True
354
+
355
+ def get_operation(self, operation_id: str) -> Optional[ManagedOperation]:
356
+ """Get operation by ID."""
357
+ with self.operations_lock:
358
+ return self.operations.get(operation_id)
359
+
360
+ def get_widget_operations(self, text_widget: tk.Text) -> List[ManagedOperation]:
361
+ """Get all operations for a specific widget."""
362
+ widget_id = id(text_widget)
363
+
364
+ with self.operations_lock:
365
+ if widget_id not in self.widget_operations:
366
+ return []
367
+
368
+ operations = []
369
+ for operation_id in self.widget_operations[widget_id]:
370
+ if operation_id in self.operations:
371
+ operations.append(self.operations[operation_id])
372
+
373
+ return operations
374
+
375
+ def get_active_operations(self) -> List[ManagedOperation]:
376
+ """Get all active (pending or running) operations."""
377
+ with self.operations_lock:
378
+ return [op for op in self.operations.values()
379
+ if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING]]
380
+
381
+ def cleanup_completed_operations(self, max_age_seconds: float = 300):
382
+ """Clean up old completed operations."""
383
+ current_time = time.time()
384
+ operations_to_remove = []
385
+
386
+ with self.operations_lock:
387
+ for operation_id, operation in self.operations.items():
388
+ if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
389
+ operations_to_remove.append(operation_id)
390
+
391
+ # Remove old operations
392
+ for operation_id in operations_to_remove:
393
+ operation = self.operations.pop(operation_id, None)
394
+ if operation and operation.text_widget_ref:
395
+ widget_id = id(operation.get_text_widget()) if operation.get_text_widget() else None
396
+ if widget_id and widget_id in self.widget_operations:
397
+ try:
398
+ self.widget_operations[widget_id].remove(operation_id)
399
+ if not self.widget_operations[widget_id]:
400
+ del self.widget_operations[widget_id]
401
+ except ValueError:
402
+ pass
403
+
404
+ def wait_for_operation(self, operation_id: str, timeout: Optional[float] = None) -> bool:
405
+ """Wait for an operation to complete."""
406
+ operation = self.get_operation(operation_id)
407
+ if not operation:
408
+ return False
409
+
410
+ return operation.completion_event.wait(timeout)
411
+
412
+ def shutdown(self):
413
+ """Shutdown the operation manager."""
414
+ self.cancel_all_operations(CancellationReason.SYSTEM_SHUTDOWN)
415
+ self.shutdown_event.set()
416
+
417
+ if self.timeout_monitor_thread and self.timeout_monitor_thread.is_alive():
418
+ self.timeout_monitor_thread.join(timeout=2.0)
419
+
420
+ # Global instance
421
+ _global_operation_manager = None
422
+
423
+ def get_operation_manager() -> SearchOperationManager:
424
+ """Get the global search operation manager instance."""
425
+ global _global_operation_manager
426
+ if _global_operation_manager is None:
427
+ _global_operation_manager = SearchOperationManager()
428
+ return _global_operation_manager
429
+
430
+ def shutdown_operation_manager():
431
+ """Shutdown the global operation manager."""
432
+ global _global_operation_manager
433
+ if _global_operation_manager is not None:
434
+ _global_operation_manager.shutdown()
435
+ _global_operation_manager = None