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.
@@ -31,7 +31,7 @@ try:
31
31
  QTextEdit, QPushButton, QComboBox, QLabel, QFrame,
32
32
  QFileDialog, QMessageBox
33
33
  )
34
- from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, pyqtSlot, QRect
34
+ from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, pyqtSlot, QRect, QMetaObject
35
35
  from PyQt5.QtGui import QFont, QPalette, QColor, QPainter, QPen, QBrush
36
36
  from PyQt5.QtCore import QPoint
37
37
  QT_AVAILABLE = "PyQt5"
@@ -42,7 +42,7 @@ except ImportError:
42
42
  QTextEdit, QPushButton, QComboBox, QLabel, QFrame,
43
43
  QFileDialog, QMessageBox
44
44
  )
45
- from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal, QThread, Slot as pyqtSlot
45
+ from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal, QThread, Slot as pyqtSlot, QMetaObject
46
46
  from PySide2.QtGui import QFont, QPalette, QColor, QPainter, QPen, QBrush
47
47
  from PySide2.QtCore import QPoint
48
48
  QT_AVAILABLE = "PySide2"
@@ -145,7 +145,7 @@ class TTSToggle(QPushButton):
145
145
  border-radius: 12px;
146
146
  font-size: 12px;
147
147
  color: {text_color};
148
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
148
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
149
149
  font-weight: 600;
150
150
  }}
151
151
  QPushButton:hover {{
@@ -212,7 +212,7 @@ class FullVoiceToggle(QPushButton):
212
212
  border-radius: 12px;
213
213
  font-size: 12px;
214
214
  color: {text_color};
215
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
215
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
216
216
  font-weight: 600;
217
217
  }}
218
218
  QPushButton:hover {{
@@ -257,7 +257,8 @@ class LLMWorker(QThread):
257
257
  self.response_ready.emit(response_text)
258
258
 
259
259
  except Exception as e:
260
- print(f"❌ LLM Error: {e}")
260
+ if self.debug:
261
+ print(f"❌ LLM Error: {e}")
261
262
  import traceback
262
263
  traceback.print_exc()
263
264
  self.error_occurred.emit(str(e))
@@ -331,6 +332,24 @@ class QtChatBubble(QWidget):
331
332
  if self.debug:
332
333
  print("✅ QtChatBubble initialized")
333
334
 
335
+ def set_response_callback(self, callback):
336
+ """Set response callback."""
337
+ self.response_callback = callback
338
+
339
+ def set_error_callback(self, callback):
340
+ """Set error callback."""
341
+ self.error_callback = callback
342
+
343
+ def set_status_callback(self, callback):
344
+ """Set status callback."""
345
+ self.status_callback = callback
346
+ if self.debug:
347
+ print("✅ Status callback set in QtChatBubble")
348
+
349
+ def set_app_quit_callback(self, callback):
350
+ """Set app quit callback."""
351
+ self.app_quit_callback = callback
352
+
334
353
  def setup_ui(self):
335
354
  """Set up the modern user interface with SOTA UX practices."""
336
355
  self.setWindowTitle("AbstractAssistant")
@@ -366,7 +385,7 @@ class QtChatBubble(QWidget):
366
385
  font-size: 14px;
367
386
  font-weight: 600;
368
387
  color: rgba(255, 255, 255, 0.9);
369
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
388
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
370
389
  }
371
390
  QPushButton:hover {
372
391
  background: rgba(255, 60, 60, 0.8);
@@ -398,7 +417,7 @@ class QtChatBubble(QWidget):
398
417
  border-radius: 11px;
399
418
  font-size: 10px;
400
419
  color: rgba(255, 255, 255, 0.7);
401
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
420
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
402
421
  padding: 0 10px;
403
422
  }
404
423
  QPushButton:hover {
@@ -440,7 +459,7 @@ class QtChatBubble(QWidget):
440
459
  font-size: 10px;
441
460
  font-weight: 600;
442
461
  color: #ffffff;
443
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
462
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
444
463
  }
445
464
  """)
446
465
  header_layout.addWidget(self.status_label)
@@ -570,7 +589,7 @@ class QtChatBubble(QWidget):
570
589
  padding: 0 8px;
571
590
  font-size: 11px;
572
591
  color: rgba(255, 255, 255, 0.9);
573
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
592
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
574
593
  }
575
594
  QComboBox:hover {
576
595
  background: rgba(255, 255, 255, 0.12);
@@ -600,7 +619,7 @@ class QtChatBubble(QWidget):
600
619
  padding: 0 8px;
601
620
  font-size: 11px;
602
621
  color: rgba(255, 255, 255, 0.9);
603
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
622
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
604
623
  }
605
624
  QComboBox:hover {
606
625
  background: rgba(255, 255, 255, 0.12);
@@ -631,7 +650,7 @@ class QtChatBubble(QWidget):
631
650
  border-radius: 14px;
632
651
  font-size: 12px;
633
652
  color: rgba(255, 255, 255, 0.6);
634
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
653
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
635
654
  }
636
655
  """)
637
656
  controls_layout.addWidget(self.token_label)
@@ -672,7 +691,7 @@ class QtChatBubble(QWidget):
672
691
  font-size: 14px;
673
692
  font-weight: 400;
674
693
  color: #ffffff;
675
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
694
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
676
695
  selection-background-color: #0066cc;
677
696
  line-height: 1.4;
678
697
  }
@@ -695,7 +714,7 @@ class QtChatBubble(QWidget):
695
714
  font-size: 11px;
696
715
  font-weight: 500;
697
716
  color: #ffffff;
698
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
717
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
699
718
  }
700
719
 
701
720
  QPushButton:hover {
@@ -723,7 +742,7 @@ class QtChatBubble(QWidget):
723
742
  font-size: 12px;
724
743
  font-weight: 400;
725
744
  color: #ffffff;
726
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
745
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
727
746
  letter-spacing: 0.01em;
728
747
  }
729
748
 
@@ -753,7 +772,7 @@ class QtChatBubble(QWidget):
753
772
  selection-background-color: #4299e1;
754
773
  color: #e2e8f0;
755
774
  padding: 4px;
756
- font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
775
+ font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
757
776
  }
