abstractassistant 0.3.5__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.
@@ -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 (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
20
- QWidget, QLabel, QFrame, QPushButton, QApplication)
21
- from PyQt6.QtCore import Qt, QTimer, pyqtSignal
22
- from PyQt6.QtGui import QFont, QCursor
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 (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
26
- QWidget, QLabel, QFrame, QPushButton, QApplication)
27
- from PyQt5.QtCore import Qt, QTimer, pyqtSignal
28
- from PyQt5.QtGui import QFont, QCursor
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 (QDialog, QVBoxLayout, QHBoxLayout, QScrollArea,
31
- QWidget, QLabel, QFrame, QPushButton, QApplication)
32
- from PySide2.QtCore import Qt, QTimer, Signal as pyqtSignal
33
- from PySide2.QtGui import QFont, QCursor
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: 400px;
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: 400px;
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: 400px;
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: 400px;
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: 400px;
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(504, 650) # Increased width by 20% (420 * 1.2 = 504)
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(94) # iPhone status bar + nav bar
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(20, 0, 20, 0)
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: 17px;
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: 17px;
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(30, 30)
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: 18px;
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: 15px;
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: 17px;
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: 400px;
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: 400px;
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
+ """