pinokiod 6.0.19 → 6.0.21

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.
@@ -33,13 +33,17 @@ body {
33
33
 
34
34
  body {
35
35
  overflow: hidden;
36
+ display: flex !important;
37
+ flex-direction: column !important;
38
+ align-items: stretch;
36
39
  }
37
40
 
38
41
  main {
39
42
  display: flex;
43
+ flex: 1 1 auto;
40
44
  min-height: 0;
41
- height: var(--terminals-viewport-height);
42
- max-height: var(--terminals-viewport-height);
45
+ height: auto;
46
+ max-height: none;
43
47
  overflow: hidden;
44
48
  }
45
49
 
@@ -72,6 +76,29 @@ main > .container.terminals-page {
72
76
  box-sizing: border-box;
73
77
  }
74
78
 
79
+ @media only screen and (max-width: 768px) {
80
+ body {
81
+ flex-direction: column !important;
82
+ }
83
+ header.navheader {
84
+ display: block;
85
+ align-self: auto;
86
+ overflow: visible;
87
+ width: 100%;
88
+ min-width: 0;
89
+ max-width: none;
90
+ height: auto;
91
+ }
92
+ header.navheader h1 {
93
+ display: flex;
94
+ flex-direction: row;
95
+ align-items: center;
96
+ overflow-x: auto;
97
+ overflow-y: hidden;
98
+ flex-wrap: nowrap;
99
+ }
100
+ }
101
+
75
102
  .container.terminals-page > form.search {
76
103
  flex: 0 0 auto;
77
104
  padding: 10px 10px 0;
@@ -184,14 +211,14 @@ body.dark .terminals-search-form .home-mode-switch .mode-link:hover {
184
211
 
185
212
  .terminals-search-form .home-mode-switch .mode-link.selected,
186
213
  .terminals-search-form .home-mode-switch .mode-link[aria-current="page"] {
187
- background: rgba(0, 0, 0, 0.9) !important;
214
+ background: royalblue !important;
188
215
  color: #fff !important;
189
216
  }
190
217
 
191
218
  body.dark .terminals-search-form .home-mode-switch .mode-link.selected,
192
219
  body.dark .terminals-search-form .home-mode-switch .mode-link[aria-current="page"] {
193
- background: rgba(255, 255, 255, 0.12) !important;
194
- color: rgba(255, 255, 255, 0.96) !important;
220
+ background: royalblue !important;
221
+ color: #fff !important;
195
222
  }
196
223
 
197
224
  #terminals-empty-main {
@@ -206,12 +233,23 @@ body.dark .terminals-search-form .home-mode-switch .mode-link[aria-current="page
206
233
  box-sizing: border-box;
207
234
  }
208
235
 
209
- #terminals-empty-main.hidden,
210
- #terminals-session-bank.hidden {
236
+ #terminals-empty-main.hidden {
211
237
  display: none;
212
238
  }
213
239
 
214
240
  .terminals-columns {
241
+ flex: 1;
242
+ min-height: 0;
243
+ display: flex;
244
+ flex-direction: column;
245
+ width: 100%;
246
+ max-width: 100%;
247
+ overflow: hidden;
248
+ gap: 8px;
249
+ box-sizing: border-box;
250
+ }
251
+
252
+ .terminals-columns-rail {
215
253
  flex: 1;
216
254
  min-height: 0;
217
255
  display: flex;
@@ -230,6 +268,103 @@ body.dark .terminals-search-form .home-mode-switch .mode-link[aria-current="page
230
268
  display: none;
231
269
  }
232
270
 
271
+ .terminals-list-intro {
272
+ margin: 10px 12px 8px;
273
+ padding: 10px 12px;
274
+ border: 1px solid rgba(127, 127, 127, 0.22);
275
+ border-radius: 10px;
276
+ background: rgba(255, 255, 255, 0.02);
277
+ position: relative;
278
+ overflow: hidden;
279
+ }
280
+
281
+ body.dark .terminals-list-intro {
282
+ border-color: rgba(255, 255, 255, 0.18);
283
+ background: rgba(255, 255, 255, 0.03);
284
+ }
285
+
286
+ .terminals-list-intro-head {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 8px;
290
+ }
291
+
292
+ .terminals-list-intro-title {
293
+ display: inline-flex;
294
+ align-items: center;
295
+ gap: 8px;
296
+ min-width: 0;
297
+ }
298
+
299
+ .terminals-list-intro h2 {
300
+ margin: 0;
301
+ font-size: 14px;
302
+ line-height: 1.2;
303
+ }
304
+
305
+ .terminals-list-intro p {
306
+ margin: 4px 0 0;
307
+ font-size: 12px;
308
+ line-height: 1.35;
309
+ opacity: 0.86;
310
+ }
311
+
312
+ .terminals-list-intro-refresh {
313
+ display: none;
314
+ align-items: center;
315
+ gap: 6px;
316
+ font-size: 11px;
317
+ line-height: 1;
318
+ opacity: 0.9;
319
+ white-space: nowrap;
320
+ color: #1d4ed8;
321
+ background: rgba(29, 78, 216, 0.12);
322
+ border: 1px solid rgba(29, 78, 216, 0.28);
323
+ border-radius: 999px;
324
+ padding: 3px 8px;
325
+ flex: 0 0 auto;
326
+ }
327
+
328
+ .terminals-list-intro.is-loading .terminals-list-intro-refresh {
329
+ display: inline-flex;
330
+ }
331
+
332
+ .terminals-list-intro-progress {
333
+ position: absolute;
334
+ left: 0;
335
+ top: 0;
336
+ width: 38%;
337
+ height: 2px;
338
+ opacity: 0;
339
+ pointer-events: none;
340
+ background: linear-gradient(90deg, rgba(29, 78, 216, 0), rgba(29, 78, 216, 0.95), rgba(29, 78, 216, 0));
341
+ transform: translateX(-140%);
342
+ }
343
+
344
+ .terminals-list-intro.is-loading .terminals-list-intro-progress {
345
+ opacity: 1;
346
+ animation: terminalsIntroRefreshBar 1.1s linear infinite;
347
+ }
348
+
349
+ .terminals-list-intro-refresh i {
350
+ font-size: 11px;
351
+ }
352
+
353
+ body.dark .terminals-list-intro-refresh {
354
+ color: #93c5fd;
355
+ background: rgba(37, 99, 235, 0.22);
356
+ border-color: rgba(96, 165, 250, 0.4);
357
+ }
358
+
359
+ @keyframes terminalsIntroRefreshBar {
360
+ 0% {
361
+ transform: translateX(-140%);
362
+ }
363
+ 100% {
364
+ transform: translateX(280%);
365
+ }
366
+ }
367
+
233
368
  .terminals-column {
234
369
  flex: 0 0 100%;
235
370
  border: 1px solid rgba(127, 127, 127, 0.2);
@@ -994,7 +1129,86 @@ body.dark .terminals-column .terminals-search input[type="search"] {
994
1129
  }
995
1130
 
996
1131
  .terminals-column .terminals-empty {
997
- margin-top: 8px;
1132
+ margin: 10px 12px 0;
1133
+ padding: 20px 16px;
1134
+ border: 1px dashed rgba(127, 127, 127, 0.34);
1135
+ border-radius: 10px;
1136
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.08));
1137
+ display: flex;
1138
+ align-items: center;
1139
+ justify-content: center;
1140
+ text-align: center;
1141
+ min-height: 176px;
1142
+ }
1143
+
1144
+ body.dark .terminals-column .terminals-empty {
1145
+ border-color: rgba(255, 255, 255, 0.2);
1146
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));
1147
+ }
1148
+
1149
+ .terminals-empty-card {
1150
+ width: 100%;
1151
+ max-width: 460px;
1152
+ display: flex;
1153
+ flex-direction: column;
1154
+ align-items: center;
1155
+ gap: 8px;
1156
+ }
1157
+
1158
+ .terminals-empty-icon {
1159
+ width: 36px;
1160
+ height: 36px;
1161
+ border-radius: 999px;
1162
+ display: inline-flex;
1163
+ align-items: center;
1164
+ justify-content: center;
1165
+ background: rgba(31, 41, 55, 0.08);
1166
+ color: rgba(17, 24, 39, 0.88);
1167
+ font-size: 15px;
1168
+ }
1169
+
1170
+ body.dark .terminals-empty-icon {
1171
+ background: rgba(255, 255, 255, 0.12);
1172
+ color: rgba(255, 255, 255, 0.92);
1173
+ }
1174
+
1175
+ .terminals-empty-title {
1176
+ margin: 2px 0 0;
1177
+ font-size: 15px;
1178
+ font-weight: 700;
1179
+ letter-spacing: 0.01em;
1180
+ }
1181
+
1182
+ .terminals-empty-message {
1183
+ font-size: 13px;
1184
+ line-height: 1.35;
1185
+ opacity: 0.9;
1186
+ }
1187
+
1188
+ .terminals-empty-hint {
1189
+ font-size: 11px;
1190
+ line-height: 1.35;
1191
+ opacity: 0.66;
1192
+ }
1193
+
1194
+ .terminals-empty[data-state="loading"] .terminals-empty-icon {
1195
+ background: rgba(37, 99, 235, 0.12);
1196
+ color: #1d4ed8;
1197
+ }
1198
+
1199
+ body.dark .terminals-empty[data-state="loading"] .terminals-empty-icon {
1200
+ background: rgba(59, 130, 246, 0.22);
1201
+ color: #93c5fd;
1202
+ }
1203
+
1204
+ .terminals-empty[data-state="error"] .terminals-empty-icon {
1205
+ background: rgba(220, 38, 38, 0.12);
1206
+ color: #b91c1c;
1207
+ }
1208
+
1209
+ body.dark .terminals-empty[data-state="error"] .terminals-empty-icon {
1210
+ background: rgba(248, 113, 113, 0.2);
1211
+ color: #fecaca;
998
1212
  }
999
1213
 