758
777
 
759
778
  QComboBox QAbstractItemView::item {
@@ -785,7 +804,7 @@ class QtChatBubble(QWidget):
785
804
  color: rgba(255, 255, 255, 0.8);
786
805
  font-size: 12px;
787
806
  font-weight: 500;
788
- font-family: "SF Pro Text", "Helvetica Neue", 'Segoe UI', Roboto, sans-serif;
807
+ font-family: "Helvetica Neue", "Helvetica", 'Segoe UI', Arial, sans-serif;
789
808
  letter-spacing: 0.3px;
790
809
  }
791
810
 
@@ -800,7 +819,7 @@ class QtChatBubble(QWidget):
800
819
  text-transform: uppercase;
801
820
  letter-spacing: 0.5px;
802
821
  color: #a6e3a1;
803
- font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
822
+ font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
804
823
  }
805
824
 
806
825
  QLabel#status_generating {
@@ -813,7 +832,7 @@ class QtChatBubble(QWidget):
813
832
  text-transform: uppercase;
814
833
  letter-spacing: 0.5px;
815
834
  color: #fab387;
816
- font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
835
+ font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
817
836
  }
818
837
 
819
838
  QLabel#status_error {
@@ -826,7 +845,7 @@ class QtChatBubble(QWidget):
826
845
  text-transform: uppercase;
827
846
  letter-spacing: 0.5px;
828
847
  color: #f38ba8;
829
- font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
848
+ font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
830
849
  }
831
850
 
832
851
  QLabel#token_label {
@@ -834,7 +853,7 @@ class QtChatBubble(QWidget):
834
853
  border: 1px solid #4a5568;
835
854
  border-radius: 8px;
836
855
  padding: 10px 12px;
837
- font-family: "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, sans-serif;
856
+ font-family: "Helvetica Neue", "Helvetica", "Segoe UI", Arial, sans-serif;
838
857
  font-size: 11px;
839
858
  font-weight: 500;
840
859
  color: #cbd5e0;
@@ -868,7 +887,8 @@ class QtChatBubble(QWidget):
868
887
  self.move(x, y)
869
888
 
870
889
  if self.debug:
871
- print(f"Positioned bubble at ({x}, {y})")
890
+ if self.debug:
891
+ print(f"Positioned bubble at ({x}, {y})")
872
892
 
873
893
  def load_providers(self):
874
894
  """Load available providers using ProviderManager."""
@@ -881,13 +901,15 @@ class QtChatBubble(QWidget):
881
901
  available_providers = self.provider_manager.get_available_providers(exclude_mock=True)
882
902
 
883
903
  if self.debug:
884
- print(f"🔍 ProviderManager found {len(available_providers)} available providers")
904
+ if self.debug:
905
+ print(f"🔍 ProviderManager found {len(available_providers)} available providers")
885
906
 
886
907
  # Add providers to dropdown
887
908
  for display_name, provider_key in available_providers:
888
909
  self.provider_combo.addItem(display_name, provider_key)
889
910
  if self.debug:
890
- print(f" ✅ Added: {display_name} ({provider_key})")
911
+ if self.debug:
912
+ print(f" ✅ Added: {display_name} ({provider_key})")
891
913
 
892
914
  # Set preferred provider
893
915
  preferred = self.provider_manager.get_preferred_provider(available_providers, 'lmstudio')
@@ -924,14 +946,16 @@ class QtChatBubble(QWidget):
924
946
  )
925
947
 
926
948
  if self.debug:
927
- print(f"🔍 Final selected provider: {self.current_provider}")
949
+ if self.debug:
950
+ print(f"🔍 Final selected provider: {self.current_provider}")
928
951
 
929
952
  # Load models for current provider
930
953
  self.update_models()
931
954
 
932
955
  except Exception as e:
933
956
  if self.debug:
934
- print(f"❌ Error loading providers: {e}")
957
+ if self.debug:
958
+ print(f"❌ Error loading providers: {e}")
935
959
  import traceback
936
960
  traceback.print_exc()
937
961
 
@@ -940,7 +964,8 @@ class QtChatBubble(QWidget):
940
964
  self.provider_combo.addItem("LMStudio (Local)", "lmstudio")
941
965
  self.current_provider = "lmstudio"
942
966
  if self.debug:
943
- print("🔄 Using fallback provider list")
967
+ if self.debug:
968
+ print("🔄 Using fallback provider list")
944
969
 
945
970
  def update_models(self):
946
971
  """Update model dropdown using ProviderManager."""
@@ -952,7 +977,8 @@ class QtChatBubble(QWidget):
952
977
  models = self.provider_manager.get_models_for_provider(self.current_provider)
953
978
 
954
979
  if self.debug:
955
- print(f"📋 ProviderManager loaded {len(models)} models for {self.current_provider}")
980
+ if self.debug:
981
+ print(f"📋 ProviderManager loaded {len(models)} models for {self.current_provider}")
956
982
 
957
983
  # Add models to dropdown with display names
958
984
  for model in models:
@@ -994,13 +1020,15 @@ class QtChatBubble(QWidget):
994
1020
  self.model_combo.setCurrentIndex(0)
995
1021
 
996
1022
  if self.debug:
997
- print(f"✅ Final selected model: {self.current_model}")
1023
+ if self.debug:
1024
+ print(f"✅ Final selected model: {self.current_model}")
998
1025
 
999
1026
  self.update_token_limits()
1000
1027
 
1001
1028
  except Exception as e:
1002
1029
  if self.debug:
1003
- print(f"❌ Error updating models: {e}")
1030
+ if self.debug:
1031
+ print(f"❌ Error updating models: {e}")
1004
1032
  import traceback
1005
1033
  traceback.print_exc()
1006
1034
 
@@ -1010,7 +1038,8 @@ class QtChatBubble(QWidget):
1010
1038
  self.current_model = "default-model"
1011
1039
  self.model_combo.setCurrentIndex(0)
1012
1040
  if self.debug:
1013
- print(f"🔄 Using final fallback model: {self.current_model}")
1041
+ if self.debug:
1042
+ print(f"🔄 Using final fallback model: {self.current_model}")
1014
1043
 
