abstractassistant 0.3.3__py3-none-any.whl → 0.3.4__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.
@@ -4,8 +4,9 @@ iPhone Messages-style history dialog for AbstractAssistant.
4
4
  This module provides an authentic iPhone Messages UI for displaying chat history.
5
5
  """
6
6
  import re
7
+ import time
7
8
  from datetime import datetime
8
- from typing import Dict, List
9
+ from typing import Dict, List, Callable, Optional
9
10
  import markdown
10
11
  from markdown.extensions.fenced_code import FencedCodeExtension
11
12
  from markdown.extensions.tables import TableExtension
@@ -33,62 +34,194 @@ except ImportError:
33
34
 
34
35
 
35
36
  class ClickableBubble(QFrame):
36
- """Clickable message bubble that copies content to clipboard."""
37
+ """Clickable message bubble that copies content to clipboard and supports deletion."""
37
38
 
38
39
  clicked = pyqtSignal()
40
+ delete_requested = pyqtSignal(int) # Signal with message index
41
+ selection_changed = pyqtSignal(int, bool) # Signal with message index and selection state
39
42
 
40
- def __init__(self, content: str, is_user: bool, parent=None):
43
+ def __init__(self, content: str, is_user: bool, message_index: int, parent=None):
41
44
  super().__init__(parent)
42
45
  self.content = content
43
46
  self.is_user = is_user
47
+ self.message_index = message_index
48
+ self.is_selected = False
49
+ self.selection_mode = False
44
50
  self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
45
51
 
46
52
  # Store original colors for animation
47
53
  if is_user:
48
54
  self.normal_bg = "#007AFF"
49
55
  self.clicked_bg = "#0066CC"
56
+ self.selected_bg = "#FF3B30" # Red for selection
50
57
  else:
51
58
  self.normal_bg = "#3a3a3c"
52
59
  self.clicked_bg = "#4a4a4c"
60
+ self.selected_bg = "#FF3B30" # Red for selection
61
+
62
+ # Long press timer for selection mode
63
+ self.long_press_timer = QTimer()
64
+ self.long_press_timer.setSingleShot(True)
65
+ self.long_press_timer.timeout.connect(self._start_selection_mode)
66
+ self.press_start_time = None
53
67
 
54
68
  def mousePressEvent(self, event):
55
- """Handle mouse press with visual feedback."""
69
+ """Handle mouse press with visual feedback and long press detection."""
56
70
  if event.button() == Qt.MouseButton.LeftButton:
57
- # Apply clicked style (darker)
58
- self.setStyleSheet(f"""
59
- QFrame {{
60
- background: {self.clicked_bg};
61
- border: none;
62
- border-radius: 18px;
63
- max-width: 400px;
64
- }}
65
- """)
71
+ self.press_start_time = time.time()
72
+
73
+ if self.selection_mode:
74
+ # In selection mode, toggle selection
75
+ self.toggle_selection()
76
+ else:
77
+ # Normal mode - start long press timer and apply clicked style
78
+ self.long_press_timer.start(800) # 800ms for long press
79
+ self.setStyleSheet(f"""
80
+ QFrame {{
81
+ background: {self.clicked_bg};
82
+ border: none;
83
+ border-radius: 18px;
84
+ max-width: 400px;
85
+ }}
86
+ """)
66
87
  super().mousePressEvent(event)
67
88
 
68
89
  def mouseReleaseEvent(self, event):
69
- """Handle mouse release - copy to clipboard and restore style."""
90
+ """Handle mouse release - copy to clipboard, handle selection, and restore style."""
70
91
  if event.button() == Qt.MouseButton.LeftButton:
71
- # Copy to clipboard
72
- clipboard = QApplication.clipboard()
73
- clipboard.setText(self.content)
74
-
75
- # Visual feedback: glossy effect (lighter color briefly)
76
- glossy_color = "#0080FF" if self.is_user else "#5a5a5c"
77
- self.setStyleSheet(f"""
78
- QFrame {{
79
- background: {glossy_color};
80
- border: none;
81
- border-radius: 18px;
82
- max-width: 400px;
83
- }}
84
- """)
85
-
86
- # Restore normal color after brief delay
87
- QTimer.singleShot(200, self._restore_normal_style)
88
-
89
- self.clicked.emit()
92
+ # Stop long press timer
93
+ self.long_press_timer.stop()
94
+
95
+ if self.selection_mode:
96
+ # In selection mode, just restore style
97
+ self._update_visual_state()
98
+ else:
99
+ # Check if this was a quick tap (not long press)
100
+ if self.press_start_time and (time.time() - self.press_start_time) < 0.5:
101
+ # Copy to clipboard
102
+ clipboard = QApplication.clipboard()
103
+ clipboard.setText(self.content)
104
+
105
+ # Visual feedback: glossy effect (lighter color briefly)
106
+ glossy_color = "#0080FF" if self.is_user else "#5a5a5c"
107
+ self.setStyleSheet(f"""
108
+ QFrame {{
109
+ background: {glossy_color};
110
+ border: none;
111
+ border-radius: 18px;
112
+ max-width: 400px;
113
+ }}
114
+ """)
115
+
116
+ # Restore normal color after brief delay
117
+ QTimer.singleShot(200, self._restore_normal_style)
118
+
119
+ self.clicked.emit()
120
+ else:
121
+ # Long press - restore style immediately
122
+ self._restore_normal_style()
90
123
  super().mouseReleaseEvent(event)
91
124
 
125
+ def _start_selection_mode(self):
126
+ """Start selection mode on long press."""
127
+ self.selection_mode = True
128
+ self.is_selected = True
129
+ self._update_visual_state()
130
+ self.selection_changed.emit(self.message_index, True)
131
+
132
+ # Provide haptic-like feedback by briefly changing color
133
+ self.setStyleSheet(f"""
134
+ QFrame {{
135
+ background: {self.selected_bg};
136
+ border: 2px solid #FFFFFF;
137
+ border-radius: 18px;
138
+ max-width: 400px;
139
+ }}
140
+ """)
141
+
142
+ # Add selection indicator
143
+ QTimer.singleShot(100, self._update_visual_state)
144
+
145
+ def toggle_selection(self):
146
+ """Toggle selection state in selection mode."""
147
+ self.is_selected = not self.is_selected
148
+ self._update_selection_circle()
149
+ self._update_visual_state()
150
+ self.selection_changed.emit(self.message_index, self.is_selected)
151
+
152
+ def set_selection_mode(self, enabled: bool):
153
+ """Set selection mode state."""
154
+ self.selection_mode = enabled
155
+ if not enabled:
156
+ self.is_selected = False
157
+
158
+ # Show/hide selection circle
159
+ if hasattr(self, 'selection_circle'):
160
+ if enabled:
161
+ self.selection_circle.show()
162
+ else:
163
+ self.selection_circle.hide()
164
+
165
+ self._update_selection_circle()
166
+ self._update_visual_state()
167
+
168
+ def set_selected(self, selected: bool):
169
+ """Set selection state."""
170
+ self.is_selected = selected
171
+ self._update_selection_circle()
172
+ self._update_visual_state()
173
+
174
+ def _update_selection_circle(self):
175
+ """Update selection circle appearance."""
176
+ if hasattr(self, 'selection_circle') and self.selection_mode:
177
+ if self.is_selected:
178
+ # Selected state - filled circle with checkmark
179
+ self.selection_circle.setStyleSheet("""
180
+ QPushButton {
181
+ background: #007AFF;
182
+ border: 2px solid #007AFF;
183
+ border-radius: 11px;
184
+ margin: 0px;
185
+ padding: 0px;
186
+ color: white;
187
+ font-size: 12px;
188
+ font-weight: bold;
189
+ }
190
+ QPushButton:hover {
191
+ background: #0066CC;
192
+ border: 2px solid #0066CC;
193
+ }
194
+ """)
195
+ self.selection_circle.setText("✓")
196
+ else:
197
+ # Unselected state - empty circle
198
+ self.selection_circle.setStyleSheet("""
199
+ QPushButton {
200
+ background: transparent;
201
+ border: 2px solid rgba(255, 255, 255, 0.6);
202
+ border-radius: 11px;
203
+ margin: 0px;
204
+ padding: 0px;
205
+ }
206
+ QPushButton:hover {
207
+ border: 2px solid rgba(255, 255, 255, 0.8);
208
+ }
209
+ """)
210
+ self.selection_circle.setText("")
211
+
212
+ def _update_visual_state(self):
213
+ """Update visual state based on selection - no visual changes to bubble itself."""
214
+ # In authentic iPhone Messages, the bubble appearance doesn't change
215
+ # Only the selection circle changes state - bubble stays the same
216
+ self.setStyleSheet(f"""
217
+ QFrame {{
218
+ background: {self.normal_bg};
219
+ border: none;
220
+ border-radius: 18px;
221
+ max-width: 400px;
222
+ }}
223
+ """)
224
+
92
225
  def _restore_normal_style(self):
93
226
  """Restore normal bubble style."""
94
227
  self.setStyleSheet(f"""
