abstractassistant 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
abstractassistant/app.py CHANGED
@@ -33,7 +33,8 @@ class EnhancedClickableIcon(pystray.Icon):
33
33
  self.DOUBLE_CLICK_TIMEOUT = 300 # milliseconds
34
34
 
35
35
  if self.debug:
36
- print(f"🔄 EnhancedClickableIcon created with single_click: {single_click_handler is not None}, double_click: {double_click_handler is not None}")
36
+ if hasattr(self, 'debug') and self.debug:
37
+ print(f"🔄 EnhancedClickableIcon created with single_click: {single_click_handler is not None}, double_click: {double_click_handler is not None}")
37
38
 
38
39
  # Create with no menu initially
39
40
  super().__init__(name, image, text, menu=None)
@@ -42,7 +43,8 @@ class EnhancedClickableIcon(pystray.Icon):
42
43
  def _menu(self):
43
44
  """Override _menu property to intercept access and handle click timing."""
44
45
  if self.debug:
45
- print(f"🔍 _menu property accessed! Click count: {self.click_count}")
46
+ if hasattr(self, 'debug') and self.debug:
47
+ print(f"🔍 _menu property accessed! Click count: {self.click_count}")
46
48
 
47
49
  self._handle_click_timing()
48
50
  # Return None so no menu is displayed
@@ -66,7 +68,8 @@ class EnhancedClickableIcon(pystray.Icon):
66
68
  self.click_timer.start()
67
69
 
68
70
  if self.debug:
69
- print("🔄 First click detected, starting timer...")
71
+ if hasattr(self, 'debug') and self.debug:
72
+ print("🔄 First click detected, starting timer...")
70
73
 
71
74
  elif self.click_count == 2:
72
75
  # Second click - cancel timer and execute double click
@@ -78,7 +81,8 @@ class EnhancedClickableIcon(pystray.Icon):
78
81
  self._execute_double_click()
79
82
 
80
83
  if self.debug:
81
- print("🔄 Double click detected!")
84
+ if hasattr(self, 'debug') and self.debug:
85
+ print("🔄 Double click detected!")
82
86
 
83
87
  def _execute_single_click(self):
84
88
  """Execute single click handler after timeout."""
@@ -86,13 +90,15 @@ class EnhancedClickableIcon(pystray.Icon):
86
90
  self.click_timer = None
87
91
 
88
92
  if self.debug:
89
- print("✅ Single click detected on system tray icon!")
93
+ if hasattr(self, 'debug') and self.debug:
94
+ print("✅ Single click detected on system tray icon!")
90
95
 
91
96
  if self.single_click_handler:
92
97
  try:
93
98
  self.single_click_handler()
94
99
  except Exception as e:
95
- print(f"❌ Single click handler error: {e}")
100
+ if hasattr(self, 'debug') and self.debug:
101
+ print(f"❌ Single click handler error: {e}")
96
102
  if self.debug:
97
103
  import traceback
98
104
  traceback.print_exc()
@@ -100,13 +106,15 @@ class EnhancedClickableIcon(pystray.Icon):
100
106
  def _execute_double_click(self):
101
107
  """Execute double click handler immediately."""
102
108
  if self.debug:
103
- print("✅ Double click detected on system tray icon!")
109
+ if hasattr(self, 'debug') and self.debug:
110
+ print("✅ Double click detected on system tray icon!")
104
111
 
105
112
  if self.double_click_handler:
106
113
  try:
107
114
  self.double_click_handler()
108
115
  except Exception as e:
109
- print(f"❌ Double click handler error: {e}")
116
+ if hasattr(self, 'debug') and self.debug:
117
+ print(f"❌ Double click handler error: {e}")
110
118
  if self.debug:
111
119
  import traceback
112
120
  traceback.print_exc()
@@ -115,7 +123,8 @@ class EnhancedClickableIcon(pystray.Icon):
115
123
  def _menu(self, value):
116
124
  """Allow setting _menu during initialization."""
117
125
  if self.debug:
118
- print(f"🔍 _menu property set to: {value}")
126
+ if hasattr(self, 'debug') and self.debug:
127
+ print(f"🔍 _menu property set to: {value}")
119
128
  self._stored_menu = value
120
129
 
121
130
 