1015
1044
  def update_token_limits(self):
1016
1045
  """Update token limits using AbstractCore's built-in detection."""
@@ -1060,7 +1089,8 @@ class QtChatBubble(QWidget):
1060
1089
  self.update_models()
1061
1090
 
1062
1091
  if self.debug:
1063
- print(f"Provider changed to: {self.current_provider}")
1092
+ if self.debug:
1093
+ print(f"Provider changed to: {self.current_provider}")
1064
1094
 
1065
1095
  def on_model_changed(self, model_name):
1066
1096
  """Handle model change."""
@@ -1095,7 +1125,8 @@ class QtChatBubble(QWidget):
1095
1125
  if file_path not in self.attached_files:
1096
1126
  self.attached_files.append(file_path)
1097
1127
  if self.debug:
1098
- print(f"📎 Attached file: {file_path}")
1128
+ if self.debug:
1129
+ print(f"📎 Attached file: {file_path}")
1099
1130
 
1100
1131
  self.update_attached_files_display()
1101
1132
 
@@ -1181,7 +1212,8 @@ class QtChatBubble(QWidget):
1181
1212
  if file_path in self.attached_files:
1182
1213
  self.attached_files.remove(file_path)
1183
1214
  if self.debug:
1184
- print(f"🗑️ Removed attached file: {file_path}")
1215
+ if self.debug:
1216
+ print(f"🗑️ Removed attached file: {file_path}")
1185
1217
  self.update_attached_files_display()
1186
1218
 
1187
1219
  def send_message(self):
@@ -1228,7 +1260,8 @@ class QtChatBubble(QWidget):
1228
1260
  if self.status_callback:
1229
1261
  self.status_callback("generating")
1230
1262
 
1231
- print("🔄 QtChatBubble: UI updated, creating worker thread...")
1263
+ if self.debug:
1264
+ print("🔄 QtChatBubble: UI updated, creating worker thread...")
1232
1265
 
1233
1266
  # 5. Start worker thread to send request with optional media files
1234
1267
  self.worker = LLMWorker(
@@ -1241,17 +1274,20 @@ class QtChatBubble(QWidget):
1241
1274
  self.worker.response_ready.connect(self.on_response_ready)
1242
1275
  self.worker.error_occurred.connect(self.on_error_occurred)
1243
1276
 
1244
- print("🔄 QtChatBubble: Starting worker thread...")
1277
+ if self.debug:
1278
+ print("🔄 QtChatBubble: Starting worker thread...")
1245
1279
  self.worker.start()
1246
1280
 
1247
- print("🔄 QtChatBubble: Worker thread started, hiding bubble...")
1281
+ if self.debug:
1282
+ print("🔄 QtChatBubble: Worker thread started, hiding bubble...")
1248
1283
  # Hide bubble after sending (like the original design)
1249
1284
  QTimer.singleShot(500, self.hide)
1250
1285
 
1251
1286
  @pyqtSlot(str)
1252
1287
  def on_response_ready(self, response):
1253
1288
  """Handle LLM response."""
1254
- print(f"✅ QtChatBubble: on_response_ready called with response: {response[:100]}...")
1289
+ if self.debug:
1290
+ print(f"✅ QtChatBubble: on_response_ready called with response: {response[:100]}...")
1255
1291
 
1256
1292
  self.send_button.setEnabled(True)
1257
1293
  self.send_button.setText("→")
@@ -1271,6 +1307,10 @@ class QtChatBubble(QWidget):
1271
1307
  }