@@ -107,13 +240,322 @@ class SafeDialog(QDialog):
107
240
  def __init__(self, parent=None):
108
241
  super().__init__(parent)
109
242
  self.hide_callback = None
243
+ self.delete_callback = None
244
+ self.selection_mode = False
245
+ self.selected_messages = set()
246
+ self.message_bubbles = []
247
+ self.trash_button = None
248
+ self.edit_button = None
110
249
 
111
250
  def set_hide_callback(self, callback):
112
251
  """Set callback to call when dialog is hidden."""
113
252
  self.hide_callback = callback
114
253
 
254
+ def set_delete_callback(self, callback):
255
+ """Set callback to call when messages are deleted."""
256
+ self.delete_callback = callback
257
+
258
+ def enter_selection_mode(self):
259
+ """Enter selection mode for message deletion."""
260
+ self.selection_mode = True
261
+ self.selected_messages.clear()
262
+
263
+ # Update all bubbles to selection mode
264
+ for bubble in self.message_bubbles:
265
+ bubble.set_selection_mode(True)
266
+
267
+ # Update navigation bar
268
+ self._update_navbar_for_selection_mode()
269
+
270
+ def exit_selection_mode(self):
271
+ """Exit selection mode."""
272
+ self.selection_mode = False
273
+ self.selected_messages.clear()
274
+
275
+ # Update all bubbles to normal mode
276
+ for bubble in self.message_bubbles:
277
+ bubble.set_selection_mode(False)
278
+
279
+ # Update navigation bar
280
+ self._update_navbar_for_normal_mode()
281
+
282
+ def _update_navbar_for_selection_mode(self):
283
+ """Update navbar for selection mode."""
284
+ if hasattr(self, 'edit_button'):
285
+ self.edit_button.setText("Cancel")
286
+ self.edit_button.clicked.disconnect()
287
+ self.edit_button.clicked.connect(self.exit_selection_mode)
288
+
289
+ # Hide trash button initially (will show when messages are selected)
290
+ if hasattr(self, 'trash_button'):
291
+ self.trash_button.hide()
292
+
293
+ def _update_navbar_for_normal_mode(self):
294
+ """Update navbar for normal mode."""
295
+ if hasattr(self, 'edit_button'):
296
+ self.edit_button.setText("Edit")
297
+ self.edit_button.clicked.disconnect()
298
+ self.edit_button.clicked.connect(self.enter_selection_mode)
299
+
300
+ # Hide trash button
301
+ if hasattr(self, 'trash_button'):
302
+ self.trash_button.hide()
303
+
304
+ def on_selection_changed(self, message_index: int, selected: bool):
305
+ """Handle selection change from a bubble."""
306
+ if selected:
307
+ self.selected_messages.add(message_index)
308
+ else:
309
+ self.selected_messages.discard(message_index)
310
+
311
+ # Show/hide trash button based on selection
312
+ if hasattr(self, 'trash_button'):
313
+ if len(self.selected_messages) > 0:
314
+ self.trash_button.show()
315
+ else:
316
+ self.trash_button.hide()
317
+
318
+ def delete_selected_messages(self):
319
+ """Delete selected messages with iPhone-style bottom action sheet."""
320
+ try:
321
+ if not self.selected_messages or not self.delete_callback:
322
+ return
323
+
324
+ # Show iPhone-style bottom action sheet
325
+ count = len(self.selected_messages)
326
+ self._show_delete_action_sheet(count)
327
+
328
+ except Exception as e:
329
+ import traceback
330
+ traceback.print_exc()
331
+
332
+ def _show_delete_action_sheet(self, count: int):
333
+ """Show iPhone-style bottom action sheet for deletion confirmation."""
334
+ # Create overlay widget that covers the entire dialog
335
+ overlay = QWidget(self)
336
+ overlay.setStyleSheet("background: rgba(0, 0, 0, 0.4);")
337
+ overlay.resize(self.size())
338
+ overlay.move(0, 0)
339
+
340
+ # Create action sheet container
341
+ action_sheet = QWidget(overlay)
342
+ action_sheet.setStyleSheet("""
343
+ QWidget {
344
+ background: #2C2C2E;
345
+ border-radius: 13px;
346
+ border: none;
347
+ }
348
+ """)
349
+
350
+ # Layout for action sheet
351
+ layout = QVBoxLayout(action_sheet)
352
+ layout.setContentsMargins(0, 0, 0, 0)
353
+ layout.setSpacing(1)
354
+
355
+ # Delete button (red, destructive)
356
+ delete_btn = QPushButton(f"Delete {count} Message{'s' if count > 1 else ''}")
357
+ delete_btn.setStyleSheet("""
358
+ QPushButton {
359
+ background: #FF3B30;
360
+ color: white;
361
+ border: none;
362
+ padding: 16px;
363
+ font-size: 17px;
364
+ font-weight: 400;
365
+ text-align: center;
366
+ }
367
+ QPushButton:hover {
368
+ background: #D70015;
369
+ }
370
+ """)
371
+ delete_btn.clicked.connect(lambda: self._confirm_deletion(overlay))
372
+
373
+ # Cancel button
374
+ cancel_btn = QPushButton("Cancel")
375
+ cancel_btn.setStyleSheet("""
376
+ QPushButton {
377
+ background: #48484A;
378
+ color: #007AFF;
379
+ border: none;
380
+ padding: 16px;
381
+ font-size: 17px;
382
+ font-weight: 600;
383
+ text-align: center;
384
+ border-radius: 13px;
385
+ margin-top: 8px;
386
+ }
387
+ QPushButton:hover {
388
+ background: #5A5A5C;
389
+ }
390
+ """)
391
+ cancel_btn.clicked.connect(lambda: self._cancel_deletion(overlay))
392
+
393
+ layout.addWidget(delete_btn)
394
+ layout.addWidget(cancel_btn)
395
+
396
+ # Position action sheet at bottom
397
+ action_sheet.setFixedWidth(self.width() - 40)
398
+ action_sheet.adjustSize()
399
+ action_sheet.move(20, self.height() - action_sheet.height() - 20)
400
+
401
+ # Show overlay and action sheet
402
+ overlay.show()
403
+ overlay.raise_()
404
+
405
+ # Store reference for cleanup
406
+ self.current_overlay = overlay
407
+
408
+ def _confirm_deletion(self, overlay):
409
+ """Confirm deletion and execute it."""
410
+ try:
411
+ # Hide overlay
412
+ overlay.hide()
413
+ overlay.deleteLater()
414
+ self.current_overlay = None
415
+
416
+ # Convert to sorted list for consistent deletion
417
+ indices_to_delete = sorted(list(self.selected_messages), reverse=True)
418
+
419
+ # Call the delete callback with error handling
420
+ if self.delete_callback:
421
+ self.delete_callback(indices_to_delete)
422
+
423
+ # Exit selection mode after deletion
424
+ self.exit_selection_mode()
425
+
426
+ except Exception as e:
427
+ import traceback
428
+ traceback.print_exc()
429
+
430
+ # Try to exit selection mode even if deletion failed
431
+ try:
432
+ self.exit_selection_mode()
433
+ except:
434
+ pass
435
+
436
+ def _cancel_deletion(self, overlay):
437
+ """Cancel deletion and hide action sheet."""
438
+ overlay.hide()
439
+ overlay.deleteLater()
440
+ self.current_overlay = None
441
+
442
+ def update_message_history(self, new_message_history: List[Dict]):
443
+ """Update the dialog with new message history without closing it."""
444
+ try:
445
+ # Exit selection mode if active
446
+ if self.selection_mode:
447
+ self.exit_selection_mode()
448
+
449
+ # Clear existing content completely using correct widget references
450
+ if hasattr(self, 'messages_layout'):
451
+ layout = self.messages_layout
452
+
453
+ # Remove all widgets except the stretch at the end
454
+ while layout.count() > 0:
455
+ child = layout.takeAt(0)
456
+ if child.widget():
457
+ widget = child.widget()
458
+ widget.setParent(None)
459
+ widget.deleteLater()
460
+
461
+ # Force immediate processing of deleteLater calls
462
+ from PyQt5.QtCore import QCoreApplication
463
+ QCoreApplication.processEvents()
464
+
465
+ # Recreate messages with new history
466
+ if new_message_history:
467
+ # Re-add messages using the same method as original creation
468
+ iPhoneMessagesDialog._add_authentic_iphone_messages(layout, new_message_history, self)
469
+ else:
470
+ # Add placeholder for empty state
471
+ placeholder = QLabel("No messages")
472
+ placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
473
+ placeholder.setStyleSheet("""
474
+ QLabel {
475
+ color: rgba(255, 255, 255, 0.5);
476
+ font-size: 16px;
477
+ padding: 40px;
478
+ }
479
+ """)
480
+ layout.addWidget(placeholder)
481
+
482
+ # Re-add stretch to push messages to top
483
+ layout.addStretch()
484
+
485
+ # Force complete UI update
486
+ if hasattr(self, 'messages_widget'):
487
+ self.messages_widget.adjustSize()
488
+ self.messages_widget.update()
489
+ self.messages_widget.repaint()
490
+
491
+ if hasattr(self, 'scroll_area'):
492
+ self.scroll_area.update()
493
+ self.scroll_area.repaint()
494
+ # Scroll to bottom to show latest messages
495
+ QTimer.singleShot(100, lambda: self.scroll_area.verticalScrollBar().setValue(
496
+ self.scroll_area.verticalScrollBar().maximum()
497
+ ))
498
+
499
+ # Update the entire dialog
500
+ self.update()
501
+ self.repaint()
502
+
503
+ else:
504
+ raise Exception("Dialog structure not found")
505
+
506
+ except Exception as e:
507
+ import traceback
508
+ traceback.print_exc()
509
+ raise # Re-raise to trigger fallback in qt_bubble.py
510
+
511
+ def _populate_messages(self, message_history: List[Dict]):
512
+ """Populate the scroll area with message bubbles."""
513
+ if not hasattr(self, 'scroll_content'):
514
+ return
515
+
516
+ layout = self.scroll_content.layout()
517
+ if not layout:
518
+ layout = QVBoxLayout(self.scroll_content)
519
+ layout.setContentsMargins(20, 20, 20, 20)
520
+ layout.setSpacing(8)
521
+
522
+ # Add messages as bubbles (only if there are messages)
523
+ if message_history:
524
+ for i, message in enumerate(message_history):
525
+ if message.get('type') in ['user', 'assistant']:
526
+ bubble = ClickableBubble(
527
+ message.get('content', ''),
528
+ message.get('type') == 'user',
529
+ i,
530
+ self
531
+ )
532
+ layout.addWidget(bubble)
533
+ else:
534
+ # If no messages, add a placeholder
535
+ placeholder = QLabel("No messages")
536
+ placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
537
+ placeholder.setStyleSheet("""
538
+ QLabel {
539
+ color: rgba(255, 255, 255, 0.5);
540
+ font-size: 16px;
541
+ padding: 40px;
542
+ }
543
+ """)
544
+ layout.addWidget(placeholder)
545
+
546
+ # Add stretch to push messages to top
547
+ layout.addStretch()
548
+
549
+ # Force layout update
550
+ self.scroll_content.update()
551
+ if hasattr(self, 'scroll_area'):
552
+ self.scroll_area.update()
553
+
554
+
115
555
  def closeEvent(self, event):