@@ -136,7 +145,8 @@ class AbstractAssistantApp:
136
145
 
137
146
  # Validate configuration
138
147
  if not self.config.validate():
139
- print("Warning: Configuration validation failed, using defaults")
148
+ if self.debug:
149
+ print("Warning: Configuration validation failed, using defaults")
140
150
  self.config = Config.default()
141
151
 
142
152
  # Initialize components
@@ -149,19 +159,31 @@ class AbstractAssistantApp:
149
159
  self.is_running: bool = False
150
160
  self.bubble_visible: bool = False
151
161
 
162
+ # Icon animation state
163
+ self.base_icon: Optional[Image.Image] = None
164
+ self.animation_timer: Optional[threading.Timer] = None
165
+ self.current_status: str = "ready"
166
+
152
167
  if self.debug:
153
168
  print(f"AbstractAssistant initialized with config: {self.config.to_dict()}")
154
169
 
155
170
  def create_system_tray_icon(self) -> pystray.Icon:
156
171
  """Create and configure the system tray icon."""
157
- # Generate a modern, clean icon - start with ready state (green, steady)
158
- icon_image = self.icon_generator.create_app_icon(
159
- color_scheme="green", # Ready state: steady green
160
- animated=False # Ready state: no animation
161
- )
172
+ # Try to use the app bundle icon first, fallback to generated icon
173
+ self.base_icon = self._load_app_bundle_icon()
174
+ if not self.base_icon:
175
+ # Generate a modern, clean icon - start with ready state (green, steady)
176
+ self.base_icon = self.icon_generator.create_app_icon(
177
+ color_scheme="green", # Ready state: steady green
178
+ animated=False # Ready state: no animation
179
+ )
180
+
181
+ # Apply initial heartbeat effect
182
+ icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, "ready")
162
183
 
163
184
  if self.debug:
164
- print("🔄 Creating enhanced system tray icon with single/double click detection")
185
+ if self.debug:
186
+ print("🔄 Creating enhanced system tray icon with single/double click detection")
165
187
 
166
188
  # Use our enhanced ClickableIcon for single/double click handling
167
189
  return EnhancedClickableIcon(
@@ -173,138 +195,189 @@ class AbstractAssistantApp:
173
195
  debug=self.debug
174
196
  )
175
197
 
198
+ def _load_app_bundle_icon(self) -> Optional[Image.Image]:
199
+ """Load the icon from the app bundle if available."""
200
+ try:
201
+ from pathlib import Path
202
+ # Try to find the app bundle icon
203
+ app_bundle_icon = Path("/Applications/AbstractAssistant.app/Contents/Resources/icon.png")
204
+
205
+ if self.debug:
206
+ if self.debug:
207
+ print(f"🔍 Looking for app bundle icon at: {app_bundle_icon}")
208
+ print(f" Exists: {app_bundle_icon.exists()}")
209
+
210
+ if app_bundle_icon.exists():
211
+ base_icon = Image.open(app_bundle_icon)
212
+
213
+ if self.debug:
214
+ if self.debug:
215
+ print(f"✅ Loaded app bundle icon: {base_icon.size} {base_icon.mode}")
216
+
217
+ # Resize to system tray size if needed
218
+ target_size = (self.config.system_tray.icon_size, self.config.system_tray.icon_size)
219
+ if base_icon.size != target_size:
220
+ if self.debug:
221
+ if self.debug:
222
+ print(f"🔄 Resizing from {base_icon.size} to {target_size}")
223
+ base_icon = base_icon.resize(target_size, Image.Resampling.LANCZOS)
224
+
225
+ return base_icon
226
+ except Exception as e:
227
+ if self.debug:
228
+ if self.debug:
229
+ print(f"❌ Could not load app bundle icon: {e}")
230
+ return None
231
+
176
232
  def update_icon_status(self, status: str):
177
233
  """Update the system tray icon based on application status.
178
234
 
179
235
  Args:
180
- status: 'ready', 'generating', 'executing', 'thinking'
236
+ status: 'ready', 'generating', 'executing', 'thinking', 'speaking'
181
237
  """