1272
1308
  """)
1273
1309
 
1310
+ # Notify main app about status change (for icon animation)
1311
+ if self.status_callback:
1312
+ self.status_callback("ready")
1313
+
1274
1314
  # Get updated message history from AbstractCore session
1275
1315
  self._update_message_history_from_session()
1276
1316
 
@@ -1280,53 +1320,83 @@ class QtChatBubble(QWidget):
1280
1320
  # Handle TTS if enabled (AbstractVoice integration)
1281
1321
  if self.tts_enabled and self.voice_manager and self.voice_manager.is_available():
1282
1322
  if self.debug:
1283
- print("🔊 TTS enabled, speaking response...")
1323
+ if self.debug:
1324
+ print("🔊 TTS enabled, speaking response...")
1284
1325
 
1285
1326
  # Don't show toast when TTS is enabled
1286
1327
  try:
1287
1328
  # Clean response for voice synthesis
1288
1329
  clean_response = self._clean_response_for_voice(response)
1289
1330
 
1331
+ # Set up callbacks to detect when speech actually starts/ends
1332
+ # Use QMetaObject.invokeMethod to ensure callbacks run on main thread
1333
+ def on_speech_start():
1334
+ if self.debug:
1335
+ print("🔊 QtChatBubble: Speech actually started (background thread)")
1336
+ # Schedule status update on main thread
1337
+ QMetaObject.invokeMethod(self, "_on_speech_started_main_thread", Qt.QueuedConnection)
1338
+
1339
+ def on_speech_end():
1340
+ if self.debug:
1341
+ print("🔊 QtChatBubble: Speech ended (background thread)")
1342
+ # Schedule completion handling on main thread
1343
+ QMetaObject.invokeMethod(self, "_on_speech_ended_main_thread", Qt.QueuedConnection)
1344
+
1345
+ # Set the callbacks on the voice manager
1346
+ self.voice_manager.on_speech_start = on_speech_start
1347
+ self.voice_manager.on_speech_end = on_speech_end
1348
+
1290
1349
  # Speak the cleaned response using AbstractVoice-compatible interface
1350
+ # Note: We don't set "speaking" status here anymore - we wait for the callback
1291
1351
  self.voice_manager.speak(clean_response)
1292
1352
 
1293
1353
  # Update toggle state to 'speaking'
1294
1354
  self._update_tts_toggle_state()
1295
-
1296
- # Wait for speech to complete in a separate thread
1297
- def wait_for_speech():
1298
- while self.voice_manager.is_speaking():
1299
- time.sleep(0.1)
1300
- # Update toggle state when speech completes
1301
- self._update_tts_toggle_state()
1302
- if self.debug:
1303
- print("🔊 TTS completed")
1304
1355
 
1305
- speech_thread = threading.Thread(target=wait_for_speech, daemon=True)
1306
- speech_thread.start()
1356
+ # Store response for callback when TTS completes
1357
+ self._pending_response = response
1307
1358
 
1308
1359
  # Show chat history after TTS starts (small delay) - only if voice mode is OFF
1309
1360
  QTimer.singleShot(800, self._show_history_if_voice_mode_off)
1310
1361
 
1311
1362
  except Exception as e:
1312
1363
  if self.debug:
1313
- print(f"❌ TTS error: {e}")
1364
+ if self.debug:
1365
+ print(f"❌ TTS error: {e}")
1314
1366
  # Show chat history as fallback - only if voice mode is OFF
1315
1367
  QTimer.singleShot(100, self._show_history_if_voice_mode_off)
1316
1368
  else:
1317
1369
  # Show chat history instead of toast when TTS is disabled - only if voice mode is OFF
1318
1370
  self._show_history_if_voice_mode_off()
1319
1371
 
1320
- # Also call response callback if set
1321
- if self.response_callback:
1322
- print(f"🔄 QtChatBubble: Response callback exists, calling it...")
1323
- self.response_callback(response)
1372
+ # Handle status transitions based on TTS mode
1373
+ tts_will_handle = self.tts_enabled and self.voice_manager and self.voice_manager.is_available()
1374
+ if self.debug:
1375
+ print(f"🔍 QtChatBubble: TTS decision - tts_enabled={self.tts_enabled}, voice_manager={self.voice_manager is not None}, is_available={self.voice_manager.is_available() if self.voice_manager else False}")
1376
+ print(f"🔍 QtChatBubble: TTS will handle callbacks: {tts_will_handle}")
1377
+
1378
+ if not tts_will_handle:
1379
+ # Non-TTS path: Go directly to ready mode
1380
+ if self.debug:
1381
+ print(f"🔄 QtChatBubble: Non-TTS path - going to ready mode immediately")
1382
+ if self.response_callback:
1383
+ self.response_callback(response)
1384
+ if self.status_callback:
1385
+ self.status_callback("ready")
1386
+ else:
1387
+ # TTS path: Stay in thinking mode until audio actually starts
1388
+ if self.debug:
1389
+ print(f"🔊 QtChatBubble: TTS path - staying in thinking mode until audio starts")
1390
+ print(f"🔊 QtChatBubble: v0.5.1 callbacks will handle status transitions")
1391
+ # DON'T call response_callback or set "ready" status here!
1392
+ # The v0.5.1 callbacks will handle everything
1324
1393
 
1325
1394
  def on_tts_toggled(self, enabled: bool):
1326
1395
  """Handle TTS toggle state change."""
1327
1396
  self.tts_enabled = enabled
1328
1397
  if self.debug:
1329
- print(f"🔊 TTS {'enabled' if enabled else 'disabled'}")
1398
+ if self.debug:
1399
+ print(f"🔊 TTS {'enabled' if enabled else 'disabled'}")
1330
1400
 
1331
1401
  # Stop any current speech when disabling
1332
1402
  if not enabled and self.voice_manager:
@@ -1335,17 +1405,20 @@ class QtChatBubble(QWidget):
1335
1405
  self._update_tts_toggle_state()
1336
1406
  except Exception as e:
1337
1407
  if self.debug:
1338
- print(f"❌ Error stopping TTS: {e}")
1408
+ if self.debug:
1409
+ print(f"❌ Error stopping TTS: {e}")
1339
1410
 
1340
- # Update LLM session with TTS-appropriate system prompt
1411
+ # Update LLM session mode while preserving chat history
1341
1412
  if self.llm_manager:
1342
1413
  try:
1343
- self.llm_manager.create_new_session(tts_mode=enabled)
1414
+ self.llm_manager.update_session_mode(tts_mode=enabled)
1344
1415
  if self.debug:
1345
- print(f"🔄 LLM session updated for {'TTS' if enabled else 'normal'} mode")
1416
+ if self.debug:
1417
+ print(f"🔄 LLM session mode updated for {'TTS' if enabled else 'normal'} mode (history preserved)")
1346
1418
  except Exception as e:
1347
1419
  if self.debug:
1348
- print(f"❌ Error updating LLM session: {e}")
1420
+ if self.debug:
1421
+ print(f"❌ Error updating LLM session: {e}")
1349
1422
 
1350
1423
  def on_tts_single_click(self):
1351
1424
  """Handle single click on TTS toggle - pause/resume functionality."""
@@ -1359,27 +1432,33 @@ class QtChatBubble(QWidget):
1359
1432
  # Pause the speech - may need multiple attempts if audio stream just started
1360
1433
  success = self._attempt_pause_with_retry()
1361
1434
  if success and self.debug:
1362
- print("🔊 TTS paused via single click")
1435
+ if self.debug:
1436
+ print("🔊 TTS paused via single click")
1363
1437
  elif self.debug:
1364
- print("🔊 TTS pause failed - audio stream may not be ready yet")
1438
+ if self.debug:
1439
+ print("🔊 TTS pause failed - audio stream may not be ready yet")
1365
1440
  elif current_state == 'paused':
1366
1441
  # Resume the speech
1367
1442
  success = self.voice_manager.resume()
1368
1443
  if success and self.debug:
1369
- print("🔊 TTS resumed via single click")
1444
+ if self.debug:
1445
+ print("🔊 TTS resumed via single click")
1370
1446
  elif self.debug:
1371
- print("🔊 TTS resume failed")
1447
+ if self.debug:
1448
+ print("🔊 TTS resume failed")
1372
1449
  else:
1373
1450
  # If idle, do nothing or could show a message
1374
1451
  if self.debug:
1375
- print("🔊 TTS single click - no active speech to pause/resume")
1452
+ if self.debug:
1453
+ print("🔊 TTS single click - no active speech to pause/resume")
1376
1454
 
1377
1455
  # Update visual state
1378
1456
  self._update_tts_toggle_state()
1379
1457
 
1380
1458
  except Exception as e:
1381
1459
  if self.debug:
1382
- print(f"❌ Error handling TTS single click: {e}")
1460
+ if self.debug:
1461
+ print(f"❌ Error handling TTS single click: {e}")
1383
1462
 
1384
1463
  def _attempt_pause_with_retry(self, max_attempts=5):
1385
1464
  """Attempt to pause with retry logic for timing issues.
