pinokiod 3.170.0 → 3.181.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.
@@ -905,6 +905,9 @@ body.dark .tab-link-popover .tab-link-popover-header {
905
905
  background: transparent;
906
906
  cursor: pointer;
907
907
  }
908
+ .tab-link-popover .tab-link-popover-item.qr-inline { flex-direction: row; align-items: center; gap: 10px; }
909
+ .tab-link-popover .tab-link-popover-item.qr-inline .textcol { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
910
+ .tab-link-popover .tab-link-popover-item.qr-inline .qr { width: 64px; height: 64px; image-rendering: pixelated; flex: 0 0 auto; margin-left: auto; }
908
911
  .tab-link-popover .tab-link-popover-item:hover,
909
912
  .tab-link-popover .tab-link-popover-item:focus-visible {
910
913
  background: rgba(15, 23, 42, 0.06);
@@ -1759,6 +1762,10 @@ body.dark .swal2-html-container label {
1759
1762
  --pinokio-custom-commit-close-color: rgba(71, 85, 105, 0.75);
1760
1763
  --pinokio-custom-commit-close-hover-bg: rgba(148, 163, 184, 0.18);
1761
1764
  --pinokio-custom-commit-close-hover-color: #0f172a;
1765
+ --pinokio-custom-commit-action-bg: rgba(59, 130, 246, 0.12);
1766
+ --pinokio-custom-commit-action-border: rgba(59, 130, 246, 0.35);
1767
+ --pinokio-custom-commit-action-color: #1d4ed8;
1768
+ --pinokio-custom-commit-action-hover: rgba(59, 130, 246, 0.18);
1762
1769
  --pinokio-commit-item-bg: rgba(226, 232, 240, 0.6);
1763
1770
  --pinokio-commit-item-hover-bg: rgba(59, 130, 246, 0.12);
1764
1771
  --pinokio-commit-item-text: #0f172a;
@@ -1837,6 +1844,10 @@ body.dark {
1837
1844
  --pinokio-custom-commit-close-color: rgba(226, 232, 240, 0.7);
1838
1845
  --pinokio-custom-commit-close-hover-bg: rgba(148, 163, 184, 0.18);
1839
1846
  --pinokio-custom-commit-close-hover-color: #f8fafc;
1847
+ --pinokio-custom-commit-action-bg: rgba(56, 189, 248, 0.12);
1848
+ --pinokio-custom-commit-action-border: rgba(56, 189, 248, 0.35);
1849
+ --pinokio-custom-commit-action-color: #7dd3fc;
1850
+ --pinokio-custom-commit-action-hover: rgba(56, 189, 248, 0.18);
1840
1851
  --pinokio-commit-item-bg: rgba(15, 23, 42, 0.35);
1841
1852
  --pinokio-commit-item-hover-bg: rgba(37, 99, 235, 0.16);
1842
1853
  --pinokio-commit-item-text: #f8fafc;
@@ -2173,6 +2184,70 @@ body.dark {
2173
2184
  flex-wrap: wrap;
2174
2185
  }
2175
2186
 
2187
+ .pinokio-history-latest-banner {
2188
+ display: flex;
2189
+ justify-content: space-between;
2190
+ align-items: center;
2191
+ padding: 12px 20px;
2192
+ margin: 12px 20px 0;
2193
+ background: rgba(59, 130, 246, 0.08);
2194
+ border: 1px solid rgba(59, 130, 246, 0.18);
2195
+ border-radius: 10px;
2196
+ color: var(--pinokio-modal-title-color);
2197
+ }
2198
+
2199
+ .pinokio-history-latest-text {
2200
+ font-size: 13px;
2201
+ font-weight: 600;
2202
+ }
2203
+
2204
+ .pinokio-history-latest-btn {
2205
+ display: inline-flex;
2206
+ align-items: center;
2207
+ gap: 6px;
2208
+ background: rgba(59, 130, 246, 0.18);
2209
+ border: 1px solid rgba(59, 130, 246, 0.35);
2210
+ color: #1d4ed8;
2211
+ padding: 6px 14px;
2212
+ border-radius: 6px;
2213
+ font-weight: 600;
2214
+ font-size: 13px;
2215
+ cursor: pointer;
2216
+ transition: background 0.15s ease, border 0.15s ease, transform 0.15s ease;
2217
+ }
2218
+
2219
+ .pinokio-history-latest-btn:hover {
2220
+ background: rgba(59, 130, 246, 0.24);
2221
+ transform: translateY(-1px);
2222
+ }
2223
+
2224
+ .pinokio-history-latest-btn:focus-visible {
2225
+ outline: 2px solid rgba(59, 130, 246, 0.45);
2226
+ outline-offset: 3px;
2227
+ }
2228
+
2229
+ .pinokio-history-latest-banner--disabled {
2230
+ opacity: 0.6;
2231
+ }
2232
+
2233
+ .pinokio-history-latest-banner--disabled .pinokio-history-latest-btn {
2234
+ pointer-events: none;
2235
+ cursor: not-allowed;
2236
+ }
2237
+
2238
+ .pinokio-history-actions {
2239
+ display: inline-flex;
2240
+ align-items: center;
2241
+ gap: 10px;
2242
+ }
2243
+
2244
+ .pinokio-history-branch-select.pinokio-modal-input {
2245
+ width: auto;
2246
+ min-width: 160px;
2247
+ padding: 6px 10px;
2248
+ font-size: 13px;
2249
+ }
2250
+
2176
2251
  .pinokio-pill {
2177
2252
  display: inline-flex;
2178
2253
  align-items: center;
@@ -2553,6 +2628,104 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2553
2628
  flex: 1;
2554
2629
  overflow: hidden;
2555
2630
  padding: 24px;
2631
+ display: flex;
2632
+ flex-direction: column;
2633
+ gap: 16px;
2634
+ }
2635
+
2636
+ .pinokio-git-commit-actions {
2637
+ display: flex;
2638
+ justify-content: flex-end;
2639
+ }
2640
+
2641
+ .pinokio-git-commit-switch-btn {
2642
+ display: inline-flex;
2643
+ align-items: center;
2644
+ gap: 6px;
2645
+ background: var(--pinokio-custom-commit-action-bg);
2646
+ border: 1px solid var(--pinokio-custom-commit-action-border);
2647
+ color: var(--pinokio-custom-commit-action-color);
2648
+ padding: 6px 14px;
2649
+ border-radius: 6px;
2650
+ font-weight: 600;
2651
+ font-size: 13px;
2652
+ cursor: pointer;
2653
+ transition: background 0.15s ease, border 0.15s ease, transform 0.15s ease;
2654
+ }
2655
+
2656
+ .pinokio-git-commit-switch-btn:hover {
2657
+ background: var(--pinokio-custom-commit-action-hover);
2658
+ transform: translateY(-1px);
2659
+ }
2660
+
2661
+ .pinokio-git-commit-switch-btn:focus-visible {
2662
+ outline: 2px solid var(--pinokio-custom-commit-action-color);
2663
+ outline-offset: 3px;
2664
+ }
2665
+
2666
+ .pinokio-custom-terminal-overlay {
2667
+ position: fixed;
2668
+ inset: 0;
2669
+ background: rgba(9, 11, 15, 0.7);
2670
+ display: flex;
2671
+ align-items: center;
2672
+ justify-content: center;
2673
+ z-index: 999999;
2674
+ }
2675
+
2676
+ .pinokio-custom-terminal-modal {
2677
+ width: min(900px, 92vw);
2678
+ height: min(620px, 88vh);
2679
+ background: var(--pinokio-modal-surface-bg, #101522);
2680
+ color: var(--pinokio-modal-title-color);
2681
+ border-radius: 18px;
2682
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.55);
2683
+ display: flex;
2684
+ flex-direction: column;
2685
+ overflow: hidden;
2686
+ border: 1px solid rgba(76, 137, 251, 0.2);
2687
+ }
2688
+
2689
+ .pinokio-custom-terminal-header {
2690
+ display: flex;
2691
+ justify-content: space-between;
2692
+ align-items: center;
2693
+ padding: 16px 20px;
2694
+ background: rgba(30, 41, 59, 0.8);
2695
+ border-bottom: 1px solid rgba(76, 137, 251, 0.25);
2696
+ }
2697
+
2698
+ .pinokio-custom-terminal-header h3 {
2699
+ margin: 0;
2700
+ font-size: 15px;
2701
+ font-weight: 600;
2702
+ }
2703
+
2704
+ .pinokio-custom-terminal-close {
2705
+ background: none;
2706
+ border: none;
2707
+ font-size: 26px;
2708
+ cursor: pointer;
2709
+ padding: 6px 10px;
2710
+ border-radius: 12px;
2711
+ color: rgba(226, 232, 240, 0.7);
2712
+ transition: background 0.2s ease, color 0.2s ease;
2713
+ }
2714
+
2715
+ .pinokio-custom-terminal-close:hover {
2716
+ background: rgba(148, 163, 184, 0.18);
2717
+ color: #f8fafc;
2718
+ }
2719
+
2720
+ .pinokio-custom-terminal-body {
2721
+ flex: 1;
2722
+ background: #0b1120;
2723
+ }
2724
+
2725
+ .pinokio-custom-terminal-body iframe {
2726
+ width: 100%;
2727
+ height: 100%;
2728
+ border: none;
2556
2729
  }
2557
2730
 
2558
2731
  .pinokio-git-commit-item {
@@ -2800,6 +2973,15 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2800
2973
  overflow: auto;
2801
2974
  flex-wrap: nowrap;
2802
2975
  }
2976
+ /* Keep minimized header horizontal and compact on small screens */
2977
+ header.navheader.minimized,
2978
+ header.navheader.minimized h1 {
2979
+ display: inline-flex;
2980
+ flex-direction: row;
2981
+ }
2982
+ header.navheader.minimized h1 .btn2 {
2983
+ width: auto;
2984
+ }
2803
2985
  .appcanvas {
2804
2986
  margin-left: 0;
2805
2987
  flex: 1 1 auto;
@@ -2848,10 +3030,12 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2848
3030
  #fs-fork-btn .fs-status-label i {
2849
3031
  font-size: 1rem;
2850
3032
  }
2851
- #fs-status .fs-status-btn .disk-usage,
2852
- #fs-status .fs-status-btn .badge {
3033
+ #fs-status .fs-status-btn .disk-usage {
2853
3034
  display: none;
2854
3035
  }
3036
+ #fs-status .git-changes .badge {
3037
+ display: inline-flex;
3038
+ }
2855
3039
  }
2856
3040
  /*
2857
3041
  @media only screen and (max-width: 800px) {
@@ -2904,7 +3088,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2904
3088
  <button class='btn2' id='screenshot' data-tippy-content="take a screenshot"><i class="fa-solid fa-camera"></i></button>
2905
3089
  <div class='sep'></div>
2906
3090
  <div class='mode-selector'>
2907
- <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Community</div></a>
3091
+ <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Forum</div></a>
3092
+ <a class="btn2 <%=type === 'files' ? 'selected' : ''%>" href="<%=files_tab%>"><div><i class="fa-solid fa-file-lines"></i></div><div class='caption'>Files</div></a>
2908
3093
  <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
2909
3094
  <a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div class='caption'>Run</div></a>
2910
3095
  </div>
@@ -2931,6 +3116,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2931
3116
  </div>
2932
3117
  </div>
2933
3118
  <% } else { %>
3119
+ <% if (type !== 'files') { %>
2934
3120
  <aside class='active'>
2935
3121
  <!--
2936
3122
  <div class='header-top header-item'>
@@ -2963,15 +3149,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2963
3149
  <div class='loader'><i class='fa-solid fa-angle-right'></i></div>
2964
3150
  -->
2965
3151
  </a>
2966
- <a id='editortab' data-mode="refresh" target="<%=editor_tab%>" href="<%=editor_tab%>" class="btn header-item frame-link edit-tab" data-index="11">
2967
- <div class='tab'>
2968
- <i class="fa-solid fa-file-lines"></i>
2969
- <div class='display'>Files</div>
2970
- <div class='tab-metric'>
2971
- <span class='disk-usage tab-metric__value' data-path="/">--</span>
2972
- </div>
2973
- </div>
2974
- </a>
3152
+
2975
3153
  <div class="dynamic <%=type==='run' ? '' : 'selected'%>">
2976
3154
  <div class='submenu'>
2977
3155
  <% if (plugin_menu) { %>
@@ -3006,6 +3184,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3006
3184
  </div>
3007
3185
  </div>
3008
3186
  </aside>
3187
+ <% } %>
3009
3188
  <% if (type === "run") { %>
3010
3189
  <div class='appcanvas_filler'></div>
3011
3190
  <% } %>
@@ -3024,6 +3203,62 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3024
3203
  </div>
3025
3204
  </div>
3026
3205
  -->
3206
+ <div class='fs-status-dropdown fs-open-explorer'>
3207
+ <button class='fs-status-btn' data-filepath="<%=path%>" type='button'>
3208
+ <span class='fs-status-label'>
3209
+ <i class="fa-solid fa-folder-open"></i>
3210
+ <span class='fs-status-title'>Open in File Explorer</span>
3211
+ </span>
3212
+ </button>
3213
+ </div>
3214
+ <div class='fs-status-dropdown git-changes'>
3215
+ <button id='fs-changes-btn' class='fs-status-btn revealer' data-group='#fs-changes-menu' type='button'>
3216
+ <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
3217
+ <div class='badge'></div>
3218
+ </button>
3219
+ <div class='fs-dropdown-menu submenu hidden' id='fs-changes-menu'></div>
3220
+ </div>
3221
+ <div class='fs-status-dropdown git-fork'>
3222
+ <button id='fs-fork-btn' class='fs-status-btn revealer' data-group='#fs-fork-menu' type='button'>
3223
+ <span class='fs-status-label'>
3224
+ <i class="fa-solid fa-code-branch"></i>
3225
+ <span class='fs-status-title'>Fork</span>
3226
+ </span>
3227
+ </button>
3228
+ <div class='fs-dropdown-menu submenu hidden' id='fs-fork-menu'></div>
3229
+ </div>
3230
+ <div class='fs-status-dropdown git-publish'>
3231
+ <button id='fs-push-btn' class='fs-status-btn revealer' data-group='#fs-push-menu' type='button'>
3232
+ <span class='fs-status-label'>
3233
+ <i class="fa-brands fa-github"></i>
3234
+ <span class='fs-status-title'>Publish</span>
3235
+ </span>
3236
+ </button>
3237
+ <div class='fs-dropdown-menu submenu hidden' id='fs-push-menu'></div>
3238
+ </div>
3239
+ </div>
3240
+ <% } else if (type === 'files') { %>
3241
+ <div id='fs-status' data-workspace="<%=name%>" data-create-uri="<%=git_create_url%>" data-history-uri="<%=git_history_url%>" data-status-uri="<%=git_status_url%>" data-uri="<%=git_monitor_url%>" data-push-uri="<%=git_push_url%>" data-fork-uri="<%=git_fork_url%>">
3242
+ <!--
3243
+ <div class='fs-status-dropdown nested-menu git blue'>
3244
+ <button type='button' class='fs-status-btn frame-link reveal'>
3245
+ <span class='fs-status-label'>
3246
+ <i class="fa-brands fa-git-alt"></i>
3247
+ Git
3248
+ </span>
3249
+ </button>
3250
+ <div class='fs-dropdown-menu submenu hidden' id='git-repos'>
3251
+ </div>
3252
+ </div>
3253
+ -->
3254
+ <div class='fs-status-dropdown fs-open-explorer'>
3255
+ <button class='fs-status-btn' data-filepath="<%=path%>" type='button'>
3256
+ <span class='fs-status-label'>
3257
+ <i class="fa-solid fa-folder-open"></i>
3258
+ <span class='fs-status-title'>Open in File Explorer</span>
3259
+ </span>
3260
+ </button>
3261
+ </div>
3027
3262
  <div class='fs-status-dropdown git-changes'>
3028
3263
  <button id='fs-changes-btn' class='fs-status-btn revealer' data-group='#fs-changes-menu' type='button'>
3029
3264
  <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
@@ -3052,6 +3287,9 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3052
3287
  </div>
3053
3288
  <% } %>
3054
3289
  <main class='browserview'>
3290
+ <% if (type === 'files') { %>
3291
+ <iframe class='selected' src="<%=editor_tab%>"></iframe>
3292
+ <% } %>
3055
3293
  </main>
3056
3294
  </div>
3057
3295
  </div>
@@ -3295,6 +3533,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3295
3533
  return false
3296
3534
  }
3297
3535
 
3536
+ const isIPv4Host = (host) => /^(\d{1,3}\.){3}\d{1,3}$/.test((host || '').trim())
3537
+
3298
3538
  const extractProjectSlug = (node) => {
3299
3539
  if (!node) {
3300
3540
  return ""
@@ -3449,22 +3689,36 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3449
3689
  if (!trimmed) {
3450
3690
  return ""
3451
3691
  }
3452
- if (!/^https?:\/\//i.test(trimmed)) {
3453
- trimmed = `https://${trimmed}`
3454
- } else {
3455
- trimmed = trimmed.replace(/^http:/i, "https:")
3692
+ // If it's already a URL, ensure it's HTTPS and not an IP host
3693
+ if (/^https?:\/\//i.test(trimmed)) {
3694
+ try {
3695
+ const parsed = new URL(trimmed)
3696
+ const host = (parsed.hostname || '').toLowerCase()
3697
+ if (!host || isIPv4Host(host)) {
3698
+ return ""
3699
+ }
3700
+ // Only accept domains (prefer *.localhost) for HTTPS targets
3701
+ if (!(host === 'localhost' || host.endsWith('.localhost') || host.includes('.'))) {
3702
+ return ""
3703
+ }
3704
+ let pathname = parsed.pathname || ""
3705
+ if (pathname === "/") pathname = ""
3706
+ const search = parsed.search || ""
3707
+ return `https://${host}${pathname}${search}`
3708
+ } catch (_) {
3709
+ return ""
3710
+ }
3456
3711
  }
3712
+ // Not a full URL: accept plain domains (prefer *.localhost), reject IPs
3457
3713
  try {
3458
- const parsed = new URL(trimmed)
3459
- if (!parsed.host) {
3714
+ const hostCandidate = trimmed.split('/')[0].toLowerCase()
3715
+ if (!hostCandidate || isIPv4Host(hostCandidate)) {
3460
3716
  return ""
3461
3717
  }
3462
- let pathname = parsed.pathname || ""
3463
- if (pathname === "/") {
3464
- pathname = ""
3718
+ if (!(hostCandidate === 'localhost' || hostCandidate.endsWith('.localhost') || hostCandidate.includes('.'))) {
3719
+ return ""
3465
3720
  }
3466
- const search = parsed.search || ""
3467
- return `https://${parsed.host}${pathname}${search}`
3721
+ return `https://${hostCandidate}`
3468
3722
  } catch (_) {
3469
3723
  return ""
3470
3724
  }
@@ -3523,7 +3777,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3523
3777
  const ensureRouterInfoMapping = async () => {
3524
3778
  const now = Date.now()
3525
3779
  if (!tabLinkRouterInfoPromise || now > tabLinkRouterInfoExpiry) {
3526
- tabLinkRouterInfoPromise = fetch("/info/system", {
3780
+ // Use lightweight router mapping to avoid favicon/installed overhead
3781
+ tabLinkRouterInfoPromise = fetch("/info/router", {
3527
3782
  method: "GET",
3528
3783
  headers: {
3529
3784
  "Accept": "application/json"
@@ -3545,6 +3800,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3545
3800
  : []
3546
3801
  const portMap = new Map()
3547
3802
  const hostPortMap = new Map()
3803
+ const externalHttpByExtPort = new Map() // ext port -> Set of host:port (external_ip)
3804
+ const externalHttpByIntPort = new Map() // internal port -> Set of host:port (external_ip)
3548
3805
  const hostAliasPortMap = new Map()
3549
3806
  if (data?.router && typeof data.router === "object") {
3550
3807
  Object.entries(data.router).forEach(([dial, hosts]) => {
@@ -3689,11 +3946,29 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3689
3946
  mergeTargets(entry.external_domain)
3690
3947
  mergeTargets(entry.https_href)
3691
3948
  mergeTargets(entry.app_href)
3692
- mergeTargets(entry.external_ip)
3693
- mergeTargets(entry.internal_router)
3694
- mergeTargets(entry.match)
3695
- mergeTargets(entry.host)
3949
+ // Some rewrite mapping entries expose domain candidates under `hosts`
3696
3950
  mergeTargets(entry.hosts)
3951
+ // Internal router can also include domain aliases (e.g., comfyui.localhost)
3952
+ mergeTargets(entry.internal_router)
3953
+
3954
+ // Record external http host:port candidates by external and internal ports for later
3955
+ if (entry.external_ip && typeof entry.external_ip === 'string') {
3956
+ const parsed = parseHostPort(entry.external_ip)
3957
+ if (parsed && parsed.port) {
3958
+ const keyExt = parsed.port
3959
+ if (!externalHttpByExtPort.has(keyExt)) {
3960
+ externalHttpByExtPort.set(keyExt, new Set())
3961
+ }
3962
+ externalHttpByExtPort.get(keyExt).add(`${parsed.host}:${parsed.port}`)
3963
+ const keyInt = String(entry.internal_port || '')
3964
+ if (keyInt) {
3965
+ if (!externalHttpByIntPort.has(keyInt)) {
3966
+ externalHttpByIntPort.set(keyInt, new Set())
3967
+ }
3968
+ externalHttpByIntPort.get(keyInt).add(`${parsed.host}:${parsed.port}`)
3969
+ }
3970
+ }
3971
+ }
3697
3972
 
3698
3973
  if (httpsTargets.size === 0) {
3699
3974
  return
@@ -3769,14 +4044,18 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3769
4044
 
3770
4045
  return {
3771
4046
  portMap,
3772
- hostPortMap
4047
+ hostPortMap,
4048
+ externalHttpByExtPort,
4049
+ externalHttpByIntPort
3773
4050
  }
3774
4051
  })
3775
4052
  .catch(() => {
3776
4053
  tabLinkRouterHttpsActive = null
3777
4054
  return {
3778
4055
  portMap: new Map(),
3779
- hostPortMap: new Map()
4056
+ hostPortMap: new Map(),
4057
+ externalHttpByExtPort: new Map(),
4058
+ externalHttpByIntPort: new Map()
3780
4059
  }
3781
4060
  })
3782
4061
  tabLinkRouterInfoExpiry = now + 3000
@@ -3829,7 +4108,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3829
4108
  const projectSlug = extractProjectSlug(link).toLowerCase()
3830
4109
  const entries = []
3831
4110
  const entryByUrl = new Map()
3832
- const addEntry = (type, label, url) => {
4111
+ const addEntry = (type, label, url, opts = {}) => {
3833
4112
  if (!url) {
3834
4113
  return
3835
4114
  }
@@ -3854,13 +4133,16 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3854
4133
  return
3855
4134
  }
3856
4135
  if (entryByUrl.has(canonical)) {
4136
+ const existing = entryByUrl.get(canonical)
4137
+ if (opts && opts.qr === true) existing.qr = true
3857
4138
  return
3858
4139
  }
3859
4140
  const entry = {
3860
4141
  type,
3861
4142
  label,
3862
4143
  url: canonical,
3863
- display: formatDisplayUrl(canonical)
4144
+ display: formatDisplayUrl(canonical),
4145
+ qr: opts && opts.qr === true
3864
4146
  }
3865
4147
  entryByUrl.set(canonical, entry)
3866
4148
  entries.push(entry)
@@ -3874,11 +4156,11 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3874
4156
  addEntry("url", "URL", baseHref)
3875
4157
  }
3876
4158
 
3877
- const httpCandidates = new Set()
4159
+ const httpCandidates = new Map() // url -> { qr: boolean }
3878
4160
  const httpsCandidates = new Set()
3879
4161
 
3880
4162
  if (isHttpUrl(baseHref)) {
3881
- httpCandidates.add(canonicalizeUrl(baseHref))
4163
+ httpCandidates.set(canonicalizeUrl(baseHref), { qr: false })
3882
4164
  } else if (isHttpsUrl(baseHref)) {
3883
4165
  httpsCandidates.add(canonicalizeUrl(baseHref))
3884
4166
  }
@@ -3896,7 +4178,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3896
4178
  const normalizedPath = pathname.toLowerCase()
3897
4179
  if (normalizedPath.includes(`/asset/api/${projectSlug}`)) {
3898
4180
  const fallbackHttp = `http://127.0.0.1:42000${pathname}`
3899
- httpCandidates.add(canonicalizeUrl(fallbackHttp))
4181
+ httpCandidates.set(canonicalizeUrl(fallbackHttp), { qr: false })
3900
4182
  }
3901
4183
  } catch (_) {
3902
4184
  // ignore fallback errors
@@ -3920,15 +4202,16 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3920
4202
  if (isHttpsUrl(canonical)) {
3921
4203
  httpsCandidates.add(canonical)
3922
4204
  } else if (isHttpUrl(canonical)) {
3923
- httpCandidates.add(canonical)
4205
+ const prev = httpCandidates.get(canonical)
4206
+ httpCandidates.set(canonical, { qr: prev ? prev.qr === true : false })
3924
4207
  }
3925
4208
  })
3926
4209
  })
3927
4210
  }
3928
4211
 
4212
+ const routerData = await ensureRouterInfoMapping()
3929
4213
  if (httpCandidates.size > 0) {
3930
- const routerData = await ensureRouterInfoMapping()
3931
- httpCandidates.forEach((httpUrl) => {
4214
+ Array.from(httpCandidates.keys()).forEach((httpUrl) => {
3932
4215
  const mapped = collectHttpsUrlsFromRouter(httpUrl, routerData)
3933
4216
  mapped.forEach((httpsUrl) => {
3934
4217
  httpsCandidates.add(httpsUrl)
@@ -3936,6 +4219,28 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3936
4219
  })
3937
4220
  }
3938
4221
 
4222
+ // Add external 192.168.* http host:port candidates mapped from the same internal port as base HTTP
4223
+ try {
4224
+ const base = new URL(baseHref, location.origin)
4225
+ let basePort = base.port
4226
+ if (!basePort) {
4227
+ basePort = base.protocol.toLowerCase() === 'https:' ? '443' : '80'
4228
+ }
4229
+ const samePortHosts = routerData && routerData.externalHttpByIntPort ? routerData.externalHttpByIntPort.get(basePort) : null
4230
+ if (samePortHosts && samePortHosts.size > 0) {
4231
+ samePortHosts.forEach((hostport) => {
4232
+ try {
4233
+ const hpUrl = `http://${hostport}${base.pathname || '/'}${base.search || ''}`
4234
+ const canonical = canonicalizeUrl(hpUrl)
4235
+ if (isHttpUrl(canonical)) {
4236
+ const prev = httpCandidates.get(canonical)
4237
+ httpCandidates.set(canonical, { qr: true || (prev ? prev.qr === true : false) })
4238
+ }
4239
+ } catch (_) {}
4240
+ })
4241
+ }
4242
+ } catch (_) {}
4243
+
3939
4244
  const httpsList = Array.from(httpsCandidates).sort()
3940
4245
 
3941
4246
  if (httpsList.length > 0) {
@@ -3950,17 +4255,20 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3950
4255
  }
3951
4256
  const hostPort = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
3952
4257
  const httpUrl = `http://${hostPort}${parsed.pathname || "/"}${parsed.search || ""}`
3953
- httpCandidates.add(canonicalizeUrl(httpUrl))
4258
+ const key = canonicalizeUrl(httpUrl)
4259
+ const prev = httpCandidates.get(key)
4260
+ httpCandidates.set(key, { qr: prev ? prev.qr === true : false })
3954
4261
  } catch (_) {
3955
4262
  // ignore failures
3956
4263
  }
3957
4264
  })
3958
4265
  }
3959
4266
 
3960
- const httpList = Array.from(httpCandidates).sort()
4267
+ const httpList = Array.from(httpCandidates.keys()).sort()
3961
4268
 
3962
4269
  httpList.forEach((url) => {
3963
- addEntry("http", "HTTP", url)
4270
+ const meta = httpCandidates.get(url) || { qr: false }
4271
+ addEntry("http", "HTTP", url, { qr: meta.qr === true })
3964
4272
  })
3965
4273
  httpsList.forEach((url) => {
3966
4274
  addEntry("https", "HTTPS", url)
@@ -4060,6 +4368,26 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
4060
4368
  tabLinkHideTimer = null
4061
4369
  }
4062
4370
 
4371
+ // Show lightweight loading popover immediately while mapping fetch runs
4372
+ try {
4373
+ const pop = ensureTabLinkPopoverEl()
4374
+ pop.innerHTML = ''
4375
+ const header = document.createElement('div')
4376
+ header.className = 'tab-link-popover-header'
4377
+ header.innerHTML = `<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Open in browser</span>`
4378
+ const item = document.createElement('div')
4379
+ item.className = 'tab-link-popover-item'
4380
+ const label = document.createElement('span')
4381
+ label.className = 'label'
4382
+ label.textContent = 'Loading…'
4383
+ const value = document.createElement('span')
4384
+ value.className = 'value muted'
4385
+ value.textContent = 'Discovering routes'
4386
+ item.append(label, value)
4387
+ pop.append(header, item)
4388
+ positionTabLinkPopover(pop, link)
4389
+ } catch (_) {}
4390
+
4063
4391
  let entries
4064
4392
  try {
4065
4393
  entries = await buildTabLinkEntries(link)
@@ -4143,7 +4471,6 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
4143
4471
  entries.forEach((entry) => {
4144
4472
  const item = document.createElement("button")
4145
4473
  item.type = "button"
4146
- item.className = "tab-link-popover-item"
4147
4474
  item.setAttribute("data-url", entry.url)
4148
4475
  const labelSpan = document.createElement("span")
4149
4476
  labelSpan.className = "label"
@@ -4151,7 +4478,24 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
4151
4478
  const valueSpan = document.createElement("span")
4152
4479
  valueSpan.className = "value"
4153
4480
  valueSpan.textContent = entry.display
4154
- item.append(labelSpan, valueSpan)
4481
+
4482
+ if (entry.type === 'http' && entry.qr === true) {
4483
+ item.className = "tab-link-popover-item qr-inline"
4484
+ const textCol = document.createElement('div')
4485
+ textCol.className = 'textcol'
4486
+ textCol.append(labelSpan, valueSpan)
4487
+ const qrImg = document.createElement('img')
4488
+ qrImg.className = 'qr'
4489
+ qrImg.alt = 'QR'
4490
+ qrImg.decoding = 'async'
4491
+ qrImg.loading = 'lazy'
4492
+ qrImg.src = `/qr?data=${encodeURIComponent(entry.url)}&s=4&m=0`
4493
+ item.append(textCol, qrImg)
4494
+ } else {
4495
+ item.className = "tab-link-popover-item"
4496
+ // Keep label and value as direct children so column layout applies
4497
+ item.append(labelSpan, valueSpan)
4498
+ }
4155
4499
  popover.appendChild(item)
4156
4500
  })
4157
4501
 
@@ -6517,7 +6861,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
6517
6861
  }
6518
6862
  });
6519
6863
  */
6520
- <% if (type === "browse") { %>
6864
+ <% if (type === "browse" || type === "files") { %>
6521
6865
  const repoStatusCache = new Map()
6522
6866
  let lastRepoList = []
6523
6867
  let currentChanges = []
@@ -7143,10 +7487,43 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7143
7487
  const showIframeView = (src) => {
7144
7488
  const iframeMarkup = `<iframe src="${src}" frameborder="0"></iframe>`
7145
7489
  const diffBody = document.querySelector('.pinokio-modal-body--diff')
7490
+ let iframeContainer = null
7146
7491
  if (diffBody) {
7147
7492
  diffBody.classList.remove('pinokio-modal-body--diff')
7148
7493
  diffBody.classList.add('pinokio-modal-body--iframe')
7149
7494
  diffBody.innerHTML = iframeMarkup
7495
+ iframeContainer = diffBody
7496
+ } else {
7497
+ const activeSwal = Swal.isVisible()
7498
+ const fallbackHtml = `
7499
+ <div class="pinokio-modal-surface pinokio-modal-surface--iframe">
7500
+ <div class="pinokio-modal-body pinokio-modal-body--iframe"></div>
7501
+ </div>
7502
+ `
7503
+ const launchPromise = Swal.fire({
7504
+ html: fallbackHtml,
7505
+ customClass: {
7506
+ popup: 'pinokio-modern-modal',
7507
+ htmlContainer: 'pinokio-modern-html',
7508
+ closeButton: 'pinokio-modern-close'
7509
+ },
7510
+ backdrop: 'rgba(9,11,15,0.65)',
7511
+ width: 'min(720px, 90vw)',
7512
+ showConfirmButton: false,
7513
+ showCloseButton: true,
7514
+ buttonsStyling: false,
7515
+ focusConfirm: false,
7516
+ })
7517
+ if (!activeSwal) {
7518
+ launchPromise.then(() => {})
7519
+ }
7520
+ const container = Swal.getHtmlContainer()
7521
+ if (container) {
7522
+ iframeContainer = container.querySelector('.pinokio-modal-body--iframe')
7523
+ if (iframeContainer) {
7524
+ iframeContainer.innerHTML = iframeMarkup
7525
+ }
7526
+ }
7150
7527
  }
7151
7528
  const commitFooter = document.querySelector('.pinokio-modal-footer--commit')
7152
7529
  if (commitFooter && commitFooter.parentNode) {
@@ -7616,7 +7993,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7616
7993
  }
7617
7994
 
7618
7995
  const historyData = await response.json()
7619
- displayGitHistory(historyData, { repoName: repoName || null, repoParam: repoParam || null })
7996
+ displayGitHistory(historyData, { repoName: repoName || null, repoParam: repoParam || null, repoData })
7620
7997
  } catch (error) {
7621
7998
  console.error('Failed to load git history:', error)
7622
7999
  Swal.fire({
@@ -7629,9 +8006,14 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7629
8006
 
7630
8007
  const displayGitHistory = (historyData, options = {}) => {
7631
8008
  const repoName = options && typeof options === 'object' ? options.repoName : null
8009
+ const repoData = options && typeof options === 'object' ? options.repoData : null
7632
8010
  const commits = historyData.log || []
7633
8011
  const remote = historyData.remote || ''
7634
8012
  const currentRef = historyData.ref || 'HEAD'
8013
+ const branchEntries = Array.isArray(historyData.branches) ? historyData.branches : []
8014
+ const isOid = (s) => typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)
8015
+ const realBranchEntries = branchEntries.filter((entry) => entry && typeof entry.branch === 'string' && entry.branch.length > 0 && !isOid(entry.branch))
8016
+ const selectedBranchName = (historyData && typeof historyData.branch === 'string' && !isOid(historyData.branch)) ? historyData.branch : null
7635
8017
 
7636
8018
  const commitCountLabel = `${commits.length} commit${commits.length === 1 ? '' : 's'}`
7637
8019
  const lastCommit = commits[0]
@@ -7659,6 +8041,26 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7659
8041
  </div>
7660
8042
  </div>
7661
8043
  ${metaBadges.length ? `<div class="pinokio-history-meta">${metaBadges.join('')}</div>` : ''}
8044
+ <div class="pinokio-history-latest-banner" data-history-latest-banner>
8045
+ <div class="pinokio-history-latest-text">
8046
+ Currently viewing ${escapeHtml(currentRef)}
8047
+ </div>
8048
+ <div class="pinokio-history-actions">
8049
+ <button type="button" class="pinokio-history-latest-btn" data-history-return-head>
8050
+ <i class="fa-solid fa-arrow-rotate-left"></i> Return to newest commit
8051
+ </button>
8052
+ ${realBranchEntries.length ? `
8053
+ <select class="pinokio-modal-input pinokio-history-branch-select" data-history-branch-select aria-label="Select branch">
8054
+ ${realBranchEntries.map(e => `
8055
+ <option value="${escapeHtml(e.branch)}"${selectedBranchName === e.branch ? ' selected' : ''}>${escapeHtml(e.branch)}</option>
8056
+ `).join('')}
8057
+ </select>
8058
+ <button type="button" class="pinokio-history-latest-btn" data-history-branch-switch>
8059
+ <i class="fa-solid fa-code-branch"></i> Switch
8060
+ </button>
8061
+ ` : ''}
8062
+ </div>
8063
+ </div>
7662
8064
  <div class="pinokio-modal-body pinokio-modal-body--history">
7663
8065
  ${commits.length === 0 ?
7664
8066
  '<div class="pinokio-history-empty">No commits found</div>' :
@@ -7689,8 +8091,76 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7689
8091
  if (commitInfo) {
7690
8092
  await showCommitDiffModal(commitInfo)
7691
8093
  }
7692
- })
8094
+ })
7693
8095
  })
8096
+
8097
+ const returnBtn = document.querySelector('[data-history-return-head]')
8098
+ const banner = document.querySelector('[data-history-latest-banner]')
8099
+ const branchSelect = document.querySelector('[data-history-branch-select]')
8100
+ const branchSwitchBtn = document.querySelector('[data-history-branch-switch]')
8101
+ if (typeof showIframeView === 'function') {
8102
+ const repoParam = options && typeof options === 'object' ? options.repoParam : null
8103
+ let checkoutCwd = null
8104
+ if (historyData && typeof historyData.dir === 'string' && historyData.dir.length > 0) {
8105
+ checkoutCwd = historyData.dir
8106
+ } else if (repoParam) {
8107
+ checkoutCwd = repoParam
8108
+ }
8109
+
8110
+ const isOid = (s) => typeof s === 'string' && /^[0-9a-f]{7,40}$/i.test(s)
8111
+ const branchEntries = Array.isArray(historyData.branches) ? historyData.branches : []
8112
+ const realBranches = branchEntries
8113
+ .map((entry) => entry && typeof entry.branch === 'string' ? entry.branch : null)
8114
+ .filter((name) => name && !isOid(name))
8115
+
8116
+ // Wire the branch selector
8117
+ if (branchSelect && checkoutCwd) {
8118
+ branchSelect.addEventListener('change', () => {
8119
+ const value = branchSelect.value
8120
+ if (!value) return
8121
+ const url = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(value)}&callback_target=parent&callback=$location.href`
8122
+ openCheckoutTerminal(url)
8123
+ })
8124
+ }
8125
+ if (branchSwitchBtn && branchSelect && checkoutCwd) {
8126
+ branchSwitchBtn.addEventListener('click', () => {
8127
+ const value = branchSelect.value
8128
+ if (!value) return
8129
+ const url = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(value)}&callback_target=parent&callback=$location.href`
8130
+ openCheckoutTerminal(url)
8131
+ })
8132
+ }
8133
+
8134
+ // Fix "Return to newest commit" target selection
8135
+ if (returnBtn) {
8136
+ let checkoutTarget = null
8137
+ if (repoData && typeof repoData.branch === 'string' && repoData.branch.length > 0 && !isOid(repoData.branch)) {
8138
+ checkoutTarget = repoData.branch
8139
+ } else if (historyData && typeof historyData.branch === 'string' && historyData.branch.length > 0 && !isOid(historyData.branch)) {
8140
+ checkoutTarget = historyData.branch
8141
+ } else if (realBranches.length > 0) {
8142
+ const prefer = ['main', 'master', 'develop', 'dev']
8143
+ checkoutTarget = prefer.find((n) => realBranches.includes(n)) || realBranches[0]
8144
+ }
8145
+
8146
+ if (checkoutCwd && checkoutTarget) {
8147
+ const checkoutUrl = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(checkoutTarget)}&callback_target=parent&callback=$location.href`
8148
+ returnBtn.addEventListener('click', () => {
8149
+ openCheckoutTerminal(checkoutUrl)
8150
+ })
8151
+ } else {
8152
+ returnBtn.disabled = true
8153
+ if (banner) {
8154
+ banner.classList.add('pinokio-history-latest-banner--disabled')
8155
+ }
8156
+ }
8157
+ }
8158
+ } else if (banner) {
8159
+ const parent = banner.parentNode
8160
+ if (parent) {
8161
+ parent.removeChild(banner)
8162
+ }
8163
+ }
7694
8164
  }
7695
8165
  })
7696
8166
  }
@@ -7731,6 +8201,20 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7731
8201
  alert('No file changes detected.')
7732
8202
  return
7733
8203
  }
8204
+
8205
+ const commitOid = typeof diffData.oid === 'string' ? diffData.oid : (Array.isArray(changes) && typeof changes[0]?.ref === 'string' ? changes[0].ref : null)
8206
+ const commitCheckoutCwd = (() => {
8207
+ const url = typeof diffData.git_commit_url === 'string' ? new URL(diffData.git_commit_url, window.location.origin) : null
8208
+ if (!url) {
8209
+ return null
8210
+ }
8211
+ const cwdParam = url.searchParams.get('cwd')
8212
+ return cwdParam && cwdParam.length > 0 ? cwdParam : null
8213
+ })()
8214
+
8215
+ const checkoutUrl = commitOid && commitCheckoutCwd
8216
+ ? `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(commitCheckoutCwd)}&commit=${encodeURIComponent(commitOid)}&callback_target=parent&callback=$location.href`
8217
+ : null
7734
8218
 
7735
8219
  // Create custom overlay modal
7736
8220
  const overlay = document.createElement('div')
@@ -7755,6 +8239,13 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7755
8239
  <div class="pinokio-git-diff-empty-state">Select a file to view its changes</div>
7756
8240
  </div>
7757
8241
  </div>
8242
+ ${checkoutUrl ? `
8243
+ <div class="pinokio-git-commit-actions">
8244
+ <button type="button" class="pinokio-git-commit-switch-btn">
8245
+ <i class="fa-solid fa-clock-rotate-left"></i> Switch to this version
8246
+ </button>
8247
+ </div>
8248
+ ` : ''}
7758
8249
  </div>
7759
8250
  </div>
7760
8251
  `
@@ -7763,14 +8254,23 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7763
8254
 
7764
8255
  // Add close handler
7765
8256
  const closeBtn = overlay.querySelector('.pinokio-custom-commit-close')
7766
- closeBtn.addEventListener('click', () => {
7767
- document.body.removeChild(overlay)
7768
- })
8257
+ let escHandler = null
8258
+ const cleanupOverlay = () => {
8259
+ if (escHandler) {
8260
+ document.removeEventListener('keydown', escHandler)
8261
+ escHandler = null
8262
+ }
8263
+ if (overlay.parentNode) {
8264
+ overlay.parentNode.removeChild(overlay)
8265
+ }
8266
+ }
8267
+
8268
+ closeBtn.addEventListener('click', cleanupOverlay)
7769
8269
 
7770
8270
  // Close on overlay click
7771
8271
  overlay.addEventListener('click', (e) => {
7772
8272
  if (e.target === overlay) {
7773
- document.body.removeChild(overlay)
8273
+ cleanupOverlay()
7774
8274
  }
7775
8275
  })
7776
8276
 
@@ -7798,16 +8298,66 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7798
8298
  }