116
556
  """Override close event to hide instead of close."""
557
+ if self.selection_mode:
558
+ self.exit_selection_mode()
117
559
  event.ignore()
118
560
  self.hide()
119
561
  if self.hide_callback:
@@ -121,6 +563,8 @@ class SafeDialog(QDialog):
121
563
 
122
564
  def reject(self):
123
565
  """Override reject to hide instead of close."""
566
+ if self.selection_mode:
567
+ self.exit_selection_mode()
124
568
  self.hide()
125
569
  if self.hide_callback:
126
570
  self.hide_callback()
@@ -130,14 +574,22 @@ class iPhoneMessagesDialog:
130
574
  """Create authentic iPhone Messages-style chat history dialog."""
131
575
 
132
576
  @staticmethod
133
- def create_dialog(message_history: List[Dict], parent=None) -> QDialog:
134
- """Create AUTHENTIC iPhone Messages dialog - EXACTLY like the real app."""
577
+ def create_dialog(message_history: List[Dict], parent=None, delete_callback: Optional[Callable] = None) -> QDialog:
578
+ """Create AUTHENTIC iPhone Messages dialog with deletion support."""
579
+ # Safety check for empty message history
580
+ if not message_history:
581
+ return None
582
+
135
583
  dialog = SafeDialog(parent)