@@ -1402,7 +1481,8 @@ class QtChatBubble(QWidget):
1402
1481
  return True
1403
1482
 
1404
1483
  if self.debug:
1405
- print(f"🔊 Pause attempt {attempt + 1}/{max_attempts} failed, retrying...")
1484
+ if self.debug:
1485
+ print(f"🔊 Pause attempt {attempt + 1}/{max_attempts} failed, retrying...")
1406
1486
 
1407
1487
  # Short delay before retry
1408
1488
  time.sleep(0.1)
@@ -1412,7 +1492,8 @@ class QtChatBubble(QWidget):
1412
1492
  def on_tts_double_click(self):
1413
1493
  """Handle double click on TTS toggle - stop TTS and open chat bubble."""
1414
1494
  if self.debug:
1415
- print("🔊 TTS double click - stopping speech and showing chat")
1495
+ if self.debug:
1496
+ print("🔊 TTS double click - stopping speech and showing chat")
1416
1497
 
1417
1498
  # Prevent double-free errors by checking if objects are still valid
1418
1499
  try:
@@ -1429,7 +1510,8 @@ class QtChatBubble(QWidget):
1429
1510
 
1430
1511
  except Exception as e:
1431
1512
  if self.debug:
1432
- print(f"❌ Error stopping TTS on double click: {e}")
1513
+ if self.debug:
1514
+ print(f"❌ Error stopping TTS on double click: {e}")
1433
1515
 
1434
1516
  # Show the chat bubble with safety checks
1435
1517
  if hasattr(self, 'show') and not self.isVisible():
@@ -1463,12 +1545,14 @@ class QtChatBubble(QWidget):
1463
1545
  try:
1464
1546
  # Ensure voice manager is available
1465
1547
  if not self.voice_manager or not self.voice_manager.is_available():
1466
- print("❌ Voice manager not available for Full Voice Mode")
1548
+ if self.debug:
1549
+ print("❌ Voice manager not available for Full Voice Mode")
1467
1550
  self.full_voice_toggle.set_enabled(False)
1468
1551
  return
1469
1552
 
1470
1553
  if self.debug:
1471
- print("🚀 Starting Full Voice Mode...")
1554
+ if self.debug:
1555
+ print("🚀 Starting Full Voice Mode...")
1472
1556
 
1473
1557
  # Hide text input UI
1474
1558
  self.hide_text_ui()
@@ -1480,9 +1564,9 @@ class QtChatBubble(QWidget):
1480
1564
  # Set up voice mode based on CLI parameter
1481
1565
  self.voice_manager.set_voice_mode(self.listening_mode)
1482
1566
 
1483
- # Update LLM session for voice-optimized responses
1567
+ # Update LLM session mode for voice-optimized responses (preserve history)
1484
1568
  if self.llm_manager:
1485
- self.llm_manager.create_new_session(tts_mode=True)
1569
+ self.llm_manager.update_session_mode(tts_mode=True)
1486
1570
 
1487
1571
  # Start listening
1488
1572
  self.voice_manager.listen(
@@ -1497,11 +1581,13 @@ class QtChatBubble(QWidget):
1497
1581
  self.voice_manager.speak("Full voice mode activated. I'm listening...")
1498
1582
 
1499
1583
  if self.debug:
1500
- print("✅ Full Voice Mode started successfully")
1584
+ if self.debug:
1585
+ print("✅ Full Voice Mode started successfully")
1501
1586
 
1502
1587
  except Exception as e:
1503
1588
  if self.debug:
1504
- print(f"❌ Error starting Full Voice Mode: {e}")
1589
+ if self.debug:
1590
+ print(f"❌ Error starting Full Voice Mode: {e}")
1505
1591
  import traceback
1506
1592
  traceback.print_exc()
1507
1593
 
@@ -1513,7 +1599,8 @@ class QtChatBubble(QWidget):
1513
1599
  """Stop Full Voice Mode and return to normal text mode."""
1514
1600
  try:
1515
1601
  if self.debug:
1516
- print("🛑 Stopping Full Voice Mode...")
1602
+ if self.debug:
1603
+ print("🛑 Stopping Full Voice Mode...")
1517
1604
 
1518
1605
  # Stop listening
1519
1606
  if self.voice_manager:
@@ -1527,11 +1614,13 @@ class QtChatBubble(QWidget):
1527
1614
  self.update_status("READY")
1528
1615
 
1529
1616
  if self.debug:
1530
- print("✅ Full Voice Mode stopped")
1617
+ if self.debug:
1618
+ print("✅ Full Voice Mode stopped")
1531
1619
 
1532
1620
  except Exception as e:
1533
1621
  if self.debug:
1534
- print(f"❌ Error stopping Full Voice Mode: {e}")
1622
+ if self.debug:
1623
+ print(f"❌ Error stopping Full Voice Mode: {e}")
1535
1624
  import traceback
1536
1625
  traceback.print_exc()
1537
1626
 
@@ -1539,7 +1628,8 @@ class QtChatBubble(QWidget):
1539
1628
  """Handle speech-to-text input from the user."""
1540
1629
  try:
1541
1630
  if self.debug:
1542
- print(f"👤 Voice input: {transcribed_text}")
1631
+ if self.debug:
1632
+ print(f"👤 Voice input: {transcribed_text}")
1543
1633
 
1544
1634
  # No longer updating voice toggle appearance - it's a simple user control
1545
1635
  self.update_status("PROCESSING")
@@ -1555,7 +1645,8 @@ class QtChatBubble(QWidget):
1555
1645
  self._update_message_history_from_session()
1556
1646
 
1557
1647
  if self.debug:
1558
- print(f"🤖 AI response: {response[:100]}...")
1648
+ if self.debug:
1649
+ print(f"🤖 AI response: {response[:100]}...")
1559
1650
 
1560
1651
  # Speak the response
1561
1652
  self.voice_manager.speak(response)
@@ -1565,7 +1656,8 @@ class QtChatBubble(QWidget):
1565
1656
 
1566
1657
  except Exception as e:
1567
1658
  if self.debug:
1568
- print(f"❌ Error handling voice input: {e}")
1659
+ if self.debug:
1660
+ print(f"❌ Error handling voice input: {e}")
1569
1661
  import traceback
1570
1662
  traceback.print_exc()
1571
1663
 
@@ -1575,7 +1667,8 @@ class QtChatBubble(QWidget):
1575
1667
  def handle_voice_stop(self):
1576
1668
  """Handle when user says 'stop' to exit Full Voice Mode."""
1577
1669
  if self.debug:
1578
- print("🛑 User said 'stop' - exiting Full Voice Mode")
1670
+ if self.debug:
1671
+ print("🛑 User said 'stop' - exiting Full Voice Mode")
1579
1672
 
1580
1673
  # Disable Full Voice Mode
1581
1674
  self.full_voice_toggle.set_enabled(False)
@@ -1623,7 +1716,7 @@ class QtChatBubble(QWidget):
1623
1716
  font-size: 10px;
1624
1717
  font-weight: 600;
1625
1718
  color: #ffffff;
1626
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
1719
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
1627
1720
  }}