182
- if not self.icon:
238
+ if self.debug:
239
+ print(f"🔄 update_icon_status called with: {status}")
240
+ print(f" Previous status: {self.current_status}")
241
+
242
+ if not self.icon and not (hasattr(self, 'qt_tray_icon') and self.qt_tray_icon):
243
+ if self.debug:
244
+ print("⚠️ No icon available for status update")
183
245
  return
184
-
246
+
247
+ if not self.base_icon:
248
+ if self.debug:
249
+ print("⚠️ No base icon available for status update")
250
+ return
251
+
185
252
  try:
186
- if status == "ready":
187
- # Ready: gentle heartbeat green
188
- self._stop_working_animation()
189
- self._start_ready_animation()
190
- elif status in ["generating", "executing", "thinking"]:
191
- # Working: start continuous animation with cycling colors
192
- self._start_working_animation()
193
- return # Don't update icon here, let the timer handle it
253
+ # Stop any existing animation timer
254
+ self._stop_animation_timer()
255
+
256
+ # Map status to animation type
257
+ animation_status = status
258
+ if status in ["thinking", "generating", "executing"]:
259
+ animation_status = "thinking" # All working states use thinking animation
260
+ elif status == "speaking":
261
+ animation_status = "speaking"
194
262
  else:
195
- # Default: steady green
196
- icon_image = self.icon_generator.create_app_icon(
197
- color_scheme="green",
198
- animated=False
199
- )
263
+ animation_status = "ready" # Default to ready
200
264
 
201
- # Update the icon
202
- self.icon.icon = icon_image
265
+ # Update current status AFTER determining animation type
266
+ self.current_status = animation_status
267
+
268
+ # Start appropriate animation
269
+ self._start_heartbeat_animation(animation_status)
203
270
 
204
271
  if self.debug:
205
- print(f"🎨 Updated icon status to: {status}")
272
+ print(f"🎨 Updated icon status: {status} -> animation: {animation_status}")
206
273
 
207
274
  except Exception as e:
208
275
  if self.debug:
209
276
  print(f"❌ Error updating icon status: {e}")
210
277
 
211
- def _start_working_animation(self):
212
- """Start the working animation timer for continuous icon updates."""
278
+ def _start_heartbeat_animation(self, status: str):
279
+ """Start smooth heartbeat animation for the given status."""
280
+ if self.debug:
281
+ print(f"🎬 Starting smooth heartbeat animation for: {status}")
282
+ print(f" pystray icon available: {self.icon is not None}")
283
+ print(f" Qt icon available: {hasattr(self, 'qt_tray_icon') and self.qt_tray_icon is not None}")
284
+ print(f" Base icon available: {self.base_icon is not None}")
285
+
286
+ # Stop any existing animation
287
+ self._stop_animation_timer()
288
+
289
+ # Use Qt timer if we're in Qt mode, otherwise use threading timer
290
+ if hasattr(self, 'qt_tray_icon') and self.qt_tray_icon is not None:
291
+ self._start_qt_animation(status)
292
+ else:
293
+ self._start_threading_animation(status)
294
+
295
+ def _start_qt_animation(self, status: str):
296
+ """Start Qt-based animation using QTimer."""
213
297
  try:
214
- import threading
215
- import time
216
-
217
- # Stop any existing timer
218
- self._stop_working_animation()
219
-
220
- # Create a heartbeat-like animation with dynamic timing
221
- def update_working_icon():
222
- if self.icon:
223
- try:
224
- icon_image = self.icon_generator.create_app_icon(
225
- color_scheme="working",
226
- animated=True
227
- )
228
- self.icon.icon = icon_image
229
- except Exception as e:
230
- if self.debug:
231
- print(f"❌ Error updating working icon: {e}")
298
+ from PyQt5.QtCore import QTimer
232
299
 
233
- # Heartbeat-like timer with dynamic intervals
234
- def heartbeat_timer_loop():
235
- while hasattr(self, 'working_active') and self.working_active:
236
- # Fast heartbeat pattern: beat-beat-pause
237
- update_working_icon()
238
- time.sleep(0.1) # First beat
239
- update_working_icon()
240
- time.sleep(0.1) # Second beat
241
- update_working_icon()
242
- time.sleep(0.8) # Longer pause between heartbeats
300
+ def update_icon():
301
+ try:
302
+ if self.base_icon and self.current_status == status and hasattr(self, 'qt_tray_icon'):
303
+ # Apply smooth heartbeat effect
304
+ icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, status)
305
+ self._update_qt_icon(icon_image)
306
+ elif self.debug:
307
+ print(f"⚠️ Qt animation stopped - status_match:{self.current_status == status}")
308
+ if hasattr(self, 'qt_animation_timer'):
309
+ self.qt_animation_timer.stop()
310
+ except Exception as e:
311
+ if self.debug:
312
+ print(f"❌ Error in Qt animation: {e}")
243
313
 