136
584
  dialog.setWindowTitle("Messages")
137
585
  dialog.setModal(False)
138
586
  dialog.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
139
587
  dialog.resize(504, 650) # Increased width by 20% (420 * 1.2 = 504)
140
588
 
589
+ # Set delete callback
590
+ if delete_callback:
591
+ dialog.set_delete_callback(delete_callback)
592
+
141
593
  # Position dialog near right edge of screen like iPhone
142
594
  iPhoneMessagesDialog._position_dialog_right(dialog)
143
595
 
@@ -146,10 +598,10 @@ class iPhoneMessagesDialog:
146
598
  main_layout.setContentsMargins(0, 0, 0, 0)
147
599
  main_layout.setSpacing(0)
148
600
 
149
- # iPhone navigation bar
601
+ # iPhone navigation bar with delete button
150
602
  navbar = iPhoneMessagesDialog._create_authentic_navbar(dialog)
151
603
  main_layout.addWidget(navbar)
152
-
604
+
153
605
  # Messages container with pure white background
154
606
  scroll_area = QScrollArea()
155
607
  scroll_area.setWidgetResizable(True)
@@ -163,8 +615,13 @@ class iPhoneMessagesDialog:
163
615
  messages_layout.setContentsMargins(0, 16, 0, 16) # iPhone spacing
164
616
  messages_layout.setSpacing(0)
165
617
 
166
- # Add messages with authentic iPhone styling
167
- iPhoneMessagesDialog._add_authentic_iphone_messages(messages_layout, message_history)
618
+ # Store references for updating
619
+ dialog.scroll_area = scroll_area
620
+ dialog.messages_widget = messages_widget
621
+ dialog.messages_layout = messages_layout
622
+
623
+ # Add messages with authentic iPhone styling and deletion support
624
+ iPhoneMessagesDialog._add_authentic_iphone_messages(messages_layout, message_history, dialog)
168
625
 
169
626
  messages_layout.addStretch()
170
627
  scroll_area.setWidget(messages_widget)
@@ -203,8 +660,8 @@ class iPhoneMessagesDialog:
203
660
  dialog.move(x, y)
204
661
 
205
662
  @staticmethod
206
- def _create_authentic_navbar(dialog: QDialog) -> QFrame:
207
- """Create AUTHENTIC iPhone Messages navigation bar."""
663
+ def _create_authentic_navbar(dialog: SafeDialog) -> QFrame:
664
+ """Create AUTHENTIC iPhone Messages navigation bar with delete functionality."""
208
665
  navbar = QFrame()
209
666
  navbar.setFixedHeight(94) # iPhone status bar + nav bar
210
667
  navbar.setStyleSheet("""
@@ -261,18 +718,57 @@ class iPhoneMessagesDialog:
261
718
 
262
719
  nav_layout.addStretch()
263
720
 
721
+ # Trash icon (initially hidden, appears when messages are selected)
722
+ trash_btn = QPushButton("🗑️")
723
+ trash_btn.setFixedSize(30, 30)
724
+ trash_btn.clicked.connect(dialog.delete_selected_messages)
725
+ trash_btn.setStyleSheet("""
726
+ QPushButton {
727
+ color: #FF3B30;
728
+ font-size: 18px;
729
+ background: transparent;
730
+ border: none;
731
+ text-align: center;
732
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
733
+ }
734
+ QPushButton:hover {
735
+ background: rgba(255, 59, 48, 0.1);
736
+ border-radius: 15px;
737
+ }
738
+ """)
739
+ trash_btn.hide() # Initially hidden
740
+ dialog.trash_button = trash_btn # Store reference
741
+ nav_layout.addWidget(trash_btn)
742
+
743
+ # Delete button (Edit in iPhone style)
744
+ edit_btn = QPushButton("Edit")
745
+ edit_btn.clicked.connect(dialog.enter_selection_mode)
746
+ edit_btn.setStyleSheet("""
747
+ QPushButton {
748
+ color: #007AFF;
749
+ font-size: 17px;
750
+ font-weight: 400;
751
+ background: transparent;
752
+ border: none;
753
+ text-align: right;
754
+ font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
755
+ }
756
+ """)
757
+ dialog.edit_button = edit_btn # Store reference
758
+ nav_layout.addWidget(edit_btn)
759
+
264
760
  layout.addWidget(nav_frame)
265
761
  return navbar
266
762
 
267
763
  @staticmethod
268
- def _add_authentic_iphone_messages(layout: QVBoxLayout, message_history: List[Dict]):
269
- """Add messages with AUTHENTIC iPhone Messages styling."""
764
+ def _add_authentic_iphone_messages(layout: QVBoxLayout, message_history: List[Dict], dialog: SafeDialog):
765
+ """Add messages with AUTHENTIC iPhone Messages styling and deletion support."""
270
766
  for index, msg in enumerate(message_history):
271
767
  message_type = msg.get('type', msg.get('role', 'unknown'))
272
768
  is_user = message_type in ['user', 'human']
273
769
 
274
- # Create authentic iPhone bubble
275
- bubble_container = iPhoneMessagesDialog._create_authentic_iphone_bubble(msg, is_user, index, message_history)
770
+ # Create authentic iPhone bubble with deletion support
771
+ bubble_container = iPhoneMessagesDialog._create_authentic_iphone_bubble(msg, is_user, index, message_history, dialog)
276
772
  layout.addWidget(bubble_container)
277
773
 
278
774
  # Add spacing between messages (6px like iPhone)
@@ -283,8 +779,8 @@ class iPhoneMessagesDialog:
283
779
  layout.addWidget(spacer)
284
780
 
285
781
  @staticmethod
286
- def _create_authentic_iphone_bubble(msg: Dict, is_user: bool, index: int, message_history: List[Dict]) -> QFrame:
287
- """Create AUTHENTIC iPhone Messages bubble - exactly like real iPhone."""
782
+ def _create_authentic_iphone_bubble(msg: Dict, is_user: bool, index: int, message_history: List[Dict], dialog: SafeDialog) -> QFrame:
783
+ """Create AUTHENTIC iPhone Messages bubble with deletion support."""
288
784
  main_container = QFrame()
289
785
  main_container.setStyleSheet("background: transparent; border: none;")
290
786
  main_layout = QVBoxLayout(main_container)
@@ -298,8 +794,32 @@ class iPhoneMessagesDialog:
298
794
  layout.setContentsMargins(12, 0, 12, 0) # Tighter margins for more width
299
795
  layout.setSpacing(0)
300
796
 
301
- # Create clickable bubble
302
- bubble = ClickableBubble(msg['content'], is_user)
797
+ # Create selection circle (initially hidden)
798
+ selection_circle = QPushButton()
799
+ selection_circle.setFixedSize(22, 22)
800
+ selection_circle.setStyleSheet("""
801
+ QPushButton {
802
+ background: transparent;
803
+ border: 2px solid rgba(255, 255, 255, 0.6);
804
+ border-radius: 11px;
805
+ margin: 0px;
806
+ padding: 0px;
807
+ }
808
+ QPushButton:hover {
809
+ border: 2px solid rgba(255, 255, 255, 0.8);
810
+ }
811
+ """)
812
+ selection_circle.hide() # Initially hidden
813
+
814
+ # Create clickable bubble with deletion support
815
+ bubble = ClickableBubble(msg['content'], is_user, index)
816
+ bubble.selection_changed.connect(dialog.on_selection_changed)
817
+ bubble.selection_circle = selection_circle # Store reference
818
+ dialog.message_bubbles.append(bubble) # Track bubbles for selection mode
819
+
820
+ # Connect selection circle click
821
+ selection_circle.clicked.connect(bubble.toggle_selection)
822
+
303
823
  bubble_layout = QVBoxLayout(bubble)
304
824
  bubble_layout.setContentsMargins(12, 7, 12, 7) # More compact padding
305
825
  bubble_layout.setSpacing(0)
@@ -331,8 +851,10 @@ class iPhoneMessagesDialog:
331
851
  font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
332
852
  }
333
853
  """)