1628
1721
  """)
1629
1722
 
@@ -1643,10 +1736,12 @@ class QtChatBubble(QWidget):
1643
1736
  self.voice_control_panel.hide()
1644
1737
 
1645
1738
  if self.debug:
1646
- print(f"🔊 TTS toggle state updated to: {current_state}")
1739
+ if self.debug:
1740
+ print(f"🔊 TTS toggle state updated to: {current_state}")
1647
1741
  except Exception as e:
1648
1742
  if self.debug:
1649
- print(f"❌ Error updating TTS toggle state: {e}")
1743
+ if self.debug:
1744
+ print(f"❌ Error updating TTS toggle state: {e}")
1650
1745
 
1651
1746
  def create_voice_control_panel(self):
1652
1747
  """Create a prominent voice control panel that appears when TTS is active."""
@@ -1747,11 +1842,13 @@ class QtChatBubble(QWidget):
1747
1842
  self.escape_shortcut.activated.connect(self.handle_escape_shortcut)
1748
1843
 
1749
1844
  if self.debug:
1750
- print("✅ Keyboard shortcuts setup: Space (pause/resume), Escape (stop)")
1845
+ if self.debug:
1846
+ print("✅ Keyboard shortcuts setup: Space (pause/resume), Escape (stop)")
1751
1847
 
1752
1848
  except Exception as e:
1753
1849
  if self.debug:
1754
- print(f"❌ Error setting up keyboard shortcuts: {e}")
1850
+ if self.debug:
1851
+ print(f"❌ Error setting up keyboard shortcuts: {e}")
1755
1852
 
1756
1853
  def handle_space_shortcut(self):
1757
1854
  """Handle space bar shortcut for pause/resume."""
@@ -1760,14 +1857,16 @@ class QtChatBubble(QWidget):
1760
1857
  not self.input_text.hasFocus()):
1761
1858
  self.on_tts_single_click()
1762
1859
  if self.debug:
1763
- print("🔊 Space shortcut triggered pause/resume")
1860
+ if self.debug:
1861
+ print("🔊 Space shortcut triggered pause/resume")
1764
1862
 
1765
1863
  def handle_escape_shortcut(self):
1766
1864
  """Handle escape key shortcut for stop."""
1767
1865
  if self.voice_manager and self.voice_manager.get_state() in ['speaking', 'paused']:
1768
1866
  self.on_tts_double_click()
1769
1867
  if self.debug:
1770
- print("🔊 Escape shortcut triggered stop")
1868
+ if self.debug:
1869
+ print("🔊 Escape shortcut triggered stop")
1771
1870
 
1772
1871
  def _clean_response_for_voice(self, text: str) -> str:
1773
1872
  """Clean response text for voice synthesis - remove formatting and make conversational."""
@@ -1817,7 +1916,8 @@ class QtChatBubble(QWidget):
1817
1916
  # NO TRUNCATION - let the LLM decide response length based on system prompt
1818
1917
 
1819
1918
  if self.debug:
1820
- print(f"🔊 Cleaned text for TTS: {text[:100]}{'...' if len(text) > 100 else ''}")
1919
+ if self.debug:
1920
+ print(f"🔊 Cleaned text for TTS: {text[:100]}{'...' if len(text) > 100 else ''}")
1821
1921
 
1822
1922
  return text
1823
1923
 
@@ -1843,11 +1943,13 @@ class QtChatBubble(QWidget):
1843
1943
  """)
1844
1944
 
1845
1945
  if self.debug:
1846
- print(f"Error occurred: {error}")
1946
+ if self.debug:
1947
+ print(f"Error occurred: {error}")
1847
1948
 
1848
1949
  # Show chat history instead of error toast
1849
1950
  if self.debug:
1850
- print(f"❌ AI Error: {error}")
1951
+ if self.debug:
1952
+ print(f"❌ AI Error: {error}")
1851
1953
 
1852
1954
  # Show history so user can see the error context - only if voice mode is OFF
1853
1955
  QTimer.singleShot(100, self._show_history_if_voice_mode_off)
@@ -1887,14 +1989,16 @@ class QtChatBubble(QWidget):
1887
1989
  if self.llm_manager:
1888
1990
  self.llm_manager.create_new_session()
1889
1991
  if self.debug:
1890
- print("🧹 AbstractCore session cleared and recreated")
1992
+ if self.debug:
1993
+ print("🧹 AbstractCore session cleared and recreated")
1891
1994
 
1892
1995
  self.message_history.clear()
1893
1996
  self.token_count = 0
1894
1997
  self.update_token_display()
1895
1998
 
1896
1999
  if self.debug:
1897
- print("🧹 Session cleared")
2000
+ if self.debug:
2001
+ print("🧹 Session cleared")
1898
2002
 
1899
2003
  def load_session(self):
1900
2004
  """Load a session using AbstractCore via LLMManager."""
@@ -1931,7 +2035,8 @@ class QtChatBubble(QWidget):
1931
2035
  )
