pinokiod 3.107.0 → 3.109.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.
@@ -1378,6 +1378,15 @@ body.dark #fs-status {
1378
1378
  z-index: 20;
1379
1379
  }
1380
1380
 
1381
+ .fs-status-dropdown.git-fork .fs-dropdown-menu,
1382
+ .fs-status-dropdown.git-publish .fs-dropdown-menu {
1383
+ left: auto;
1384
+ right: 0;
1385
+ width: min(420px, calc(100vw - 24px));
1386
+ max-width: min(420px, calc(100vw - 24px));
1387
+ white-space: normal;
1388
+ }
1389
+
1381
1390
  body.dark .fs-dropdown-menu {
1382
1391
  background: rgba(30, 41, 59, 0.96);
1383
1392
  border: 1px solid rgba(148, 163, 184, 0.35);
@@ -1938,6 +1947,40 @@ body.dark {
1938
1947
  background: #2d6ae0 !important;
1939
1948
  }
1940
1949
 
1950
+ .pinokio-github-login-modal.swal2-popup {
1951
+ max-width: 420px !important;
1952
+ width: calc(100vw - 48px) !important;
1953
+ }
1954
+ .pinokio-github-login {
1955
+ padding: 36px 40px 32px 40px;
1956
+ display: flex;
1957
+ flex-direction: column;
1958
+ align-items: center;
1959
+ text-align: center;
1960
+ gap: 14px;
1961
+ }
1962
+ .pinokio-github-login__icon {
1963
+ width: 64px;
1964
+ height: 64px;
1965
+ border-radius: 18px;
1966
+ display: grid;
1967
+ place-items: center;
1968
+ background: var(--pinokio-modal-icon-bg);
1969
+ color: var(--pinokio-modal-icon-color);
1970
+ font-size: 28px;
1971
+ }
1972
+ .pinokio-github-login__title {
1973
+ font-size: 20px;
1974
+ font-weight: 600;
1975
+ color: var(--pinokio-modal-text);
1976
+ }
1977
+ .pinokio-github-login__body {
1978
+ font-size: 14px;
1979
+ line-height: 1.6;
1980
+ color: var(--pinokio-modal-subtitle-color);
1981
+ max-width: 280px;
1982
+ }
1983
+
1941
1984
  .pinokio-modern-modal.swal2-popup {
1942
1985
  background: var(--pinokio-modal-bg) !important;
1943
1986
  color: var(--pinokio-modal-text) !important;
@@ -2114,6 +2157,157 @@ body.dark {
2114
2157
  background: rgba(127, 91, 243, 1) !important;
2115
2158
  }
2116
2159
 
2160
+ .pinokio-modal-body--fork {
2161
+ display: flex;
2162
+ flex-direction: column;
2163
+ gap: 16px;
2164
+ padding: 0;
2165
+ }
2166
+
2167
+ .pinokio-fork-modal {
2168
+ display: flex;
2169
+ flex-direction: column;
2170
+ gap: 16px;
2171
+ }
2172
+
2173
+ .pinokio-fork-help {
2174
+ margin: 0;
2175
+ font-size: 0.875rem;
2176
+ color: rgba(255, 255, 255, 0.75);
2177
+ }
2178
+
2179
+ .pinokio-fork-item {
2180
+ border: 1px solid rgba(255, 255, 255, 0.08);
2181
+ border-radius: 10px;
2182
+ padding: 14px 16px;
2183
+ display: flex;
2184
+ flex-direction: column;
2185
+ gap: 10px;
2186
+ background: rgba(15, 18, 24, 0.65);
2187
+ }
2188
+
2189
+ .pinokio-fork-item[data-disabled='true'] {
2190
+ opacity: 0.55;
2191
+ }
2192
+
2193
+ .pinokio-fork-item-header {
2194
+ display: flex;
2195
+ align-items: center;
2196
+ gap: 10px;
2197
+ }
2198
+
2199
+ .pinokio-fork-item-header label {
2200
+ flex: 1;
2201
+ display: flex;
2202
+ align-items: center;
2203
+ gap: 8px;
2204
+ cursor: pointer;
2205
+ }
2206
+
2207
+ .pinokio-fork-item-title {
2208
+ font-weight: 600;
2209
+ font-size: 0.95rem;
2210
+ }
2211
+
2212
+ .pinokio-fork-item-url {
2213
+ font-size: 0.85rem;
2214
+ color: rgba(255, 255, 255, 0.65);
2215
+ word-break: break-word;
2216
+ }
2217
+
2218
+ .pinokio-fork-item-url.empty {
2219
+ font-style: italic;
2220
+ }
2221
+
2222
+ .pinokio-fork-name-input {
2223
+ display: flex;
2224
+ flex-direction: column;
2225
+ gap: 6px;
2226
+ }
2227
+
2228
+ .pinokio-fork-name-input label {
2229
+ font-size: 0.8rem;
2230
+ font-weight: 500;
2231
+ text-transform: uppercase;
2232
+ letter-spacing: 0.04em;
2233
+ color: rgba(255, 255, 255, 0.7);
2234
+ }
2235
+
2236
+ .pinokio-modal-input.pinokio-modal-input--error {
2237
+ border-color: #ff6b6b;
2238
+ box-shadow: 0 0 0 1px rgba(255, 107, 107, 0.25);
2239
+ }
2240
+
2241
+ .pinokio-fork-checkbox-row {
2242
+ display: flex;
2243
+ align-items: center;
2244
+ gap: 8px;
2245
+ font-size: 0.85rem;
2246
+ }
2247
+
2248
+ .pinokio-fork-checkbox-row label {
2249
+ cursor: pointer;
2250
+ }
2251
+
2252
+ .pinokio-fork-org-input {
2253
+ display: flex;
2254
+ flex-direction: column;
2255
+ gap: 6px;
2256
+ }
2257
+
2258
+ .pinokio-fork-org-input.hidden {
2259
+ display: none !important;
2260
+ }
2261
+
2262
+ .pinokio-fork-org-input label {
2263
+ font-size: 0.8rem;
2264
+ font-weight: 500;
2265
+ text-transform: uppercase;
2266
+ letter-spacing: 0.04em;
2267
+ color: rgba(255, 255, 255, 0.7);
2268
+ }
2269
+
2270
+ .pinokio-fork-org-hint {
2271
+ margin: 0;
2272
+ font-size: 0.75rem;
2273
+ color: rgba(255, 255, 255, 0.55);
2274
+ }
2275
+
2276
+ .pinokio-fork-dropdown-item,
2277
+ .pinokio-publish-dropdown-item {
2278
+ display: flex;
2279
+ flex-direction: column;
2280
+ align-items: flex-start;
2281
+ gap: 4px;
2282
+ text-align: left;
2283
+ }
2284
+
2285
+ .pinokio-fork-dropdown-title,
2286
+ .pinokio-publish-dropdown-title {
2287
+ font-weight: 600;
2288
+ font-size: 0.9rem;
2289
+ }
2290
+
2291
+ .pinokio-fork-dropdown-remote,
2292
+ .pinokio-publish-dropdown-remote {
2293
+ font-size: 0.75rem;
2294
+ color: rgba(255, 255, 255, 0.6);
2295
+ word-break: break-word;
2296
+ white-space: normal;
2297
+ overflow-wrap: anywhere;
2298
+ }
2299
+
2300
+ .pinokio-fork-dropdown-remote.empty,
2301
+ .pinokio-publish-dropdown-remote.empty {
2302
+ font-style: italic;
2303
+ color: rgba(255, 255, 255, 0.45);
2304
+ }
2305
+
2306
+ .fs-dropdown-item--disabled {
2307
+ opacity: 0.5;
2308
+ cursor: not-allowed;
2309
+ }
2310
+
2117
2311
  .pinokio-git-history-list {
2118
2312
  display: flex;
2119
2313
  flex-direction: column;
@@ -2526,13 +2720,16 @@ body.dark {
2526
2720
  font-size: 1rem;
2527
2721
  }
2528
2722
  */
2529
- #fs-push-btn {
2723
+ #fs-push-btn,
2724
+ #fs-fork-btn {
2530
2725
  min-width: 0;
2531
2726
  }
2532
- #fs-push-btn .fs-status-label {
2727
+ #fs-push-btn .fs-status-label,
2728
+ #fs-fork-btn .fs-status-label {
2533
2729
  font-size: 0;
2534
2730
  }
2535
- #fs-push-btn .fs-status-label i {
2731
+ #fs-push-btn .fs-status-label i,
2732
+ #fs-fork-btn .fs-status-label i {
2536
2733
  font-size: 1rem;
2537
2734
  }