244
- self.working_active = True
245
- self.working_timer = threading.Thread(target=heartbeat_timer_loop, daemon=True)
246
- self.working_timer.start()
314
+ # Create Qt timer for smooth animation
315
+ self.qt_animation_timer = QTimer()
316
+ self.qt_animation_timer.timeout.connect(update_icon)
317
+ self.qt_animation_timer.start(50) # 20 FPS (50ms intervals)
247
318
 
248
319
  if self.debug:
249
- print("🎨 Started working animation")
320
+ print(" Qt animation timer started")
250
321
 
251
322
  except Exception as e:
252
323
  if self.debug:
253
- print(f"❌ Error starting working animation: {e}")
324
+ print(f"❌ Error starting Qt animation: {e}")
325
+
326
+ def _start_threading_animation(self, status: str):
327
+ """Start threading-based animation using threading.Timer."""
328
+ def update_icon():
329
+ try:
330
+ if self.icon and self.base_icon and self.current_status == status:
331
+ # Apply smooth heartbeat effect
332
+ icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, status)
333
+ self.icon.icon = icon_image
334
+
335
+ # Schedule next update at 20 FPS for smooth animation
336
+ self.animation_timer = threading.Timer(0.05, update_icon)
337
+ self.animation_timer.start()
338
+ elif self.debug:
339
+ print(f"⚠️ Threading animation stopped - status_match:{self.current_status == status}")
340
+ except Exception as e:
341
+ if self.debug:
342
+ print(f"❌ Error in threading animation: {e}")
343
+
344
+ # Start the threading animation
345
+ update_icon()
254
346
 
255
- def _start_ready_animation(self):
256
- """Start the gentle ready state heartbeat animation."""
347
+ def _update_qt_icon(self, icon_image):
348
+ """Update Qt system tray icon with new image."""
257
349
  try:
258
- import threading
259
- import time
260
-
261
- # Stop any existing animations
262
- self._stop_working_animation()
263
- self._stop_ready_animation()
350
+ from PyQt5.QtGui import QIcon, QPixmap
351
+ import io
264
352
 
265
- def update_ready_icon():
266
- if self.icon:
267
- try:
268
- icon_image = self.icon_generator.create_app_icon(
269
- color_scheme="green",
270
- animated=True
271
- )
272
- self.icon.icon = icon_image
273
- except Exception as e:
274
- if self.debug:
275
- print(f"❌ Error updating ready icon: {e}")
353
+ # Convert PIL image to QPixmap
354
+ img_buffer = io.BytesIO()
355
+ icon_image.save(img_buffer, format='PNG')
356
+ img_buffer.seek(0)
276
357
 
277
- # Gentle heartbeat timer - slower, more subtle
278
- def ready_timer_loop():
279
- while hasattr(self, 'ready_active') and self.ready_active:
280
- update_ready_icon()
281
- time.sleep(0.1) # Update every 100ms for smooth animation
358
+ pixmap = QPixmap()
359
+ pixmap.loadFromData(img_buffer.getvalue())
360
+ qt_icon = QIcon(pixmap)
282
361
 
283
- self.ready_active = True
284
- self.ready_timer = threading.Thread(target=ready_timer_loop, daemon=True)
285
- self.ready_timer.start()
362
+ # Update the Qt tray icon
363
+ self.qt_tray_icon.setIcon(qt_icon)
286
364
 
287
- if self.debug:
288
- print("🎨 Started ready heartbeat animation")
289
-
290
365
  except Exception as e:
291
366
  if self.debug:
292
- print(f"❌ Error starting ready animation: {e}")
367
+ print(f"❌ Error updating Qt icon: {e}")
293
368
 