1932
2036
 
1933
2037
  if self.debug:
1934
- print(f"📂 Loaded session via AbstractCore from {file_path}")
2038
+ if self.debug:
2039
+ print(f"📂 Loaded session via AbstractCore from {file_path}")
1935
2040
  else:
1936
2041
  raise Exception("Session loaded but not available in LLMManager")
1937
2042
  else:
@@ -1944,7 +2049,8 @@ class QtChatBubble(QWidget):
1944
2049
  f"Failed to load session via AbstractCore:\n{str(e)}"
1945
2050
  )
1946
2051
  if self.debug:
1947
- print(f"❌ Failed to load session: {e}")
2052
+ if self.debug:
2053
+ print(f"❌ Failed to load session: {e}")
1948
2054
 
1949
2055
  def save_session(self):
1950
2056
  """Save the current session using AbstractCore via LLMManager."""
@@ -1980,7 +2086,8 @@ class QtChatBubble(QWidget):
1980
2086
  )
1981
2087
 
1982
2088
  if self.debug:
1983
- print(f"💾 Saved session via AbstractCore to {file_path}")
2089
+ if self.debug:
2090
+ print(f"💾 Saved session via AbstractCore to {file_path}")
1984
2091
  else:
1985
2092
  raise Exception("AbstractCore session saving failed")
1986
2093
 
@@ -1991,7 +2098,8 @@ class QtChatBubble(QWidget):
1991
2098
  f"Failed to save session via AbstractCore:\n{str(e)}"
1992
2099
  )
1993
2100
  if self.debug:
1994
- print(f"❌ Failed to save session: {e}")
2101
+ if self.debug:
2102
+ print(f"❌ Failed to save session: {e}")
1995
2103
 
1996
2104
  def _is_voice_mode_active(self):
1997
2105
  """Centralized source of truth: Check if ANY voice mode is active."""
@@ -2038,11 +2146,13 @@ class QtChatBubble(QWidget):
2038
2146
  self.message_history.append(message)
2039
2147
 
2040
2148
  if self.debug:
2041
- print(f"📚 Updated message history from AbstractCore: {len(self.message_history)} messages")
2149
+ if self.debug:
2150
+ print(f"📚 Updated message history from AbstractCore: {len(self.message_history)} messages")
2042
2151
 
2043
2152
  except Exception as e:
2044
2153
  if self.debug:
2045
- print(f"❌ Error updating message history from session: {e}")
2154
+ if self.debug:
2155
+ print(f"❌ Error updating message history from session: {e}")
2046
2156
 
2047
2157
  def _update_token_count_from_session(self):
2048
2158
  """Update token count from AbstractCore session."""
@@ -2053,10 +2163,12 @@ class QtChatBubble(QWidget):
2053
2163
  self.update_token_display()
2054
2164
 
2055
2165
  if self.debug:
2056
- print(f"📊 Updated token count from AbstractCore: {self.token_count}")
2166
+ if self.debug:
2167
+ print(f"📊 Updated token count from AbstractCore: {self.token_count}")
2057
2168
  except Exception as e:
2058
2169
  if self.debug:
2059
- print(f"❌ Error updating token count from session: {e}")
2170
+ if self.debug:
2171
+ print(f"❌ Error updating token count from session: {e}")
2060
2172
 
2061
2173
  def _show_history_if_voice_mode_off(self):
2062
2174
  """Show chat history only if voice mode is OFF."""
@@ -2125,7 +2237,7 @@ class QtChatBubble(QWidget):
2125
2237
  border-radius: 11px;
2126
2238
  font-size: 10px;
2127
2239
  color: #ffffff;
2128
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
2240
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
2129
2241
  padding: 0 10px;
2130
2242
  font-weight: 600;
2131
2243
  }
@@ -2142,7 +2254,7 @@ class QtChatBubble(QWidget):
2142
2254
  border-radius: 11px;
2143
2255
  font-size: 10px;
2144
2256
  color: rgba(255, 255, 255, 0.7);
2145
- font-family: "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
2257
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
2146
2258
  padding: 0 10px;
2147
2259
  }