2538
2735
  #fs-status .fs-status-btn .disk-usage,
@@ -2679,7 +2876,7 @@ body.dark {
2679
2876
  <% } %>
2680
2877
  <div class='container'>
2681
2878
  <% if (type === "browse") { %>
2682
- <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%>">
2879
+ <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%>">
2683
2880
  <a target="<%=src%>" href="<%=src%>" class='fs-status-btn frame-link' data-index="0" data-mode="refresh" data-type="n">
2684
2881
  <span class='fs-status-label'>
2685
2882
  <i class="fa-regular fa-folder-open"></i>
@@ -2733,12 +2930,24 @@ body.dark {
2733
2930
  </button>
2734
2931
  <div class='fs-dropdown-menu submenu hidden' id='fs-changes-menu'></div>
2735
2932
  </div>
2736
- <button id='fs-push-btn' class='fs-status-btn'>
2737
- <span class='fs-status-label'>
2738
- <i class="fa-brands fa-github"></i>
2739
- <span class='fs-status-title'>Publish</span>
2740
- </span>
2741
- </button>
2933
+ <div class='fs-status-dropdown git-fork'>
2934
+ <button id='fs-fork-btn' class='fs-status-btn revealer' data-group='#fs-fork-menu' type='button'>
2935
+ <span class='fs-status-label'>
2936
+ <i class="fa-solid fa-code-branch"></i>
2937
+ <span class='fs-status-title'>Fork</span>
2938
+ </span>
2939
+ </button>
2940
+ <div class='fs-dropdown-menu submenu hidden' id='fs-fork-menu'></div>
2941
+ </div>
2942
+ <div class='fs-status-dropdown git-publish'>
2943
+ <button id='fs-push-btn' class='fs-status-btn revealer' data-group='#fs-push-menu' type='button'>
2944
+ <span class='fs-status-label'>
2945
+ <i class="fa-brands fa-github"></i>
2946
+ <span class='fs-status-title'>Publish</span>
2947
+ </span>
2948
+ </button>
2949
+ <div class='fs-dropdown-menu submenu hidden' id='fs-push-menu'></div>
2950
+ </div>
2742
2951
  </div>
2743
2952
  <% } %>
2744
2953
  <main class='browserview'>
@@ -2772,17 +2981,20 @@ body.dark {
2772
2981
  let ignorePersistedSelection = pluginLaunchActive
2773
2982
  let lastForegroundSignature = null
2774
2983
  const iframe_onerror = (iframe) => {
2984
+ if (iframe && iframe.dataset && iframe.dataset.forceVisible === 'true') {
2985
+ return
2986
+ }
2775
2987
  let originalSrc = iframe.src
2776
2988
  iframe.onload = function() {
2777
2989
  try {
2778
2990
  // Try to access the iframe's document
2991
+ const iframeDoc = iframe.contentDocument || (iframe.contentWindow ? iframe.contentWindow.document : null)
2779
2992
  const currentSrc = iframe.src
2780
2993
  // Check if it's a chrome error page or empty
2781
2994
  if (currentSrc !== originalSrc &&
2782
2995
  (currentSrc.includes('chrome-error://') ||
2783
2996
  currentSrc === 'about:blank' ||
2784
2997
  currentSrc.includes('data:'))) {
2785
- iframeDoc.classList.add("hidden")
2786
2998
  Swal.fire({
2787
2999
  html: `<i class="fa-solid fa-circle-notch fa-spin"></i> Loading...`,
2788
3000
  customClass: {
@@ -2798,22 +3010,9 @@ body.dark {
2798
3010
  }, 3000)
2799
3011
  }
2800
3012
  } catch (e) {
2801
- iframe.classList.add("hidden")
2802
3013
  // Cross-origin restriction - assume it loaded successfully
2803
3014
  // if no error was thrown during the initial load
2804
- Swal.fire({
2805
- html: `<i class="fa-solid fa-circle-notch fa-spin"></i> Loading...`,
2806
- customClass: {
2807
- container: "loader-container",
2808
- popup: "loader-popup",
2809
- htmlContainer: "loader-dialog",
2810
- footer: "hidden",
2811
- actions: "hidden"
2812
- }
2813
- });
2814
- setTimeout(() => {
2815
- location.href = location.href
2816
- }, 3000);
3015
+ console.warn('Iframe load warning', e)
2817
3016
  }
2818
3017
  }
2819
3018
  }
@@ -2821,7 +3020,7 @@ body.dark {
2821
3020
  document.querySelectorAll(".menu-container .selected").forEach((el) => {
2822
3021
  el.classList.remove("selected")
2823
3022
  })
2824
- document.querySelectorAll("iframe").forEach((el) => {
3023
+ document.querySelectorAll("main.browserview iframe").forEach((el) => {
2825
3024
  el.classList.add("hidden")
2826
3025
  })
2827
3026
  let frame = document.createElement("iframe")
@@ -3995,6 +4194,13 @@ body.dark {
3995
4194
  })(),
3996
4195
  target: node.getAttribute('target') || null,
3997
4196
  dataIndex: node.getAttribute('data-index') || null,
4197
+ pagePath: (() => {
4198
+ try {
4199
+ return window.location?.pathname || null
4200
+ } catch (_) {
4201
+ return null
4202
+ }
4203
+ })()
3998
4204
  }
3999
4205
  try {
4000
4206
  const key = selectionStorageKey()
@@ -4008,24 +4214,41 @@ body.dark {
4008
4214
  })
4009
4215
  } catch (_) {}
4010
4216
  }
4011
- const restorePersistedFrameLink = () => {
4217
+ const restorePersistedFrameLink = (providedPayload = null) => {
4012
4218
  const storage = getWindowStorage()
4013
- if (!storage) {
4014
- return null
4015
- }
4016
4219
  const key = selectionStorageKey()
4017
- if (!key) {
4018
- return null
4220
+ const currentPath = (() => {
4221
+ try {
4222
+ return window.location?.pathname || ""
4223
+ } catch (_) {
4224
+ return ""
4225
+ }
4226
+ })()
4227
+ let payload = providedPayload
4228
+ if (!payload) {
4229
+ if (!storage || !key) {
4230
+ return null
4231
+ }
4232
+ const raw = storage.getItem(key)
4233
+ if (!raw) {
4234
+ return null
4235
+ }
4236
+ try {
4237
+ payload = JSON.parse(raw)
4238
+ } catch (_) {
4239
+ payload = { selector: raw }
4240
+ }
4019
4241
  }
4020
- const raw = storage.getItem(key)
4021
- if (!raw) {
4242
+ if (!payload) {
4022
4243
  return null
4023
4244
  }
4024
- let payload
4025
- try {
4026
- payload = JSON.parse(raw)
4027
- } catch (_) {
4028
- payload = { selector: raw }
4245
+ if (typeof payload === 'object' && payload !== null && typeof payload.pagePath === 'string') {
4246
+ if (currentPath && payload.pagePath !== currentPath) {
4247
+ return null
4248
+ }
4249
+ }
4250
+ if (typeof payload === 'string') {
4251
+ payload = { selector: payload }
4029
4252
  }
4030
4253
  const trySelector = (selector) => {
4031
4254
  if (!selector || typeof selector !== 'string') {
@@ -4588,7 +4811,7 @@ body.dark {
4588
4811
  if (!sourceWindow) {
4589
4812
  return null
4590
4813
  }
4591
- const frames = Array.from(document.querySelectorAll("iframe"))
4814
+ const frames = Array.from(document.querySelectorAll("main.browserview iframe"))
4592
4815
  for (const frame of frames) {
4593
4816
  if (frame.contentWindow === sourceWindow) {
4594
4817
  return frame.name || null
@@ -4684,14 +4907,48 @@ body.dark {
4684
4907
  const renderSelection = async ({ event: eventParam = null, target: explicitTarget = null, force = false } = {}) => {
4685
4908
  const storage = getWindowStorage()
4686
4909
  const selectionKey = selectionStorageKey()
4687
- const originalHasPersistedSelection = Boolean(selectionKey && storage && storage.getItem(selectionKey))
4910
+ const currentPath = (() => {
4911
+ try {
4912
+ return window.location?.pathname || ""
4913
+ } catch (_) {
4914
+ return ""
4915
+ }
4916
+ })()
4688
4917
  let persistedSelectionRaw = selectionKey && storage ? storage.getItem(selectionKey) : null
4918
+ const devRouteActive = /\/dev(?:$|\/)/.test(currentPath || "")
4919
+ let persistedSelectionPayload = null
4920
+ if (persistedSelectionRaw) {
4921
+ try {
4922
+ const parsed = JSON.parse(persistedSelectionRaw)
4923
+ if (parsed && typeof parsed === "object") {
4924
+ if (typeof parsed.pagePath === "string") {
4925
+ if (!currentPath || parsed.pagePath === currentPath) {
4926
+ persistedSelectionPayload = parsed
4927
+ }
4928
+ } else if (!devRouteActive) {
4929
+ persistedSelectionPayload = parsed
4930
+ }
4931
+ }
4932
+ } catch (_) {
4933
+ if (!devRouteActive) {
4934
+ persistedSelectionPayload = { selector: persistedSelectionRaw }
4935
+ }
4936
+ }
4937
+ }
4938
+ if (!persistedSelectionPayload) {
4939
+ persistedSelectionRaw = null
4940
+ }
4941
+ const originalHasPersistedSelection = Boolean(persistedSelectionPayload)
4689
4942
  const skipPersistedSelection = ignorePersistedSelection
4690
4943
  let hasPersistedSelection = skipPersistedSelection ? false : originalHasPersistedSelection
4691
4944
  if (skipPersistedSelection) {
4692
4945
  persistedSelectionRaw = null
4946
+ persistedSelectionPayload = null
4693
4947
  }
4694
4948
 
4949
+ const triggeredByUser = Boolean(eventParam || explicitTarget)
4950
+ let resolvedByGlobalSelector = false
4951
+
4695
4952
  let target = explicitTarget
4696
4953
  let preselected = null
4697
4954
 
@@ -4704,22 +4961,22 @@ body.dark {
4704
4961
  if (candidate) {
4705
4962
  target = candidate
4706
4963
  global_selector = null
4964
+ resolvedByGlobalSelector = true
4707
4965
  } else {
4708
4966
  scheduleSelectionRetry()
4709
4967
  return
4710
4968
  }
4711
4969
  }
4712
4970
 
4971
+ console.log({ target, skipPersistedSelection })
4713
4972
  if (!target && !skipPersistedSelection) {
4714
4973
  preselected = document.querySelector('#devtab.frame-link.selected') || document.querySelector('.frame-link.selected')
4715
- if (preselected) {
4716
- target = preselected
4717
- }
4974
+ console.log({ preselected })
4718
4975
  }
4719
4976
 
4720
- if (!target && persistedSelectionRaw) {
4721
- target = restorePersistedFrameLink()
4722
- if (!target && persistedSelectionRaw) {
4977
+ if (!target && persistedSelectionPayload) {
4978
+ target = restorePersistedFrameLink(persistedSelectionPayload)
4979
+ if (!target && originalHasPersistedSelection) {
4723
4980
  scheduleSelectionRetry()
4724
4981
  return
4725
4982
  }
@@ -4733,11 +4990,18 @@ body.dark {
4733
4990
  }
4734
4991
  }
4735
4992
 
4736
- if (!target && skipPersistedSelection) {
4993
+ const devTab = document.querySelector('#devtab.frame-link')
4994
+ if (!triggeredByUser && !resolvedByGlobalSelector && !target && devRouteActive && devTab) {
4995
+ target = devTab
4996
+ } else if (!target && preselected) {
4997
+ target = preselected
4998
+ }
4999
+
5000
+ if (!target) {
4737
5001
  const defaultSelection = document.querySelector("[data-default]")
4738
5002
  if (defaultSelection) {
4739
5003
  target = defaultSelection
4740
- } else {
5004
+ } else if (skipPersistedSelection) {
4741
5005
  scheduleSelectionRetry()
4742
5006
  return
4743
5007
  }
@@ -4774,6 +5038,12 @@ body.dark {
4774
5038
  }
4775
5039
  <% } %>
4776
5040
 
5041
+ if (!target && preselected) {
5042
+ target = preselected
5043
+ }
5044
+
5045
+ console.log({ targetAfter: target })
5046
+
4777
5047
  if (!target) {
4778
5048
  target = document.querySelector(".frame-link")
4779
5049
  }
@@ -4829,7 +5099,7 @@ body.dark {
4829
5099
  <% } %>
4830
5100
 
4831
5101
  // hide all frames
4832
- document.querySelectorAll("iframe").forEach((el) => {
5102
+ document.querySelectorAll("main.browserview iframe").forEach((el) => {
4833
5103
  el.classList.add("hidden")
4834
5104
  })
4835
5105
 
@@ -5074,7 +5344,7 @@ body.dark {
5074
5344
  item.href = url
5075
5345
  item.setAttribute("data-index", index)
5076
5346
  item.className = "btn header-item frame-link"
5077
- item.innerHTML = `<div class='tab'><i class="fa-solid fa-link"></i><div class='display'>${url}</div><div class='flexible'></div><button class='btn2 del'><i class="fa-solid fa-xmark"></i></button></div>`
5347
+ item.innerHTML = `<div class='tab'><i class="fa-solid fa-link"></i><div class='display'>${url}</div><div class='flexible'></div><button class='btn2 del'><i class="fa-solid fa-circle-stop"></i></button></div>`
5078
5348
 
5079
5349
  document.querySelector(".temp-menu").appendChild(item)
5080
5350
 
@@ -5755,7 +6025,7 @@ body.dark {
5755
6025
  })
5756
6026
  setupTabLinkHover()
5757
6027
  document.addEventListener("click", (event) => {
5758
- if (event.target.closest("#fs-status .fs-dropdown-menu")) {
6028
+ if (event.target.closest("#fs-status .fs-dropdown-menu") || event.target.closest("#fs-status .fs-status-btn")) {
5759
6029
  return
5760
6030
  }
5761
6031
  closeStatusDropdowns()
@@ -5975,9 +6245,9 @@ body.dark {
5975
6245
  } else {
5976
6246
  global_selector = null
5977
6247
  }
5978
- const frameExists = Array.from(document.querySelectorAll("iframe")).some((frame) => {
5979
- return frame.name === rawName
5980
- })
6248
+ const frameExists = Array.from(document.querySelectorAll("main.browserview iframe")).some((frame) => {
6249
+ return frame.name === rawName
6250
+ })
5981
6251
  if (!frameExists) {
5982
6252
  create_iframe(rawName, event.data.launch.href)
5983
6253
  }
@@ -6082,6 +6352,13 @@ body.dark {
6082
6352
  const changesMenu = document.getElementById('fs-changes-menu')
6083
6353
  const changesBtn = document.getElementById('fs-changes-btn')
6084
6354
  const badgeElement = changesBtn ? changesBtn.querySelector('.badge') : null
6355
+ const forkBtn = document.getElementById('fs-fork-btn')
6356
+ const forkDropdownContainer = document.querySelector('#fs-status .git-fork')
6357
+ const forkMenu = document.getElementById('fs-fork-menu')
6358
+ const pushBtn = document.getElementById('fs-push-btn')
6359
+ const publishMenu = document.getElementById('fs-push-menu')
6360
+
6361
+ let latestGitIntegration = null
6085
6362
 
6086
6363
  const readDataAttr = (node, attr) => {
6087
6364
  if (!node) {
@@ -6097,6 +6374,9 @@ body.dark {
6097
6374
  const statusUri = readDataAttr(fsStatusEl, 'data-status-uri')
6098
6375
  const monitorUri = readDataAttr(fsStatusEl, 'data-uri')
6099
6376
  const workspaceName = readDataAttr(fsStatusEl, 'data-workspace')
6377
+ const historyUri = readDataAttr(fsStatusEl, 'data-history-uri')
6378
+ const defaultPushUri = readDataAttr(fsStatusEl, 'data-push-uri')
6379
+ const defaultForkUri = readDataAttr(fsStatusEl, 'data-fork-uri')
6100
6380
 
6101
6381
  const encodeRepoPath = (value) => {
6102
6382
  if (typeof value !== 'string' || value.length === 0) {
@@ -6105,42 +6385,369 @@ body.dark {
6105
6385
  return value.split('/').map(encodeURIComponent).join('/')
6106
6386
  }
6107
6387
 
6108
- const updateCombinedBadge = (total) => {
6109
- if (!badgeElement) {
6110
- return
6388
+ function escapeHtml(value) {
6389
+ if (value === null || value === undefined) {
6390
+ return ''
6111
6391
  }
6112
- badgeElement.textContent = total > 0 ? String(total) : ''
6392
+ return String(value).replace(/[&<>"']/g, (match) => {
6393
+ switch (match) {
6394
+ case '&':
6395
+ return '&amp;'
6396
+ case '<':
6397
+ return '&lt;'
6398
+ case '>':
6399
+ return '&gt;'
6400
+ case '"':
6401
+ return '&quot;'
6402
+ case '\'':
6403
+ return '&#39;'
6404
+ default:
6405
+ return match
6406
+ }
6407
+ })
6113
6408
  }
6114
6409
 
6115
- const updateChangesButtonState = (hasRepos) => {
6116
- if (!changesBtn) {
6117
- return
6410
+ function getRepoListSnapshot() {
6411
+ if (Array.isArray(lastRepoList) && lastRepoList.length > 0) {
6412
+ return lastRepoList.slice()
6118
6413
  }
6119
- if (hasRepos) {
6120
- changesBtn.disabled = false
6121
- changesBtn.classList.remove('fs-status-btn--disabled')
6414
+ return Array.from(repoStatusCache.values())
6415
+ }
6416
+
6417
+ function parseRemoteSlug(remoteUrl) {
6418
+ if (!remoteUrl || typeof remoteUrl !== 'string') {
6419
+ return { owner: null, name: null, full: null }
6420
+ }
6421
+ const trimmed = remoteUrl.trim()
6422
+ if (!trimmed) {
6423
+ return { owner: null, name: null, full: null }
6424
+ }
6425
+ const withoutGit = trimmed.replace(/\.git$/i, '')
6426
+
6427
+ const parsePathSegments = (pathValue) => {
6428
+ if (!pathValue) {
6429
+ return []
6430
+ }
6431
+ const cleaned = pathValue.replace(/^\/+/, '')
6432
+ return cleaned.split('/').filter(Boolean)
6433
+ }
6434
+
6435
+ let pathSegment = ''
6436
+ if (withoutGit.startsWith('git@')) {
6437
+ const colonSplit = withoutGit.split(':')
6438
+ pathSegment = colonSplit.length > 1 ? colonSplit.slice(1).join(':') : ''
6122
6439
  } else {
6123
- changesBtn.disabled = true
6124
- changesBtn.classList.add('fs-status-btn--disabled')
6440
+ try {
6441
+ const prefixed = withoutGit.includes('://') ? withoutGit : `https://${withoutGit}`
6442
+ const url = new URL(prefixed)
6443
+ pathSegment = url.pathname
6444
+ } catch (error) {
6445
+ pathSegment = withoutGit
6446
+ }
6447
+ }
6448
+
6449
+ const segments = parsePathSegments(pathSegment)
6450
+ if (segments.length >= 2) {
6451
+ const owner = segments[segments.length - 2]
6452
+ const name = segments[segments.length - 1]
6453
+ return { owner, name, full: `${owner}/${name}` }
6454
+ }
6455
+
6456
+ if (segments.length === 1) {
6457
+ const name = segments[0]
6458
+ return { owner: null, name, full: name }
6125
6459
  }
6460
+
6461
+ return { owner: null, name: null, full: null }
6126
6462
  }
6127
6463
 
6128
- const setChangesMenuMessage = (message) => {
6129
- if (!changesMenu) {
6130
- return
6464
+ function deriveForkDefaultName(repo) {
6465
+ if (!repo) {
6466
+ return workspaceName || 'fork'
6131
6467
  }
6132
- const messageEl = document.createElement('div')
6133
- messageEl.className = 'fs-dropdown-empty'
6134
- messageEl.textContent = message
6135
- changesMenu.innerHTML = ''
6136
- changesMenu.append(messageEl)
6468
+ const remoteSlug = parseRemoteSlug(repo.url || '')
6469
+ if (remoteSlug && remoteSlug.name) {
6470
+ return remoteSlug.name
6471
+ }
6472
+ if (typeof repo.name === 'string' && repo.name.length > 0) {
6473
+ const segments = repo.name.split('/').filter(Boolean)
6474
+ if (segments.length > 0) {
6475
+ return segments[segments.length - 1]
6476
+ }
6477
+ }
6478
+ if (typeof repo.repoParam === 'string' && repo.repoParam.length > 0) {
6479
+ const repoSegments = repo.repoParam.split('/').filter(Boolean)
6480
+ if (repoSegments.length > 0) {
6481
+ return repoSegments[repoSegments.length - 1]
6482
+ }
6483
+ }
6484
+ return workspaceName || 'fork'
6137
6485
  }
6138
6486
 
6139
- const attachRepoDropdownHandlers = () => {
6140
- if (!changesMenu) {
6487
+ function hasRemoteConfigured(repo) {
6488
+ return Boolean(repo && typeof repo.url === 'string' && repo.url.trim().length > 0)
6489
+ }
6490
+
6491
+ function resolveForkUri(repo) {
6492
+ if (repo && typeof repo.git_fork_url === 'string' && repo.git_fork_url.length > 0) {
6493
+ return repo.git_fork_url
6494
+ }
6495
+ return defaultForkUri || null
6496
+ }
6497
+
6498
+ function resolvePushUri(repo) {
6499
+ if (repo && typeof repo.git_push_url === 'string' && repo.git_push_url.length > 0) {
6500
+ return repo.git_push_url
6501
+ }
6502
+ return defaultPushUri || null
6503
+ }
6504
+
6505
+ function updateForkButton() {
6506
+ if (!forkBtn) {
6141
6507
  return
6142
6508
  }
6143
- const items = changesMenu.querySelectorAll('.git-changes-item')
6509
+ const labelEl = forkBtn.querySelector('.fs-status-title')
6510
+ const repos = getRepoListSnapshot()
6511
+ const forkTargets = Array.isArray(repos)
6512
+ ? repos.filter((repo) => hasRemoteConfigured(repo) && resolveForkUri(repo))
6513
+ : []
6514
+
6515
+ const detachHandlers = () => {
6516
+ forkBtn.removeEventListener('click', handlePushLogin)
6517
+ forkBtn.removeEventListener('click', requireGitLogin)
6518
+ }
6519
+
6520
+ const enableDropdown = () => {
6521
+ forkBtn.classList.add('revealer')
6522
+ forkBtn.setAttribute('data-group', '#fs-fork-menu')
6523
+ }
6524
+
6525
+ const disableDropdown = () => {
6526
+ forkBtn.classList.remove('revealer')
6527
+ forkBtn.removeAttribute('data-group')
6528
+ const forkMenuEl = document.querySelector('#fs-fork-menu')
6529
+ if (forkMenuEl && !forkMenuEl.classList.contains('hidden')) {
6530
+ forkMenuEl.classList.add('hidden')
6531
+ }
6532
+ setDropdownState(forkBtn, false)
6533
+ }
6534
+
6535
+ detachHandlers()
6536
+
6537
+ const isConnected = Boolean(latestGitIntegration && latestGitIntegration.connected)
6538
+
6539
+ renderForkDropdown(repos, {
6540
+ emptyMessage: isConnected
6541
+ ? 'No remotes available to fork'
6542
+ : 'Connect GitHub to enable forking',
6543
+ })
6544
+
6545
+ if (!isConnected) {
6546
+ disableDropdown()
6547
+ if (labelEl) {
6548
+ labelEl.textContent = 'Fork'
6549
+ }
6550
+ forkBtn.disabled = false
6551
+ forkBtn.classList.remove('fs-status-btn--disabled')
6552
+ forkBtn.setAttribute('title', 'Log in to GitHub to fork this workspace')
6553
+ forkBtn.addEventListener('click', requireGitLogin)
6554
+ return
6555
+ }
6556
+
6557
+ if (labelEl) {
6558
+ labelEl.textContent = 'Fork'
6559
+ }
6560
+
6561
+ forkBtn.disabled = false
6562
+ forkBtn.classList.remove('fs-status-btn--disabled')
6563
+ enableDropdown()
6564
+
6565
+ if (!Array.isArray(repos) || repos.length === 0) {
6566
+ forkBtn.setAttribute('title', 'No Git repositories detected')
6567
+ return
6568
+ }
6569
+
6570
+ if (forkTargets.length === 0) {
6571
+ forkBtn.setAttribute('title', 'No remotes available to fork')
6572
+ return
6573
+ }
6574
+
6575
+ forkBtn.removeAttribute('title')
6576
+ }
6577
+
6578
+ const updateCombinedBadge = (total) => {
6579
+ if (!badgeElement) {
6580
+ return
6581
+ }
6582
+ badgeElement.textContent = total > 0 ? String(total) : ''
6583
+ }
6584
+
6585
+ const updateChangesButtonState = (hasRepos) => {
6586
+ if (!changesBtn) {
6587
+ return
6588
+ }
6589
+ if (hasRepos) {
6590
+ changesBtn.disabled = false
6591
+ changesBtn.classList.remove('fs-status-btn--disabled')
6592
+ } else {
6593
+ changesBtn.disabled = true
6594
+ changesBtn.classList.add('fs-status-btn--disabled')
6595
+ }
6596
+ }
6597
+
6598
+ const setChangesMenuMessage = (message) => {
6599
+ if (!changesMenu) {
6600
+ return
6601
+ }
6602
+ const messageEl = document.createElement('div')
6603
+ messageEl.className = 'fs-dropdown-empty'
6604
+ messageEl.textContent = message
6605
+ changesMenu.innerHTML = ''
6606
+ changesMenu.append(messageEl)
6607
+ }
6608
+
6609
+ const setForkMenuMessage = (message) => {
6610
+ if (!forkMenu) {
6611
+ return
6612
+ }
6613
+ const messageEl = document.createElement('div')
6614
+ messageEl.className = 'fs-dropdown-empty'
6615
+ messageEl.textContent = message
6616
+ forkMenu.innerHTML = ''
6617
+ forkMenu.append(messageEl)
6618
+ }
6619
+
6620
+ const setPublishMenuMessage = (message) => {
6621
+ if (!publishMenu) {
6622
+ return
6623
+ }
6624
+ const messageEl = document.createElement('div')
6625
+ messageEl.className = 'fs-dropdown-empty'
6626
+ messageEl.textContent = message
6627
+ publishMenu.innerHTML = ''
6628
+ publishMenu.append(messageEl)
6629
+ }
6630
+
6631
+ const renderForkDropdown = (repos, options = {}) => {
6632
+ if (!forkMenu) {
6633
+ return
6634
+ }
6635
+
6636
+ const opts = typeof options === 'object' && options !== null ? options : {}
6637
+ const list = Array.isArray(repos) ? repos : []
6638
+
6639
+ forkMenu.innerHTML = ''
6640
+
6641
+ if (list.length === 0) {
6642
+ setForkMenuMessage(opts.emptyMessage || 'No repositories available')
6643
+ return
6644
+ }
6645
+
6646
+ const fragment = document.createDocumentFragment()
6647
+
6648
+ list.forEach((repo) => {
6649
+ const key = repo && typeof repo.repoParam === 'string' ? repo.repoParam : ''
6650
+ const name = repo && repo.name ? repo.name : (key || 'Repository')
6651
+ const remoteUrl = repo && repo.url ? repo.url : ''
6652
+ const forkUri = resolveForkUri(repo)
6653
+ const hasRemote = hasRemoteConfigured(repo)
6654
+
6655
+ const item = document.createElement('button')
6656
+ item.type = 'button'
6657
+ item.className = 'fs-dropdown-item pinokio-fork-dropdown-item'
6658
+ item.dataset.repo = key
6659
+
6660
+ const remoteClass = remoteUrl
6661
+ ? 'pinokio-fork-dropdown-remote'
6662
+ : 'pinokio-fork-dropdown-remote empty'
6663
+ const remoteDisplay = remoteUrl ? escapeHtml(remoteUrl) : 'No remote detected'
6664
+
6665
+ item.innerHTML = `
6666
+ <div class="pinokio-fork-dropdown-title">${escapeHtml(name)}</div>
6667
+ <div class="${remoteClass}">${remoteDisplay}</div>
6668
+ `
6669
+
6670
+ if (!hasRemote || !forkUri) {
6671
+ item.disabled = true
6672
+ item.classList.add('fs-dropdown-item--disabled')
6673
+ } else {
6674
+ item.addEventListener('click', (event) => {
6675
+ event.preventDefault()
6676
+ event.stopPropagation()
6677
+ closeStatusDropdowns()
6678
+ showForkModalForRepo(key)
6679
+ })
6680
+ }
6681
+
6682
+ fragment.appendChild(item)
6683
+ })
6684
+
6685
+ forkMenu.appendChild(fragment)
6686
+ }
6687
+
6688
+ const renderPublishDropdown = (repos, options = {}) => {
6689
+ if (!publishMenu) {
6690
+ return
6691
+ }
6692
+
6693
+ const opts = typeof options === 'object' && options !== null ? options : {}
6694
+ const list = Array.isArray(repos) ? repos : []
6695
+
6696
+ publishMenu.innerHTML = ''
6697
+
6698
+ if (list.length === 0) {
6699
+ setPublishMenuMessage(opts.emptyMessage || 'No repositories available')
6700
+ return
6701
+ }
6702
+
6703
+ const fragment = document.createDocumentFragment()
6704
+
6705
+ list.forEach((repo) => {
6706
+ const key = repo && typeof repo.repoParam === 'string' ? repo.repoParam : ''
6707
+ const name = repo && repo.name ? repo.name : (key || 'Repository')
6708
+ const remoteUrl = repo && repo.url ? repo.url : ''
6709
+ const pushUri = resolvePushUri(repo)
6710
+ const hasRemote = hasRemoteConfigured(repo)
6711
+
6712
+ const item = document.createElement('button')
6713
+ item.type = 'button'
6714
+ item.className = 'fs-dropdown-item pinokio-publish-dropdown-item'
6715
+ item.dataset.repo = key
6716
+ item.dataset.name = name
6717
+
6718
+ const remoteClass = remoteUrl
6719
+ ? 'pinokio-publish-dropdown-remote'
6720
+ : 'pinokio-publish-dropdown-remote empty'
6721
+ const remoteDisplay = remoteUrl ? escapeHtml(remoteUrl) : 'No remote detected'
6722
+
6723
+ item.innerHTML = `
6724
+ <div class="pinokio-publish-dropdown-title">${escapeHtml(name)}</div>
6725
+ <div class="${remoteClass}">${remoteDisplay}</div>
6726
+ `
6727
+
6728
+ if (!hasRemote || !pushUri) {
6729
+ item.disabled = true
6730
+ item.classList.add('fs-dropdown-item--disabled')
6731
+ } else {
6732
+ item.addEventListener('click', (event) => {
6733
+ event.preventDefault()
6734
+ event.stopPropagation()
6735
+ closeStatusDropdowns()
6736
+ showPublishModal({ repoParam: key, repoName: name })
6737
+ })
6738
+ }
6739
+
6740
+ fragment.appendChild(item)
6741
+ })
6742
+
6743
+ publishMenu.appendChild(fragment)
6744
+ }
6745
+
6746
+ const attachRepoDropdownHandlers = () => {
6747
+ if (!changesMenu) {
6748
+ return
6749
+ }
6750
+ const items = changesMenu.querySelectorAll('.git-changes-item')
6144
6751
  items.forEach((item) => {
6145
6752
  item.addEventListener('click', async (event) => {
6146
6753
  event.preventDefault()
@@ -6233,6 +6840,8 @@ body.dark {
6233
6840
  changes: Array.isArray(data.changes) ? data.changes : [],
6234
6841
  git_commit_url: data.git_commit_url || null,
6235
6842
  git_history_url: workspaceName ? `/info/git/HEAD/${encodeRepoPath(workspaceName)}` : readDataAttr(fsStatusEl, 'data-history-uri'),
6843
+ git_fork_url: defaultForkUri,
6844
+ git_push_url: defaultPushUri,
6236
6845
  url: null,
6237
6846
  }
6238
6847
 
@@ -6246,12 +6855,14 @@ body.dark {
6246
6855
  renderChangesDropdown(lastRepoList)
6247
6856
  updateCombinedBadge(fallbackRepo.changeCount)
6248
6857
  updatePublishButton()
6858
+ updateForkButton()
6249
6859
  return true
6250
6860
  } catch (error) {
6251
6861
  console.error('check_git fallback error:', error)
6252
6862
  setChangesMenuMessage('Unable to load repositories')
6253
6863
  updateCombinedBadge(0)
6254
6864
  updateChangesButtonState(false)
6865
+ updateForkButton()
6255
6866
  return false
6256
6867
  }
6257
6868
  }
@@ -6316,6 +6927,7 @@ body.dark {
6316
6927
  : sortedRepos.reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6317
6928
  updateCombinedBadge(total)
6318
6929
  updateChangesButtonState(true)
6930
+ updateForkButton()
6319
6931
  }
6320
6932
 
6321
6933
  updatePublishButton()
@@ -6526,6 +7138,12 @@ body.dark {
6526
7138
  updatedRepo.git_commit_url = freshData.git_commit_url || null
6527
7139
  updatedRepo.name = updatedRepo.name || repoDisplayName || repoParam
6528
7140
  updatedRepo.git_history_url = updatedRepo.git_history_url || (repoData && repoData.git_history_url) || (repoParam ? `/info/git/HEAD/${encodeRepoPath(repoParam)}` : null)
7141
+ if (!updatedRepo.git_fork_url) {
7142
+ updatedRepo.git_fork_url = repoData && repoData.git_fork_url ? repoData.git_fork_url : resolveForkUri(updatedRepo)
7143
+ }
7144
+ if (!updatedRepo.git_push_url) {
7145
+ updatedRepo.git_push_url = repoData && repoData.git_push_url ? repoData.git_push_url : resolvePushUri(updatedRepo)
7146
+ }
6529
7147
  repoStatusCache.set(repoParam, updatedRepo)
6530
7148
  repoData = updatedRepo
6531
7149
  const listEntry = lastRepoList.find((entry) => entry.repoParam === repoParam)
@@ -6536,6 +7154,12 @@ body.dark {
6536
7154
  if (updatedRepo.git_history_url) {
6537
7155
  listEntry.git_history_url = updatedRepo.git_history_url
6538
7156
  }
7157
+ if (!listEntry.git_fork_url && updatedRepo.git_fork_url) {
7158
+ listEntry.git_fork_url = updatedRepo.git_fork_url
7159
+ }
7160
+ if (!listEntry.git_push_url && updatedRepo.git_push_url) {
7161
+ listEntry.git_push_url = updatedRepo.git_push_url
7162
+ }
6539
7163
  }
6540
7164
  const total = Array.from(repoStatusCache.values()).reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6541
7165
  updateCombinedBadge(total)
@@ -7030,6 +7654,12 @@ body.dark {
7030
7654
  closeBtn.addEventListener('click', () => Swal.close())
7031
7655
  }
7032
7656
 
7657
+ if (iframe) {
7658
+ iframe.dataset.forceVisible = 'true'
7659
+ iframe.classList.remove('hidden')
7660
+ iframe.removeAttribute('hidden')
7661
+ }
7662
+
7033
7663
  disconnectHandler = (event) => {
7034
7664
  if (!iframe || event.source !== iframe.contentWindow) return
7035
7665
  if (!event.data || event.data.type !== 'pinokio:socket-closed') return
@@ -7055,16 +7685,62 @@ body.dark {
7055
7685
  }
7056
7686
  }
7057
7687
 
7058
- const showPublishModal = () => {
7059
- const pushUri = document.querySelector('#fs-status').getAttribute('data-push-uri')
7688
+ const showPublishModal = (target) => {
7689
+ let repoParam = null
7690
+ let repoLabel = null
7691
+
7692
+ if (typeof target === 'string') {
7693
+ repoParam = target
7694
+ } else if (target && typeof target === 'object') {
7695
+ if (typeof target.repoParam === 'string') {
7696
+ repoParam = target.repoParam
7697
+ }
7698
+ if (typeof target.repoName === 'string' && target.repoName.trim().length > 0) {
7699
+ repoLabel = target.repoName.trim()
7700
+ }
7701
+ }
7702
+
7703
+ if (!repoParam) {
7704
+ repoParam = activeRepoKey || null
7705
+ }
7706
+
7707
+ let repoData = null
7708
+ if (repoParam) {
7709
+ repoData = repoStatusCache.get(repoParam) || null
7710
+ if (!repoData && lastRepoList.length > 0) {
7711
+ const fallbackMatch = lastRepoList.find((repo) => repo.repoParam === repoParam)
7712
+ if (fallbackMatch) {
7713
+ repoData = fallbackMatch
7714
+ }
7715
+ }
7716
+ }
7717
+
7718
+ if (!repoLabel && repoData && typeof repoData.name === 'string') {
7719
+ repoLabel = repoData.name
7720
+ }
7721
+ if (!repoLabel && repoParam) {
7722
+ repoLabel = repoParam
7723
+ }
7724
+ if (!repoLabel) {
7725
+ repoLabel = workspaceName || 'Repository'
7726
+ }
7727
+
7728
+ const pushUri = resolvePushUri(repoData)
7060
7729
  if (!pushUri) {
7061
7730
  Swal.fire({
7062
7731
  title: 'Error',
7063
- text: 'Publish URL not available.',
7732
+ text: 'Publish URL not available for this repository.',
7064
7733
  icon: 'error'
7065
7734
  })
7066
7735
  return
7067
7736
  }
7737
+
7738
+ const remoteDisplay = repoData && repoData.url ? escapeHtml(repoData.url) : null
7739
+ const subtitle = `${escapeHtml(repoLabel)} — review your latest changes before publishing.`
7740
+ const remoteHtml = remoteDisplay ? `<div class="pinokio-fork-item-url">${remoteDisplay}</div>` : ''
7741
+ const timestampedUri = pushUri.includes('?')
7742
+ ? `${pushUri}&ts=${Date.now()}`
7743
+ : `${pushUri}?ts=${Date.now()}`
7068
7744
 
7069
7745
  const modalHtml = `
7070
7746
  <div class="pinokio-modal-surface">
@@ -7072,11 +7748,12 @@ body.dark {
7072
7748
  <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
7073
7749
  <div class="pinokio-modal-heading">
7074
7750
  <div class="pinokio-modal-title">Publish to GitHub</div>
7075
- <div class="pinokio-modal-subtitle">Review your latest changes and push them to your remote repository.</div>
7751
+ <div class="pinokio-modal-subtitle">${subtitle}</div>
7752
+ ${remoteHtml}
7076
7753
  </div>
7077
7754
  </div>
7078
7755
  <div class="pinokio-modal-body pinokio-modal-body--iframe">
7079
- <iframe src="${pushUri}"></iframe>
7756
+ <iframe src="${timestampedUri}"></iframe>
7080
7757
  </div>
7081
7758
  <div class="pinokio-modal-footer pinokio-modal-footer--publish" data-publish-footer>
7082
7759
  <button type="button" class="pinokio-publish-close-btn" data-publish-close>Close</button>
@@ -7329,65 +8006,424 @@ body.dark {
7329
8006
  focusConfirm: false
7330
8007
  })
7331
8008
  }
8009
+
8010
+ async function showForkModalForRepo(repoParam) {
8011
+ const key = typeof repoParam === 'string' && repoParam.length > 0 ? repoParam : (activeRepoKey || null)
8012
+ if (!key) {
8013
+ Swal.fire({
8014
+ title: 'No repository selected',
8015
+ text: 'Select a repository to fork from the dropdown.',
8016
+ icon: 'info'
8017
+ })
8018
+ return
8019
+ }
8020
+
8021
+ let repo = repoStatusCache.get(key)
8022
+ if (!repo) {
8023
+ await check_git()
8024
+ repo = repoStatusCache.get(key)
8025
+ }
8026
+
8027
+ if (!repo) {
8028
+ Swal.fire({
8029
+ title: 'Repository unavailable',
8030
+ text: 'The selected repository could not be found. Refresh Git status and try again.',
8031
+ icon: 'error'
8032
+ })
8033
+ return
8034
+ }
8035
+
8036
+ if (!hasRemoteConfigured(repo)) {
8037
+ Swal.fire({
8038
+ title: 'Remote required',
8039
+ text: 'This repository does not have a remote configured, so it cannot be forked.',
8040
+ icon: 'info'
8041
+ })
8042
+ return
8043
+ }
8044
+
8045
+ const forkUri = resolveForkUri(repo)
8046
+ if (!forkUri) {
8047
+ Swal.fire({
8048
+ title: 'Fork script unavailable',
8049
+ text: 'Unable to locate the fork script for this repository.',
8050
+ icon: 'error'
8051
+ })
8052
+ return
8053
+ }
8054
+
8055
+ const defaultName = deriveForkDefaultName(repo)
8056
+ const remoteDisplay = repo.url ? escapeHtml(repo.url) : 'No remote detected'
8057
+ const remoteClass = repo.url ? 'pinokio-fork-item-url' : 'pinokio-fork-item-url empty'
8058
+ const nameInputId = 'pinokio-fork-name-input'
8059
+ const orgToggleId = 'pinokio-fork-org-toggle'
8060
+ const orgInputId = 'pinokio-fork-org-input'
8061
+
8062
+ const modalHtml = `
8063
+ <div class="pinokio-modal-surface">
8064
+ <div class="pinokio-modal-header">
8065
+ <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
8066
+ <div class="pinokio-modal-heading">
8067
+ <div class="pinokio-modal-title">Fork repository</div>
8068
+ <div class="pinokio-modal-subtitle">${escapeHtml(repo.name || key)}</div>
8069
+ </div>
8070
+ </div>
8071
+ <div class="pinokio-modal-body">
8072
+ <div class="pinokio-fork-modal">
8073
+ <div class="pinokio-fork-item" data-disabled="false">
8074
+ <div class="pinokio-fork-item-url ${remoteClass}">${remoteDisplay}</div>
8075
+ <div class="pinokio-fork-name-input">
8076
+ <label for="${nameInputId}">Fork name</label>
8077
+ <input id="${nameInputId}" class="pinokio-modal-input" value="${escapeHtml(defaultName || '')}">
8078
+ </div>
8079
+ <div class="pinokio-fork-checkbox-row">
8080
+ <input type="checkbox" id="${orgToggleId}" data-fork-org-toggle>
8081
+ <label for="${orgToggleId}">Fork into organization</label>
8082
+ </div>
8083
+ <div class="pinokio-fork-org-input hidden" data-fork-org-section>
8084
+ <label for="${orgInputId}">Organization</label>
8085
+ <input id="${orgInputId}" class="pinokio-modal-input" placeholder="my-org">
8086
+ <p class="pinokio-fork-org-hint">Provide an organization where you have permission to create repositories.</p>
8087
+ </div>
8088
+ </div>
8089
+ </div>
8090
+ </div>
8091
+ </div>
8092
+ `
8093
+
8094
+ Swal.fire({
8095
+ html: modalHtml,
8096
+ customClass: {
8097
+ popup: 'pinokio-modern-modal',
8098
+ htmlContainer: 'pinokio-modern-html',
8099
+ closeButton: 'pinokio-modern-close',
8100
+ confirmButton: 'pinokio-modern-confirm',
8101
+ cancelButton: 'pinokio-modern-cancel'
8102
+ },
8103
+ backdrop: 'rgba(9,11,15,0.65)',
8104
+ width: 'min(520px, 90vw)',
8105
+ buttonsStyling: false,
8106
+ showCloseButton: true,
8107
+ showCancelButton: true,
8108
+ showConfirmButton: true,
8109
+ confirmButtonText: 'Fork on GitHub',
8110
+ cancelButtonText: 'Cancel',
8111
+ focusConfirm: false,
8112
+ didOpen: (popup) => {
8113
+ const input = popup.querySelector(`#${nameInputId}`)
8114
+ if (input) {
8115
+ input.addEventListener('input', () => {
8116
+ input.classList.remove('pinokio-modal-input--error')
8117
+ })
8118
+ input.focus()
8119
+ input.select()
8120
+ }
8121
+ const orgToggle = popup.querySelector('#' + orgToggleId)
8122
+ const orgSection = popup.querySelector('[data-fork-org-section]')
8123
+ const orgInput = popup.querySelector('#' + orgInputId)
8124
+ const syncOrgSection = () => {
8125
+ if (!orgSection) {
8126
+ return
8127
+ }
8128
+ if (orgToggle && orgToggle.checked) {
8129
+ orgSection.classList.remove('hidden')
8130
+ if (orgInput) {
8131
+ requestAnimationFrame(() => orgInput.focus())
8132
+ }
8133
+ } else {
8134
+ orgSection.classList.add('hidden')
8135
+ if (orgInput) {
8136
+ orgInput.classList.remove('pinokio-modal-input--error')
8137
+ }
8138
+ }
8139
+ }
8140
+ if (orgToggle) {
8141
+ orgToggle.addEventListener('change', syncOrgSection)
8142
+ syncOrgSection()
8143
+ }
8144
+ if (orgInput) {
8145
+ orgInput.addEventListener('input', () => {
8146
+ orgInput.classList.remove('pinokio-modal-input--error')
8147
+ })
8148
+ }
8149
+ },
8150
+ preConfirm: () => {
8151
+ const input = document.getElementById(nameInputId)
8152
+ const forkName = input ? input.value.trim() : ''
8153
+ if (!forkName) {
8154
+ if (input) {
8155
+ input.classList.add('pinokio-modal-input--error')
8156
+ setTimeout(() => input.focus(), 0)
8157
+ }
8158
+ Swal.showValidationMessage('Enter a repository name for the fork')
8159
+ return false
8160
+ }
8161
+ const orgToggle = document.getElementById(orgToggleId)
8162
+ const orgInput = document.getElementById(orgInputId)
8163
+ let orgValue = null
8164
+ if (orgToggle && orgToggle.checked) {
8165
+ orgValue = orgInput ? orgInput.value.trim() : ''
8166
+ if (!orgValue) {
8167
+ if (orgInput) {
8168
+ orgInput.classList.add('pinokio-modal-input--error')
8169
+ setTimeout(() => orgInput.focus(), 0)
8170
+ }
8171
+ Swal.showValidationMessage('Enter the organization name or uncheck the option')
8172
+ return false
8173
+ }
8174
+ }
8175
+ return {
8176
+ job: {
8177
+ repoParam: key,
8178
+ repoName: repo.name || key,
8179
+ forkUri,
8180
+ forkName,
8181
+ remoteUrl: repo.url || '',
8182
+ org: orgValue,
8183
+ }
8184
+ }
8185
+ }
8186
+ }).then((result) => {
8187
+ if (result.isConfirmed && result.value && result.value.job) {
8188
+ showForkShellModal(result.value.job)
8189
+ }
8190
+ })
8191
+ }
8192
+
8193
+ function showForkShellModal(job) {
8194
+ if (!job || !job.forkUri) {
8195
+ return
8196
+ }
8197
+
8198
+ const lifecycle = createPublishModalLifecycle()
8199
+ const forkNameValue = typeof job.forkName === 'string' && job.forkName.trim().length > 0
8200
+ ? job.forkName.trim()
8201
+ : deriveForkDefaultName({ name: job.repoName, url: job.remoteUrl })
8202
+ const queryParts = [`name=${encodeURIComponent(forkNameValue)}`]
8203
+ if (job.org) {
8204
+ queryParts.push(`org=${encodeURIComponent(job.org)}`)
8205
+ }
8206
+ queryParts.push(`ts=${Date.now()}`)
8207
+ const separator = job.forkUri.includes('?') ? '&' : '?'
8208
+ const finalUri = `${job.forkUri}${separator}${queryParts.join('&')}`
8209
+
8210
+ const remoteDisplay = job.remoteUrl ? escapeHtml(job.remoteUrl) : 'No remote detected'
8211
+ const titleDisplay = job.repoName ? escapeHtml(job.repoName) : 'Repository'
8212
+ const forkDisplay = escapeHtml(forkNameValue)
8213
+ const subtitleParts = [titleDisplay, forkDisplay]
8214
+ if (job.org) {
8215
+ subtitleParts.push(`Org: ${escapeHtml(job.org)}`)
8216
+ }
8217
+ const subtitleHtml = subtitleParts.join(' · ')
8218
+ Swal.fire({
8219
+ html: `
8220
+ <div class="pinokio-modal-surface">
8221
+ <div class="pinokio-modal-header">
8222
+ <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
8223
+ <div class="pinokio-modal-heading">
8224
+ <div class="pinokio-modal-title">Fork on GitHub</div>
8225
+ <div class="pinokio-modal-subtitle">${subtitleHtml}</div>
8226
+ <div class="pinokio-fork-item-url">${remoteDisplay}</div>
8227
+ </div>
8228
+ </div>
8229
+ <div class="pinokio-modal-body pinokio-modal-body--iframe">
8230
+ <iframe src="${finalUri}"></iframe>
8231
+ </div>
8232
+ <div class="pinokio-modal-footer pinokio-modal-footer--publish" data-publish-footer>
8233
+ <button type="button" class="pinokio-publish-close-btn" data-publish-close>Close</button>
8234
+ </div>
8235
+ </div>
8236
+ `,
8237
+ customClass: {
8238
+ popup: 'pinokio-modern-modal',
8239
+ htmlContainer: 'pinokio-modern-html',
8240
+ closeButton: 'pinokio-modern-close'
8241
+ },
8242
+ backdrop: 'rgba(9,11,15,0.65)',
8243
+ width: 'min(760px, 94vw)',
8244
+ buttonsStyling: false,
8245
+ showConfirmButton: false,
8246
+ showCloseButton: true,
8247
+ focusConfirm: false,
8248
+ didOpen: lifecycle.didOpen,
8249
+ willClose: lifecycle.willClose
8250
+ }).then(() => {
8251
+ setTimeout(() => {
8252
+ updateForkButton()
8253
+ check_git()
8254
+ }, 250)
8255
+ })
8256
+ }
7332
8257
 
7333
- const handlePushLogin = () => {
8258
+ function handlePushLogin() {
7334
8259
  window.location.href = '/github'
7335
8260
  }
8261
+ const promptGitLogin = () => {
8262
+ Swal.fire({
8263
+ html: `
8264
+ <div class="pinokio-github-login">
8265
+ <div class="pinokio-github-login__icon"><i class="fa-brands fa-github"></i></div>
8266
+ <div class="pinokio-github-login__title">Log in to GitHub</div>
8267
+ <div class="pinokio-github-login__body">Connect your GitHub account to fork or publish this workspace.</div>
8268
+ </div>
8269
+ `,
8270
+ showCancelButton: true,
8271
+ confirmButtonText: 'Log in',
8272
+ cancelButtonText: 'Not now',
8273
+ reverseButtons: true,
8274
+ showCloseButton: true,
8275
+ focusConfirm: true,
8276
+ customClass: {
8277
+ popup: 'pinokio-modern-modal pinokio-github-login-modal',
8278
+ htmlContainer: 'pinokio-modern-html',
8279
+ confirmButton: 'pinokio-modern-confirm',
8280
+ cancelButton: 'pinokio-modern-cancel',
8281
+ closeButton: 'pinokio-modern-close'
8282
+ }
8283
+ }).then((result) => {
8284
+ if (result.isConfirmed) {
8285
+ handlePushLogin()
8286
+ }
8287
+ })
8288
+ }
8289
+ const requireGitLogin = (event) => {
8290
+ if (event) {
8291
+ event.preventDefault()
8292
+ event.stopPropagation()
8293
+ }
8294
+ promptGitLogin()
8295
+ }
7336
8296
 
7337
8297
  // Function to update publish/create button
7338
8298
  const updatePublishButton = async () => {
7339
- const historyUri = document.querySelector('#fs-status').getAttribute('data-history-uri')
7340
- const pushBtn = document.querySelector('#fs-push-btn')
7341
-
7342
- if (!historyUri || !pushBtn) return
8299
+ if (!pushBtn) {
8300
+ latestGitIntegration = null
8301
+ updateForkButton()
8302
+ return
8303
+ }
8304
+
8305
+ const labelEl = pushBtn.querySelector('.fs-status-title')
8306
+ const setLabel = (text) => {
8307
+ if (labelEl) {
8308
+ labelEl.textContent = text
8309
+ }
8310
+ }
8311
+ const detachHandlers = () => {
8312
+ pushBtn.removeEventListener('click', showPublishModal)
8313
+ pushBtn.removeEventListener('click', showCreateModal)
8314
+ pushBtn.removeEventListener('click', handlePushLogin)
8315
+ pushBtn.removeEventListener('click', requireGitLogin)
8316
+ }
8317
+ const enableDropdown = () => {
8318
+ pushBtn.classList.add('revealer')
8319
+ pushBtn.setAttribute('data-group', '#fs-push-menu')
8320
+ }
8321
+ const disableDropdown = () => {
8322
+ pushBtn.classList.remove('revealer')
8323
+ pushBtn.removeAttribute('data-group')
8324
+ const pushMenuEl = document.querySelector('#fs-push-menu')
8325
+ if (pushMenuEl && !pushMenuEl.classList.contains('hidden')) {
8326
+ pushMenuEl.classList.add('hidden')
8327
+ }
8328
+ setDropdownState(pushBtn, false)
8329
+ }
8330
+
8331
+ const repos = getRepoListSnapshot()
8332
+ const publishTargets = Array.isArray(repos)
8333
+ ? repos.filter((repo) => hasRemoteConfigured(repo) && resolvePushUri(repo))
8334
+ : []
8335
+
8336
+ detachHandlers()
8337
+
8338
+ const syncForkButton = () => {
8339
+ updateForkButton()
8340
+ }
8341
+
8342
+ if (!historyUri) {
8343
+ setPublishMenuMessage('Git integration unavailable')
8344
+ setLabel('Publish')
8345
+ disableDropdown()
8346
+ pushBtn.disabled = publishTargets.length === 0
8347
+ if (pushBtn.disabled) {
8348
+ pushBtn.classList.add('fs-status-btn--disabled')
8349
+ } else {
8350
+ pushBtn.classList.remove('fs-status-btn--disabled')
8351
+ }
8352
+ pushBtn.setAttribute('title', 'Git integration is not available')
8353
+ latestGitIntegration = null
8354
+ syncForkButton()
8355
+ return
8356
+ }
8357
+
8358
+ if (!publishMenu) {
8359
+ setLabel('Publish')
8360
+ disableDropdown()
8361
+ } else if (!publishMenu.hasChildNodes()) {
8362
+ setPublishMenuMessage('Loading repositories...')
8363
+ }
7343
8364
 
7344
8365
  try {
7345
8366
  const response = await fetch(historyUri)
7346
- const data = await response.json()
7347
- const labelEl = pushBtn.querySelector('.fs-status-title')
7348
- const setLabel = (text) => {
7349
- if (labelEl) {
7350
- labelEl.textContent = text
7351
- }
8367
+ if (!response.ok) {
8368
+ throw new Error(`HTTP ${response.status}`)
7352
8369
  }
7353
- const resetHandlers = () => {
7354
- pushBtn.removeEventListener('click', showPublishModal)
7355
- pushBtn.removeEventListener('click', showCreateModal)
7356
- pushBtn.removeEventListener('click', handlePushLogin)
8370
+ const data = await response.json()
8371
+ latestGitIntegration = data
8372
+
8373
+ const isConnected = Boolean(data && data.connected)
8374
+ const emptyMessage = isConnected
8375
+ ? 'No Git repositories detected'
8376
+ : 'Connect GitHub to publish this workspace'
8377
+ renderPublishDropdown(repos, { emptyMessage })
8378
+
8379
+ if (!isConnected) {
8380
+ setLabel('Publish')
8381
+ pushBtn.disabled = false
8382
+ pushBtn.classList.remove('fs-status-btn--disabled')
8383
+ pushBtn.setAttribute('title', 'Log in to GitHub to publish this workspace')
8384
+ disableDropdown()
8385
+ pushBtn.addEventListener('click', requireGitLogin)
8386
+ syncForkButton()
8387
+ return
7357
8388
  }
7358
8389
 
7359
- // Check if GitHub is connected
7360
- if (!data.connected) {
7361
- // Not logged in - show "Login" button
7362
- setLabel('Login')
7363
- resetHandlers()
7364
- pushBtn.addEventListener('click', handlePushLogin)
8390
+ if (!Array.isArray(repos) || repos.length === 0) {
8391
+ setLabel('Publish')
8392
+ pushBtn.disabled = true
8393
+ pushBtn.classList.add('fs-status-btn--disabled')
8394
+ pushBtn.setAttribute('title', 'No Git repositories detected')
8395
+ disableDropdown()
8396
+ syncForkButton()
7365
8397
  return
7366
8398
  }
7367
-
7368
- // GitHub is connected - show appropriate button based on remotes
7369
- if (!data.remotes || data.remotes.length === 0) {
7370
- // No remotes - show "Create" button
8399
+
8400
+ pushBtn.disabled = false
8401
+ pushBtn.classList.remove('fs-status-btn--disabled')
8402
+
8403
+ if (publishTargets.length === 0) {
7371
8404
  setLabel('Create')
7372
- resetHandlers()
8405
+ pushBtn.setAttribute('title', 'Create a GitHub repository for this project')
8406
+ disableDropdown()
7373
8407
  pushBtn.addEventListener('click', showCreateModal)
7374
- } else {
7375
- // Has remotes - show "Publish" button
7376
- setLabel('Publish')
7377
- resetHandlers()
7378
- pushBtn.addEventListener('click', showPublishModal)
8408
+ syncForkButton()
8409
+ return
7379
8410
  }
8411
+
8412
+ setLabel('Publish')
8413
+ pushBtn.removeAttribute('title')
8414
+ enableDropdown()
8415
+ syncForkButton()
7380
8416
  } catch (error) {
7381
8417
  console.error('Error checking remotes:', error)
7382
- // Default to "Login" on error
7383
- const labelEl = pushBtn.querySelector('.fs-status-title')
7384
- if (labelEl) {
7385
- labelEl.textContent = 'Login'
7386
- }
7387
- pushBtn.removeEventListener('click', showPublishModal)
7388
- pushBtn.removeEventListener('click', showCreateModal)
7389
- pushBtn.removeEventListener('click', handlePushLogin)
7390
- pushBtn.addEventListener('click', handlePushLogin)
8418
+ setPublishMenuMessage('Git integration unavailable')
8419
+ setLabel('Publish')
8420
+ pushBtn.disabled = false
8421
+ pushBtn.classList.remove('fs-status-btn--disabled')
8422
+ pushBtn.setAttribute('title', 'Log in to GitHub to publish this workspace')
8423
+ disableDropdown()
8424
+ pushBtn.addEventListener('click', requireGitLogin)
8425
+ latestGitIntegration = null
8426
+ updateForkButton()
7391
8427
  }
7392
8428
  }
7393
8429