7799
8299
  })
7800
8300
  })
8301
+
8302
+ if (checkoutUrl) {
8303
+ const switchBtn = overlay.querySelector('.pinokio-git-commit-switch-btn')
8304
+ if (switchBtn) {
8305
+ switchBtn.addEventListener('click', () => {
8306
+ cleanupOverlay()
8307
+ showIframeView(checkoutUrl)
8308
+ })
8309
+ }
8310
+ }
7801
8311
 
7802
8312
  // Handle escape key
7803
- const escHandler = (e) => {
8313
+ escHandler = (e) => {
7804
8314
  if (e.key === 'Escape') {
7805
- document.body.removeChild(overlay)
7806
- document.removeEventListener('keydown', escHandler)
8315
+ cleanupOverlay()
7807
8316
  }
7808
8317
  }
7809
8318
  document.addEventListener('keydown', escHandler)
7810
8319
  }
8320
+
8321
+ const openCheckoutTerminal = (src) => {
8322
+ const overlay = document.createElement('div')
8323
+ overlay.className = 'pinokio-custom-terminal-overlay'
8324
+ overlay.innerHTML = `
8325
+ <div class="pinokio-custom-terminal-modal">
8326
+ <div class="pinokio-custom-terminal-header">
8327
+ <h3>Git Checkout</h3>
8328
+ <button class="pinokio-custom-terminal-close">×</button>
8329
+ </div>
8330
+ <div class="pinokio-custom-terminal-body">
8331
+ <iframe src="${src}" frameborder="0"></iframe>
8332
+ </div>
8333
+ </div>
8334
+ `
8335
+
8336
+ const cleanup = () => {
8337
+ document.removeEventListener('keydown', escHandler)
8338
+ if (overlay.parentNode) {
8339
+ overlay.parentNode.removeChild(overlay)
8340
+ }
8341
+ }
8342
+
8343
+ const closeBtn = overlay.querySelector('.pinokio-custom-terminal-close')
8344
+ closeBtn.addEventListener('click', cleanup)
8345
+
8346
+ overlay.addEventListener('click', (event) => {
8347
+ if (event.target === overlay) {
8348
+ cleanup()
8349
+ }
8350
+ })
8351
+
8352
+ const escHandler = (event) => {
8353
+ if (event.key === 'Escape') {
8354
+ cleanup()
8355
+ }
8356
+ }
8357
+
8358
+ document.addEventListener('keydown', escHandler)
8359
+ document.body.appendChild(overlay)
8360
+ }
7811
8361
 
7812
8362
  const createCommitItem = (commitData) => {
7813
8363
  const commit = commitData.commit