pinokiod 3.133.0 → 3.135.0

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.
@@ -47,7 +47,6 @@ class Backend {
47
47
  let authPath = this.kernel.path(`connect/${this.name}/auth.json`)
48
48
  this.auth = (await this.kernel.loader.load(authPath)).resolved
49
49
  if (!this.auth) {
50
- console.log("not authenticated")
51
50
  return null
52
51
  }
53
52
  if (!this.auth.refresh_token) {
@@ -38,7 +38,6 @@ class Huggingface {
38
38
  let authPath = this.kernel.path('connect/huggingface.json')
39
39
  this.auth = (await this.kernel.loader.load(authPath)).resolved
40
40
  if (!this.auth) {
41
- console.log("not authenticated")
42
41
  return null
43
42
  }
44
43
  if (!this.auth.refresh_token) {
package/kernel/index.js CHANGED
@@ -488,7 +488,7 @@ class Kernel {
488
488
  let changed
489
489
  let new_config = JSON.stringify(await this.peer.current_host())
490
490
  if (this.old_config !== new_config) {
491
- console.log("Proc config has changed. update router.")
491
+ // console.log("Proc config has changed. update router.")
492
492
  changed = true
493
493
  } else {
494
494
  // console.log("Proc config is the same")
package/kernel/util.js CHANGED
@@ -30,6 +30,70 @@ const {
30
30
 
31
31
 
32
32
  const platform = os.platform()
33
+ const WINDOWS_TOAST_APP_ID = process.env.PINOKIO_WINDOWS_APP_ID || 'computer.pinokio'
34
+ const DEFAULT_CHIME_URL_PATH = '/chime.mp3'
35
+ const pushListeners = new Set()
36
+
37
+ function registerPushListener(listener) {
38
+ if (typeof listener !== 'function') {
39
+ throw new TypeError('push listener must be a function')
40
+ }
41
+ pushListeners.add(listener)
42
+ return () => pushListeners.delete(listener)
43
+ }
44
+
45
+ function emitPushEvent(event) {
46
+ if (!event) {
47
+ return
48
+ }
49
+ pushListeners.forEach((listener) => {
50
+ try {
51
+ listener(event)
52
+ } catch (err) {
53
+ console.error('Push listener error:', err)
54
+ }
55
+ })
56
+ }
57
+
58
+ function resolvePublicAssetUrl(filePath) {
59
+ if (!filePath) {
60
+ return null
61
+ }
62
+ if (/^https?:\/\//i.test(filePath)) {
63
+ return filePath
64
+ }
65
+ try {
66
+ const absolute = path.resolve(filePath)
67
+ const publicRoot = path.resolve(__dirname, '../server/public')
68
+ if (absolute === publicRoot) {
69
+ return '/'
70
+ }
71
+ if (absolute.startsWith(publicRoot + path.sep) || absolute === publicRoot) {
72
+ const relative = path.relative(publicRoot, absolute).replace(/\\/g, '/')
73
+ return '/' + relative
74
+ }
75
+ } catch (_) {
76
+ // ignore resolution failures
77
+ }
78
+ return null
79
+ }
80
+
81
+ function soundTargetToClientUrl(target) {
82
+ if (!target) {
83
+ return null
84
+ }
85
+ if (target.kind === 'url') {
86
+ return target.value
87
+ }
88
+ if (target.kind === 'file') {
89
+ const resolved = path.resolve(target.value)
90
+ if (resolved === DEFAULT_CHIME_PATH) {
91
+ return DEFAULT_CHIME_URL_PATH
92
+ }
93
+ return resolvePublicAssetUrl(resolved)
94
+ }
95
+ return null
96
+ }
33
97
  function ensureNotifierBinaries() {
34
98
  if (platform !== 'darwin') {
35
99
  return
@@ -729,10 +793,39 @@ function push(params) {
729
793
  if (!notifyParams.contentImage) {
730
794
  notifyParams.contentImage = path.resolve(__dirname, "../server/public/pinokio-black.png")
731
795
  }
796
+ if (platform === 'win32') {
797
+ // Ensure Windows toast branding aligns with Pinokio assets.
798
+ if (!notifyParams.icon && notifyParams.contentImage) {
799
+ notifyParams.icon = notifyParams.contentImage
800
+ }
801
+ if (!notifyParams.appID && !notifyParams.appName) {
802
+ notifyParams.appID = WINDOWS_TOAST_APP_ID
803
+ }
804
+ }
805
+
806
+ const clientSoundUrl = soundTargetToClientUrl(customSoundTarget)
732
807
  if (customSoundTarget) {
733
808
  notifyParams.sound = false
734
- scheduleSoundPlayback(customSoundTarget)
809
+ const shouldPlayLocally = platform !== 'win32' || !clientSoundUrl
810
+ if (shouldPlayLocally) {
811
+ scheduleSoundPlayback(customSoundTarget)
812
+ }
813
+ }
814
+
815
+ const clientImage = resolvePublicAssetUrl(notifyParams.contentImage) || resolvePublicAssetUrl(notifyParams.image)
816
+ const eventPayload = {
817
+ id: randomUUID(),
818
+ title: notifyParams.title,
819
+ subtitle: notifyParams.subtitle || null,
820
+ message: notifyParams.message || '',
821
+ image: clientImage,
822
+ sound: clientSoundUrl,
823
+ timestamp: Date.now(),
824
+ platform,
735
825
  }
826
+
827
+ emitPushEvent(eventPayload)
828
+
736
829
  console.log("notifyParams", notifyParams)
737
830
  notifier.notify(notifyParams)
738
831
  }
@@ -1076,5 +1169,34 @@ const rewrite_localhost= (kernel, obj, source) => {
1076
1169
 
1077
1170
 
1078
1171
  module.exports = {
1079
- parse_env, log_path, api_path, api_name, update_env, parse_env_detail, openfs, port_running, du, is_port_available, find_python, find_venv, fill_object, run, openURL, u2p, p2u, log, diffLinesWithContext, classifyChange, push, filepicker, exists, clipboard, mergeLines, ignore_subrepos, rewrite_localhost, symlink, file_type
1172
+ parse_env,
1173
+ log_path,
1174
+ api_path,
1175
+ api_name,
1176
+ update_env,
1177
+ parse_env_detail,
1178
+ openfs,
1179
+ port_running,
1180
+ du,
1181
+ is_port_available,
1182
+ find_python,
1183
+ find_venv,
1184
+ fill_object,
1185
+ run,
1186
+ openURL,
1187
+ u2p,
1188
+ p2u,
1189
+ log,
1190
+ diffLinesWithContext,
1191
+ classifyChange,
1192
+ push,
1193
+ filepicker,
1194
+ exists,
1195
+ clipboard,
1196
+ mergeLines,
1197
+ ignore_subrepos,
1198
+ rewrite_localhost,
1199
+ symlink,
1200
+ file_type,
1201
+ registerPushListener,
1080
1202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.133.0",
3
+ "version": "3.135.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -449,9 +449,6 @@ class Server {
449
449
  } else {
450
450
  cfg.menu = cfg.menu(this.kernel, this.kernel.info)
451
451
  }
452
- cfg = await this.renderIndex(name, cfg)
453
- } else if (Array.isArray(cfg.menu)) {
454
- cfg = await this.renderIndex(name, cfg)
455
452
  }
456
453
  } else {
457
454
  cfg = await this.renderIndex(name, cfg)
@@ -698,7 +695,7 @@ class Server {
698
695
  if (config && config.version) {
699
696
  let coerced = semver.coerce(config.version)
700
697
  if (semver.satisfies(coerced, this.kernel.schema)) {
701
- console.log("semver satisfied", config.version, this.kernel.schema)
698
+ // console.log("semver satisfied", config.version, this.kernel.schema)
702
699
  } else {
703
700
  console.log("semver NOT satisfied", config.version, this.kernel.schema)
704
701
  err = `Please update to the latest Pinokio (current script version: ${config.version}, supported: ${this.kernel.schema})`
@@ -3722,7 +3719,11 @@ class Server {
3722
3719
  this.app.use(express.json());
3723
3720
  this.app.use(express.urlencoded({ extended: true }));
3724
3721
  this.app.use(cookieParser());
3725
- this.app.use(session({secret: "secret" }))
3722
+ this.app.use(session({
3723
+ secret: "secret",
3724
+ resave: false,
3725
+ saveUninitialized: false
3726
+ }))
3726
3727
  this.app.use((req, res, next) => {
3727
3728
  const originalRedirect = res.redirect;
3728
3729
  res.redirect = function (url) {
@@ -1398,14 +1398,323 @@ const open_url2 = (href, target, features) => {
1398
1398
  window.open(href, "_self", features)
1399
1399
  }
1400
1400
  }
1401
- hotkeys("ctrl+t,cmd+t,ctrl+n,cmd+n", (e) => {
1402
- let agent = document.body.getAttribute("data-agent")
1403
- if (agent === "electron") {
1404
- window.open(location.href, "_blank", "pinokio")
1401
+ if (typeof hotkeys === 'function') {
1402
+ hotkeys("ctrl+t,cmd+t,ctrl+n,cmd+n", (e) => {
1403
+ let agent = document.body.getAttribute("data-agent")
1404
+ if (agent === "electron") {
1405
+ window.open(location.href, "_blank", "pinokio")
1406
+ } else {
1407
+ window.open(location.href, "_blank")
1408
+ }
1409
+ })
1410
+ }
1411
+
1412
+ (function initNotificationAudioBridge() {
1413
+ if (typeof window === 'undefined') {
1414
+ return;
1415
+ }
1416
+ if (window.__pinokioNotificationAudioInitialized) {
1417
+ return;
1418
+ }
1419
+ window.__pinokioNotificationAudioInitialized = true;
1420
+
1421
+ const CHANNEL_ID = 'kernel.notifications';
1422
+ const pendingSounds = [];
1423
+ let isPlaying = false;
1424
+ let currentSocket = null;
1425
+ let reconnectTimeout = null;
1426
+ let activeAudio = null;
1427
+
1428
+ const leaderStorageKey = 'pinokio.notification.leader';
1429
+ const leaderHeartbeatMs = 5000;
1430
+ const leaderStaleMs = 15000;
1431
+ const tabId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
1432
+ const storageEnabled = (() => {
1433
+ try {
1434
+ const testKey = '__pinokio_notification_test__';
1435
+ localStorage.setItem(testKey, '1');
1436
+ localStorage.removeItem(testKey);
1437
+ return true;
1438
+ } catch (_) {
1439
+ return false;
1440
+ }
1441
+ })();
1442
+ let isLeader = false;
1443
+ let heartbeatTimer = null;
1444
+ let leadershipCheckTimer = null;
1445
+
1446
+ const parseLeaderValue = (value) => {
1447
+ if (!value) return null;
1448
+ try {
1449
+ return JSON.parse(value);
1450
+ } catch (_) {
1451
+ return null;
1452
+ }
1453
+ };
1454
+
1455
+ const writeHeartbeat = () => {
1456
+ try {
1457
+ localStorage.setItem(leaderStorageKey, JSON.stringify({ id: tabId, ts: Date.now() }));
1458
+ } catch (err) {
1459
+ console.warn('Notification leader heartbeat failed:', err);
1460
+ }
1461
+ };
1462
+
1463
+ const clearHeartbeat = () => {
1464
+ if (heartbeatTimer) {
1465
+ clearInterval(heartbeatTimer);
1466
+ heartbeatTimer = null;
1467
+ }
1468
+ };
1469
+
1470
+ const stopSocket = () => {
1471
+ if (currentSocket && typeof currentSocket.close === 'function') {
1472
+ try {
1473
+ currentSocket.close();
1474
+ } catch (_) {
1475
+ // ignore
1476
+ }
1477
+ }
1478
+ currentSocket = null;
1479
+ };
1480
+
1481
+ const stopAudio = () => {
1482
+ pendingSounds.length = 0;
1483
+ if (activeAudio) {
1484
+ try {
1485
+ activeAudio.pause();
1486
+ activeAudio.currentTime = 0;
1487
+ } catch (_) {
1488
+ // ignore
1489
+ }
1490
+ activeAudio = null;
1491
+ }
1492
+ isPlaying = false;
1493
+ };
1494
+
1495
+ const resignLeadership = () => {
1496
+ if (!isLeader) {
1497
+ return;
1498
+ }
1499
+ isLeader = false;
1500
+ clearHeartbeat();
1501
+ if (reconnectTimeout != null) {
1502
+ clearTimeout(reconnectTimeout);
1503
+ reconnectTimeout = null;
1504
+ }
1505
+ stopSocket();
1506
+ stopAudio();
1507
+ try {
1508
+ const stored = parseLeaderValue(localStorage.getItem(leaderStorageKey));
1509
+ if (stored && stored.id === tabId) {
1510
+ localStorage.removeItem(leaderStorageKey);
1511
+ }
1512
+ } catch (_) {
1513
+ // ignore
1514
+ }
1515
+ };
1516
+
1517
+ const startLeadership = () => {
1518
+ if (isLeader) {
1519
+ return;
1520
+ }
1521
+ isLeader = true;
1522
+ writeHeartbeat();
1523
+ if (!heartbeatTimer) {
1524
+ heartbeatTimer = setInterval(writeHeartbeat, leaderHeartbeatMs);
1525
+ }
1526
+ connect();
1527
+ };
1528
+
1529
+ const playNextSound = () => {
1530
+ if (isPlaying) {
1531
+ return;
1532
+ }
1533
+ const next = pendingSounds.shift();
1534
+ if (!next) {
1535
+ return;
1536
+ }
1537
+ isPlaying = true;
1538
+ activeAudio = new Audio(next);
1539
+ activeAudio.preload = 'auto';
1540
+ const cleanup = () => {
1541
+ if (!activeAudio) {
1542
+ return;
1543
+ }
1544
+ activeAudio.removeEventListener('ended', handleEnded);
1545
+ activeAudio.removeEventListener('error', handleError);
1546
+ activeAudio = null;
1547
+ isPlaying = false;
1548
+ playNextSound();
1549
+ };
1550
+ const handleEnded = () => cleanup();
1551
+ const handleError = (err) => {
1552
+ console.error('Notification audio playback failed:', err);
1553
+ cleanup();
1554
+ };
1555
+ activeAudio.addEventListener('ended', handleEnded, { once: true });
1556
+ activeAudio.addEventListener('error', handleError, { once: true });
1557
+ const playPromise = activeAudio.play();
1558
+ if (playPromise && typeof playPromise.catch === 'function') {
1559
+ playPromise.catch((err) => {
1560
+ console.error('Notification audio play() rejected:', err);
1561
+ cleanup();
1562
+ });
1563
+ }
1564
+ };
1565
+
1566
+ const enqueueSound = (url) => {
1567
+ if (!url) {
1568
+ return;
1569
+ }
1570
+ pendingSounds.push(url);
1571
+ playNextSound();
1572
+ };
1573
+
1574
+ const handlePacket = (packet) => {
1575
+ if (!packet || packet.id !== CHANNEL_ID || packet.type !== 'notification') {
1576
+ return;
1577
+ }
1578
+ const payload = packet.data || {};
1579
+ if (typeof payload.sound === 'string' && payload.sound) {
1580
+ enqueueSound(payload.sound);
1581
+ }
1582
+ };
1583
+
1584
+ const attemptLeadership = () => {
1585
+ if (isLeader) {
1586
+ writeHeartbeat();
1587
+ return;
1588
+ }
1589
+ let current = null;
1590
+ try {
1591
+ current = parseLeaderValue(localStorage.getItem(leaderStorageKey));
1592
+ } catch (_) {
1593
+ current = null;
1594
+ }
1595
+ const now = Date.now();
1596
+ const isStale = !current || !current.ts || (now - current.ts) > leaderStaleMs;
1597
+ if (isStale || (current && current.id === tabId)) {
1598
+ writeHeartbeat();
1599
+ const freshlyStored = parseLeaderValue(localStorage.getItem(leaderStorageKey));
1600
+ if (freshlyStored && freshlyStored.id === tabId) {
1601
+ startLeadership();
1602
+ }
1603
+ }
1604
+ };
1605
+
1606
+ const scheduleReconnect = (delay) => {
1607
+ if (!isLeader) {
1608
+ return;
1609
+ }
1610
+ if (reconnectTimeout != null) {
1611
+ return;
1612
+ }
1613
+ reconnectTimeout = setTimeout(() => {
1614
+ reconnectTimeout = null;
1615
+ connect();
1616
+ }, delay);
1617
+ };
1618
+
1619
+ const connect = () => {
1620
+ if (!isLeader) {
1621
+ return;
1622
+ }
1623
+ const SocketCtor = typeof window.Socket === 'function' ? window.Socket : (typeof Socket === 'function' ? Socket : null);
1624
+ if (!SocketCtor) {
1625
+ return;
1626
+ }
1627
+ if (typeof WebSocket === 'undefined') {
1628
+ return;
1629
+ }
1630
+ if (currentSocket && currentSocket.ws && currentSocket.ws.readyState === WebSocket.OPEN) {
1631
+ return;
1632
+ }
1633
+ if (currentSocket && typeof currentSocket.close === 'function') {
1634
+ try {
1635
+ currentSocket.close();
1636
+ } catch (_) {
1637
+ // ignore
1638
+ }
1639
+ }
1640
+ const socket = new SocketCtor();
1641
+ try {
1642
+ const promise = socket.run(
1643
+ {
1644
+ method: CHANNEL_ID,
1645
+ mode: 'listen',
1646
+ },
1647
+ handlePacket
1648
+ );
1649
+ currentSocket = socket;
1650
+ promise.then(() => {
1651
+ // Attempt to reconnect after a brief delay when the socket closes normally.
1652
+ if (currentSocket === socket) {
1653
+ currentSocket = null;
1654
+ scheduleReconnect(1500);
1655
+ }
1656
+ }).catch((err) => {
1657
+ console.warn('Notification listener socket closed with error:', err);
1658
+ if (currentSocket === socket) {
1659
+ currentSocket = null;
1660
+ scheduleReconnect(2500);
1661
+ }
1662
+ });
1663
+ window.__pinokioNotificationSocket = socket;
1664
+ } catch (err) {
1665
+ console.error('Failed to establish notification listener socket:', err);
1666
+ scheduleReconnect(3000);
1667
+ }
1668
+ };
1669
+
1670
+ if (!storageEnabled) {
1671
+ isLeader = true;
1672
+ const start = () => connect();
1673
+ if (document.readyState === 'loading') {
1674
+ document.addEventListener('DOMContentLoaded', start, { once: true });
1675
+ } else {
1676
+ connect();
1677
+ }
1678
+ window.addEventListener('beforeunload', () => {
1679
+ stopSocket();
1680
+ stopAudio();
1681
+ });
1682
+ return;
1683
+ }
1684
+
1685
+ if (document.readyState === 'loading') {
1686
+ document.addEventListener('DOMContentLoaded', connect, { once: true });
1405
1687
  } else {
1406
- window.open(location.href, "_blank")
1688
+ connect();
1407
1689
  }
1408
- })
1690
+
1691
+ if (!leadershipCheckTimer) {
1692
+ leadershipCheckTimer = setInterval(attemptLeadership, leaderHeartbeatMs);
1693
+ }
1694
+
1695
+ window.addEventListener('storage', (event) => {
1696
+ if (event.key !== leaderStorageKey) {
1697
+ return;
1698
+ }
1699
+ const data = parseLeaderValue(event.newValue);
1700
+ if (data && data.id === tabId) {
1701
+ startLeadership();
1702
+ } else {
1703
+ resignLeadership();
1704
+ }
1705
+ });
1706
+
1707
+ window.addEventListener('beforeunload', () => {
1708
+ if (leadershipCheckTimer) {
1709
+ clearInterval(leadershipCheckTimer);
1710
+ leadershipCheckTimer = null;
1711
+ }
1712
+ resignLeadership();
1713
+ });
1714
+
1715
+ // Attempt to become leader immediately on load.
1716
+ attemptLeadership();
1717
+ })();
1409
1718
  const refreshParent = (e) => {
1410
1719
  // if (window.parent === window.top) {
1411
1720
  window.parent.postMessage(e, "*")
@@ -18,6 +18,16 @@ document.addEventListener("DOMContentLoaded", () => {
18
18
  return;
19
19
  }
20
20
 
21
+ const dispatchHeaderState = (minimized, detail = {}) => {
22
+ if (typeof window === "undefined" || typeof window.CustomEvent !== "function") {
23
+ return;
24
+ }
25
+ const payload = { minimized, ...detail };
26
+ document.dispatchEvent(new CustomEvent("pinokio:header-state", { detail: payload }));
27
+ const aliasEvent = minimized ? "pinokio:header-minimized" : "pinokio:header-restored";
28
+ document.dispatchEvent(new CustomEvent(aliasEvent, { detail: { ...payload } }));
29
+ };
30
+
21
31
  const headerTitle = header.querySelector("h1") || header;
22
32
  let dragHandle = headerTitle.querySelector(".header-drag-handle");
23
33
  if (!dragHandle) {
@@ -45,6 +55,8 @@ document.addEventListener("DOMContentLoaded", () => {
45
55
  transitionHandler: null,
46
56
  };
47
57
 
58
+ dispatchHeaderState(state.minimized, { phase: "init" });
59
+
48
60
  const MIN_MARGIN = 8;
49
61
 
50
62
  const clampPosition = (left, top) => {
@@ -139,6 +151,8 @@ document.addEventListener("DOMContentLoaded", () => {
139
151
  header.classList.add("minimized");
140
152
  applyPosition(targetLeft, targetTop);
141
153
 
154
+ dispatchHeaderState(true, { phase: "start" });
155
+
142
156
  const lastRect = header.getBoundingClientRect();
143
157
  const deltaX = firstRect.left - lastRect.left;
144
158
  const deltaY = firstRect.top - lastRect.top;
@@ -164,6 +178,7 @@ document.addEventListener("DOMContentLoaded", () => {
164
178
  state.transitionHandler = null;
165
179
  stopTransition();
166
180
  state.minimized = true;
181
+ dispatchHeaderState(true, { phase: "settled" });
167
182
  };
168
183
 
169
184
  header.addEventListener("transitionend", state.transitionHandler);
@@ -189,6 +204,8 @@ document.addEventListener("DOMContentLoaded", () => {
189
204
  header.style.right = state.originalPosition.right;
190
205
  header.style.bottom = state.originalPosition.bottom;
191
206
 
207
+ dispatchHeaderState(false, { phase: "start" });
208
+
192
209
  const lastRect = header.getBoundingClientRect();
193
210
  const deltaX = firstRect.left - lastRect.left;
194
211
  const deltaY = firstRect.top - lastRect.top;
@@ -213,6 +230,7 @@ document.addEventListener("DOMContentLoaded", () => {
213
230
  state.hasCustomPosition = false;
214
231
  state.lastLeft = parseFloat(header.style.left) || 0;
215
232
  state.lastTop = parseFloat(header.style.top) || 0;
233
+ dispatchHeaderState(false, { phase: "settled" });
216
234
  };
217
235
 
218
236
  header.addEventListener("transitionend", state.transitionHandler);
package/server/socket.js CHANGED
@@ -3,6 +3,7 @@ const WebSocket = require('ws');
3
3
  const path = require('path')
4
4
  const Util = require("../kernel/util")
5
5
  const Environment = require("../kernel/environment")
6
+ const NOTIFICATION_CHANNEL = 'kernel.notifications'
6
7
  class Socket {
7
8
  constructor(parent) {
8
9
  this.buffer = {}
@@ -15,6 +16,8 @@ class Socket {
15
16
  // this.kernel = parent.kernel
16
17
  const wss = new WebSocket.Server({ server: this.parent.server })
17
18
  this.subscriptions = new Map(); // Initialize a Map to store the WebSocket connections interested in each event
19
+ this.notificationChannel = NOTIFICATION_CHANNEL
20
+ this.notificationBridgeDispose = null
18
21
  this.parent.kernel.api.listen("server.socket", this.trigger.bind(this))
19
22
  wss.on('connection', (ws, request) => {
20
23
  ws._headers = request.headers;
@@ -25,8 +28,15 @@ class Socket {
25
28
  ws._origin = request.headers.origin;
26
29
  ws.on('close', () => {
27
30
  this.subscriptions.forEach((set, eventName) => {
31
+ if (!set) {
32
+ return;
33
+ }
28
34
  set.delete(ws);
35
+ if (set.size === 0) {
36
+ this.subscriptions.delete(eventName);
37
+ }
29
38
  });
39
+ this.checkNotificationBridge();
30
40
  });
31
41
  ws.on('message', async (message, isBinary) => {
32
42
  let req
@@ -217,7 +227,11 @@ class Socket {
217
227
  if (!this.subscriptions.has(id)) {
218
228
  this.subscriptions.set(id, new Set());
219
229
  }
220
- this.subscriptions.get(id).add(ws);
230
+ const set = this.subscriptions.get(id)
231
+ set.add(ws);
232
+ if (set.size === 1 && id === this.notificationChannel) {
233
+ this.ensureNotificationBridge();
234
+ }
221
235
  }
222
236
  trigger(e) {
223
237
  // send to id session
@@ -321,6 +335,49 @@ class Socket {
321
335
  }
322
336
  }
323
337
  }
338
+
339
+ broadcastNotification(payload) {
340
+ if (!payload) {
341
+ return
342
+ }
343
+ const subscribers = this.subscriptions.get(this.notificationChannel)
344
+ if (!subscribers || subscribers.size === 0) {
345
+ return
346
+ }
347
+ const envelope = {
348
+ id: this.notificationChannel,
349
+ type: 'notification',
350
+ data: payload,
351
+ }
352
+ const frame = JSON.stringify(envelope)
353
+ subscribers.forEach((subscriber) => {
354
+ if (subscriber.readyState === WebSocket.OPEN) {
355
+ subscriber.send(frame)
356
+ }
357
+ })
358
+ }
359
+
360
+ ensureNotificationBridge() {
361
+ if (this.notificationBridgeDispose) {
362
+ return
363
+ }
364
+ this.notificationBridgeDispose = Util.registerPushListener((payload) => {
365
+ this.broadcastNotification(payload)
366
+ })
367
+ }
368
+
369
+ checkNotificationBridge() {
370
+ const subscribers = this.subscriptions.get(this.notificationChannel)
371
+ if ((!subscribers || subscribers.size === 0) && this.notificationBridgeDispose) {
372
+ try {
373
+ this.notificationBridgeDispose()
374
+ } catch (err) {
375
+ console.error('Failed to dispose notification bridge:', err)
376
+ }
377
+ this.notificationBridgeDispose = null
378
+ }
379
+ }
380
+
324
381
  async log_buffer(key, buf) {
325
382
 
326
383
  /*
@@ -180,6 +180,20 @@ body.dark .appcanvas_filler {
180
180
  --sidebar-tab-outline: var(--pinokio-sidebar-tabbar-border);
181
181
  }
182
182
 
183
+ .appcanvas > aside.appcanvas-aside-animating {
184
+ will-change: height, opacity;
185
+ }
186
+
187
+ .appcanvas > aside.appcanvas-aside-collapsed {
188
+ height: 0 !important;
189
+ opacity: 0;
190
+ pointer-events: none;
191
+ padding-top: 0;
192
+ padding-bottom: 0;
193
+ margin-top: 0;
194
+ margin-bottom: 0;
195
+ }
196
+
183
197
  /*
184
198
  body.dark .appcanvas > aside {
185
199
  background: var(--pinokio-sidebar-tabbar-bg);
@@ -2829,7 +2843,7 @@ body.dark {
2829
2843
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
2830
2844
  </a>
2831
2845
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
2832
- <div><i class="fa-solid fa-window-maximize"></i></div>
2846
+ <div><i class="fa-solid fa-expand"></i></div>
2833
2847
  </button>
2834
2848
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
2835
2849
  <div><i class="fa-solid fa-plus"></i></div>
@@ -8554,6 +8568,153 @@ body.dark {
8554
8568
 
8555
8569
  </script>
8556
8570
  <% } %>
8571
+ <script>
8572
+ (() => {
8573
+ const aside = document.querySelector(".appcanvas > aside")
8574
+ if (!aside) {
8575
+ return
8576
+ }
8577
+
8578
+ const collapsedClass = "appcanvas-aside-collapsed"
8579
+ const animatingClass = "appcanvas-aside-animating"
8580
+ const collapseDuration = 420
8581
+ const collapseEasing = "cubic-bezier(0.22, 1, 0.36, 1)"
8582
+ const expandEasing = "cubic-bezier(0.18, 0.85, 0.4, 1)"
8583
+
8584
+ let transitionHandler = null
8585
+ let queuedFrame = null
8586
+
8587
+ const clearAnimation = () => {
8588
+ if (transitionHandler) {
8589
+ aside.removeEventListener("transitionend", transitionHandler)
8590
+ transitionHandler = null
8591
+ }
8592
+ if (queuedFrame !== null) {
8593
+ cancelAnimationFrame(queuedFrame)
8594
+ queuedFrame = null
8595
+ }
8596
+ aside.classList.remove(animatingClass)
8597
+ aside.style.transition = ""
8598
+ aside.style.height = ""
8599
+ aside.style.opacity = ""
8600
+ }
8601
+
8602
+ const finish = (minimized) => {
8603
+ aside.classList.toggle(collapsedClass, minimized)
8604
+ aside.classList.remove(animatingClass)
8605
+ aside.style.transition = ""
8606
+ aside.style.height = ""
8607
+ aside.style.opacity = ""
8608
+ aside.dataset.asideState = minimized ? "collapsed" : "expanded"
8609
+ }
8610
+
8611
+ const collapse = (immediate) => {
8612
+ if (aside.dataset.asideState === "collapsed" && !immediate) {
8613
+ return
8614
+ }
8615
+ clearAnimation()
8616
+ if (immediate) {
8617
+ finish(true)
8618
+ return
8619
+ }
8620
+ const startHeight = aside.getBoundingClientRect().height
8621
+ if (startHeight <= 0.5) {
8622
+ finish(true)
8623
+ return
8624
+ }
8625
+
8626
+ aside.classList.add(animatingClass)
8627
+ aside.style.height = `${startHeight}px`
8628
+ aside.style.opacity = getComputedStyle(aside).opacity || "1"
8629
+ aside.dataset.asideState = "animating"
8630
+
8631
+ queuedFrame = requestAnimationFrame(() => {
8632
+ queuedFrame = null
8633
+ aside.style.transition = `height ${collapseDuration}ms ${collapseEasing}, opacity ${collapseDuration - 120}ms ease`
8634
+ aside.style.height = "0px"
8635
+ aside.style.opacity = "0"
8636
+ })
8637
+
8638
+ transitionHandler = (event) => {
8639
+ if (event.target !== aside || event.propertyName !== "height") {
8640
+ return
8641
+ }
8642
+ aside.removeEventListener("transitionend", transitionHandler)
8643
+ transitionHandler = null
8644
+ finish(true)
8645
+ }
8646
+ aside.addEventListener("transitionend", transitionHandler)
8647
+ }
8648
+
8649
+ const expand = (immediate) => {
8650
+ if (aside.dataset.asideState === "expanded" && !immediate) {
8651
+ return
8652
+ }
8653
+ clearAnimation()
8654
+ if (immediate) {
8655
+ finish(false)
8656
+ return
8657
+ }
8658
+
8659
+ aside.classList.remove(collapsedClass)
8660
+ const targetHeight = aside.scrollHeight
8661
+ aside.classList.add(animatingClass)
8662
+ aside.style.height = "0px"
8663
+ aside.style.opacity = "0"
8664
+ aside.dataset.asideState = "animating"
8665
+
8666
+ aside.offsetHeight
8667
+
8668
+ queuedFrame = requestAnimationFrame(() => {
8669
+ queuedFrame = null
8670
+ aside.style.transition = `height ${collapseDuration}ms ${expandEasing}, opacity ${collapseDuration - 140}ms ease`
8671
+ aside.style.height = `${targetHeight}px`
8672
+ aside.style.opacity = ""
8673
+ })
8674
+
8675
+ transitionHandler = (event) => {
8676
+ if (event.target !== aside || event.propertyName !== "height") {
8677
+ return
8678
+ }
8679
+ aside.removeEventListener("transitionend", transitionHandler)
8680
+ transitionHandler = null
8681
+ finish(false)
8682
+ }
8683
+ aside.addEventListener("transitionend", transitionHandler)
8684
+ }
8685
+
8686
+ const setAside = (minimized, immediate) => {
8687
+ if (minimized) {
8688
+ collapse(immediate)
8689
+ } else {
8690
+ expand(immediate)
8691
+ }
8692
+ }
8693
+
8694
+ const header = document.querySelector("header.navheader")
8695
+ const initialMinimized = !!(header && header.classList.contains("minimized"))
8696
+ setAside(initialMinimized, true)
8697
+
8698
+ document.addEventListener("pinokio:header-state", (event) => {
8699
+ if (!event || !event.detail) {
8700
+ return
8701
+ }
8702
+ const { minimized, phase } = event.detail
8703
+ if (phase === "init") {
8704
+ setAside(!!minimized, true)
8705
+ return
8706
+ }
8707
+ if (phase === "start") {
8708
+ setAside(!!minimized, false)
8709
+ return
8710
+ }
8711
+ if (phase === "settled") {
8712
+ clearAnimation()
8713
+ finish(!!minimized)
8714
+ }
8715
+ })
8716
+ })()
8717
+ </script>
8557
8718
  <script src="/tab-idle-notifier.js"></script>
8558
8719
  </body>
8559
8720
  </html>
@@ -206,7 +206,7 @@ pre {
206
206
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
207
207
  </a>
208
208
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
209
- <div><i class="fa-solid fa-window-maximize"></i></div>
209
+ <div><i class="fa-solid fa-expand"></i></div>
210
210
  </button>
211
211
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
212
212
  <div><i class="fa-solid fa-plus"></i></div>
@@ -831,7 +831,7 @@ document.addEventListener('DOMContentLoaded', function() {
831
831
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
832
832
  </a>
833
833
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
834
- <div><i class="fa-solid fa-window-maximize"></i></div>
834
+ <div><i class="fa-solid fa-expand"></i></div>
835
835
  </button>
836
836
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
837
837
  <div><i class="fa-solid fa-plus"></i></div>
@@ -345,7 +345,7 @@ iframe {
345
345
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
346
346
  </a>
347
347
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
348
- <div><i class="fa-solid fa-window-maximize"></i></div>
348
+ <div><i class="fa-solid fa-expand"></i></div>
349
349
  </button>
350
350
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
351
351
  <div><i class="fa-solid fa-plus"></i></div>
@@ -134,7 +134,7 @@ body.frozen {
134
134
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
135
135
  </a>
136
136
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
137
- <div><i class="fa-solid fa-window-maximize"></i></div>
137
+ <div><i class="fa-solid fa-expand"></i></div>
138
138
  </button>
139
139
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
140
140
  <div><i class="fa-solid fa-plus"></i></div>
@@ -135,7 +135,7 @@ body main iframe {
135
135
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
136
136
  </a>
137
137
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
138
- <div><i class="fa-solid fa-window-maximize"></i></div>
138
+ <div><i class="fa-solid fa-expand"></i></div>
139
139
  </button>
140
140
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
141
141
  <div><i class="fa-solid fa-plus"></i></div>
@@ -168,7 +168,7 @@ document.addEventListener("DOMContentLoaded", async () => {
168
168
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
169
169
  </a>
170
170
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
171
- <div><i class="fa-solid fa-window-maximize"></i></div>
171
+ <div><i class="fa-solid fa-expand"></i></div>
172
172
  </button>
173
173
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
174
174
  <div><i class="fa-solid fa-plus"></i></div>
@@ -64,7 +64,7 @@ main iframe {
64
64
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
65
65
  </a>
66
66
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
67
- <div><i class="fa-solid fa-window-maximize"></i></div>
67
+ <div><i class="fa-solid fa-expand"></i></div>
68
68
  </button>
69
69
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
70
70
  <div><i class="fa-solid fa-plus"></i></div>
@@ -247,7 +247,7 @@ ol {
247
247
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
248
248
  </a>
249
249
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
250
- <div><i class="fa-solid fa-window-maximize"></i></div>
250
+ <div><i class="fa-solid fa-expand"></i></div>
251
251
  </button>
252
252
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
253
253
  <div><i class="fa-solid fa-plus"></i></div>
@@ -268,7 +268,7 @@ body.dark .item .tile .badge {
268
268
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
269
269
  </a>
270
270
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
271
- <div><i class="fa-solid fa-window-maximize"></i></div>
271
+ <div><i class="fa-solid fa-expand"></i></div>
272
272
  </button>
273
273
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
274
274
  <div><i class="fa-solid fa-plus"></i></div>
@@ -436,7 +436,7 @@ body.dark aside .current.selected {
436
436
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
437
437
  </a>
438
438
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
439
- <div><i class="fa-solid fa-window-maximize"></i></div>
439
+ <div><i class="fa-solid fa-expand"></i></div>
440
440
  </button>
441
441
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
442
442
  <div><i class="fa-solid fa-plus"></i></div>
@@ -273,7 +273,7 @@ body.dark .open-menu, body.dark .browse {
273
273
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
274
274
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
275
275
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
276
- <div><i class="fa-solid fa-window-maximize"></i></div>
276
+ <div><i class="fa-solid fa-expand"></i></div>
277
277
  </button>
278
278
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
279
279
  </div>
@@ -1533,7 +1533,7 @@ body.dark .ace-editor {
1533
1533
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1534
1534
  </a>
1535
1535
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1536
- <div><i class="fa-solid fa-window-maximize"></i></div>
1536
+ <div><i class="fa-solid fa-expand"></i></div>
1537
1537
  </button>
1538
1538
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1539
1539
  <div><i class="fa-solid fa-plus"></i></div>
@@ -768,7 +768,7 @@ body.dark .appcanvas {
768
768
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
769
769
  </a>
770
770
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
771
- <div><i class="fa-solid fa-window-maximize"></i></div>
771
+ <div><i class="fa-solid fa-expand"></i></div>
772
772
  </button>
773
773
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
774
774
  <div><i class="fa-solid fa-plus"></i></div>
@@ -554,7 +554,7 @@ document.addEventListener('DOMContentLoaded', function() {
554
554
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
555
555
  </a>
556
556
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
557
- <div><i class="fa-solid fa-window-maximize"></i></div>
557
+ <div><i class="fa-solid fa-expand"></i></div>
558
558
  </button>
559
559
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
560
560
  <div><i class="fa-solid fa-plus"></i></div>
@@ -1067,7 +1067,7 @@ document.addEventListener('DOMContentLoaded', function() {
1067
1067
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1068
1068
  </a>
1069
1069
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1070
- <div><i class="fa-solid fa-window-maximize"></i></div>
1070
+ <div><i class="fa-solid fa-expand"></i></div>
1071
1071
  </button>
1072
1072
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1073
1073
  <div><i class="fa-solid fa-plus"></i></div>
@@ -429,7 +429,7 @@ table h3 {
429
429
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
430
430
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
431
431
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
432
- <div><i class="fa-solid fa-window-maximize"></i></div>
432
+ <div><i class="fa-solid fa-expand"></i></div>
433
433
  </button>
434
434
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
435
435
  </div>
@@ -415,7 +415,7 @@ input:checked + .slider:before {
415
415
  <a id='downloadlogs' download class='hidden btn2' href="/pinokio/logs.zip"><div><i class="fa-solid fa-download"></i></div><div>Download logs</div></a>
416
416
  <a class='btn2' href="/home?mode=settings"><div><i class="fa-solid fa-gear"></i></div><div>Settings</div></a>
417
417
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
418
- <div><i class="fa-solid fa-window-maximize"></i></div>
418
+ <div><i class="fa-solid fa-expand"></i></div>
419
419
  </button>
420
420
  <button id='new-window' title='open a new window' class='btn2' data-agent="<%=agent%>"><div><i class="fa-solid fa-plus"></i></div><div>Window</div></button>
421
421
  </div>
@@ -1015,7 +1015,7 @@ body.dark .appcanvas {
1015
1015
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1016
1016
  </a>
1017
1017
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1018
- <div><i class="fa-solid fa-window-maximize"></i></div>
1018
+ <div><i class="fa-solid fa-expand"></i></div>
1019
1019
  </button>
1020
1020
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1021
1021
  <div><i class="fa-solid fa-plus"></i></div>
@@ -955,7 +955,7 @@ body.dark .top-menu .btn2.selected {
955
955
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
956
956
  </a>
957
957
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
958
- <div><i class="fa-solid fa-window-maximize"></i></div>
958
+ <div><i class="fa-solid fa-expand"></i></div>
959
959
  </button>
960
960
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
961
961
  <div><i class="fa-solid fa-plus"></i></div>
@@ -727,7 +727,7 @@ document.addEventListener('DOMContentLoaded', function() {
727
727
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
728
728
  </a>
729
729
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
730
- <div><i class="fa-solid fa-window-maximize"></i></div>
730
+ <div><i class="fa-solid fa-expand"></i></div>
731
731
  </button>
732
732
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
733
733
  <div><i class="fa-solid fa-plus"></i></div>
@@ -399,7 +399,7 @@ document.addEventListener('DOMContentLoaded', function() {
399
399
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
400
400
  </a>
401
401
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
402
- <div><i class="fa-solid fa-window-maximize"></i></div>
402
+ <div><i class="fa-solid fa-expand"></i></div>
403
403
  </button>
404
404
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
405
405
  <div><i class="fa-solid fa-plus"></i></div>
@@ -145,7 +145,7 @@ body {
145
145
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
146
146
  </a>
147
147
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
148
- <div><i class="fa-solid fa-window-maximize"></i></div>
148
+ <div><i class="fa-solid fa-expand"></i></div>
149
149
  </button>
150
150
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
151
151
  <div><i class="fa-solid fa-plus"></i></div>
@@ -161,7 +161,7 @@ body.dark .card {
161
161
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
162
162
  </a>
163
163
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
164
- <div><i class="fa-solid fa-window-maximize"></i></div>
164
+ <div><i class="fa-solid fa-expand"></i></div>
165
165
  </button>
166
166
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
167
167
  <div><i class="fa-solid fa-plus"></i></div>
@@ -382,7 +382,7 @@ body.dark .plugin-option:hover {
382
382
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
383
383
  </a>
384
384
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
385
- <div><i class="fa-solid fa-window-maximize"></i></div>
385
+ <div><i class="fa-solid fa-expand"></i></div>
386
386
  </button>
387
387
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
388
388
  <div><i class="fa-solid fa-plus"></i></div>
@@ -1117,7 +1117,7 @@ document.addEventListener('DOMContentLoaded', function() {
1117
1117
  <div><i class="fa-solid fa-table-columns fa-rotate-270"></i></div>
1118
1118
  </a>
1119
1119
  <button class='btn2' id='minimize-header' data-tippy-content="minimize header" title='minimize header'>
1120
- <div><i class="fa-solid fa-window-maximize"></i></div>
1120
+ <div><i class="fa-solid fa-expand"></i></div>
1121
1121
  </button>
1122
1122
  <button class='btn2' id='new-window' data-tippy-content="open a new window" title='open a new window' data-agent="<%=agent%>">
1123
1123
  <div><i class="fa-solid fa-plus"></i></div>