334
- # Right align
854
+ # Right align - selection circle on the left of bubble (towards center)
335
855
  layout.addStretch()
856
+ layout.addWidget(selection_circle, 0, Qt.AlignmentFlag.AlignCenter)
857
+ layout.addSpacing(8) # Small gap between circle and bubble
336
858
  layout.addWidget(bubble)
337
859
  else:
338
860
  # Received bubble: Light gray with black text
@@ -354,8 +876,10 @@ class iPhoneMessagesDialog:
354
876
  font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
355
877
  }
356
878
  """)
357
- # Left align
879
+ # Left align - selection circle on the right of bubble (towards center)
358
880
  layout.addWidget(bubble)
881
+ layout.addSpacing(8) # Small gap between bubble and circle
882
+ layout.addWidget(selection_circle, 0, Qt.AlignmentFlag.AlignCenter)
359
883
  layout.addStretch()
360
884
 
361
885
  bubble_layout.addWidget(content_label)
@@ -405,6 +405,7 @@ class QtChatBubble(QWidget):
405
405
  ("Clear", self.clear_session),
406
406
  ("Load", self.load_session),
407
407
  ("Save", self.save_session),
408
+ # ("Compact", self.compact_session), # Hidden for now - functionality preserved
408
409
  ("History", self.show_history)
409
410
  ]
410
411
 
@@ -1971,6 +1972,268 @@ class QtChatBubble(QWidget):
1971
1972
  if self.debug:
1972
1973
  print("🧹 Session cleared (including attached files and file tracking)")
1973
1974
 
1975
+ def compact_session(self):
1976
+ """Compact the current session using AbstractCore's summarizer functionality."""
1977
+ if not self.message_history:
1978
+ QMessageBox.information(
1979
+ self,
1980
+ "No Session",
1981
+ "No conversation history to compact. Start a conversation first."
1982
+ )
1983
+ return
1984
+
1985
+ # Check if session is too short to compact
1986
+ if len(self.message_history) < 4: # Need at least 2 exchanges to be worth compacting
1987
+ QMessageBox.information(
1988
+ self,
1989
+ "Session Too Short",
1990
+ "Session is too short to compact. Need at least 2 exchanges (4 messages)."
1991
+ )
1992
+ return
1993
+
1994
+ reply = QMessageBox.question(
1995
+ self,
1996
+ "Compact Session",
1997
+ "This will summarize the conversation history into a concise system message, "
1998
+ "keeping only the most recent 2 exchanges for context.\n\n"
1999
+ "This action cannot be undone. Continue?",
2000
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
2001
+ QMessageBox.StandardButton.No
2002
+ )
2003
+
2004
+ if reply == QMessageBox.StandardButton.Yes:
2005
+ try:
2006
+ # Show progress
2007
+ self.status_label.setText("compacting")
2008
+ self.status_label.setStyleSheet("""
2009
+ QLabel {
2010
+ background: rgba(250, 179, 135, 0.2);
2011
+ border: 1px solid rgba(250, 179, 135, 0.3);
2012
+ border-radius: 12px;
2013
+ padding: 4px 12px;
2014
+ font-size: 11px;
2015
+ font-weight: 600;
2016
+ text-transform: uppercase;
2017
+ letter-spacing: 0.5px;
2018
+ color: #fab387;
2019
+ }
2020
+ """)
2021
+
2022
+ # Notify main app about status change
2023
+ if self.status_callback:
2024
+ self.status_callback("compacting")
2025
+
2026
+ # Create conversation text for summarization
2027
+ conversation_text = self._format_conversation_for_summarization()
2028
+
2029
+ # Use AbstractCore's summarizer functionality through LLMManager
2030
+ summary = self._generate_conversation_summary(conversation_text)
2031
+
2032
+ if summary:
2033
+ # Keep the last 2 exchanges (4 messages) for context
2034
+ recent_messages = self.message_history[-4:] if len(self.message_history) >= 4 else self.message_history[-2:]
2035
+
2036
+ # Create new session with summary as system context
2037
+ self._create_compacted_session(summary, recent_messages)
2038
+
2039
+ # Update UI
2040
+ self.token_count = 0 # Reset token count
2041
+ self.update_token_display()
2042
+
2043
+ # Show success message
2044
+ QMessageBox.information(
2045
+ self,
2046
+ "Session Compacted",
2047
+ f"Session successfully compacted!\n\n"
2048
+ f"Original: {len(self.message_history)} messages\n"
2049
+ f"Compacted: Summary + {len(recent_messages)} recent messages"
2050
+ )
2051
+
2052
+ if self.debug:
2053
+ print(f"🗜️ Session compacted: {len(self.message_history)} -> summary + {len(recent_messages)} recent")
2054
+ else:
2055
+ raise Exception("Failed to generate summary")
2056
+
2057
+ except Exception as e:
2058
+ QMessageBox.critical(
2059
+ self,
2060
+ "Compaction Error",
2061
+ f"Failed to compact session:\n{str(e)}"
2062
+ )
2063
+ if self.debug:
2064
+ print(f"❌ Failed to compact session: {e}")
2065
+ import traceback
2066
+ traceback.print_exc()
2067
+ finally:
2068
+ # Reset status
2069
+ self.status_label.setText("ready")
2070
+ self.status_label.setStyleSheet("""
2071
+ QLabel {
2072
+ background: rgba(166, 227, 161, 0.2);
2073
+ border: 1px solid rgba(166, 227, 161, 0.3);
2074
+ border-radius: 12px;
2075
+ padding: 4px 12px;
2076
+ font-size: 11px;
2077
+ font-weight: 600;
2078
+ text-transform: uppercase;
2079
+ letter-spacing: 0.5px;
2080
+ color: #a6e3a1;
2081
+ }
2082
+ """)
2083
+ if self.status_callback:
2084
+ self.status_callback("ready")
2085
+
2086
+ def _format_conversation_for_summarization(self) -> str:
2087
+ """Format the conversation history for summarization."""
2088
+ lines = []
2089
+ lines.append("=== CONVERSATION HISTORY ===\n")
2090
+
2091
+ for i, msg in enumerate(self.message_history):
2092
+ role = "USER" if msg.get('type') == 'user' else "ASSISTANT"
2093
+ content = msg.get('content', '')
2094
+ timestamp = msg.get('timestamp', '')
2095
+
2096
+ # Add timestamp if available
2097
+ if timestamp:
2098
+ try:
2099
+ from datetime import datetime
2100
+ if isinstance(timestamp, str):
2101
+ dt = datetime.fromisoformat(timestamp)
2102
+ time_str = dt.strftime("%Y-%m-%d %H:%M")
2103
+ lines.append(f"[{time_str}] {role}:")
2104
+ else:
2105
+ lines.append(f"{role}:")
2106
+ except:
2107
+ lines.append(f"{role}:")
2108
+ else:
2109
+ lines.append(f"{role}:")
2110
+
2111
+ lines.append(content)
2112
+ lines.append("") # Empty line between messages
2113
+
2114
+ return "\n".join(lines)
2115
+
2116
+ def _generate_conversation_summary(self, conversation_text: str) -> str:
2117
+ """Generate a conversation summary using AbstractCore's summarizer functionality."""
2118
+ try:
2119
+ # Use the current LLM to generate a summary
2120
+ # This mimics what the AbstractCore summarizer CLI does
2121
+ summary_prompt = f"""Please provide a comprehensive but concise summary of the following conversation.
2122
+ Focus on:
2123
+ - Key topics discussed
2124
+ - Important decisions or conclusions reached
2125
+ - Relevant context that should be preserved
2126
+ - Any ongoing tasks or questions
2127
+
2128
+ The summary should be detailed enough to provide context for continuing the conversation, but concise enough to save tokens.
2129
+
2130
+ Conversation to summarize:
2131
+ {conversation_text}
2132
+
2133
+ Please provide the summary in a clear, structured format:"""
2134
+
2135
+ if self.llm_manager and self.llm_manager.llm:
2136
+ # Generate summary using current LLM
2137
+ response = self.llm_manager.llm.generate(summary_prompt)
2138
+
2139
+ if hasattr(response, 'content'):
2140
+ return response.content
2141
+ else:
2142
+ return str(response)
2143
+ else:
2144
+ raise Exception("No LLM available for summarization")
2145
+
2146
+ except Exception as e:
2147
+ if self.debug:
2148
+ print(f"❌ Error generating summary: {e}")
2149
+ raise
2150
+
2151
+ def _create_compacted_session(self, summary: str, recent_messages: list):
2152
+ """Create a new session with the summary and recent messages."""
2153
+ try:
2154
+ # Create new session with summary as enhanced system prompt
2155
+ enhanced_system_prompt = f"""You are a helpful AI assistant who has access to tools to help the user.
2156
+ Always be a critical and creative thinker who leverage constructive skepticism to progress and evolve its reasoning and answers.
2157
+ Always answer in nicely formatted markdown.
2158
+
2159
+ === CONVERSATION CONTEXT ===
2160
+ The following is a summary of our previous conversation:
2161
+
2162
+ {summary}
2163
+
2164
+ === END CONTEXT ===
2165
+
2166
+ Continue the conversation naturally, referring to the context above when relevant."""
2167
+
2168
+ # Create new session with enhanced system prompt
2169
+ if self.llm_manager:
2170
+ # Create new session with custom system prompt
2171
+ from abstractcore import BasicSession
2172
+
2173
+ # Prepare tools list (same as in LLMManager)
2174
+ tools = []
2175
+ try:
2176
+ from abstractcore.tools.common_tools import (
2177
+ list_files, search_files, read_file, edit_file,
2178
+ write_file, execute_command, web_search
2179
+ )
2180
+ tools = [
2181
+ list_files, search_files, read_file, edit_file,
2182
+ write_file, execute_command, web_search
2183
+ ]
2184
+ except ImportError:
2185
+ pass
2186
+
2187
+ # Create new session with summary in system prompt
2188
+ new_session = BasicSession(
2189
+ self.llm_manager.llm,
2190
+ system_prompt=enhanced_system_prompt,
2191
+ tools=tools
2192
+ )
2193
+
2194
+ # Add recent messages to the new session
2195
+ for msg in recent_messages:
2196
+ if msg.get('type') == 'user':
2197
+ # Add user message without generating response
2198
+ from abstractcore.messages import UserMessage
2199
+ user_msg = UserMessage(content=msg.get('content', ''))
2200
+ new_session.messages.append(user_msg)
2201
+ elif msg.get('type') == 'assistant':
2202
+ # Add assistant message
2203
+ from abstractcore.messages import AssistantMessage
2204
+ assistant_msg = AssistantMessage(content=msg.get('content', ''))
2205
+ new_session.messages.append(assistant_msg)
2206
+
2207
+ # Replace current session
2208
+ self.llm_manager.current_session = new_session
2209
+
2210
+ # Update local message history to reflect the compacted state
2211
+ # Create a special "system" message to represent the summary
2212
+ compacted_history = [
2213
+ {
2214
+ 'timestamp': datetime.now().isoformat(),
2215
+ 'type': 'system',
2216
+ 'content': f"📋 **Session Compacted**\n\n{summary}",
2217
+ 'provider': self.current_provider,
2218
+ 'model': self.current_model,
2219
+ 'attached_files': []
2220
+ }
2221
+ ]
2222
+
2223
+ # Add recent messages
2224
+ compacted_history.extend(recent_messages)
2225
+
2226
+ # Update message history
2227
+ self.message_history = compacted_history
2228
+
2229
+ if self.debug:
2230
+ print(f"✅ Created compacted session with enhanced system prompt")
2231
+
2232
+ except Exception as e:
2233
+ if self.debug:
2234
+ print(f"❌ Error creating compacted session: {e}")
2235
+ raise
2236
+
1974
2237
  def load_session(self):