294
- def _stop_ready_animation(self):
295
- """Stop the ready animation."""
296
- if hasattr(self, 'ready_active'):
297
- self.ready_active = False
298
- if self.debug:
299
- print("🎨 Stopped ready animation")
369
+ def _stop_animation_timer(self):
370
+ """Stop the current animation timer (both Qt and threading)."""
371
+ # Stop Qt timer if it exists
372
+ if hasattr(self, 'qt_animation_timer') and self.qt_animation_timer:
373
+ self.qt_animation_timer.stop()
374
+ self.qt_animation_timer = None
375
+
376
+ # Stop threading timer if it exists
377
+ if hasattr(self, 'animation_timer') and self.animation_timer:
378
+ self.animation_timer.cancel()
379
+ self.animation_timer = None
300
380
 
301
- def _stop_working_animation(self):
302
- """Stop the working animation."""
303
- if hasattr(self, 'working_active'):
304
- self.working_active = False
305
- self._stop_ready_animation() # Also stop ready animation
306
- if self.debug:
307
- print("🎨 Stopped working animation")
308
381
 
309
382
 
310
383
  def show_chat_bubble(self, icon=None, item=None):
@@ -501,10 +574,12 @@ class AbstractAssistantApp:
501
574
  def show_toast_notification(self, message: str, type: str = "info"):
502
575
  """Show a toast notification."""
503
576
  icon = "✅" if type == "success" else "❌" if type == "error" else "ℹ️"
504
- print(f"{icon} {message}")
577
+ if self.debug:
578
+ print(f"{icon} {message}")
505
579
 
506
580
  if self.debug:
507
- print(f"Toast notification: {type} - {message}")
581
+ if self.debug:
582
+ print(f"Toast notification: {type} - {message}")
508
583
 
509
584
  # Show a proper macOS notification
510
585
  try:
@@ -538,22 +613,40 @@ class AbstractAssistantApp:
538
613
  """Update application status."""
539
614
  # Status is now handled by the web interface
540
615
  if self.debug:
541
- print(f"Status update: {status}")
616
+ if self.debug:
617
+ print(f"Status update: {status}")
542
618
 
543
619
  def clear_session(self, icon=None, item=None):
544
- """Clear the current session."""
620
+ """Clear the current session with user confirmation."""
545
621
  try:
546
622
  if self.debug:
547
- print("🔄 Clearing session...")
623
+ print("🔄 System tray clear session requested...")
548
624
 
549
- self.llm_manager.clear_session()
625
+ # CRITICAL: System tray actions MUST have user confirmation
626
+ # Use the bubble's clear_session method which includes confirmation dialog
627
+ if hasattr(self, 'bubble_manager') and self.bubble_manager:
628
+ # Delegate to bubble manager which has proper user confirmation
629
+ bubble = self.bubble_manager.get_current_bubble()
630
+ if bubble:
631
+ bubble.clear_session() # This includes user confirmation dialog
632
+ return
550
633
 
634
+ # Fallback: Show notification that clearing requires UI interaction
635
+ try:
636
+ from .ui.toast_window import show_toast_notification
637
+ show_toast_notification(
638
+ "To clear session, please use the Clear button in the chat interface",
639
+ debug=self.debug
640
+ )
641
+ except:
642
+ print("💬 To clear session, please use the Clear button in the chat interface")
643
+
551
644
  if self.debug:
552
- print(" Session cleared")
645
+ print("⚠️ System tray clear session requires user confirmation via UI")
553
646
 
554
647
  except Exception as e:
555
648
  if self.debug:
556
- print(f"❌ Error clearing session: {e}")
649
+ print(f"❌ Error in clear session request: {e}")
557
650
 
558
651
  def save_session(self, icon=None, item=None):
559
652
  """Save the current session to file."""
@@ -593,58 +686,36 @@ class AbstractAssistantApp:
593
686
  print(f"❌ Error saving session: {e}")
594
687
 
595
688
  def load_session(self, icon=None, item=None):
596
- """Load a session from file."""
689
+ """Load a session from file with user confirmation."""
597
690
  try:
598
691
  if self.debug:
599
- print("🔄 Loading session...")
600
-
601
- # Get sessions directory
602
- import os
603
- sessions_dir = os.path.join(os.path.expanduser("~"), ".abstractassistant", "sessions")
604
-
605
- if not os.path.exists(sessions_dir):
606
- if self.debug:
607
- print("❌ No sessions directory found")
608
- return
609
-
610
- # Get list of session files
611
- session_files = [f for f in os.listdir(sessions_dir) if f.endswith('.json')]
692
+ print("🔄 System tray load session requested...")
612
693
 
