abstractassistant 0.2.6__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 +290 -179
- abstractassistant/core/llm_manager.py +189 -19
- abstractassistant/core/tts_manager.py +75 -25
- abstractassistant/create_app_bundle.py +6 -1
- abstractassistant/ui/history_dialog.py +5 -5
- abstractassistant/ui/qt_bubble.py +291 -121
- abstractassistant/ui/toast_window.py +14 -15
- abstractassistant/ui/ui_styles.py +2 -2
- abstractassistant/utils/icon_generator.py +269 -139
- abstractassistant/utils/markdown_renderer.py +1 -1
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/METADATA +13 -9
- abstractassistant-0.3.0.dist-info/RECORD +28 -0
- setup_macos_app.py +64 -10
- abstractassistant-0.2.6.dist-info/RECORD +0 -28
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/WHEEL +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.2.6.dist-info → abstractassistant-0.3.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
|
202
|
-
self.
|
|
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
|
|
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
|
|
212
|
-
"""Start
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
self.
|
|
246
|
-
self.
|
|
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("
|
|
320
|
+
print("✅ Qt animation timer started")
|
|
250
321
|
|
|
251
322
|
except Exception as e:
|
|
252
323
|
if self.debug:
|
|
253
|
-
print(f"❌ Error starting
|
|
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
|
|
256
|
-
"""
|
|
347
|
+
def _update_qt_icon(self, icon_image):
|
|
348
|
+
"""Update Qt system tray icon with new image."""
|
|
257
349
|
try:
|
|
258
|
-
import
|
|
259
|
-
import
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
self.
|
|
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
|
|
367
|
+
print(f"❌ Error updating Qt icon: {e}")
|
|
293
368
|
|
|
294
|
-
def
|
|
295
|
-
"""Stop the
|
|
296
|
-
if
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
577
|
+
if self.debug:
|
|
578
|
+
print(f"{icon} {message}")
|
|
505
579
|
|
|
506
580
|
if self.debug:
|
|
507
|
-
|
|
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
|
-
|
|
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("🔄
|
|
623
|
+
print("🔄 System tray clear session requested...")
|
|
548
624
|
|
|
549
|
-
|
|
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("
|
|
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
|
|
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("🔄
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
912
|
+
print("✅ Qt-based system tray icon created with smooth animations")
|
|
802
913
|
|
|
803
914
|
return tray_icon
|
|
804
915
|
|