1975
2238
  """Load a session using AbstractCore via LLMManager."""
1976
2239
  file_path, _ = QFileDialog.getOpenFileName(
@@ -2192,8 +2455,12 @@ class QtChatBubble(QWidget):
2192
2455
  # Toggle behavior: create dialog if doesn't exist, toggle visibility if it does
2193
2456
  if iPhoneMessagesDialog:
2194
2457
  if self.history_dialog is None:
2195
- # Create dialog first time
2196
- self.history_dialog = iPhoneMessagesDialog.create_dialog(self.message_history, self)
2458
+ # Create dialog first time with deletion support
2459
+ self.history_dialog = iPhoneMessagesDialog.create_dialog(
2460
+ self.message_history,
2461
+ self,
2462
+ delete_callback=self._handle_message_deletion
2463
+ )
2197
2464
  # Set callback to update button when dialog is hidden via Back button
2198
2465
  self.history_dialog.set_hide_callback(lambda: self._update_history_button_appearance(False))
2199
2466
  self.history_dialog.show()
@@ -2205,7 +2472,11 @@ class QtChatBubble(QWidget):
2205
2472
  self._update_history_button_appearance(False)
2206
2473
  else:
2207
2474
  # Update dialog with latest messages before showing
2208
- self.history_dialog = iPhoneMessagesDialog.create_dialog(self.message_history, self)
2475
+ self.history_dialog = iPhoneMessagesDialog.create_dialog(
2476
+ self.message_history,
2477
+ self,
2478
+ delete_callback=self._handle_message_deletion
2479
+ )
2209
2480
  # Set callback to update button when dialog is hidden via Back button
2210
2481
  self.history_dialog.set_hide_callback(lambda: self._update_history_button_appearance(False))
2211
2482
  self.history_dialog.show()
@@ -2256,6 +2527,159 @@ class QtChatBubble(QWidget):
2256
2527
  }