613
- if not session_files:
614
- if self.debug:
615
- print("❌ No session files found")
616
- try:
617
- from .ui.toast_window import show_toast_notification
618
- show_toast_notification("No saved sessions found", debug=self.debug)
619
- except:
620
- print("📂 No saved sessions found")
621
- return
622
-
623
- # For now, load the most recent session
624
- # TODO: Add proper file picker dialog
625
- session_files.sort(reverse=True) # Most recent first
626
- latest_session = session_files[0]
627
- filepath = os.path.join(sessions_dir, latest_session)
628
-
629
- # Load session
630
- success = self.llm_manager.load_session(filepath)
694
+ # CRITICAL: System tray actions MUST NOT automatically replace sessions
695
+ # Use the bubble's load_session method which includes proper file picker
696
+ if hasattr(self, 'bubble_manager') and self.bubble_manager:
697
+ # Delegate to bubble manager which has proper user file selection
698
+ bubble = self.bubble_manager.get_current_bubble()
699
+ if bubble:
700
+ bubble.load_session() # This includes user file picker dialog
701
+ return
631
702
 
632
- if success:
633
- if self.debug:
634
- print(f"✅ Session loaded from: {filepath}")
635
- # Show notification
636
- try:
637
- from .ui.toast_window import show_toast_notification
638
- show_toast_notification(f"Session loaded:\n{latest_session}", debug=self.debug)
639
- except:
640
- print(f"📂 Session loaded: {latest_session}")
641
- else:
642
- if self.debug:
643
- print(" Failed to load session")
703
+ # Fallback: Show notification that loading requires UI interaction
704
+ try:
705
+ from .ui.toast_window import show_toast_notification
706
+ show_toast_notification(
707
+ "To load session, please use the Load button in the chat interface",
708
+ debug=self.debug
709
+ )
710
+ except:
711
+ print("💬 To load session, please use the Load button in the chat interface")
712
+
713
+ if self.debug:
714
+ print("⚠️ System tray load session requires user file selection via UI")
644
715
 
645
716
  except Exception as e:
646
717
  if self.debug:
647
- print(f"❌ Error loading session: {e}")
718
+ print(f"❌ Error in load session request: {e}")
648
719
 
649
720
  def _preflight_initialization(self):
650
721
  """Pre-initialize components for instant bubble display on first click."""
@@ -667,7 +738,7 @@ class AbstractAssistantApp:
667
738
  # Set up callbacks
668
739
  self.bubble_manager.set_response_callback(self.handle_bubble_response)
669
740
  self.bubble_manager.set_error_callback(self.handle_bubble_error)
670
- self.bubble_manager.set_status_callback(self.update_icon_status)
741
+ # Note: Status callback will be set after preflight initialization to avoid TTS init interference
671
742
  self.bubble_manager.set_app_quit_callback(self.quit_application)
672
743
 
673
744
  if self.debug:
@@ -680,6 +751,12 @@ class AbstractAssistantApp:
680
751
  # This creates the bubble without showing it
681
752
  self.bubble_manager._prepare_bubble()
682
753
 
754
+ # Now set the status callback after TTS initialization is complete
755
+ if self.bubble_manager:
756
+ self.bubble_manager.set_status_callback(self.update_icon_status)
757
+ if self.debug:
758
+ print("✅ Status callback set after TTS initialization")
759
+
683
760
  if self.debug:
684
761
  print("✅ Preflight initialization completed - bubble ready for instant display")
685
762
 
@@ -687,10 +764,21 @@ class AbstractAssistantApp:
687
764
  if self.debug:
688
765
  print(f"⚠️ Preflight initialization failed: {e}")
689
766
  print(" First click will still work but with delay")
767
+
768
+ # Still set status callback even if preflight failed
769
+ if self.bubble_manager:
770
+ self.bubble_manager.set_status_callback(self.update_icon_status)
690
771
 
691
772
  def quit_application(self, icon=None, item=None):
692
773
  """Quit the application gracefully."""