2148
2260
  QPushButton:hover {
@@ -2154,7 +2266,8 @@ class QtChatBubble(QWidget):
2154
2266
  def close_app(self):
2155
2267
  """Close the entire application completely."""
2156
2268
  if self.debug:
2157
- print("🔄 Close button clicked - shutting down application")
2269
+ if self.debug:
2270
+ print("🔄 Close button clicked - shutting down application")
2158
2271
 
2159
2272
  # Stop TTS if running
2160
2273
  if hasattr(self, 'voice_manager') and self.voice_manager:
@@ -2170,16 +2283,19 @@ class QtChatBubble(QWidget):
2170
2283
  # ALWAYS try to call the app quit callback first
2171
2284
  if hasattr(self, 'app_quit_callback') and self.app_quit_callback:
2172
2285
  if self.debug:
2173
- print("🔄 Calling app quit callback")
2286
+ if self.debug:
2287
+ print("🔄 Calling app quit callback")
2174
2288
  try:
2175
2289
  self.app_quit_callback()
2176
2290
  except Exception as e:
2177
2291
  if self.debug:
2178
- print(f"❌ App callback failed: {e}")
2292
+ if self.debug:
2293
+ print(f"❌ App callback failed: {e}")
2179
2294
 
2180
2295
  # ALWAYS force quit as well to ensure the app terminates
2181
2296
  if self.debug:
2182
- print("🔄 Force quitting application")
2297
+ if self.debug:
2298
+ print("🔄 Force quitting application")
2183
2299
 
2184
2300
  # Get the QApplication instance
2185
2301
  app = QApplication.instance()
@@ -2193,7 +2309,8 @@ class QtChatBubble(QWidget):
2193
2309
  import sys
2194
2310
  import os
2195
2311
  if self.debug:
2196
- print("🔄 Force exit with sys.exit and os._exit")
2312
+ if self.debug:
2313
+ print("🔄 Force exit with sys.exit and os._exit")
2197
2314
  try:
2198
2315
  sys.exit(0)
2199
2316
  except:
@@ -2204,6 +2321,53 @@ class QtChatBubble(QWidget):
2204
2321
  """Set callback to properly quit the main application."""
2205
2322
  self.app_quit_callback = callback
2206
2323
 
2324
+ @pyqtSlot()
2325
+ def _on_speech_started_main_thread(self):
2326
+ """Handle speech start on main thread (called via QMetaObject.invokeMethod)."""
2327
+ if self.debug:
2328
+ print("🔊 QtChatBubble: Speech started - updating status on main thread")
2329
+ if self.status_callback:
2330
+ self.status_callback("speaking")
2331
+
2332
+ @pyqtSlot()
2333
+ def _on_speech_ended_main_thread(self):
2334
+ """Handle speech end on main thread (called via QMetaObject.invokeMethod)."""
2335
+ if self.debug:
2336
+ print("🔊 QtChatBubble: Speech ended - handling completion on main thread")
2337
+
2338
+ # Update toggle state when speech completes
2339
+ self._update_tts_toggle_state()
2340
+
2341
+ # Call response callback now that TTS is done
2342
+ if self.response_callback and hasattr(self, '_pending_response'):
2343
+ if self.debug:
2344
+ print(f"🔄 QtChatBubble: TTS completed, calling response callback...")
2345
+ self.response_callback(self._pending_response)
2346
+ delattr(self, '_pending_response')
2347
+
2348
+ # Notify main app that speaking is done (back to ready)
2349
+ if self.status_callback:
2350
+ if self.debug:
2351
+ print("🔊 QtChatBubble: Speech ended, setting ready status")
2352
+ self.status_callback("ready")
2353
+
2354
+ @pyqtSlot()
2355
+ def _execute_tts_completion_callbacks(self):
2356
+ """Execute TTS completion callbacks on the main thread."""
2357
+ if hasattr(self, '_tts_completion_callback') and self._tts_completion_callback:
2358
+ if self.debug:
2359
+ print("🔊 QtChatBubble: Executing TTS completion callbacks on main thread...")
2360
+
2361
+ # Execute the stored callback
2362
+ try:
2363
+ self._tts_completion_callback()
2364
+ except Exception as e:
2365
+ if self.debug:
2366
+ print(f"❌ Error executing TTS completion callback: {e}")
2367
+ finally:
2368
+ # Clear the callback
2369
+ self._tts_completion_callback = None
2370
+
2207
2371
 
2208
2372
  def closeEvent(self, event):
2209
2373
  """Handle close event."""
@@ -2217,7 +2381,8 @@ class QtChatBubble(QWidget):
2217
2381
  self.voice_manager.cleanup()
2218
2382
  except Exception as e:
2219
2383
  if self.debug:
2220
- print(f"❌ Error cleaning up voice manager: {e}")
2384
+ if self.debug:
2385
+ print(f"❌ Error cleaning up voice manager: {e}")
2221
2386
 
2222
2387
  event.accept()
2223
2388
 
@@ -2241,20 +2406,21 @@ class QtBubbleManager:
2241
2406
  raise RuntimeError("No Qt library available. Install PyQt5, PySide2, or PyQt6")
2242
2407
 
2243
2408
  if self.debug:
2244
- print(f"✅ QtBubbleManager initialized with {QT_AVAILABLE}")
2409
+ if self.debug:
2410
+ print(f"✅ QtBubbleManager initialized with {QT_AVAILABLE}")
2245
2411
 
2246
2412
  def _prepare_bubble(self):
2247
2413
  """Pre-initialize the bubble for instant display later."""
2248
2414
  if not self.app:
2249
- # Create QApplication if it doesn't exist
2250
- if not QApplication.instance():
2251
- self.app = QApplication(sys.argv)
2252
- else:
2253
- self.app = QApplication.instance()
2415
+ # Always use existing QApplication instance (never create a new one)
2416
+ self.app = QApplication.instance()
2417
+ if not self.app:
2418
+ raise RuntimeError("No QApplication instance found. This should be created by the main app first.")
2254
2419
 
2255
2420
  if not self.bubble:
2256
2421
  if self.debug:
2257
- print("🔄 Pre-creating QtChatBubble...")
2422
+ if self.debug:
2423
+ print("🔄 Pre-creating QtChatBubble...")
2258
2424
 
2259
2425
  # Create the bubble but don't show it yet
2260
2426
  self.bubble = QtChatBubble(self.llm_manager, self.config, self.debug, self.listening_mode)
@@ -2268,7 +2434,8 @@ class QtBubbleManager:
2268
2434
  self.bubble.set_status_callback(self.status_callback)
2269
2435
 
2270
2436
  if self.debug:
2271
- print("✅ QtChatBubble pre-created and ready")
2437
+ if self.debug:
2438
+ print("✅ QtChatBubble pre-created and ready")
2272
2439
 
2273
2440
  def show(self):
2274
2441
  """Show the chat bubble (instantly if pre-initialized)."""
@@ -2286,7 +2453,8 @@ class QtBubbleManager:
2286
2453
  self.bubble.activateWindow()
2287
2454
 
2288
2455
  if self.debug:
2289
- print("💬 Qt chat bubble shown")
2456
+ if self.debug:
2457
+ print("💬 Qt chat bubble shown")
2290
2458
 
2291
2459
  def hide(self):
2292
2460
  """Hide the chat bubble."""
@@ -2294,7 +2462,8 @@ class QtBubbleManager:
2294
2462
  self.bubble.hide()
2295
2463
 
2296
2464
  if self.debug:
2297
- print("💬 Qt chat bubble hidden")
2465
+ if self.debug:
2466
+ print("💬 Qt chat bubble hidden")
2298
2467
 
2299
2468
  def destroy(self):
2300
2469
  """Destroy the chat bubble."""
@@ -2303,7 +2472,8 @@ class QtBubbleManager:
2303
2472
  self.bubble = None
2304
2473
 
2305
2474
  if self.debug:
2306
- print("💬 Qt chat bubble destroyed")
2475
+ if self.debug:
2476
+ print("💬 Qt chat bubble destroyed")
2307
2477
 
2308
2478
  def set_response_callback(self, callback):
2309
2479
  """Set response callback."""