2257
2528
  """)
2258
2529
 
2530
+ def _handle_message_deletion(self, indices_to_delete: List[int]):
2531
+ """Handle deletion of messages from the history dialog."""
2532
+ try:
2533
+ if not indices_to_delete:
2534
+ return
2535
+
2536
+ # Validate indices
2537
+ for index in indices_to_delete:
2538
+ if not (0 <= index < len(self.message_history)):
2539
+ QMessageBox.critical(
2540
+ self,
2541
+ "Invalid Selection",
2542
+ f"Invalid message index {index}. Please refresh and try again."
2543
+ )
2544
+ return
2545
+
2546
+ # Delete messages from local history (indices are sorted in reverse order)
2547
+ original_count = len(self.message_history)
2548
+
2549
+ for index in indices_to_delete:
2550
+ if 0 <= index < len(self.message_history):
2551
+ del self.message_history[index]
2552
+
2553
+ # Update AbstractCore session to reflect deletions
2554
+ self._update_abstractcore_session_after_deletion()
2555
+
2556
+ # Update token count
2557
+ self._update_token_count_from_session()
2558
+
2559
+ # Update history dialog if it's open (keep it open!)
2560
+ if self.history_dialog and self.history_dialog.isVisible():
2561
+ try:
2562
+ # Update the dialog content without closing it
2563
+ self.history_dialog.update_message_history(self.message_history)
2564
+ except Exception as dialog_error:
2565
+ import traceback
2566
+ traceback.print_exc()
2567
+ # Fallback: recreate dialog if update fails
2568
+ try:
2569
+ if len(self.message_history) == 0:
2570
+ self.history_dialog.hide()
2571
+ self._update_history_button_appearance(False)
2572
+ else:
2573
+ new_dialog = iPhoneMessagesDialog.create_dialog(
2574
+ self.message_history,
2575
+ self,
2576
+ delete_callback=self._handle_message_deletion
2577
+ )
2578
+ if new_dialog:
2579
+ old_pos = self.history_dialog.pos()
2580
+ self.history_dialog.hide()
2581
+ self.history_dialog = new_dialog
2582
+ self.history_dialog.move(old_pos) # Keep same position
2583
+ self.history_dialog.set_hide_callback(lambda: self._update_history_button_appearance(False))
2584
+ self.history_dialog.show()
2585
+ except:
2586
+ try:
2587
+ self.history_dialog.hide()
2588
+ self._update_history_button_appearance(False)
2589
+ except:
2590
+ pass
2591
+
2592
+ # Log success (no popup)
2593
+ deleted_count = original_count - len(self.message_history)
2594
+
2595
+ if self.debug:
2596
+ print(f"🗑️ Deleted {deleted_count} messages from history")
2597
+
2598
+ except Exception as e:
2599
+ print(f"❌ Critical error in _handle_message_deletion: {e}")
2600
+ import traceback
2601
+ traceback.print_exc()
2602
+
2603
+ try:
2604
+ QMessageBox.critical(
2605
+ self,
2606
+ "Deletion Error",
2607
+ f"Failed to delete messages:\n{str(e)}\n\nCheck console for details."
2608
+ )
2609
+ except:
2610
+ print("❌ Could not show error dialog")
2611
+
2612
+ if self.debug:
2613
+ print(f"❌ Failed to delete messages: {e}")
2614
+ import traceback
2615
+ traceback.print_exc()
2616
+
2617
+ def _update_abstractcore_session_after_deletion(self):
2618
+ """Update AbstractCore session to reflect message deletions."""
2619
+ try:
2620
+ if not self.llm_manager or not self.llm_manager.current_session:
2621
+ return
2622
+
2623
+ # Get current system prompt
2624
+ current_session = self.llm_manager.current_session
2625
+ system_prompt = getattr(current_session, 'system_prompt', None) or """
2626
+ You are a helpful AI assistant who has access to tools to help the user.
2627
+ Always be a critical and creative thinker who leverage constructive skepticism to progress and evolve its reasoning and answers.
2628
+ Always answer in nicely formatted markdown.
2629
+ """
2630
+
2631
+ # Prepare tools list (same as in LLMManager)
2632
+ tools = []
2633
+ try:
2634
+ from abstractcore.tools.common_tools import (
2635
+ list_files, search_files, read_file, edit_file,
2636
+ write_file, execute_command, web_search
2637
+ )
2638
+ tools = [
2639
+ list_files, search_files, read_file, edit_file,
2640
+ write_file, execute_command, web_search
2641
+ ]
2642
+ except ImportError as import_error:
2643
+ pass
2644
+ pass
2645
+
2646
+ # Create new session with updated message history
2647
+ from abstractcore import BasicSession
2648
+ new_session = BasicSession(
2649
+ self.llm_manager.llm,
2650
+ system_prompt=system_prompt,
2651
+ tools=tools
2652
+ )
2653
+
2654
+ # Add remaining messages to the new session
2655
+ for i, msg in enumerate(self.message_history):
2656
+ try:
2657
+ if msg.get('type') == 'user':
2658
+ from abstractcore.messages import UserMessage
2659
+ user_msg = UserMessage(content=msg.get('content', ''))
2660
+ new_session.messages.append(user_msg)
2661
+ elif msg.get('type') == 'assistant':
2662
+ from abstractcore.messages import AssistantMessage
2663
+ assistant_msg = AssistantMessage(content=msg.get('content', ''))
2664
+ new_session.messages.append(assistant_msg)
2665
+ elif msg.get('type') == 'system':
2666
+ # Skip system messages (handled by system_prompt)
2667
+ pass
2668
+ else:
2669
+ # Unknown message type
2670
+ pass
2671
+ except Exception as msg_error:
2672
+ # Continue with other messages
2673
+ pass
2674
+
2675
+ # Replace current session
2676
+ self.llm_manager.current_session = new_session
2677
+
2678
+ except Exception as e:
2679
+ import traceback
2680
+ traceback.print_exc()
2681
+ # Don't raise - this is not critical for the UI operation
2682
+
2259
2683
  def close_app(self):
2260
2684
  """Close the entire application completely."""
2261
2685
  if self.debug:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractassistant
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: A sleek (macOS) system tray application providing instant access to LLMs
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
6
  License-Expression: MIT
@@ -9,9 +9,9 @@ abstractassistant/core/llm_manager.py,sha256=hJun-nDfRv9zxv_3tfrHAmVYSYT96E-0zDJ
9
9
  abstractassistant/core/tts_manager.py,sha256=Cxh302EgIycwkWxe7XntmLW-j_WusbJOYRCs3Jms3CU,9892
10
10
  abstractassistant/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
11
11
  abstractassistant/ui/chat_bubble.py,sha256=bY48b4IeQzOrRN2_sJ5OazhZcJ8IMaBM6R3EexvU30Q,11885
12
- abstractassistant/ui/history_dialog.py,sha256=949l8fVgRpUfRGcoOVRutERVXrShYgpKkFJCxo2JG-4,19323
12
+ abstractassistant/ui/history_dialog.py,sha256=lVyNrZVu73CZo593DvnuWU1iCpmZybTCFTjlu4RrqBM,39701
13
13
  abstractassistant/ui/provider_manager.py,sha256=9IM-BxIs6lUlk6cDCBi7oZFMXmn4CFMlxh0s-_vhzXY,8403
14
- abstractassistant/ui/qt_bubble.py,sha256=ujJtoDuVuXthxfXsvHX-YbqV4GuUgNp2FGy_wT3t4UY,96990
14
+ abstractassistant/ui/qt_bubble.py,sha256=kCgj1zqWKxxvVFAroz8NDh1GtcobyhatlrcXT9PhYOI,115598
15
15
  abstractassistant/ui/toast_manager.py,sha256=1aU4DPo-J45bC61gTEctHq98ZrHIFxRfZa_9Q8KF588,13721
16
16
  abstractassistant/ui/toast_window.py,sha256=BRSwEBlaND5LLipn1HOX0ISWxVH-zOHsYplFkiPaj_g,21727
17
17
  abstractassistant/ui/tts_state_manager.py,sha256=UF_zrfl9wf0hNHBGxevcoKxW5Dh7zXibUSVoSSjGP4o,10565
@@ -19,9 +19,9 @@ abstractassistant/ui/ui_styles.py,sha256=FvE2CVUbHmHu1PKVTBBGyhbt781qh4WjLMrHvil
19
19
  abstractassistant/utils/__init__.py,sha256=7Q3BxyXETkt3tm5trhuLTyL8PoECOK0QiK-0KUVAR2Q,16
20
20
  abstractassistant/utils/icon_generator.py,sha256=SWPgi1V6_8544Zbc2vAfFXAy15H35neyUGCYt2eKoic,16475
21
21
  abstractassistant/utils/markdown_renderer.py,sha256=u5tVIhulSwRYADiqJcZNoHhU8e6pJVgzrwZRd61Bov0,12585
22
- abstractassistant-0.3.3.dist-info/licenses/LICENSE,sha256=QUjFNAE-0yOkW9-Rle2axkpkt9H7xiZ2VbN-VeONhxc,1106
23
- abstractassistant-0.3.3.dist-info/METADATA,sha256=gHXdK078vCVh3vMAdjAxEd32JPShNpV3DCkNWLlUjZ0,11564
24
- abstractassistant-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- abstractassistant-0.3.3.dist-info/entry_points.txt,sha256=MIzeCh0XG6MbhIzBHtkdEjmjxYBsQrGFevq8Y1L8Jkc,118
26
- abstractassistant-0.3.3.dist-info/top_level.txt,sha256=qZc_LQH3CBxLq2P4B1aHayzkj8hn0euR31edkXQVzDA,18
27
- abstractassistant-0.3.3.dist-info/RECORD,,
22
+ abstractassistant-0.3.4.dist-info/licenses/LICENSE,sha256=QUjFNAE-0yOkW9-Rle2axkpkt9H7xiZ2VbN-VeONhxc,1106
23
+ abstractassistant-0.3.4.dist-info/METADATA,sha256=CXoP8mvao4NdI9qmyght-lTppQvuX3IgveQT7Ku0DVE,11564
24
+ abstractassistant-0.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ abstractassistant-0.3.4.dist-info/entry_points.txt,sha256=MIzeCh0XG6MbhIzBHtkdEjmjxYBsQrGFevq8Y1L8Jkc,118
26
+ abstractassistant-0.3.4.dist-info/top_level.txt,sha256=qZc_LQH3CBxLq2P4B1aHayzkj8hn0euR31edkXQVzDA,18
27
+ abstractassistant-0.3.4.dist-info/RECORD,,