774
+ if self.debug:
775
+ print("🔄 Quitting AbstractAssistant...")
776
+
693
777
  self.is_running = False
778
+
779
+ # Stop animation timer
780
+ self._stop_animation_timer()
781
+
694
782
  if self.icon:
695
783
  self.icon.stop()
696
784
 
@@ -701,6 +789,9 @@ class AbstractAssistantApp:
701
789
  except Exception as e:
702
790
  if self.debug:
703
791
  print(f"Error destroying bubble manager: {e}")
792
+
793
+ if self.debug:
794
+ print("✅ AbstractAssistant quit successfully")
704
795
 
705
796
  def run(self):
706
797
  """Start the application using Qt event loop for proper threading."""
@@ -721,7 +812,7 @@ class AbstractAssistantApp:
721
812
 
722
813
  # Check if system tray is available
723
814
  if not QSystemTrayIcon.isSystemTrayAvailable():
724
- print("❌ System tray is not available on this system")
815
+ print("❌ System tray is not available on this system") # Always show this error
725
816
  return
726
817
 
727
818
  # Create Qt-based system tray icon
@@ -730,31 +821,42 @@ class AbstractAssistantApp:
730
821
  # Preflight initialization: Pre-load bubble manager for instant display
731
822
  self._preflight_initialization()
732
823
 
733
- print("AbstractAssistant started. Check your menu bar!")
734
- print("Click the icon to open the chat interface.")
824
+ if not self.debug:
825
+ print("AbstractAssistant started. Check your menu bar!")
826
+ print("Click the icon to open the chat interface.")
827
+ else:
828
+ print("AbstractAssistant started. Check your menu bar!")
829
+ print("Click the icon to open the chat interface.")
735
830
 
736
831
  # Run Qt event loop (this blocks until quit)
737
832
  self.qt_app.exec_()
738
833
 
739
834
  except ImportError:
740
- print("❌ PyQt5 not available. Falling back to pystray...")
835
+ if self.debug:
836
+ print("❌ PyQt5 not available. Falling back to pystray...")
741
837
  # Fallback to original pystray implementation
742
838
  self.icon = self.create_system_tray_icon()
743
839
  self.icon.run()
744
840
 
745
841
  def _create_qt_system_tray_icon(self):
746
- """Create Qt-based system tray icon with proper click detection."""
842
+ """Create Qt-based system tray icon with smooth animations."""
747
843
  from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction
748
844
  from PyQt5.QtCore import QTimer
749
845
  from PyQt5.QtGui import QIcon, QPixmap
750
846
  from PIL import Image
751
847
  import io
752
848
 
753
- # Generate icon using our icon generator
754
- icon_image = self.icon_generator.create_app_icon(
755
- color_scheme="green", # Ready state: steady green
756
- animated=False # Ready state: no animation
757
- )
849
+ # Load base icon (same as pystray version)
850
+ self.base_icon = self._load_app_bundle_icon()
851
+ if not self.base_icon:
852
+ # Generate a base icon if app bundle icon not available
853
+ self.base_icon = self.icon_generator.create_app_icon(
854
+ color_scheme="green", # Ready state: steady green
855
+ animated=False # Ready state: no animation
856
+ )
857
+
858
+ # Apply initial smooth heartbeat effect
859
+ icon_image = self.icon_generator.apply_heartbeat_effect(self.base_icon, "ready")
758
860
 
759
861
  # Convert PIL image to QPixmap
760
862
  img_buffer = io.BytesIO()
@@ -794,11 +896,20 @@ class AbstractAssistantApp:
794
896
 
795
897
  tray_icon.setContextMenu(context_menu)
796
898
 
899
+ # Store reference for animations
900
+ self.qt_tray_icon = tray_icon
901
+
797
902
  # Show the tray icon
798
903
  tray_icon.show()
799
904
 
905
+ # Start initial animation
906
+ if self.debug:
907
+ print(f"🎨 Starting initial animation with status: ready")
908
+ print(f" Current status in app: {self.current_status}")
909
+ self._start_heartbeat_animation("ready")
910
+
800
911
  if self.debug:
801
- print("✅ Qt-based system tray icon created successfully")
912
+ print("✅ Qt-based system tray icon created with smooth animations")
802
913
 
803
914
  return tray_icon
804
915