1000
1214
  .terminals-list {
@@ -1002,6 +1216,17 @@ body.dark .terminals-column .terminals-search input[type="search"] {
1002
1216
  padding: 0;
1003
1217
  }
1004
1218
 
1219
+ .terminals-load-more-indicator {
1220
+ padding: 10px 12px 14px;
1221
+ font-size: 12px;
1222
+ text-align: center;
1223
+ opacity: 0.68;
1224
+ }
1225
+
1226
+ body.dark .terminals-load-more-indicator {
1227
+ opacity: 0.76;
1228
+ }
1229
+
1005
1230
  .terminals-list .line {
1006
1231
  margin: 0;
1007
1232
  display: block;
@@ -1077,8 +1302,8 @@ body.dark .terminals-list .line.active {
1077
1302
  }
1078
1303
 
1079
1304
  .terminals-list .line .terminals-session-dot.is-online {
1080
- background: #18a954;
1081
- box-shadow: 0 0 0 1px rgba(24, 169, 84, 0.42);
1305
+ background: yellowgreen;
1306
+ box-shadow: 0 0 0 1px rgba(154, 205, 50, 0.42);
1082
1307
  }
1083
1308
 
1084
1309
  .terminals-list .line .terminals-session-dot.is-offline {
@@ -1092,8 +1317,8 @@ body.dark .terminals-list .line .terminals-session-dot {
1092
1317
  }
1093
1318
 
1094
1319
  body.dark .terminals-list .line .terminals-session-dot.is-online {
1095
- background: #5ed392;
1096
- box-shadow: 0 0 0 1px rgba(94, 211, 146, 0.42);
1320
+ background: yellowgreen;
1321
+ box-shadow: 0 0 0 1px rgba(154, 205, 50, 0.42);
1097
1322
  }
1098
1323
 
1099
1324
  body.dark .terminals-list .line .terminals-session-dot.is-offline {
@@ -1200,8 +1425,10 @@ body.dark .terminals-list .line .terminals-session-action:hover {
1200
1425
 
1201
1426
  .terminals-column .terminals-open-folder,
1202
1427
  .terminals-column .terminals-fork-session,
1428
+ .terminals-column .terminals-deploy-local,
1203
1429
  .terminals-top-session .terminals-open-folder,
1204
- .terminals-top-session .terminals-fork-session {
1430
+ .terminals-top-session .terminals-fork-session,
1431
+ .terminals-top-session .terminals-deploy-local {
1205
1432
  display: inline-flex;
1206
1433
  align-items: center;
1207
1434
  gap: 6px;
@@ -1214,11 +1441,22 @@ body.dark .terminals-list .line .terminals-session-action:hover {
1214
1441
  font-size: 12px;
1215
1442
  white-space: nowrap;
1216
1443
  }
1444
+ .terminals-copy-icon {
1445
+ width: 1.25em;
1446
+ height: 1em;
1447
+ line-height: 1em;
1448
+ }
1449
+ .terminals-copy-icon .fa-copy {
1450
+ font-size: 0.55em;
1451
+ transform: translate(0.5em, 0.48em);
1452
+ }
1217
1453
 
1218
1454
  .terminals-column .terminals-open-folder:disabled,
1219
1455
  .terminals-column .terminals-fork-session:disabled,
1456
+ .terminals-column .terminals-deploy-local:disabled,
1220
1457
  .terminals-top-session .terminals-open-folder:disabled,
1221
- .terminals-top-session .terminals-fork-session:disabled {
1458
+ .terminals-top-session .terminals-fork-session:disabled,
1459
+ .terminals-top-session .terminals-deploy-local:disabled {
1222
1460
  opacity: 0.45;
1223
1461
  cursor: not-allowed;
1224
1462
  }
@@ -1229,6 +1467,93 @@ body.dark .terminals-list .line .terminals-session-action:hover {
1229
1467
  gap: 6px;
1230
1468
  }
1231
1469
 
1470
+ .terminals-top-session .terminals-chooser-actions {
1471
+ position: relative;
1472
+ }
1473
+
1474
+ .terminals-overflow-toggle {
1475
+ display: none;
1476
+ align-items: center;
1477
+ gap: 6px;
1478
+ height: 25px;
1479
+ padding: 0 10px;
1480
+ border-radius: 5px;
1481
+ border: 1px solid rgba(127, 127, 127, 0.25);
1482
+ background: rgba(0, 0, 0, 0.02);
1483
+ color: inherit;
1484
+ cursor: pointer;
1485
+ font-size: 12px;
1486
+ line-height: 1;
1487
+ white-space: nowrap;
1488
+ box-sizing: border-box;
1489
+ }
1490
+
1491
+ .terminals-overflow-toggle:disabled {
1492
+ opacity: 0.45;
1493
+ cursor: not-allowed;
1494
+ }
1495
+
1496
+ .terminals-overflow-menu {
1497
+ position: absolute;
1498
+ right: 0;
1499
+ top: calc(100% + 6px);
1500
+ min-width: 190px;
1501
+ display: flex;
1502
+ flex-direction: column;
1503
+ gap: 4px;
1504
+ padding: 6px;
1505
+ border: 1px solid rgba(127, 127, 127, 0.25);
1506
+ border-radius: 8px;
1507
+ background: #fff;
1508
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
1509
+ z-index: 30;
1510
+ }
1511
+
1512
+ body.dark .terminals-overflow-menu {
1513
+ background: #111827;
1514
+ border-color: rgba(255, 255, 255, 0.12);
1515
+ }
1516
+
1517
+ .terminals-overflow-menu button {
1518
+ display: inline-flex;
1519
+ align-items: center;
1520
+ gap: 8px;
1521
+ width: 100%;
1522
+ height: 30px;
1523
+ padding: 0 10px;
1524
+ border: 1px solid transparent;
1525
+ border-radius: 6px;
1526
+ background: transparent;
1527
+ color: inherit;
1528
+ font-size: 12px;
1529
+ text-align: left;
1530
+ cursor: pointer;
1531
+ }
1532
+
1533
+ .terminals-overflow-menu button:hover {
1534
+ background: rgba(0, 0, 0, 0.06);
1535
+ }
1536
+
1537
+ body.dark .terminals-overflow-menu button:hover {
1538
+ background: rgba(255, 255, 255, 0.08);
1539
+ }
1540
+
1541
+ .terminals-overflow-menu button:disabled {
1542
+ opacity: 0.45;
1543
+ cursor: not-allowed;
1544
+ }
1545
+
1546
+ @media only screen and (max-width: 980px) {
1547
+ .terminals-top-session .terminals-open-folder,
1548
+ .terminals-top-session .terminals-deploy-local,
1549
+ .terminals-top-session .close-column {
1550
+ display: none;
1551
+ }
1552
+ .terminals-top-session .terminals-overflow-toggle {
1553
+ display: inline-flex;
1554
+ }
1555
+ }
1556
+
1232
1557
  .terminals-column iframe {
1233
1558
  width: 100%;
1234
1559
  flex: 1;
@@ -1315,6 +1640,194 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1315
1640
  color: rgba(255, 255, 255, 0.8);
1316
1641
  border-color: rgba(255, 255, 255, 0.2);
1317
1642
  }
1643
+
1644
+ .swal2-popup.pinokio-modern-modal {
1645
+ border-radius: 20px !important;
1646
+ padding: 0 !important;
1647
+ background: #ffffff !important;
1648
+ color: #0f172a !important;
1649
+ box-shadow: 0 32px 80px rgba(15, 23, 42, 0.25) !important;
1650
+ overflow: hidden !important;
1651
+ }
1652
+
1653
+ body.dark .swal2-popup.pinokio-modern-modal {
1654
+ background: #0f172a !important;
1655
+ color: #e2e8f0 !important;
1656
+ box-shadow: 0 40px 120px rgba(2, 8, 23, 0.7) !important;
1657
+ }
1658
+
1659
+ .pinokio-modern-html {
1660
+ margin: 0 !important;
1661
+ padding: 0 !important;
1662
+ }
1663
+
1664
+ .pinokio-modern-close.swal2-close {
1665
+ color: rgba(71, 85, 105, 0.65) !important;
1666
+ font-size: 18px !important;
1667
+ margin: 12px 12px 0 0 !important;
1668
+ }
1669
+
1670
+ body.dark .pinokio-modern-close.swal2-close {
1671
+ color: rgba(226, 232, 240, 0.6) !important;
1672
+ }
1673
+
1674
+ .pinokio-modern-confirm.swal2-confirm {
1675
+ background: #2563eb !important;
1676
+ color: #ffffff !important;
1677
+ border: none !important;
1678
+ border-radius: 10px !important;
1679
+ padding: 10px 18px !important;
1680
+ font-size: 13px !important;
1681
+ font-weight: 700 !important;
1682
+ }
1683
+
1684
+ .pinokio-modern-confirm.swal2-confirm:hover {
1685
+ background: #1d4ed8 !important;
1686
+ }
1687
+
1688
+ .pinokio-modern-cancel.swal2-cancel {
1689
+ background: rgba(148, 163, 184, 0.16) !important;
1690
+ color: #0f172a !important;
1691
+ border: none !important;
1692
+ border-radius: 10px !important;
1693
+ padding: 10px 18px !important;
1694
+ font-size: 13px !important;
1695
+ font-weight: 600 !important;
1696
+ }
1697
+
1698
+ body.dark .pinokio-modern-cancel.swal2-cancel {
1699
+ background: rgba(148, 163, 184, 0.14) !important;
1700
+ color: #e2e8f0 !important;
1701
+ }
1702
+
1703
+ .pinokio-modern-cancel.swal2-cancel:hover {
1704
+ background: rgba(148, 163, 184, 0.24) !important;
1705
+ }
1706
+
1707
+ body.dark .pinokio-modern-cancel.swal2-cancel:hover {
1708
+ background: rgba(148, 163, 184, 0.22) !important;
1709
+ }
1710
+
1711
+ .pinokio-modal-surface {
1712
+ display: flex;
1713
+ flex-direction: column;
1714
+ }
1715
+
1716
+ .pinokio-modal-header {
1717
+ display: flex;
1718
+ gap: 12px;
1719
+ align-items: center;
1720
+ padding: 18px 20px 8px 20px;
1721
+ }
1722
+
1723
+ .pinokio-modal-icon {
1724
+ width: 40px;
1725
+ height: 40px;
1726
+ border-radius: 12px;
1727
+ display: inline-flex;
1728
+ align-items: center;
1729
+ justify-content: center;
1730
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.18), rgba(59, 130, 246, 0.03));
1731
+ color: #2563eb;
1732
+ }
1733
+
1734
+ body.dark .pinokio-modal-icon {
1735
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(59, 130, 246, 0.05));
1736
+ color: #60a5fa;
1737
+ }
1738
+
1739
+ .pinokio-modal-heading {
1740
+ min-width: 0;
1741
+ }
1742
+
1743
+ .pinokio-modal-title {
1744
+ font-size: 18px;
1745
+ font-weight: 700;
1746
+ line-height: 1.2;
1747
+ color: #0f172a;
1748
+ }
1749
+
1750
+ body.dark .pinokio-modal-title {
1751
+ color: #f8fafc;
1752
+ }
1753
+
1754
+ .pinokio-modal-subtitle {
1755
+ margin-top: 4px;
1756
+ font-size: 13px;
1757
+ line-height: 1.4;
1758
+ color: rgba(71, 85, 105, 0.82);
1759
+ }
1760
+
1761
+ body.dark .pinokio-modal-subtitle {
1762
+ color: rgba(148, 163, 184, 0.85);
1763
+ }
1764
+
1765
+ .pinokio-modal-body {
1766
+ padding: 10px 20px 20px 20px;
1767
+ display: flex;
1768
+ flex-direction: column;
1769
+ gap: 10px;
1770
+ }
1771
+
1772
+ .pinokio-modal-note {
1773
+ margin: 0;
1774
+ font-size: 13px;
1775
+ line-height: 1.5;
1776
+ color: rgba(30, 41, 59, 0.9);
1777
+ }
1778
+
1779
+ body.dark .pinokio-modal-note {
1780
+ color: rgba(226, 232, 240, 0.92);
1781
+ }
1782
+
1783
+ .pinokio-modal-note code {
1784
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1785
+ background: rgba(148, 163, 184, 0.2);
1786
+ padding: 1px 5px;
1787
+ border-radius: 6px;
1788
+ }
1789
+
1790
+ body.dark .pinokio-modal-note code {
1791
+ background: rgba(148, 163, 184, 0.24);
1792
+ }
1793
+
1794
+ .pinokio-modal-label {
1795
+ font-size: 12px;
1796
+ font-weight: 600;
1797
+ color: rgba(71, 85, 105, 0.95);
1798
+ }
1799
+
1800
+ body.dark .pinokio-modal-label {
1801
+ color: rgba(203, 213, 225, 0.9);
1802
+ }
1803
+
1804
+ .pinokio-modal-input {
1805
+ width: 100%;
1806
+ box-sizing: border-box;
1807
+ border-radius: 10px;
1808
+ border: 1px solid rgba(148, 163, 184, 0.5);
1809
+ background: #ffffff;
1810
+ color: #0f172a;
1811
+ font-size: 14px;
1812
+ padding: 10px 12px;
1813
+ outline: none;
1814
+ }
1815
+
1816
+ .pinokio-modal-input:focus {
1817
+ border-color: rgba(59, 130, 246, 0.6);
1818
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
1819
+ }
1820
+
1821
+ body.dark .pinokio-modal-input {
1822
+ background: rgba(15, 23, 42, 0.85);
1823
+ border-color: rgba(148, 163, 184, 0.25);
1824
+ color: #f8fafc;
1825
+ }
1826
+
1827
+ body.dark .pinokio-modal-input:focus {
1828
+ border-color: rgba(96, 165, 250, 0.7);
1829
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
1830
+ }
1318
1831
  </style>
1319
1832
  </head>
1320
1833
  <body class="<%= theme %>" data-agent="<%= agent %>">
@@ -1349,7 +1862,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1349
1862
  <form class='search terminals-search-form'>
1350
1863
  <div class="home-mode-switch" role="tablist" aria-label="Home modes">
1351
1864
  <a class="mode-link" href="/home">
1352
- <i class="fa-solid fa-box"></i>
1865
+ <i class="fa-solid fa-computer"></i>
1353
1866
  <span>Apps</span>
1354
1867
  </a>
1355
1868
  <a class="mode-link selected" href="/home?mode=terminals" aria-current="page">
@@ -1359,8 +1872,8 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1359
1872
  </div>
1360
1873
  <div class="terminals-top-search-controls" id="terminals-top-search-controls">
1361
1874
  <input id="terminals-session-search" type='search' class="flexible" placeholder='Search sessions'>
1362
- <button class="terminals-create-toggle" id="terminals-create-session-button" type="button" aria-label="Chat" aria-expanded="false">
1363
- <i class="fa-solid fa-plus"></i><span>Chat</span>
1875
+ <button class="terminals-create-toggle" id="terminals-create-session-button" type="button" aria-label="Create workspace" aria-expanded="false">
1876
+ <i class="fa-solid fa-plus"></i><span>Create workspace</span>
1364
1877
  </button>
1365
1878
  </div>
1366
1879
  <div class="terminals-top-session hidden" id="terminals-top-session" aria-live="polite">
@@ -1368,7 +1881,14 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1368
1881
  <div class="terminals-chooser-actions">
1369
1882
  <button class="terminals-fork-session" id="terminals-top-fork-session" type="button" aria-label="Fork current session" disabled><i class="fa-solid fa-code-branch"></i><span>Fork</span></button>
1370
1883
  <button class="terminals-open-folder" id="terminals-top-open-folder" type="button" aria-label="Open in File Explorer" disabled><i class="fa-solid fa-folder-open"></i><span>Open in File Explorer</span></button>
1884
+ <button class="terminals-deploy-local" id="terminals-top-deploy-local" type="button" aria-label="Copy to apps" disabled><span class="fa-stack terminals-copy-icon" aria-hidden="true"><i class="fa-solid fa-computer fa-stack-1x"></i><i class="fa-solid fa-copy fa-stack-1x"></i></span><span>Copy to apps</span></button>
1371
1885
  <button class="close-column" id="terminals-top-close-session" type="button" aria-label="Close selected session">×</button>
1886
+ <button class="terminals-overflow-toggle" id="terminals-top-more-actions" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="More actions"><i class="fa-solid fa-ellipsis"></i><span>More</span></button>
1887
+ <div class="terminals-overflow-menu hidden" id="terminals-top-overflow-menu" role="menu" aria-label="More session actions">
1888
+ <button type="button" data-action="open-folder" role="menuitem"><i class="fa-solid fa-folder-open"></i><span>Open in File Explorer</span></button>
1889
+ <button type="button" data-action="deploy-local" role="menuitem"><span class="fa-stack terminals-copy-icon" aria-hidden="true"><i class="fa-solid fa-computer fa-stack-1x"></i><i class="fa-solid fa-copy fa-stack-1x"></i></span><span>Copy to apps</span></button>
1890
+ <button type="button" data-action="close-session" role="menuitem"><i class="fa-solid fa-xmark"></i><span>Close session</span></button>
1891
+ </div>
1372
1892
  </div>
1373
1893
  </div>
1374
1894
  </form>
@@ -1381,21 +1901,12 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1381
1901
  <span>Run one from your terminal and come back here to continue.</span>
1382
1902
  </div>
1383
1903
  </div>
1384
- <div id='terminals-columns' class='terminals-columns hidden'></div>
1904
+ <div id='terminals-columns' class='terminals-columns hidden'>
1905
+ <div id='terminals-columns-rail' class='terminals-columns-rail'></div>
1906
+ </div>
1385
1907
  </section>
1386
1908
  </div>
1387
1909
 
1388
- <div id='terminals-session-bank' class='hidden'>
1389
- <div class='terminals-list'>
1390
- <% (items || []).forEach((item, index) => { %>
1391
- <a href="#" role="button" data-description="<%=item.description%>" data-provider-label="<%=item.provider_label || ''%>" data-cwd="<%=item.cwd || ''%>" data-timestamp="<%=item.timestamp || ''%>" data-index="<%=index%>" data-name="<%=item.name%>" data-uri="<%=item.uri%>" data-url="<%=item.browser_url%>" data-fork-url="<%=item.fork_url || ''%>" data-fork-capable="<%=item.fork_capable === false ? '0' : '1'%>" data-fork-disabled-reason="<%=item.fork_disabled_reason || ''%>" data-online="<%=item.online ? '1' : '0'%>" class='line'>
1392
- <h3><%=item.name%></h3>
1393
- <div class='description'><%=item.description%></div>
1394
- </a>
1395
- <% }) %>
1396
- </div>
1397
- </div>
1398
-
1399
1910
  <div id="terminals-launcher-modal" class="terminals-launcher-modal hidden" aria-hidden="true">
1400
1911
  <div class="terminals-launcher-backdrop" data-launcher-close="1"></div>
1401
1912
  <section class="terminals-launcher-panel" role="dialog" aria-modal="true" aria-labelledby="terminals-launcher-title">
@@ -1467,7 +1978,8 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1467
1978
  (() => {
1468
1979
  const navHeader = document.querySelector("header.navheader")
1469
1980
  const root = document.documentElement
1470
- const columns = document.querySelector("#terminals-columns")
1981
+ const columnsWrap = document.querySelector("#terminals-columns")
1982
+ const columns = document.querySelector("#terminals-columns-rail")
1471
1983
  const emptyMain = document.querySelector("#terminals-empty-main")
1472
1984
  const searchForm = document.querySelector(".terminals-search-form")
1473
1985
  const sessionSearchInput = document.querySelector("#terminals-session-search")
@@ -1476,7 +1988,10 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1476
1988
  const topSessionTitle = document.querySelector("#terminals-top-session-title")
1477
1989
  const topSessionForkButton = document.querySelector("#terminals-top-fork-session")
1478
1990
  const topSessionOpenFolderButton = document.querySelector("#terminals-top-open-folder")
1991
+ const topSessionDeployLocalButton = document.querySelector("#terminals-top-deploy-local")
1479
1992
  const topSessionCloseButton = document.querySelector("#terminals-top-close-session")
1993
+ const topSessionMoreButton = document.querySelector("#terminals-top-more-actions")
1994
+ const topSessionOverflowMenu = document.querySelector("#terminals-top-overflow-menu")
1480
1995
  const startProviders = <%-JSON.stringify(providers || [])%>
1481
1996
  const initialSkills = <%-JSON.stringify(skills || [])%>
1482
1997
  let availableSkills = Array.isArray(initialSkills) ? initialSkills : []
@@ -1529,35 +2044,37 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1529
2044
  return parseOnlineFlag(value)
1530
2045
  }
1531
2046
 
1532
- const rootItems = Array.from(document.querySelectorAll("#terminals-session-bank .line"))
1533
- let sessionItems = rootItems.map((item) => {
1534
- const index = item.getAttribute("data-index") || ""
1535
- const name = item.getAttribute("data-name") || "Session"
1536
- const description = item.getAttribute("data-description") || ""
1537
- const providerLabel = item.getAttribute("data-provider-label") || ""
1538
- const cwd = item.getAttribute("data-cwd") || ""
1539
- const timestamp = item.getAttribute("data-timestamp") || ""
1540
- const uri = item.getAttribute("data-uri") || ""
1541
- const url = item.getAttribute("data-url") || "#"
1542
- const forkUrl = item.getAttribute("data-fork-url") || ""
1543
- const forkCapable = parseBooleanFlag(item.getAttribute("data-fork-capable"), true)
1544
- const forkDisabledReason = item.getAttribute("data-fork-disabled-reason") || ""
1545
- const online = parseOnlineFlag(item.getAttribute("data-online"))
1546
- return {
1547
- index,
1548
- name,
1549
- description,
1550
- providerLabel,
1551
- cwd,
1552
- timestamp,
1553
- uri,
1554
- url,
1555
- forkUrl,
1556
- forkCapable,
1557
- forkDisabledReason,
1558
- online,
2047
+ const SESSION_PAGE_SIZE = 120
2048
+ const SESSION_LOAD_MORE_THRESHOLD_PX = 160
2049
+ const SESSION_SEARCH_DEBOUNCE_MS = 250
2050
+ let sessionItems = []
2051
+ let hasAttemptedSessionLoad = false
2052
+ let lastSessionLoadFailed = false
2053
+ let sessionHasMore = true
2054
+ let sessionNextCursor = 0
2055
+ let sessionAppendInProgress = false
2056
+ let currentSessionQuery = ""
2057
+ let sessionSearchDebounceTimer = null
2058
+ let sessionFetchAbortController = null
2059
+ let sessionFetchRequestId = 0
2060
+ let hasTriggeredBackgroundRegistrySync = false
2061
+ let sessionRefreshActivityCount = 0
2062
+
2063
+ const syncIntroLoadingIndicators = () => {
2064
+ if (!columns) {
2065
+ return
1559
2066
  }
1560
- })
2067
+ const isRefreshing = sessionRefreshActivityCount > 0
2068
+ const introNodes = columns.querySelectorAll(".terminals-list-intro")
2069
+ introNodes.forEach((introNode) => {
2070
+ introNode.classList.toggle("is-loading", isRefreshing)
2071
+ })
2072
+ }
2073
+
2074
+ const adjustSessionRefreshActivity = (delta) => {
2075
+ sessionRefreshActivityCount = Math.max(0, sessionRefreshActivityCount + delta)
2076
+ syncIntroLoadingIndicators()
2077
+ }
1561
2078
 
1562
2079
  const normalizeSessionItem = (item, indexFallback = 0) => {
1563
2080
  const index = normalizeIndex(item && typeof item.index !== "undefined" ? item.index : indexFallback)
@@ -1568,6 +2085,15 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1568
2085
  const timestamp = parseTimestamp(item && item.timestamp ? item.timestamp : null)
1569
2086
  const uri = item && item.uri ? item.uri : ""
1570
2087
  const url = item && (item.browser_url || item.url) ? (item.browser_url || item.url) : "#"
2088
+ const hasResumeCapable = Boolean(item)
2089
+ && (Object.prototype.hasOwnProperty.call(item, "resume_capable") || Object.prototype.hasOwnProperty.call(item, "resumeCapable"))
2090
+ const resumeCapableValue = hasResumeCapable
2091
+ ? (Object.prototype.hasOwnProperty.call(item, "resume_capable") ? item.resume_capable : item.resumeCapable)
2092
+ : undefined
2093
+ const resumeCapable = parseBooleanFlag(resumeCapableValue, true)
2094
+ const resumeDisabledReason = item && (item.resume_disabled_reason || item.resumeDisabledReason)
2095
+ ? String(item.resume_disabled_reason || item.resumeDisabledReason)
2096
+ : ""
1571
2097
  const forkUrl = item && (item.fork_url || item.forkUrl) ? (item.fork_url || item.forkUrl) : ""
1572
2098
  const hasForkCapable = Boolean(item)
1573
2099
  && (Object.prototype.hasOwnProperty.call(item, "fork_capable") || Object.prototype.hasOwnProperty.call(item, "forkCapable"))
@@ -1588,6 +2114,8 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1588
2114
  timestamp,
1589
2115
  uri,
1590
2116
  url,
2117
+ resumeCapable,
2118
+ resumeDisabledReason,
1591
2119
  forkUrl,
1592
2120
  forkCapable,
1593
2121
  forkDisabledReason,
@@ -1605,6 +2133,9 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1605
2133
  const normalizeIndex = (value) => {
1606
2134
  return String(value || "").trim()
1607
2135
  }
2136
+ const normalizeCwdKey = (value) => {
2137
+ return normalizeIndex(value).replace(/\/+$/, "")
2138
+ }
1608
2139
 
1609
2140
  const isTerminalColumn = (column) => {
1610
2141
  return Boolean(column && column.isConnected && column.dataset && column.dataset.state === "terminal")
@@ -1641,6 +2172,11 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1641
2172
  if (topSessionOpenFolderButton) {
1642
2173
  topSessionOpenFolderButton.disabled = true
1643
2174
  }
2175
+ if (topSessionDeployLocalButton) {
2176
+ topSessionDeployLocalButton.disabled = true
2177
+ }
2178
+ closeTopSessionOverflowMenu()
2179
+ syncTopSessionOverflowMenu()
1644
2180
  return
1645
2181
  }
1646
2182
 
@@ -1671,6 +2207,43 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1671
2207
  if (topSessionOpenFolderButton) {
1672
2208
  topSessionOpenFolderButton.disabled = !sessionCwd
1673
2209
  }
2210
+ if (topSessionDeployLocalButton) {
2211
+ topSessionDeployLocalButton.disabled = !sessionCwd
2212
+ }
2213
+ syncTopSessionOverflowMenu()
2214
+ }
2215
+
2216
+ const closeTopSessionOverflowMenu = () => {
2217
+ if (topSessionOverflowMenu) {
2218
+ topSessionOverflowMenu.classList.add("hidden")
2219
+ }
2220
+ if (topSessionMoreButton) {
2221
+ topSessionMoreButton.setAttribute("aria-expanded", "false")
2222
+ }
2223
+ }
2224
+
2225
+ const syncTopSessionOverflowMenu = () => {
2226
+ if (!topSessionOverflowMenu) {
2227
+ return
2228
+ }
2229
+ const openFolderOverflowButton = topSessionOverflowMenu.querySelector("[data-action='open-folder']")
2230
+ if (openFolderOverflowButton && topSessionOpenFolderButton) {
2231
+ openFolderOverflowButton.disabled = Boolean(topSessionOpenFolderButton.disabled)
2232
+ }
2233
+ const deployOverflowButton = topSessionOverflowMenu.querySelector("[data-action='deploy-local']")
2234
+ if (deployOverflowButton && topSessionDeployLocalButton) {
2235
+ deployOverflowButton.disabled = Boolean(topSessionDeployLocalButton.disabled)
2236
+ }
2237
+ const closeOverflowButton = topSessionOverflowMenu.querySelector("[data-action='close-session']")
2238
+ if (closeOverflowButton && topSessionCloseButton) {
2239
+ closeOverflowButton.disabled = Boolean(topSessionCloseButton.disabled)
2240
+ }
2241
+ if (topSessionMoreButton) {
2242
+ const allDisabled = [openFolderOverflowButton, deployOverflowButton, closeOverflowButton]
2243
+ .filter(Boolean)
2244
+ .every((button) => button.disabled)
2245
+ topSessionMoreButton.disabled = allDisabled
2246
+ }
1674
2247
  }
1675
2248
 
1676
2249
  const setActiveTerminalColumn = (column) => {
@@ -1806,10 +2379,29 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1806
2379
  }
1807
2380
  return "Fork unavailable for this session."
1808
2381
  }
2382
+ const getResumeUnavailableReason = (session) => {
2383
+ if (session && typeof session.resumeDisabledReason === "string" && session.resumeDisabledReason.trim().length > 0) {
2384
+ return session.resumeDisabledReason.trim()
2385
+ }
2386
+ return "Resume unavailable for this session."
2387
+ }
2388
+ const canSessionResume = (session) => {
2389
+ if (!session || typeof session !== "object") {
2390
+ return false
2391
+ }
2392
+ if (!parseBooleanFlag(session.resumeCapable, true)) {
2393
+ return false
2394
+ }
2395
+ const resumeUrl = typeof session.url === "string" ? session.url.trim() : ""
2396
+ return resumeUrl.length > 0 && resumeUrl !== "#"
2397
+ }
1809
2398
  const canSessionFork = (session) => {
1810
2399
  if (!session || typeof session !== "object") {
1811
2400
  return false
1812
2401
  }
2402
+ if (!canSessionResume(session)) {
2403
+ return false
2404
+ }
1813
2405
  if (!parseBooleanFlag(session.forkCapable, true)) {
1814
2406
  return false
1815
2407
  }
@@ -1843,10 +2435,12 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1843
2435
  const source = session && typeof session === "object" ? session : {}
1844
2436
  const forkSuffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`
1845
2437
  const baseName = source && source.name ? String(source.name) : "Session"
2438
+ const providerKey = normalizeProviderKey(getProviderKey(source) || "session")
1846
2439
  const forkUrl = buildForkLaunchUrl(source)
1847
2440
  return {
1848
2441
  ...source,
1849
2442
  index: `fork-${normalizeIndex(source.index)}-${forkSuffix}`,
2443
+ uri: `${providerKey}:pending-fork-${forkSuffix}`,
1850
2444
  name: `${baseName} (fork)`,
1851
2445
  url: forkUrl,
1852
2446
  forkCapable: forkUrl !== "#",
@@ -1911,32 +2505,15 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
1911
2505
  if (aOnline !== bOnline) {
1912
2506
  return bOnline - aOnline
1913
2507
  }
1914
- const ta = parseTimestamp(a && a.timestamp ? a.timestamp : null) || 0
1915
- const tb = parseTimestamp(b && b.timestamp ? b.timestamp : null) || 0
1916
- if (ta !== tb) {
1917
- return tb - ta
1918
- }
1919
- const an = (a && a.name ? String(a.name) : "").toLowerCase()
1920
- const bn = (b && b.name ? String(b.name) : "").toLowerCase()
1921
- return an.localeCompare(bn)
1922
- }
1923
- const getExistingOnlineSessionByProvider = (providerKey) => {
1924
- const normalizedProvider = normalizeProviderKey(providerKey)
1925
- if (!normalizedProvider) {
1926
- return null
1927
- }
1928
- for (let i = 0; i < sessionItems.length; i++) {
1929
- const session = sessionItems[i]
1930
- if (!session || !isSessionOnlineNow(session)) {
1931
- continue
1932
- }
1933
- if (getProviderKey(session) === normalizedProvider) {
1934
- return session
1935
- }
1936
- }
1937
- return null
2508
+ const ta = parseTimestamp(a && a.timestamp ? a.timestamp : null) || 0
2509
+ const tb = parseTimestamp(b && b.timestamp ? b.timestamp : null) || 0
2510
+ if (ta !== tb) {
2511
+ return tb - ta
2512
+ }
2513
+ const an = (a && a.name ? String(a.name) : "").toLowerCase()
2514
+ const bn = (b && b.name ? String(b.name) : "").toLowerCase()
2515
+ return an.localeCompare(bn)
1938
2516
  }
1939
-
1940
2517
  if (!launcherState.provider && startProviders && startProviders[0] && startProviders[0].key) {
1941
2518
  launcherState.provider = normalizeProviderKey(startProviders[0].key)
1942
2519
  }
@@ -2360,14 +2937,6 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2360
2937
  setLauncherBusy(true)
2361
2938
  let shouldClose = false
2362
2939
  try {
2363
- await refreshSessionItems({ force: true })
2364
- refreshChooserRows()
2365
- const existingSession = getExistingOnlineSessionByProvider(provider)
2366
- if (existingSession) {
2367
- openItemInColumn(column, existingSession)
2368
- shouldClose = true
2369
- return
2370
- }
2371
2940
  let uploadToken = ""
2372
2941
  if (launcherState.pendingFiles.length > 0) {
2373
2942
  const uploaded = await uploadLauncherFiles()
@@ -2536,6 +3105,156 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2536
3105
  }
2537
3106
  }
2538
3107
 
3108
+ const deployLocalCopy = async ({ folderName, sessionUri, sessionCwd }) => {
3109
+ try {
3110
+ const response = await fetch("/terminals/deploy/local", {
3111
+ method: "POST",
3112
+ headers: {
3113
+ "Content-Type": "application/json"
3114
+ },
3115
+ body: JSON.stringify({
3116
+ folderName,
3117
+ sessionUri,
3118
+ sessionCwd
3119
+ })
3120
+ })
3121
+ let payload = null
3122
+ try {
3123
+ payload = await response.json()
3124
+ } catch (error) {
3125
+ }
3126
+ if (!response.ok) {
3127
+ const message = payload && payload.error ? payload.error : "Failed to deploy locally."
3128
+ return {
3129
+ ok: false,
3130
+ error: message
3131
+ }
3132
+ }
3133
+ return payload
3134
+ } catch (error) {
3135
+ return {
3136
+ ok: false,
3137
+ error: error && error.message ? error.message : "Failed to deploy locally."
3138
+ }
3139
+ }
3140
+ }
3141
+
3142
+ const openDeployLocalModal = async (column) => {
3143
+ if (!column || !column.dataset) {
3144
+ return false
3145
+ }
3146
+ const sessionCwd = column.dataset.sessionCwd || ""
3147
+ const sessionUri = column.dataset.sessionUri || ""
3148
+ if (!sessionCwd) {
3149
+ return false
3150
+ }
3151
+ if (typeof Swal === "undefined" || !Swal || typeof Swal.fire !== "function") {
3152
+ const folderName = window.prompt("Deploy to ~/pinokio/api/<folder-name>. Enter folder name:")
3153
+ if (!folderName) {
3154
+ return false
3155
+ }
3156
+ const result = await deployLocalCopy({
3157
+ folderName,
3158
+ sessionUri,
3159
+ sessionCwd
3160
+ })
3161
+ if (result && result.code === "exists") {
3162
+ window.alert("Folder already exists.")
3163
+ return false
3164
+ }
3165
+ if (!result || result.ok !== true) {
3166
+ window.alert(result && result.error ? result.error : "Failed to deploy locally.")
3167
+ return false
3168
+ }
3169
+ return true
3170
+ }
3171
+
3172
+ const result = await Swal.fire({
3173
+ html: `
3174
+ <div class="pinokio-modal-surface">
3175
+ <div class="pinokio-modal-header">
3176
+ <div class="pinokio-modal-icon"><i class="fa-solid fa-rocket"></i></div>
3177
+ <div class="pinokio-modal-heading">
3178
+ <div class="pinokio-modal-title">Deploy locally</div>
3179
+ <div class="pinokio-modal-subtitle">Copy this active session workspace to a new local project folder.</div>
3180
+ </div>
3181
+ </div>
3182
+ <div class="pinokio-modal-body">
3183
+ <p class="pinokio-modal-note">This will copy the current workspace to <code>~/pinokio/api/&lt;folder-name&gt;</code>.</p>
3184
+ <label class="pinokio-modal-label" for="terminals-deploy-folder-input">New folder name</label>
3185
+ <input id="terminals-deploy-folder-input" class="pinokio-modal-input" placeholder="my-local-workspace" autocomplete="off">
3186
+ </div>
3187
+ </div>
3188
+ `,
3189
+ showCancelButton: true,
3190
+ confirmButtonText: "Create",
3191
+ cancelButtonText: "Cancel",
3192
+ showCloseButton: true,
3193
+ buttonsStyling: false,
3194
+ backdrop: "rgba(9,11,15,0.65)",
3195
+ width: "min(520px, 90vw)",
3196
+ focusConfirm: false,
3197
+ showLoaderOnConfirm: true,
3198
+ allowOutsideClick: () => !Swal.isLoading(),
3199
+ customClass: {
3200
+ popup: "pinokio-modern-modal",
3201
+ htmlContainer: "pinokio-modern-html",
3202
+ closeButton: "pinokio-modern-close",
3203
+ confirmButton: "pinokio-modern-confirm",
3204
+ cancelButton: "pinokio-modern-cancel"
3205
+ },
3206
+ didOpen: () => {
3207
+ const input = Swal.getPopup().querySelector("#terminals-deploy-folder-input")
3208
+ if (input) {
3209
+ input.focus()
3210
+ }
3211
+ },
3212
+ preConfirm: async () => {
3213
+ const input = Swal.getPopup().querySelector("#terminals-deploy-folder-input")
3214
+ const folderName = input ? String(input.value || "").trim() : ""
3215
+ if (!folderName) {
3216
+ Swal.showValidationMessage("Enter a folder name.")
3217
+ return false
3218
+ }
3219
+ if (folderName === "." || folderName === ".." || /[\\/]/.test(folderName) || folderName.includes("\0")) {
3220
+ Swal.showValidationMessage("Folder name cannot include path separators.")
3221
+ return false
3222
+ }
3223
+ const payload = await deployLocalCopy({
3224
+ folderName,
3225
+ sessionUri,
3226
+ sessionCwd
3227
+ })
3228
+ if (payload && payload.code === "exists") {
3229
+ window.alert("Folder already exists.")
3230
+ return false
3231
+ }
3232
+ if (!payload || payload.ok !== true) {
3233
+ Swal.showValidationMessage(payload && payload.error ? payload.error : "Failed to deploy locally.")
3234
+ return false
3235
+ }
3236
+ return payload
3237
+ }
3238
+ })
3239
+
3240
+ if (!result || !result.isConfirmed || !result.value || result.value.ok !== true) {
3241
+ return false
3242
+ }
3243
+ await Swal.fire({
3244
+ title: "Created",
3245
+ text: `Created /api/${result.value.folder || ""}`,
3246
+ icon: "success",
3247
+ buttonsStyling: false,
3248
+ customClass: {
3249
+ popup: "pinokio-modern-modal",
3250
+ htmlContainer: "pinokio-modern-html",
3251
+ closeButton: "pinokio-modern-close",
3252
+ confirmButton: "pinokio-modern-confirm"
3253
+ }
3254
+ })
3255
+ return true
3256
+ }
3257
+
2539
3258
  const getSessionFromColumn = (column) => {
2540
3259
  if (!column || !column.dataset) {
2541
3260
  return null
@@ -2602,9 +3321,30 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2602
3321
  return
2603
3322
  }
2604
3323
  topSessionOpenFolderButton.disabled = true
3324
+ syncTopSessionOverflowMenu()
2605
3325
  await openInFileExplorer(cwd)
2606
3326
  if (topSessionOpenFolderButton.isConnected) {
2607
3327
  topSessionOpenFolderButton.disabled = false
3328
+ syncTopSessionOverflowMenu()
3329
+ }
3330
+ })
3331
+ }
3332
+
3333
+ if (topSessionDeployLocalButton) {
3334
+ topSessionDeployLocalButton.addEventListener("click", async () => {
3335
+ const activeColumn = getActiveTerminalColumn()
3336
+ if (!activeColumn) {
3337
+ return
3338
+ }
3339
+ const cwd = activeColumn.dataset.sessionCwd || ""
3340
+ if (!cwd) {
3341
+ return
3342
+ }
3343
+ topSessionDeployLocalButton.disabled = true
3344
+ syncTopSessionOverflowMenu()
3345
+ await openDeployLocalModal(activeColumn)
3346
+ if (topSessionDeployLocalButton.isConnected) {
3347
+ renderTopSessionBar()
2608
3348
  }
2609
3349
  })
2610
3350
  }
@@ -2617,9 +3357,11 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2617
3357
  return
2618
3358
  }
2619
3359
  topSessionForkButton.disabled = true
3360
+ syncTopSessionOverflowMenu()
2620
3361
  const started = forkSessionFromColumn(activeColumn)
2621
3362
  if (!started && topSessionForkButton.isConnected) {
2622
3363
  topSessionForkButton.disabled = false
3364
+ syncTopSessionOverflowMenu()
2623
3365
  }
2624
3366
  })
2625
3367
  }
@@ -2634,6 +3376,57 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2634
3376
  })
2635
3377
  }
2636
3378
 
3379
+ if (topSessionMoreButton && topSessionOverflowMenu) {
3380
+ topSessionMoreButton.addEventListener("click", (event) => {
3381
+ event.preventDefault()
3382
+ event.stopPropagation()
3383
+ if (topSessionMoreButton.disabled) {
3384
+ return
3385
+ }
3386
+ const willOpen = topSessionOverflowMenu.classList.contains("hidden")
3387
+ topSessionOverflowMenu.classList.toggle("hidden", !willOpen)
3388
+ topSessionMoreButton.setAttribute("aria-expanded", willOpen ? "true" : "false")
3389
+ })
3390
+
3391
+ topSessionOverflowMenu.addEventListener("click", (event) => {
3392
+ const button = event.target && event.target.closest ? event.target.closest("button[data-action]") : null
3393
+ if (!button || button.disabled) {
3394
+ return
3395
+ }
3396
+ const action = String(button.dataset.action || "")
3397
+ closeTopSessionOverflowMenu()
3398
+ if (action === "open-folder" && topSessionOpenFolderButton && !topSessionOpenFolderButton.disabled) {
3399
+ topSessionOpenFolderButton.click()
3400
+ return
3401
+ }
3402
+ if (action === "deploy-local" && topSessionDeployLocalButton && !topSessionDeployLocalButton.disabled) {
3403
+ topSessionDeployLocalButton.click()
3404
+ return
3405
+ }
3406
+ if (action === "close-session" && topSessionCloseButton && !topSessionCloseButton.disabled) {
3407
+ topSessionCloseButton.click()
3408
+ }
3409
+ })
3410
+
3411
+ document.addEventListener("click", (event) => {
3412
+ if (topSessionOverflowMenu.classList.contains("hidden")) {
3413
+ return
3414
+ }
3415
+ const withinMenu = event.target && event.target.closest && event.target.closest("#terminals-top-overflow-menu")
3416
+ const withinToggle = event.target && event.target.closest && event.target.closest("#terminals-top-more-actions")
3417
+ if (withinMenu || withinToggle) {
3418
+ return
3419
+ }
3420
+ closeTopSessionOverflowMenu()
3421
+ })
3422
+
3423
+ document.addEventListener("keydown", (event) => {
3424
+ if (event.key === "Escape") {
3425
+ closeTopSessionOverflowMenu()
3426
+ }
3427
+ })
3428
+ }
3429
+
2637
3430
  const isFrameSessionAlreadyStopped = (frame) => {
2638
3431
  if (!frame) {
2639
3432
  return false
@@ -2659,69 +3452,280 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2659
3452
  }
2660
3453
  return false
2661
3454
  }
3455
+ const getCurrentSessionRefreshLimit = () => {
3456
+ return Math.min(Math.max(sessionItems.length || 0, SESSION_PAGE_SIZE), 500)
3457
+ }
3458
+ const collectOpenColumnSessions = () => {
3459
+ if (!columns) {
3460
+ return []
3461
+ }
3462
+ const terminalColumns = columns.querySelectorAll(".terminals-column[data-state='terminal']")
3463
+ const collected = []
3464
+ for (let i = 0; i < terminalColumns.length; i++) {
3465
+ const column = terminalColumns[i]
3466
+ const session = getSessionFromColumn(column)
3467
+ if (!canSessionResume(session)) {
3468
+ continue
3469
+ }
3470
+ const normalized = normalizeSessionItem({
3471
+ ...session,
3472
+ online: true,
3473
+ timestamp: session && session.timestamp ? session.timestamp : Date.now()
3474
+ }, session && session.index ? session.index : i)
3475
+ collected.push(normalized)
3476
+ }
3477
+ return collected
3478
+ }
3479
+ const mergeOpenColumnSessions = (incomingItems) => {
3480
+ const merged = Array.isArray(incomingItems) ? incomingItems.slice() : []
3481
+ const existingKeys = new Set()
3482
+ const existingContextKeys = new Set()
3483
+ const registerSession = (session) => {
3484
+ const key = session && session.uri ? `uri:${session.uri}` : `idx:${normalizeIndex(session && session.index)}`
3485
+ existingKeys.add(key)
3486
+ const providerKey = normalizeProviderKey(getProviderKey(session))
3487
+ const cwdKey = normalizeCwdKey(session && session.cwd ? session.cwd : "")
3488
+ if (providerKey && cwdKey) {
3489
+ existingContextKeys.add(`${providerKey}|${cwdKey}`)
3490
+ }
3491
+ }
3492
+ for (let i = 0; i < merged.length; i++) {
3493
+ registerSession(merged[i])
3494
+ }
3495
+ const openColumnSessions = collectOpenColumnSessions()
3496
+ for (let i = 0; i < openColumnSessions.length; i++) {
3497
+ const session = openColumnSessions[i]
3498
+ const key = session && session.uri ? `uri:${session.uri}` : `idx:${normalizeIndex(session && session.index)}`
3499
+ const providerKey = normalizeProviderKey(getProviderKey(session))
3500
+ const cwdKey = normalizeCwdKey(session && session.cwd ? session.cwd : "")
3501
+ const contextKey = providerKey && cwdKey ? `${providerKey}|${cwdKey}` : ""
3502
+ if (existingKeys.has(key)) {
3503
+ continue
3504
+ }
3505
+ if (contextKey && existingContextKeys.has(contextKey)) {
3506
+ for (let j = 0; j < merged.length; j++) {
3507
+ const candidate = merged[j]
3508
+ if (!candidate) {
3509
+ continue
3510
+ }
3511
+ const candidateProvider = normalizeProviderKey(getProviderKey(candidate))
3512
+ const candidateCwd = normalizeCwdKey(candidate.cwd)
3513
+ if (candidateProvider !== providerKey || candidateCwd !== cwdKey) {
3514
+ continue
3515
+ }
3516
+ merged[j] = {
3517
+ ...candidate,
3518
+ online: true
3519
+ }
3520
+ break
3521
+ }
3522
+ continue
3523
+ }
3524
+ merged.unshift(session)
3525
+ registerSession(session)
3526
+ }
3527
+ return merged
3528
+ }
2662
3529
 
2663
3530
  let refreshSessionPromise = null
3531
+ const appendSessionPage = (incomingItems) => {
3532
+ if (!Array.isArray(incomingItems) || incomingItems.length === 0) {
3533
+ return false
3534
+ }
3535
+ const existingKeys = new Set()
3536
+ for (let i = 0; i < sessionItems.length; i++) {
3537
+ const item = sessionItems[i]
3538
+ if (!item) {
3539
+ continue
3540
+ }
3541
+ const key = item.uri ? `uri:${item.uri}` : `idx:${normalizeIndex(item.index)}`
3542
+ existingKeys.add(key)
3543
+ }
3544
+ let changed = false
3545
+ for (let i = 0; i < incomingItems.length; i++) {
3546
+ const candidate = normalizeSessionItem(incomingItems[i], sessionItems.length + i)
3547
+ const key = candidate.uri ? `uri:${candidate.uri}` : `idx:${normalizeIndex(candidate.index)}`
3548
+ if (existingKeys.has(key)) {
3549
+ continue
3550
+ }
3551
+ existingKeys.add(key)
3552
+ sessionItems.push(candidate)
3553
+ changed = true
3554
+ }
3555
+ return changed
3556
+ }
2664
3557
  const refreshSessionItems = async (options = {}) => {
2665
- const forceRefresh = Boolean(options && options.force)
3558
+ const syncRefresh = Boolean(options && (options.sync || options.force))
2666
3559
  const includeSkills = Boolean(options && options.skills)
3560
+ const append = Boolean(options && options.append)
3561
+ const hasQueryOverride = Boolean(options && Object.prototype.hasOwnProperty.call(options, "query"))
3562
+ const queryText = hasQueryOverride ? normalizeIndex(options.query) : currentSessionQuery
3563
+ const normalizedQuery = queryText ? String(queryText).trim() : ""
3564
+ const pageSize = Number.isFinite(options && options.limit) && options.limit > 0
3565
+ ? Math.min(Math.max(Math.floor(options.limit), 1), 500)
3566
+ : SESSION_PAGE_SIZE
3567
+ if (refreshSessionPromise && append) {
3568
+ return refreshSessionPromise
3569
+ }
3570
+ if (append && (!sessionHasMore || sessionAppendInProgress)) {
3571
+ return false
3572
+ }
3573
+ if (!append && sessionFetchAbortController) {
3574
+ try {
3575
+ sessionFetchAbortController.abort()
3576
+ } catch (error) {}
3577
+ sessionFetchAbortController = null
3578
+ }
3579
+ if (append) {
3580
+ sessionAppendInProgress = true
3581
+ } else {
3582
+ currentSessionQuery = normalizedQuery
3583
+ sessionNextCursor = 0
3584
+ sessionHasMore = true
3585
+ }
3586
+ const requestId = ++sessionFetchRequestId
3587
+ const controller = new AbortController()
3588
+ sessionFetchAbortController = controller
2667
3589
  const params = new URLSearchParams()
2668
3590
  params.set("mode", "terminals")
2669
3591
  params.set("fetch", "1")
2670
- if (forceRefresh) {
2671
- params.set("refresh", "1")
3592
+ params.set("limit", String(pageSize))
3593
+ if (append && sessionNextCursor > 0) {
3594
+ params.set("cursor", String(sessionNextCursor))
3595
+ }
3596
+ if (currentSessionQuery) {
3597
+ params.set("q", currentSessionQuery)
3598
+ }
3599
+ if (syncRefresh) {
3600
+ params.set("sync", "1")
2672
3601
  }
2673
3602
  if (includeSkills) {
2674
3603
  params.set("skills", "1")
2675
3604
  }
2676
3605
  const endpoint = `/home?${params.toString()}`
2677
- if (refreshSessionPromise) {
2678
- return refreshSessionPromise
2679
- }
3606
+ adjustSessionRefreshActivity(1)
2680
3607
  refreshSessionPromise = (async () => {
3608
+ let requestSucceeded = false
3609
+ let shouldMarkError = false
3610
+ let aborted = false
2681
3611
  try {
2682
3612
  const response = await fetch(endpoint, {
2683
3613
  method: "GET",
3614
+ signal: controller.signal,
2684
3615
  headers: {
2685
3616
  "Accept": "application/json"
2686
3617
  }
2687
3618
  })
2688
3619
  if (!response.ok) {
3620
+ shouldMarkError = !append
2689
3621
  return false
2690
3622
  }
2691
3623
  const payload = await response.json()
3624
+ if (requestId !== sessionFetchRequestId || controller.signal.aborted) {
3625
+ aborted = true
3626
+ return false
3627
+ }
2692
3628
  const items = Array.isArray(payload && payload.items) ? payload.items : []
2693
- sessionItems = items.map((item, index) => normalizeSessionItem(item, index))
2694
- for (let i = 0; i < sessionItems.length; i++) {
2695
- sessionItems[i].index = i
3629
+ if (append) {
3630
+ appendSessionPage(items)
3631
+ } else {
3632
+ const normalizedItems = items.map((item, index) => normalizeSessionItem(item, index))
3633
+ sessionItems = mergeOpenColumnSessions(normalizedItems)
3634
+ }
3635
+ const pagination = payload && payload.pagination && typeof payload.pagination === "object" ? payload.pagination : null
3636
+ if (pagination) {
3637
+ const nextCursorRaw = Number.parseInt(pagination.nextCursor, 10)
3638
+ const hasMore = Boolean(pagination.hasMore)
3639
+ sessionHasMore = hasMore
3640
+ sessionNextCursor = hasMore && Number.isFinite(nextCursorRaw) && nextCursorRaw >= 0
3641
+ ? nextCursorRaw
3642
+ : sessionItems.length
3643
+ } else {
3644
+ sessionHasMore = false
3645
+ sessionNextCursor = sessionItems.length
2696
3646
  }
2697
3647
  if (payload && Array.isArray(payload.skills)) {
2698
3648
  availableSkills = payload.skills.slice()
2699
3649
  }
3650
+ requestSucceeded = true
2700
3651
  return true
2701
3652
  } catch (error) {
3653
+ if (error && error.name === "AbortError") {
3654
+ aborted = true
3655
+ return false
3656
+ }
3657
+ shouldMarkError = !append
2702
3658
  return false
2703
3659
  } finally {
2704
- refreshSessionPromise = null
3660
+ if (requestId === sessionFetchRequestId) {
3661
+ if (!aborted) {
3662
+ hasAttemptedSessionLoad = true
3663
+ if (shouldMarkError) {
3664
+ lastSessionLoadFailed = !requestSucceeded
3665
+ } else if (requestSucceeded) {
3666
+ lastSessionLoadFailed = false
3667
+ }
3668
+ }
3669
+ if (sessionFetchAbortController === controller) {
3670
+ sessionFetchAbortController = null
3671
+ }
3672
+ refreshSessionPromise = null
3673
+ }
3674
+ if (append) {
3675
+ sessionAppendInProgress = false
3676
+ }
3677
+ adjustSessionRefreshActivity(-1)
2705
3678
  }
2706
3679
  })()
2707
3680
  return refreshSessionPromise
2708
3681
  }
2709
- const findLatestSessionByProviderAndCwd = (providerKey, cwd, excludeUri = "") => {
3682
+ const loadMoreSessionItems = async () => {
3683
+ if (!sessionHasMore || sessionAppendInProgress) {
3684
+ return false
3685
+ }
3686
+ const loaded = await refreshSessionItems({ append: true })
3687
+ if (loaded) {
3688
+ refreshChooserRows()
3689
+ }
3690
+ return loaded
3691
+ }
3692
+ const maybeLoadMoreSessionsForColumn = (column) => {
3693
+ if (!column || !column.isConnected || column.dataset.state !== "chooser") {
3694
+ return
3695
+ }
3696
+ const liveQuery = normalizeIndex(getSessionSearchQuery()).trim()
3697
+ if (liveQuery !== currentSessionQuery) {
3698
+ return
3699
+ }
3700
+ if (!sessionHasMore || sessionAppendInProgress) {
3701
+ return
3702
+ }
3703
+ const listWrap = column.querySelector(".terminals-list-wrap")
3704
+ if (!listWrap) {
3705
+ return
3706
+ }
3707
+ const remaining = listWrap.scrollHeight - (listWrap.scrollTop + listWrap.clientHeight)
3708
+ if (remaining > SESSION_LOAD_MORE_THRESHOLD_PX) {
3709
+ return
3710
+ }
3711
+ void loadMoreSessionItems()
3712
+ }
3713
+ const findLatestSessionByProviderAndCwdIn = (sessions, providerKey, cwd, excludeUri = "") => {
2710
3714
  const normalizedProvider = normalizeProviderKey(providerKey)
2711
- const normalizedCwd = normalizeIndex(cwd)
3715
+ const normalizedCwd = normalizeCwdKey(cwd)
2712
3716
  if (!normalizedProvider || !normalizedCwd) {
2713
3717
  return null
2714
3718
  }
2715
3719
  let best = null
2716
- for (let i = 0; i < sessionItems.length; i++) {
2717
- const session = sessionItems[i]
3720
+ for (let i = 0; i < sessions.length; i++) {
3721
+ const session = sessions[i]
2718
3722
  if (!session) {
2719
3723
  continue
2720
3724
  }
2721
3725
  if (normalizeProviderKey(getProviderKey(session)) !== normalizedProvider) {
2722
3726
  continue
2723
3727
  }
2724
- if (normalizeIndex(session.cwd) !== normalizedCwd) {
3728
+ if (normalizeCwdKey(session.cwd) !== normalizedCwd) {
2725
3729
  continue
2726
3730
  }
2727
3731
  if (excludeUri && String(session.uri || "") === excludeUri) {
@@ -2733,10 +3737,16 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2733
3737
  }
2734
3738
  return best
2735
3739
  }
3740
+ const findLatestSessionByProviderAndCwd = (providerKey, cwd, excludeUri = "") => {
3741
+ return findLatestSessionByProviderAndCwdIn(sessionItems, providerKey, cwd, excludeUri)
3742
+ }
2736
3743
  const waitForDiscoveredSession = async (providerKey, cwd, excludeUri = "") => {
2737
3744
  const maxAttempts = 10
2738
3745
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2739
- await refreshSessionItems()
3746
+ await refreshSessionItems({
3747
+ force: attempt === 0,
3748
+ limit: getCurrentSessionRefreshLimit()
3749
+ })
2740
3750
  const found = findLatestSessionByProviderAndCwd(providerKey, cwd, excludeUri)
2741
3751
  if (found) {
2742
3752
  return found
@@ -2745,6 +3755,70 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2745
3755
  }
2746
3756
  return null
2747
3757
  }
3758
+ const syncDiscoveredSessionForOpenColumn = (column, discoveredSession) => {
3759
+ if (!column || !column.isConnected || !discoveredSession) {
3760
+ return
3761
+ }
3762
+ const discovered = normalizeSessionItem(discoveredSession, 0)
3763
+ const previousIndex = normalizeIndex(column.dataset.sessionIndex)
3764
+ const previousUri = normalizeIndex(column.dataset.sessionUri)
3765
+ const discoveredIndex = normalizeIndex(discovered.index)
3766
+ const discoveredUri = normalizeIndex(discovered.uri)
3767
+ const runningUrl = normalizeIndex(column.dataset.sessionUrl)
3768
+
3769
+ let mergedInList = false
3770
+ for (let i = 0; i < sessionItems.length; i++) {
3771
+ const item = sessionItems[i]
3772
+ if (!item) {
3773
+ continue
3774
+ }
3775
+ const itemIndex = normalizeIndex(item.index)
3776
+ const itemUri = normalizeIndex(item.uri)
3777
+ const matchesByIndex = Boolean(
3778
+ (previousIndex && itemIndex && itemIndex === previousIndex) ||
3779
+ (discoveredIndex && itemIndex && itemIndex === discoveredIndex)
3780
+ )
3781
+ const matchesByUri = Boolean(
3782
+ (previousUri && itemUri && itemUri === previousUri) ||
3783
+ (discoveredUri && itemUri && itemUri === discoveredUri)
3784
+ )
3785
+ if (!matchesByIndex && !matchesByUri) {
3786
+ continue
3787
+ }
3788
+ sessionItems[i] = {
3789
+ ...item,
3790
+ ...discovered,
3791
+ url: discovered.url || item.url
3792
+ }
3793
+ mergedInList = true
3794
+ break
3795
+ }
3796
+ if (!mergedInList) {
3797
+ sessionItems.unshift(discovered)
3798
+ }
3799
+
3800
+ if (previousIndex && discoveredIndex && previousIndex !== discoveredIndex) {
3801
+ setOpenState(previousIndex, "remove", column)
3802
+ }
3803
+ column.dataset.sessionIndex = discoveredIndex || previousIndex
3804
+ column.dataset.sessionName = discovered.name || column.dataset.sessionName || "Session"
3805
+ column.dataset.sessionCwd = discovered.cwd || column.dataset.sessionCwd || ""
3806
+ column.dataset.sessionUri = discoveredUri || previousUri
3807
+ column.dataset.sessionUrl = runningUrl || discovered.url || ""
3808
+ column.dataset.sessionForkUrl = discovered.forkUrl || ""
3809
+ column.dataset.sessionForkCapable = parseBooleanFlag(discovered.forkCapable, true) ? "1" : "0"
3810
+ column.dataset.sessionForkDisabledReason = discovered.forkDisabledReason || ""
3811
+
3812
+ const providerKey = getProviderKey(discovered) || normalizeIndex(column.dataset.sessionProviderKey).toLowerCase()
3813
+ column.dataset.sessionProviderKey = providerKey
3814
+ column.dataset.sessionProviderLabel = discovered.providerLabel || column.dataset.sessionProviderLabel || providerKey || "Session"
3815
+ column.dataset.sessionProviderIcon = getProviderIconSrc(discovered) || getProviderIconByKey(providerKey) || column.dataset.sessionProviderIcon || ""
3816
+
3817
+ if (column.dataset.sessionIndex) {
3818
+ setOpenState(column.dataset.sessionIndex, "add", column)
3819
+ }
3820
+ syncLocalOnlineState(discovered, true)
3821
+ }
2748
3822
 
2749
3823
  const startProviderSession = async (column, providerKey, startButton, providerSelect, selectedSkillIds = [], uploadToken = "") => {
2750
3824
  const normalizedProvider = normalizeIndex(providerKey).toLowerCase()
@@ -2794,9 +3868,6 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2794
3868
  online: true
2795
3869
  }
2796
3870
  sessionItems.unshift(normalizeSessionItem(session, 0))
2797
- for (let i = 0; i < sessionItems.length; i++) {
2798
- sessionItems[i].index = i
2799
- }
2800
3871
  openItemInColumn(column, session)
2801
3872
  const optimisticUri = session.uri
2802
3873
  const sessionCwd = payload.cwd || ""
@@ -2818,10 +3889,10 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2818
3889
  }
2819
3890
  return
2820
3891
  }
2821
- openItemInColumn(column, discovered)
3892
+ syncDiscoveredSessionForOpenColumn(column, discovered)
2822
3893
  refreshChooserRows()
2823
3894
  })()
2824
- void refreshSessionItems({ force: true }).then((refreshed) => {
3895
+ void refreshSessionItems({ sync: true, limit: getCurrentSessionRefreshLimit() }).then((refreshed) => {
2825
3896
  if (refreshed) {
2826
3897
  refreshChooserRows()
2827
3898
  }
@@ -2885,7 +3956,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2885
3956
  if (isFrameSessionAlreadyStopped(frame)) {
2886
3957
  closeColumn(column)
2887
3958
  syncLocalOnlineState(columnSession, false)
2888
- void refreshSessionItems({ force: true }).then((refreshed) => {
3959
+ void refreshSessionItems({ sync: true, limit: getCurrentSessionRefreshLimit() }).then((refreshed) => {
2889
3960
  if (refreshed) {
2890
3961
  refreshChooserRows()
2891
3962
  }
@@ -2908,7 +3979,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2908
3979
  }
2909
3980
  closeColumn(column)
2910
3981
  syncLocalOnlineState(columnSession, false)
2911
- void refreshSessionItems({ force: true }).then((refreshed) => {
3982
+ void refreshSessionItems({ sync: true, limit: getCurrentSessionRefreshLimit() }).then((refreshed) => {
2912
3983
  if (refreshed) {
2913
3984
  refreshChooserRows()
2914
3985
  }
@@ -2977,6 +4048,55 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2977
4048
  return row
2978
4049
  }
2979
4050
 
4051
+ const setPlaceholderState = (placeholder, state, details = {}) => {
4052
+ if (!placeholder) {
4053
+ return
4054
+ }
4055
+ const normalizedState = normalizeIndex(state) || "empty"
4056
+ const title = details && typeof details.title === "string" ? details.title : "Session history"
4057
+ const message = details && typeof details.message === "string" ? details.message : ""
4058
+ const hint = details && typeof details.hint === "string" ? details.hint : ""
4059
+ let iconClass = "fa-solid fa-clock-rotate-left"
4060
+ if (normalizedState === "loading") {
4061
+ iconClass = "fa-solid fa-circle-notch fa-spin"
4062
+ } else if (normalizedState === "error") {
4063
+ iconClass = "fa-solid fa-triangle-exclamation"
4064
+ } else if (normalizedState === "search-empty") {
4065
+ iconClass = "fa-solid fa-magnifying-glass"
4066
+ }
4067
+ const hintHtml = hint ? `<div class="terminals-empty-hint">${escapeHtml(hint)}</div>` : ""
4068
+ placeholder.dataset.state = normalizedState
4069
+ placeholder.innerHTML = `
4070
+ <div class="terminals-empty-card">
4071
+ <div class="terminals-empty-icon" aria-hidden="true"><i class="${iconClass}"></i></div>
4072
+ <h1 class="terminals-empty-title">${escapeHtml(title)}</h1>
4073
+ <div class="terminals-empty-message">${escapeHtml(message)}</div>
4074
+ ${hintHtml}
4075
+ </div>
4076
+ `
4077
+ }
4078
+ const makeListIntroNode = () => {
4079
+ const intro = document.createElement("div")
4080
+ intro.className = "terminals-list-intro"
4081
+ if (sessionRefreshActivityCount > 0) {
4082
+ intro.classList.add("is-loading")
4083
+ }
4084
+ intro.innerHTML = `
4085
+ <div class="terminals-list-intro-progress" aria-hidden="true"></div>
4086
+ <div class="terminals-list-intro-head">
4087
+ <div class="terminals-list-intro-title">
4088
+ <h2>Agent Workspaces</h2>
4089
+ <span class="terminals-list-intro-refresh" role="status" aria-live="polite">
4090
+ <i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
4091
+ <span>Refreshing</span>
4092
+ </span>
4093
+ </div>
4094
+ </div>
4095
+ <p>Resume or fork existing sessions, or start a new workspace powered by Codex, Claude, or Gemini.</p>
4096
+ `
4097
+ return intro
4098
+ }
4099
+
2980
4100
  const buildRows = (column, query = "") => {
2981
4101
  const list = column.querySelector(".terminals-list")
2982
4102
  const placeholder = column.querySelector(".terminals-empty")
@@ -2997,6 +4117,10 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
2997
4117
  }
2998
4118
  visibleSessions.sort(compareSessionsForChooser)
2999
4119
  let visible = 0
4120
+ if (visibleSessions.length > 0) {
4121
+ const introNode = makeListIntroNode()
4122
+ list.appendChild(introNode)
4123
+ }
3000
4124
  for (let i = 0; i < visibleSessions.length; i++) {
3001
4125
  const session = visibleSessions[i]
3002
4126
  const row = makeRowNode(session, normalized)
@@ -3006,10 +4130,15 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3006
4130
  })
3007
4131
  const resumeButton = row.querySelector("[data-session-action='resume']")
3008
4132
  if (resumeButton) {
3009
- resumeButton.title = "Resume this session"
4133
+ const resumeAvailable = canSessionResume(session)
4134
+ resumeButton.disabled = !resumeAvailable
4135
+ resumeButton.title = resumeAvailable ? "Resume this session" : getResumeUnavailableReason(session)
3010
4136
  resumeButton.addEventListener("click", (event) => {
3011
4137
  event.preventDefault()
3012
4138
  event.stopPropagation()
4139
+ if (!resumeAvailable) {
4140
+ return
4141
+ }
3013
4142
  openItemInColumn(column, session)
3014
4143
  })
3015
4144
  }
@@ -3031,27 +4160,76 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3031
4160
  openItemInColumn(column, forkedSession)
3032
4161
  })
3033
4162
  }
3034
- row.setAttribute("aria-label", canSessionFork(session) ? `${session.name}. Use Resume or Fork.` : `${session.name}. Use Resume.`)
4163
+ const resumeAvailable = canSessionResume(session)
4164
+ const forkAvailable = canSessionFork(session)
4165
+ if (resumeAvailable && forkAvailable) {
4166
+ row.setAttribute("aria-label", `${session.name}. Use Resume or Fork.`)
4167
+ } else if (resumeAvailable) {
4168
+ row.setAttribute("aria-label", `${session.name}. Use Resume.`)
4169
+ } else {
4170
+ row.setAttribute("aria-label", `${session.name}. Resume unavailable.`)
4171
+ }
3035
4172
  list.appendChild(row)
3036
4173
  visible += 1
3037
4174
  }
4175
+ if (visible > 0 && (sessionAppendInProgress || sessionHasMore)) {
4176
+ const loadMoreRow = document.createElement("div")
4177
+ loadMoreRow.className = "terminals-load-more-indicator"
4178
+ loadMoreRow.textContent = sessionAppendInProgress ? "Loading more sessions..." : "Scroll to load more sessions."
4179
+ list.appendChild(loadMoreRow)
4180
+ }
3038
4181
 
3039
4182
  if (!placeholder) {
3040
4183
  return
3041
4184
  }
3042
4185
 
4186
+ if (!hasAttemptedSessionLoad) {
4187
+ setPlaceholderState(placeholder, "loading", {
4188
+ title: "Loading session history",
4189
+ message: "Finding recent Codex, Claude, and Gemini sessions.",
4190
+ hint: "You can continue using Pinokio while this finishes."
4191
+ })
4192
+ placeholder.classList.remove("hidden")
4193
+ return
4194
+ }
4195
+
3043
4196
  if (sessionItems.length === 0 || visible === 0) {
3044
- const title = placeholder.querySelector("h1")
3045
- const message = placeholder.querySelector("div")
4197
+ if (lastSessionLoadFailed && sessionItems.length === 0 && !normalized) {
4198
+ setPlaceholderState(placeholder, "error", {
4199
+ title: "Could not load sessions",
4200
+ message: "Session history is temporarily unavailable.",
4201
+ hint: "Click Refresh to retry."
4202
+ })
4203
+ placeholder.classList.remove("hidden")
4204
+ return
4205
+ }
3046
4206
  if (normalized && sessionItems.length > 0) {
3047
- if (title) title.innerText = "No matches."
3048
- if (message) message.innerText = "No sessions matched your search."
4207
+ if (sessionHasMore || sessionAppendInProgress) {
4208
+ setPlaceholderState(placeholder, "loading", {
4209
+ title: "Searching more sessions",
4210
+ message: sessionAppendInProgress
4211
+ ? "Loading additional sessions for your search."
4212
+ : "Scroll to load more results."
4213
+ })
4214
+ } else {
4215
+ setPlaceholderState(placeholder, "search-empty", {
4216
+ title: "No matching sessions",
4217
+ message: "No sessions matched your current search.",
4218
+ hint: "Try a provider name, session topic, or path keyword."
4219
+ })
4220
+ }
3049
4221
  } else if (sessionItems.length === 0) {
3050
- if (title) title.innerText = "Terminal sessions."
3051
- if (message) message.innerText = "No resumable Codex, Claude, or Gemini sessions were found."
4222
+ setPlaceholderState(placeholder, "empty", {
4223
+ title: "No sessions yet",
4224
+ message: "No resumable Codex, Claude, or Gemini sessions were found.",
4225
+ hint: "Start an agent once, then return here to resume it."
4226
+ })
3052
4227
  } else {
3053
- if (title) title.innerText = "Terminal sessions."
3054
- if (message) message.innerText = "No resumable Codex, Claude, or Gemini sessions were found."
4228
+ setPlaceholderState(placeholder, "empty", {
4229
+ title: "No sessions available",
4230
+ message: "No resumable sessions are currently available.",
4231
+ hint: ""
4232
+ })
3055
4233
  }
3056
4234
  placeholder.classList.remove("hidden")
3057
4235
  } else {
@@ -3064,6 +4242,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3064
4242
  const chooserColumns = columns.querySelectorAll(".terminals-column[data-state='chooser']")
3065
4243
  chooserColumns.forEach((chooserColumn) => {
3066
4244
  buildRows(chooserColumn, query)
4245
+ maybeLoadMoreSessionsForColumn(chooserColumn)
3067
4246
  })
3068
4247
  }
3069
4248
 
@@ -3080,36 +4259,55 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3080
4259
  </div>
3081
4260
  </header>` : ""}
3082
4261
  <div class="terminals-chooser-content">
3083
- <div class="terminals-empty">
3084
- <h1>Terminal sessions.</h1>
3085
- <br>
3086
- <div>No resumable Codex, Claude, or Gemini sessions were found.</div>
3087
- <span>Run one from your terminal and come back here to continue.</span>
3088
- </div>
4262
+ <div class="terminals-empty" data-state="loading" aria-live="polite"></div>
3089
4263
  <div class="terminals-list-wrap">
3090
4264
  <div class="terminals-list"></div>
3091
4265
  </div>
3092
4266
  </div>
3093
4267
  `
3094
4268
  const closeButton = column.querySelector(".close-column")
4269
+ const listWrap = column.querySelector(".terminals-list-wrap")
3095
4270
 
3096
4271
  if (closeButton) {
3097
4272
  closeButton.addEventListener("click", () => {
3098
4273
  removeColumn(column)
3099
4274
  })
3100
4275
  }
4276
+ if (listWrap) {
4277
+ listWrap.addEventListener("scroll", () => {
4278
+ maybeLoadMoreSessionsForColumn(column)
4279
+ })
4280
+ }
3101
4281
 
3102
4282
  const resolvedRefreshOptions = refreshOptions && typeof refreshOptions === "object" ? refreshOptions : {}
4283
+ const refreshQuery = getSessionSearchQuery()
4284
+ const resolvedRequestOptions = {
4285
+ ...resolvedRefreshOptions,
4286
+ query: refreshQuery
4287
+ }
3103
4288
  buildRows(column, getSessionSearchQuery())
3104
- refreshSessionItems(resolvedRefreshOptions).then(() => {
4289
+ refreshSessionItems(resolvedRequestOptions).then(() => {
3105
4290
  if (!column.isConnected || column.dataset.state !== "chooser") {
3106
4291
  return
3107
4292
  }
3108
4293
  buildRows(column, getSessionSearchQuery())
4294
+ maybeLoadMoreSessionsForColumn(column)
3109
4295
  if (launcherState.open) {
3110
4296
  renderLauncherSelectedChips()
3111
4297
  renderLauncherSkillList()
3112
4298
  }
4299
+ if (!hasTriggeredBackgroundRegistrySync) {
4300
+ hasTriggeredBackgroundRegistrySync = true
4301
+ void refreshSessionItems({
4302
+ sync: true,
4303
+ limit: getCurrentSessionRefreshLimit(),
4304
+ query: getSessionSearchQuery()
4305
+ }).then((refreshed) => {
4306
+ if (refreshed || hasAttemptedSessionLoad) {
4307
+ refreshChooserRows()
4308
+ }
4309
+ })
4310
+ }
3113
4311
  })
3114
4312
  syncActiveStates()
3115
4313
  ensureLayout()
@@ -3154,11 +4352,11 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3154
4352
  const ensureLayout = () => {
3155
4353
  if (columns.children.length === 0) {
3156
4354
  emptyMain.classList.remove("hidden")
3157
- columns.classList.add("hidden")
4355
+ columnsWrap.classList.add("hidden")
3158
4356
  renderTopSessionBar()
3159
4357
  return
3160
4358
  }
3161
- columns.classList.remove("hidden")
4359
+ columnsWrap.classList.remove("hidden")
3162
4360
  emptyMain.classList.add("hidden")
3163
4361
  renderTopSessionBar()
3164
4362
  }
@@ -3207,8 +4405,16 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3207
4405
  const rawGap = columnsStyle.getPropertyValue("gap") || columnsStyle.getPropertyValue("column-gap") || "10px"
3208
4406
  const parsedGap = parseFloat(rawGap)
3209
4407
  const gap = Number.isNaN(parsedGap) ? 10 : parsedGap
3210
- const available = Math.max(0, columns.clientWidth - (gap * Math.max(columnCount - 1, 0)))
3211
- const width = `${Math.max(0, available / columnCount)}px`
4408
+ const measuredWidth = columns.getBoundingClientRect().width
4409
+ const parentWidth = columns.parentElement ? columns.parentElement.getBoundingClientRect().width : 0
4410
+ const wrapWidth = columnsWrap ? columnsWrap.getBoundingClientRect().width : 0
4411
+ const fallbackViewportWidth = window.innerWidth || 0
4412
+ const containerWidth = Math.max(columns.clientWidth || 0, measuredWidth || 0, parentWidth || 0, wrapWidth || 0, fallbackViewportWidth || 0)
4413
+ if (!(containerWidth > 0)) {
4414
+ return
4415
+ }
4416
+ const available = Math.max(0, containerWidth - (gap * Math.max(columnCount - 1, 0)))
4417
+ const width = `${Math.max(1, available / columnCount)}px`
3212
4418
  Array.from(columns.children).forEach((col) => {
3213
4419
  col.style.flex = `0 0 ${width}`
3214
4420
  })
@@ -3233,6 +4439,9 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3233
4439
  }
3234
4440
 
3235
4441
  const openItemInColumn = (column, session) => {
4442
+ if (!canSessionResume(session)) {
4443
+ return
4444
+ }
3236
4445
  const existingColumn = getOpenColumnByIndex(session.index)
3237
4446
  if (existingColumn && existingColumn !== column) {
3238
4447
  existingColumn.scrollIntoView({ behavior: "smooth", inline: "end" })
@@ -3258,7 +4467,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3258
4467
  requestTerminalFocus(frame)
3259
4468
  }, 40)
3260
4469
  setTimeout(() => {
3261
- void refreshSessionItems({ force: true }).then((refreshed) => {
4470
+ void refreshSessionItems({ sync: true, limit: getCurrentSessionRefreshLimit() }).then((refreshed) => {
3262
4471
  if (refreshed) {
3263
4472
  refreshChooserRows()
3264
4473
  }
@@ -3353,7 +4562,19 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3353
4562
 
3354
4563
  if (sessionSearchInput) {
3355
4564
  sessionSearchInput.addEventListener("input", () => {
4565
+ if (sessionSearchDebounceTimer) {
4566
+ clearTimeout(sessionSearchDebounceTimer)
4567
+ }
4568
+ const nextQuery = sessionSearchInput.value || ""
3356
4569
  refreshChooserRows()
4570
+ sessionSearchDebounceTimer = setTimeout(() => {
4571
+ sessionSearchDebounceTimer = null
4572
+ void refreshSessionItems({ query: nextQuery }).then((loaded) => {
4573
+ if (loaded || hasAttemptedSessionLoad) {
4574
+ refreshChooserRows()
4575
+ }
4576
+ })
4577
+ }, SESSION_SEARCH_DEBOUNCE_MS)
3357
4578
  })
3358
4579
  }
3359
4580
 
@@ -3390,10 +4611,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3390
4611
  })
3391
4612
 
3392
4613
  const setViewport = () => {
3393
- const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight
3394
- const headerHeight = navHeader ? navHeader.getBoundingClientRect().height : 0
3395
- const frameHeight = Math.max(0, viewportHeight - headerHeight)
3396
- root.style.setProperty("--terminals-viewport-height", `${frameHeight}px`)
4614
+ closeTopSessionOverflowMenu()
3397
4615
  setColumnLayout()
3398
4616
  }
3399
4617
 
@@ -3407,7 +4625,7 @@ body.dark .swal2-popup.terminals-confirm-modal .swal2-styled.swal2-cancel {
3407
4625
  window.visualViewport.addEventListener("scroll", setViewport)
3408
4626
  }
3409
4627
 
3410
- addChooserColumn({ force: true })
4628
+ addChooserColumn()
3411
4629
  refreshChooserRows()
3412
4630
  if (sessionSearchInput) {
3413
4631
  requestAnimationFrame(() => {