abstractassistant 0.3.4__py3-none-any.whl → 0.4.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 +69 -6
- abstractassistant/cli.py +104 -85
- abstractassistant/core/agent_host.py +583 -0
- abstractassistant/core/llm_manager.py +338 -431
- abstractassistant/core/session_index.py +293 -0
- abstractassistant/core/session_store.py +79 -0
- abstractassistant/core/tool_policy.py +58 -0
- abstractassistant/core/transcript_summary.py +434 -0
- abstractassistant/ui/history_dialog.py +504 -29
- abstractassistant/ui/provider_manager.py +2 -2
- abstractassistant/ui/qt_bubble.py +2289 -489
- abstractassistant-0.4.0.dist-info/METADATA +168 -0
- abstractassistant-0.4.0.dist-info/RECORD +32 -0
- {abstractassistant-0.3.4.dist-info → abstractassistant-0.4.0.dist-info}/WHEEL +1 -1
- {abstractassistant-0.3.4.dist-info → abstractassistant-0.4.0.dist-info}/entry_points.txt +1 -0
- abstractassistant-0.3.4.dist-info/METADATA +0 -297
- abstractassistant-0.3.4.dist-info/RECORD +0 -27
- {abstractassistant-0.3.4.dist-info → abstractassistant-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {abstractassistant-0.3.4.dist-info → abstractassistant-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -16,21 +16,51 @@ from pygments.lexers import get_lexer_by_name, TextLexer
|
|
|
16
16
|
from pygments.formatters import HtmlFormatter
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
|
-
from PyQt6.QtWidgets import (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
from PyQt6.QtWidgets import (
|
|
20
|
+
QDialog,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QHBoxLayout,
|
|
23
|
+
QGridLayout,
|
|
24
|
+
QScrollArea,
|
|
25
|
+
QWidget,
|
|
26
|
+
QLabel,
|
|
27
|
+
QFrame,
|
|
28
|
+
QPushButton,
|
|
29
|
+
QApplication,
|
|
30
|
+
)
|
|
31
|
+
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize
|
|
32
|
+
from PyQt6.QtGui import QFont, QCursor, QPixmap, QIcon
|
|
23
33
|
except ImportError:
|
|
24
34
|
try:
|
|
25
|
-
from PyQt5.QtWidgets import (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
from PyQt5.QtWidgets import (
|
|
36
|
+
QDialog,
|
|
37
|
+
QVBoxLayout,
|
|
38
|
+
QHBoxLayout,
|
|
39
|
+
QGridLayout,
|
|
40
|
+
QScrollArea,
|
|
41
|
+
QWidget,
|
|
42
|
+
QLabel,
|
|
43
|
+
QFrame,
|
|
44
|
+
QPushButton,
|
|
45
|
+
QApplication,
|
|
46
|
+
)
|
|
47
|
+
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
|
|
48
|
+
from PyQt5.QtGui import QFont, QCursor, QPixmap, QIcon
|
|
29
49
|
except ImportError:
|
|
30
|
-
from PySide2.QtWidgets import (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
from PySide2.QtWidgets import (
|
|
51
|
+
QDialog,
|
|
52
|
+
QVBoxLayout,
|
|
53
|
+
QHBoxLayout,
|
|
54
|
+
QGridLayout,
|
|
55
|
+
QScrollArea,
|
|
56
|
+
QWidget,
|
|
57
|
+
QLabel,
|
|
58
|
+
QFrame,
|
|
59
|
+
QPushButton,
|
|
60
|
+
QApplication,
|
|
61
|
+
)
|
|
62
|
+
from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal, QSize
|
|
63
|
+
from PySide2.QtGui import QFont, QCursor, QPixmap, QIcon
|
|
34
64
|
|
|
35
65
|
|
|
36
66
|
class ClickableBubble(QFrame):
|
|
@@ -81,7 +111,7 @@ class ClickableBubble(QFrame):
|
|
|
81
111
|
background: {self.clicked_bg};
|
|
82
112
|
border: none;
|
|
83
113
|
border-radius: 18px;
|
|
84
|
-
max-width:
|
|
114
|
+
max-width: 490px;
|
|
85
115
|
}}
|
|
86
116
|
""")
|
|
87
117
|
super().mousePressEvent(event)
|
|
@@ -109,7 +139,7 @@ class ClickableBubble(QFrame):
|
|
|
109
139
|
background: {glossy_color};
|
|
110
140
|
border: none;
|
|
111
141
|
border-radius: 18px;
|
|
112
|
-
max-width:
|
|
142
|
+
max-width: 490px;
|
|
113
143
|
}}
|
|
114
144
|
""")
|
|
115
145
|
|
|
@@ -135,7 +165,7 @@ class ClickableBubble(QFrame):
|
|
|
135
165
|
background: {self.selected_bg};
|
|
136
166
|
border: 2px solid #FFFFFF;
|
|
137
167
|
border-radius: 18px;
|
|
138
|
-
max-width:
|
|
168
|
+
max-width: 490px;
|
|
139
169
|
}}
|
|
140
170
|
""")
|
|
141
171
|
|
|
@@ -218,7 +248,7 @@ class ClickableBubble(QFrame):
|
|
|
218
248
|
background: {self.normal_bg};
|
|
219
249
|
border: none;
|
|
220
250
|
border-radius: 18px;
|
|
221
|
-
max-width:
|
|
251
|
+
max-width: 490px;
|
|
222
252
|
}}
|
|
223
253
|
""")
|
|
224
254
|
|
|
@@ -229,7 +259,7 @@ class ClickableBubble(QFrame):
|
|
|
229
259
|
background: {self.normal_bg};
|
|
230
260
|
border: none;
|
|
231
261
|
border-radius: 18px;
|
|
232
|
-
max-width:
|
|
262
|
+
max-width: 490px;
|
|
233
263
|
}}
|
|
234
264
|
""")
|
|
235
265
|
|
|
@@ -584,7 +614,11 @@ class iPhoneMessagesDialog:
|
|
|
584
614
|
dialog.setWindowTitle("Messages")
|
|
585
615
|
dialog.setModal(False)
|
|
586
616
|
dialog.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
587
|
-
dialog.resize(
|
|
617
|
+
dialog.resize(617, 553) # ~15% smaller than the previous 726x650
|
|
618
|
+
try:
|
|
619
|
+
dialog.setWindowOpacity(0.97)
|
|
620
|
+
except Exception:
|
|
621
|
+
pass
|
|
588
622
|
|
|
589
623
|
# Set delete callback
|
|
590
624
|
if delete_callback:
|
|
@@ -663,7 +697,7 @@ class iPhoneMessagesDialog:
|
|
|
663
697
|
def _create_authentic_navbar(dialog: SafeDialog) -> QFrame:
|
|
664
698
|
"""Create AUTHENTIC iPhone Messages navigation bar with delete functionality."""
|
|
665
699
|
navbar = QFrame()
|
|
666
|
-
navbar.setFixedHeight(
|
|
700
|
+
navbar.setFixedHeight(44) # Compact header (about half the previous height)
|
|
667
701
|
navbar.setStyleSheet("""
|
|
668
702
|
QFrame {
|
|
669
703
|
background: #1c1c1e;
|
|
@@ -684,7 +718,7 @@ class iPhoneMessagesDialog:
|
|
|
684
718
|
nav_frame = QFrame()
|
|
685
719
|
nav_frame.setFixedHeight(44)
|
|
686
720
|
nav_layout = QHBoxLayout(nav_frame)
|
|
687
|
-
nav_layout.setContentsMargins(
|
|
721
|
+
nav_layout.setContentsMargins(16, 0, 16, 0)
|
|
688
722
|
|
|
689
723
|
# Back button
|
|
690
724
|
back_btn = QPushButton("‹ Back")
|
|
@@ -692,7 +726,7 @@ class iPhoneMessagesDialog:
|
|
|
692
726
|
back_btn.setStyleSheet("""
|
|
693
727
|
QPushButton {
|
|
694
728
|
color: #007AFF;
|
|
695
|
-
font-size:
|
|
729
|
+
font-size: 13px;
|
|
696
730
|
font-weight: 400;
|
|
697
731
|
background: transparent;
|
|
698
732
|
border: none;
|
|
@@ -709,7 +743,7 @@ class iPhoneMessagesDialog:
|
|
|
709
743
|
title.setStyleSheet("""
|
|
710
744
|
QLabel {
|
|
711
745
|
color: #ffffff;
|
|
712
|
-
font-size:
|
|
746
|
+
font-size: 13px;
|
|
713
747
|
font-weight: 600;
|
|
714
748
|
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
715
749
|
}
|
|
@@ -720,12 +754,12 @@ class iPhoneMessagesDialog:
|
|
|
720
754
|
|
|
721
755
|
# Trash icon (initially hidden, appears when messages are selected)
|
|
722
756
|
trash_btn = QPushButton("🗑️")
|
|
723
|
-
trash_btn.setFixedSize(
|
|
757
|
+
trash_btn.setFixedSize(24, 24)
|
|
724
758
|
trash_btn.clicked.connect(dialog.delete_selected_messages)
|
|
725
759
|
trash_btn.setStyleSheet("""
|
|
726
760
|
QPushButton {
|
|
727
761
|
color: #FF3B30;
|
|
728
|
-
font-size:
|
|
762
|
+
font-size: 14px;
|
|
729
763
|
background: transparent;
|
|
730
764
|
border: none;
|
|
731
765
|
text-align: center;
|
|
@@ -733,7 +767,7 @@ class iPhoneMessagesDialog:
|
|
|
733
767
|
}
|
|
734
768
|
QPushButton:hover {
|
|
735
769
|
background: rgba(255, 59, 48, 0.1);
|
|
736
|
-
border-radius:
|
|
770
|
+
border-radius: 12px;
|
|
737
771
|
}
|
|
738
772
|
""")
|
|
739
773
|
trash_btn.hide() # Initially hidden
|
|
@@ -746,7 +780,7 @@ class iPhoneMessagesDialog:
|
|
|
746
780
|
edit_btn.setStyleSheet("""
|
|
747
781
|
QPushButton {
|
|
748
782
|
color: #007AFF;
|
|
749
|
-
font-size:
|
|
783
|
+
font-size: 13px;
|
|
750
784
|
font-weight: 400;
|
|
751
785
|
background: transparent;
|
|
752
786
|
border: none;
|
|
@@ -838,7 +872,7 @@ class iPhoneMessagesDialog:
|
|
|
838
872
|
background: #007AFF;
|
|
839
873
|
border: none;
|
|
840
874
|
border-radius: 18px;
|
|
841
|
-
max-width:
|
|
875
|
+
max-width: 490px;
|
|
842
876
|
}
|
|
843
877
|
""")
|
|
844
878
|
content_label.setStyleSheet("""
|
|
@@ -863,7 +897,7 @@ class iPhoneMessagesDialog:
|
|
|
863
897
|
background: #3a3a3c;
|
|
864
898
|
border: none;
|
|
865
899
|
border-radius: 18px;
|
|
866
|
-
max-width:
|
|
900
|
+
max-width: 490px;
|
|
867
901
|
}
|
|
868
902
|
""")
|
|
869
903
|
content_label.setStyleSheet("""
|
|
@@ -900,6 +934,181 @@ class iPhoneMessagesDialog:
|
|
|
900
934
|
}
|
|
901
935
|
""")
|
|
902
936
|
bubble_layout.addWidget(file_indicator)
|
|
937
|
+
|
|
938
|
+
# Images: render thumbnails under the message when present.
|
|
939
|
+
image_thumbnails = msg.get("image_thumbnails")
|
|
940
|
+
if not isinstance(image_thumbnails, list):
|
|
941
|
+
image_thumbnails = []
|
|
942
|
+
thumbs: List[Dict] = [dict(x) for x in image_thumbnails if isinstance(x, dict)]
|
|
943
|
+
thumb_targets = {
|
|
944
|
+
str(x.get("target") or "").strip()
|
|
945
|
+
for x in thumbs
|
|
946
|
+
if isinstance(x, dict) and str(x.get("target") or "").strip()
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
# Add a compact tool execution summary (attached to assistant messages).
|
|
950
|
+
tool_summary = msg.get("tool_summary")
|
|
951
|
+
tool_links = msg.get("tool_links")
|
|
952
|
+
if not isinstance(tool_links, list):
|
|
953
|
+
tool_links = []
|
|
954
|
+
# Don't duplicate image resources as both chips and thumbnails.
|
|
955
|
+
if thumb_targets:
|
|
956
|
+
tool_links = [
|
|
957
|
+
link for link in tool_links
|
|
958
|
+
if isinstance(link, dict) and str(link.get("target") or "").strip() not in thumb_targets
|
|
959
|
+
]
|
|
960
|
+
tool_summary_text = str(tool_summary or "").strip()
|
|
961
|
+
if tool_summary_text or tool_links:
|
|
962
|
+
tools_container = QFrame()
|
|
963
|
+
tools_container.setStyleSheet("QFrame { background: transparent; border: none; }")
|
|
964
|
+
tools_layout = QVBoxLayout(tools_container)
|
|
965
|
+
tools_layout.setContentsMargins(0, 2, 0, 0)
|
|
966
|
+
tools_layout.setSpacing(4)
|
|
967
|
+
|
|
968
|
+
if tool_summary_text:
|
|
969
|
+
tools_label = QLabel(tool_summary_text)
|
|
970
|
+
tools_label.setWordWrap(True)
|
|
971
|
+
tools_label.setStyleSheet("""
|
|
972
|
+
QLabel {
|
|
973
|
+
background: transparent;
|
|
974
|
+
color: rgba(255, 255, 255, 0.65);
|
|
975
|
+
font-size: 11px;
|
|
976
|
+
font-weight: 500;
|
|
977
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
978
|
+
padding: 0px;
|
|
979
|
+
margin: 0px;
|
|
980
|
+
}
|
|
981
|
+
""")
|
|
982
|
+
tools_layout.addWidget(tools_label)
|
|
983
|
+
|
|
984
|
+
if tool_links:
|
|
985
|
+
max_chips = 6
|
|
986
|
+
chips_per_row = 3
|
|
987
|
+
shown = tool_links[:max_chips]
|
|
988
|
+
extra = max(0, len(tool_links) - len(shown))
|
|
989
|
+
|
|
990
|
+
links_frame = QFrame()
|
|
991
|
+
links_frame.setStyleSheet("QFrame { background: transparent; border: none; }")
|
|
992
|
+
links_layout = QGridLayout(links_frame)
|
|
993
|
+
links_layout.setContentsMargins(0, 0, 0, 0)
|
|
994
|
+
links_layout.setHorizontalSpacing(6)
|
|
995
|
+
links_layout.setVerticalSpacing(6)
|
|
996
|
+
|
|
997
|
+
def _mk_chip(link: Dict) -> QPushButton:
|
|
998
|
+
kind = str(link.get("kind") or "url")
|
|
999
|
+
target = str(link.get("target") or "").strip()
|
|
1000
|
+
label = str(link.get("label") or target).strip() or target
|
|
1001
|
+
icon = "🌐" if kind == "url" else "📄"
|
|
1002
|
+
btn = QPushButton(f"{icon} {label}")
|
|
1003
|
+
btn.setToolTip(target)
|
|
1004
|
+
btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
1005
|
+
btn.setStyleSheet("""
|
|
1006
|
+
QPushButton {
|
|
1007
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1008
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
1009
|
+
border-radius: 10px;
|
|
1010
|
+
padding: 2px 8px;
|
|
1011
|
+
font-size: 11px;
|
|
1012
|
+
color: rgba(255, 255, 255, 0.85);
|
|
1013
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1014
|
+
text-align: left;
|
|
1015
|
+
}
|
|
1016
|
+
QPushButton:hover {
|
|
1017
|
+
background: rgba(255, 255, 255, 0.12);
|
|
1018
|
+
border: 1px solid rgba(0, 122, 255, 0.6);
|
|
1019
|
+
color: rgba(255, 255, 255, 0.95);
|
|
1020
|
+
}
|
|
1021
|
+
""")
|
|
1022
|
+
btn.clicked.connect(
|
|
1023
|
+
lambda _checked=False, k=kind, t=target: iPhoneMessagesDialog._open_tool_link(k, t)
|
|
1024
|
+
)
|
|
1025
|
+
return btn
|
|
1026
|
+
|
|
1027
|
+
for idx, link in enumerate(shown):
|
|
1028
|
+
if not isinstance(link, dict):
|
|
1029
|
+
continue
|
|
1030
|
+
row = idx // chips_per_row
|
|
1031
|
+
col = idx % chips_per_row
|
|
1032
|
+
links_layout.addWidget(_mk_chip(link), row, col)
|
|
1033
|
+
|
|
1034
|
+
if extra > 0:
|
|
1035
|
+
more_btn = QPushButton(f"+{extra}")
|
|
1036
|
+
more_btn.setToolTip("Show all tool links")
|
|
1037
|
+
more_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
1038
|
+
more_btn.setStyleSheet("""
|
|
1039
|
+
QPushButton {
|
|
1040
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1041
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1042
|
+
border-radius: 10px;
|
|
1043
|
+
padding: 2px 8px;
|
|
1044
|
+
font-size: 11px;
|
|
1045
|
+
color: rgba(255, 255, 255, 0.75);
|
|
1046
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1047
|
+
}
|
|
1048
|
+
QPushButton:hover {
|
|
1049
|
+
background: rgba(255, 255, 255, 0.10);
|
|
1050
|
+
border: 1px solid rgba(0, 122, 255, 0.45);
|
|
1051
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1052
|
+
}
|
|
1053
|
+
""")
|
|
1054
|
+
more_btn.clicked.connect(lambda: iPhoneMessagesDialog._show_tool_links_dialog(tool_links, parent=dialog))
|
|
1055
|
+
row = len(shown) // chips_per_row
|
|
1056
|
+
col = len(shown) % chips_per_row
|
|
1057
|
+
links_layout.addWidget(more_btn, row, col)
|
|
1058
|
+
|
|
1059
|
+
tools_layout.addWidget(links_frame)
|
|
1060
|
+
|
|
1061
|
+
bubble_layout.addWidget(tools_container)
|
|
1062
|
+
|
|
1063
|
+
if thumbs:
|
|
1064
|
+
thumb_container = QFrame()
|
|
1065
|
+
thumb_container.setStyleSheet("QFrame { background: transparent; border: none; }")
|
|
1066
|
+
thumb_layout = QHBoxLayout(thumb_container)
|
|
1067
|
+
thumb_layout.setContentsMargins(0, 2, 0, 0)
|
|
1068
|
+
thumb_layout.setSpacing(6)
|
|
1069
|
+
|
|
1070
|
+
max_thumbs = 3
|
|
1071
|
+
shown_thumbs = thumbs[:max_thumbs]
|
|
1072
|
+
for th in shown_thumbs:
|
|
1073
|
+
kind = str(th.get("kind") or "url")
|
|
1074
|
+
target = str(th.get("target") or "").strip()
|
|
1075
|
+
label = str(th.get("label") or target).strip()
|
|
1076
|
+
if not target:
|
|
1077
|
+
continue
|
|
1078
|
+
thumb_layout.addWidget(
|
|
1079
|
+
iPhoneMessagesDialog._make_thumbnail_button(
|
|
1080
|
+
kind=kind,
|
|
1081
|
+
target=target,
|
|
1082
|
+
label=label,
|
|
1083
|
+
parent=dialog,
|
|
1084
|
+
)
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
extra_thumbs = max(0, len(thumbs) - len(shown_thumbs))
|
|
1088
|
+
if extra_thumbs > 0:
|
|
1089
|
+
more_btn = QPushButton(f"+{extra_thumbs}")
|
|
1090
|
+
more_btn.setToolTip("Show all images")
|
|
1091
|
+
more_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
1092
|
+
more_btn.setStyleSheet("""
|
|
1093
|
+
QPushButton {
|
|
1094
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1095
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1096
|
+
border-radius: 12px;
|
|
1097
|
+
padding: 2px 10px;
|
|
1098
|
+
font-size: 12px;
|
|
1099
|
+
color: rgba(255, 255, 255, 0.75);
|
|
1100
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1101
|
+
}
|
|
1102
|
+
QPushButton:hover {
|
|
1103
|
+
background: rgba(255, 255, 255, 0.10);
|
|
1104
|
+
border: 1px solid rgba(0, 122, 255, 0.45);
|
|
1105
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1106
|
+
}
|
|
1107
|
+
""")
|
|
1108
|
+
more_btn.clicked.connect(lambda: iPhoneMessagesDialog._show_tool_links_dialog(thumbs, parent=dialog))
|
|
1109
|
+
thumb_layout.addWidget(more_btn)
|
|
1110
|
+
|
|
1111
|
+
bubble_layout.addWidget(thumb_container)
|
|
903
1112
|
|
|
904
1113
|
main_layout.addWidget(container)
|
|
905
1114
|
|
|
@@ -1006,6 +1215,272 @@ class iPhoneMessagesDialog:
|
|
|
1006
1215
|
|
|
1007
1216
|
return html
|
|
1008
1217
|
|
|
1218
|
+
@staticmethod
|
|
1219
|
+
def _make_thumbnail_button(*, kind: str, target: str, label: str, parent=None) -> QPushButton:
|
|
1220
|
+
"""Create a clickable thumbnail button for a local file or remote image URL."""
|
|
1221
|
+
k = str(kind or "").strip().lower()
|
|
1222
|
+
t = str(target or "").strip()
|
|
1223
|
+
lbl = str(label or "").strip()
|
|
1224
|
+
|
|
1225
|
+
btn = QPushButton("🖼")
|
|
1226
|
+
btn.setToolTip(t or lbl)
|
|
1227
|
+
btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
1228
|
+
thumb_w, thumb_h = 150, 94
|
|
1229
|
+
btn.setFixedSize(thumb_w, thumb_h)
|
|
1230
|
+
btn.setIconSize(QSize(thumb_w - 6, thumb_h - 6))
|
|
1231
|
+
btn.setStyleSheet("""
|
|
1232
|
+
QPushButton {
|
|
1233
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1234
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
1235
|
+
border-radius: 12px;
|
|
1236
|
+
color: rgba(255, 255, 255, 0.75);
|
|
1237
|
+
font-size: 20px;
|
|
1238
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1239
|
+
}
|
|
1240
|
+
QPushButton:hover {
|
|
1241
|
+
background: rgba(255, 255, 255, 0.10);
|
|
1242
|
+
border: 1px solid rgba(0, 122, 255, 0.6);
|
|
1243
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1244
|
+
}
|
|
1245
|
+
""")
|
|
1246
|
+
btn.clicked.connect(lambda _checked=False: iPhoneMessagesDialog._open_tool_link(k, t))
|
|
1247
|
+
|
|
1248
|
+
def _apply_pixmap(pix: QPixmap) -> None:
|
|
1249
|
+
if pix.isNull():
|
|
1250
|
+
return
|
|
1251
|
+
try:
|
|
1252
|
+
try:
|
|
1253
|
+
aspect = Qt.AspectRatioMode.KeepAspectRatio
|
|
1254
|
+
transform = Qt.TransformationMode.SmoothTransformation
|
|
1255
|
+
except Exception:
|
|
1256
|
+
aspect = Qt.KeepAspectRatio # type: ignore[attr-defined]
|
|
1257
|
+
transform = Qt.SmoothTransformation # type: ignore[attr-defined]
|
|
1258
|
+
scaled = pix.scaled(thumb_w - 6, thumb_h - 6, aspect, transform)
|
|
1259
|
+
except Exception:
|
|
1260
|
+
scaled = pix
|
|
1261
|
+
btn.setText("")
|
|
1262
|
+
btn.setIcon(QIcon(scaled))
|
|
1263
|
+
|
|
1264
|
+
# Local file thumbnail
|
|
1265
|
+
if k == "file" and t:
|
|
1266
|
+
try:
|
|
1267
|
+
pix = QPixmap(t)
|
|
1268
|
+
if not pix.isNull():
|
|
1269
|
+
_apply_pixmap(pix)
|
|
1270
|
+
except Exception:
|
|
1271
|
+
pass
|
|
1272
|
+
return btn
|
|
1273
|
+
|
|
1274
|
+
# Remote image thumbnail (download + cache).
|
|
1275
|
+
if k != "url" or not t:
|
|
1276
|
+
return btn
|
|
1277
|
+
|
|
1278
|
+
cache_path = iPhoneMessagesDialog._thumbnail_cache_path(t)
|
|
1279
|
+
try:
|
|
1280
|
+
if cache_path is not None and cache_path.exists():
|
|
1281
|
+
data = cache_path.read_bytes()
|
|
1282
|
+
pix = QPixmap()
|
|
1283
|
+
if data and pix.loadFromData(data):
|
|
1284
|
+
_apply_pixmap(pix)
|
|
1285
|
+
return btn
|
|
1286
|
+
except Exception:
|
|
1287
|
+
pass
|
|
1288
|
+
|
|
1289
|
+
import threading
|
|
1290
|
+
|
|
1291
|
+
def _download() -> None:
|
|
1292
|
+
data = iPhoneMessagesDialog._fetch_image_bytes(t)
|
|
1293
|
+
if not data:
|
|
1294
|
+
return
|
|
1295
|
+
|
|
1296
|
+
def _set_on_ui() -> None:
|
|
1297
|
+
try:
|
|
1298
|
+
pix = QPixmap()
|
|
1299
|
+
if pix.loadFromData(data):
|
|
1300
|
+
_apply_pixmap(pix)
|
|
1301
|
+
except Exception:
|
|
1302
|
+
return
|
|
1303
|
+
|
|
1304
|
+
try:
|
|
1305
|
+
QTimer.singleShot(0, _set_on_ui)
|
|
1306
|
+
except Exception:
|
|
1307
|
+
_set_on_ui()
|
|
1308
|
+
|
|
1309
|
+
threading.Thread(target=_download, daemon=True).start()
|
|
1310
|
+
return btn
|
|
1311
|
+
|
|
1312
|
+
@staticmethod
|
|
1313
|
+
def _thumbnail_cache_path(url: str):
|
|
1314
|
+
"""Return a filesystem cache path for a remote image URL (under ~/.abstractassistant/cache/images/)."""
|
|
1315
|
+
u = str(url or "").strip()
|
|
1316
|
+
if not u:
|
|
1317
|
+
return None
|
|
1318
|
+
try:
|
|
1319
|
+
import hashlib
|
|
1320
|
+
from pathlib import Path
|
|
1321
|
+
from urllib.parse import urlparse
|
|
1322
|
+
|
|
1323
|
+
parsed = urlparse(u)
|
|
1324
|
+
suffix = Path(parsed.path or "").suffix.lower()
|
|
1325
|
+
if suffix not in {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif"}:
|
|
1326
|
+
suffix = ".img"
|
|
1327
|
+
|
|
1328
|
+
h = hashlib.sha256(u.encode("utf-8")).hexdigest()[:32]
|
|
1329
|
+
base = Path.home() / ".abstractassistant" / "cache" / "images"
|
|
1330
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1331
|
+
return base / f"{h}{suffix}"
|
|
1332
|
+
except Exception:
|
|
1333
|
+
return None
|
|
1334
|
+
|
|
1335
|
+
@staticmethod
|
|
1336
|
+
def _fetch_image_bytes(url: str, *, timeout_s: float = 4.0, max_bytes: int = 4_000_000) -> Optional[bytes]:
|
|
1337
|
+
"""Download an image URL (bounded) and cache it; returns bytes or None."""
|
|
1338
|
+
u = str(url or "").strip()
|
|
1339
|
+
if not u:
|
|
1340
|
+
return None
|
|
1341
|
+
|
|
1342
|
+
cache_path = iPhoneMessagesDialog._thumbnail_cache_path(u)
|
|
1343
|
+
if cache_path is not None:
|
|
1344
|
+
try:
|
|
1345
|
+
if cache_path.exists() and cache_path.stat().st_size > 0:
|
|
1346
|
+
return cache_path.read_bytes()
|
|
1347
|
+
except Exception:
|
|
1348
|
+
pass
|
|
1349
|
+
|
|
1350
|
+
try:
|
|
1351
|
+
import urllib.request
|
|
1352
|
+
|
|
1353
|
+
req = urllib.request.Request(
|
|
1354
|
+
u,
|
|
1355
|
+
headers={
|
|
1356
|
+
"User-Agent": "AbstractAssistant/1.0",
|
|
1357
|
+
"Accept": "image/*,*/*;q=0.8",
|
|
1358
|
+
},
|
|
1359
|
+
)
|
|
1360
|
+
with urllib.request.urlopen(req, timeout=float(timeout_s)) as resp:
|
|
1361
|
+
ct = str(resp.headers.get("Content-Type") or "").lower()
|
|
1362
|
+
if ct and not ct.startswith("image/"):
|
|
1363
|
+
return None
|
|
1364
|
+
try:
|
|
1365
|
+
clen = resp.headers.get("Content-Length")
|
|
1366
|
+
if clen is not None and int(clen) > int(max_bytes):
|
|
1367
|
+
return None
|
|
1368
|
+
except Exception:
|
|
1369
|
+
pass
|
|
1370
|
+
data = resp.read(int(max_bytes))
|
|
1371
|
+
except Exception:
|
|
1372
|
+
return None
|
|
1373
|
+
|
|
1374
|
+
if not data:
|
|
1375
|
+
return None
|
|
1376
|
+
|
|
1377
|
+
if cache_path is not None:
|
|
1378
|
+
try:
|
|
1379
|
+
tmp = cache_path.with_suffix(cache_path.suffix + ".tmp")
|
|
1380
|
+
tmp.write_bytes(data)
|
|
1381
|
+
tmp.replace(cache_path)
|
|
1382
|
+
except Exception:
|
|
1383
|
+
pass
|
|
1384
|
+
return data
|
|
1385
|
+
|
|
1386
|
+
@staticmethod
|
|
1387
|
+
def _open_tool_link(kind: str, target: str) -> None:
|
|
1388
|
+
"""Open a tool-produced resource (URL or file path) using the OS default handler."""
|
|
1389
|
+
k = str(kind or "").strip().lower()
|
|
1390
|
+
t = str(target or "").strip()
|
|
1391
|
+
if not t:
|
|
1392
|
+
return
|
|
1393
|
+
|
|
1394
|
+
try:
|
|
1395
|
+
if k == "url":
|
|
1396
|
+
import webbrowser
|
|
1397
|
+
|
|
1398
|
+
webbrowser.open(t)
|
|
1399
|
+
return
|
|
1400
|
+
|
|
1401
|
+
# Default: treat as a file path (best-effort).
|
|
1402
|
+
from urllib.parse import unquote, urlparse
|
|
1403
|
+
|
|
1404
|
+
if t.startswith("file://"):
|
|
1405
|
+
parsed = urlparse(t)
|
|
1406
|
+
t = unquote(parsed.path)
|
|
1407
|
+
|
|
1408
|
+
import os
|
|
1409
|
+
import sys
|
|
1410
|
+
import subprocess
|
|
1411
|
+
|
|
1412
|
+
if sys.platform == "darwin":
|
|
1413
|
+
subprocess.Popen(["open", t])
|
|
1414
|
+
elif sys.platform.startswith("win"):
|
|
1415
|
+
os.startfile(t) # type: ignore[attr-defined]
|
|
1416
|
+
else:
|
|
1417
|
+
subprocess.Popen(["xdg-open", t])
|
|
1418
|
+
except Exception:
|
|
1419
|
+
return
|
|
1420
|
+
|
|
1421
|
+
@staticmethod
|
|
1422
|
+
def _show_tool_links_dialog(tool_links: List[Dict], parent=None) -> None:
|
|
1423
|
+
"""Show a small dialog listing all tool links for quick access."""
|
|
1424
|
+
links: List[Dict] = [dict(x) for x in (tool_links or []) if isinstance(x, dict)]
|
|
1425
|
+
if not links:
|
|
1426
|
+
return
|
|
1427
|
+
|
|
1428
|
+
dlg = QDialog(parent)
|
|
1429
|
+
dlg.setWindowTitle("Tool Links")
|
|
1430
|
+
dlg.setModal(True)
|
|
1431
|
+
dlg.resize(560, 320)
|
|
1432
|
+
|
|
1433
|
+
layout = QVBoxLayout(dlg)
|
|
1434
|
+
layout.setContentsMargins(12, 12, 12, 12)
|
|
1435
|
+
layout.setSpacing(8)
|
|
1436
|
+
|
|
1437
|
+
for link in links:
|
|
1438
|
+
kind = str(link.get("kind") or "url")
|
|
1439
|
+
target = str(link.get("target") or "").strip()
|
|
1440
|
+
label = str(link.get("label") or target).strip() or target
|
|
1441
|
+
icon = "🌐" if kind == "url" else "📄"
|
|
1442
|
+
btn = QPushButton(f"{icon} {label}")
|
|
1443
|
+
btn.setToolTip(target)
|
|
1444
|
+
btn.setStyleSheet("""
|
|
1445
|
+
QPushButton {
|
|
1446
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1447
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
1448
|
+
border-radius: 10px;
|
|
1449
|
+
padding: 6px 10px;
|
|
1450
|
+
font-size: 12px;
|
|
1451
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1452
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1453
|
+
text-align: left;
|
|
1454
|
+
}
|
|
1455
|
+
QPushButton:hover {
|
|
1456
|
+
background: rgba(255, 255, 255, 0.12);
|
|
1457
|
+
border: 1px solid rgba(0, 122, 255, 0.6);
|
|
1458
|
+
}
|
|
1459
|
+
""")
|
|
1460
|
+
btn.clicked.connect(lambda _checked=False, k=kind, t=target: iPhoneMessagesDialog._open_tool_link(k, t))
|
|
1461
|
+
layout.addWidget(btn)
|
|
1462
|
+
|
|
1463
|
+
close_btn = QPushButton("Close")
|
|
1464
|
+
close_btn.clicked.connect(dlg.accept)
|
|
1465
|
+
close_btn.setStyleSheet("""
|
|
1466
|
+
QPushButton {
|
|
1467
|
+
background: rgba(255, 255, 255, 0.10);
|
|
1468
|
+
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
1469
|
+
border-radius: 12px;
|
|
1470
|
+
padding: 8px 12px;
|
|
1471
|
+
font-size: 12px;
|
|
1472
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1473
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
1474
|
+
}
|
|
1475
|
+
QPushButton:hover {
|
|
1476
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1477
|
+
border: 1px solid rgba(0, 122, 255, 0.6);
|
|
1478
|
+
}
|
|
1479
|
+
""")
|
|
1480
|
+
layout.addWidget(close_btn)
|
|
1481
|
+
|
|
1482
|
+
dlg.exec()
|
|
1483
|
+
|
|
1009
1484
|
@staticmethod
|
|
1010
1485
|
def _get_authentic_iphone_styles() -> str:
|
|
1011
1486
|
"""Get AUTHENTIC iPhone Messages styles - dark background like real iPhone."""
|
|
@@ -1023,4 +1498,4 @@ class iPhoneMessagesDialog:
|
|
|
1023
1498
|
QWidget {
|
|
1024
1499
|
background: transparent;
|
|
1025
1500
|
}
|
|
1026
|
-
"""
|
|
1501
|
+
"""
|
|
@@ -186,8 +186,8 @@ class ProviderManager:
|
|
|
186
186
|
Returns:
|
|
187
187
|
Formatted display name
|
|
188
188
|
"""
|
|
189
|
-
#
|
|
190
|
-
display_name = model
|
|
189
|
+
# Use the full model name (preserving provider prefix)
|
|
190
|
+
display_name = model
|
|
191
191
|
|
|
192
192
|
# Truncate if too long
|
|
193
193
|
if len(display_name) > max_length:
|