pinokiod 3.107.0 → 3.108.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);
@@ -2114,6 +2123,157 @@ body.dark {
2114
2123
  background: rgba(127, 91, 243, 1) !important;
2115
2124
  }
2116
2125
 
2126
+ .pinokio-modal-body--fork {
2127
+ display: flex;
2128
+ flex-direction: column;
2129
+ gap: 16px;
2130
+ padding: 0;
2131
+ }
2132
+
2133
+ .pinokio-fork-modal {
2134
+ display: flex;
2135
+ flex-direction: column;
2136
+ gap: 16px;
2137
+ }
2138
+
2139
+ .pinokio-fork-help {
2140
+ margin: 0;
2141
+ font-size: 0.875rem;
2142
+ color: rgba(255, 255, 255, 0.75);
2143
+ }
2144
+
2145
+ .pinokio-fork-item {
2146
+ border: 1px solid rgba(255, 255, 255, 0.08);
2147
+ border-radius: 10px;
2148
+ padding: 14px 16px;
2149
+ display: flex;
2150
+ flex-direction: column;
2151
+ gap: 10px;
2152
+ background: rgba(15, 18, 24, 0.65);
2153
+ }
2154
+
2155
+ .pinokio-fork-item[data-disabled='true'] {
2156
+ opacity: 0.55;
2157
+ }
2158
+
2159
+ .pinokio-fork-item-header {
2160
+ display: flex;
2161
+ align-items: center;
2162
+ gap: 10px;
2163
+ }
2164
+
2165
+ .pinokio-fork-item-header label {
2166
+ flex: 1;
2167
+ display: flex;
2168
+ align-items: center;
2169
+ gap: 8px;
2170
+ cursor: pointer;
2171
+ }
2172
+
2173
+ .pinokio-fork-item-title {
2174
+ font-weight: 600;
2175
+ font-size: 0.95rem;
2176
+ }
2177
+
2178
+ .pinokio-fork-item-url {
2179
+ font-size: 0.85rem;
2180
+ color: rgba(255, 255, 255, 0.65);
2181
+ word-break: break-word;
2182
+ }
2183
+
2184
+ .pinokio-fork-item-url.empty {
2185
+ font-style: italic;
2186
+ }
2187
+
2188
+ .pinokio-fork-name-input {
2189
+ display: flex;
2190
+ flex-direction: column;
2191
+ gap: 6px;
2192
+ }
2193
+
2194
+ .pinokio-fork-name-input label {
2195
+ font-size: 0.8rem;
2196
+ font-weight: 500;
2197
+ text-transform: uppercase;
2198
+ letter-spacing: 0.04em;
2199
+ color: rgba(255, 255, 255, 0.7);
2200
+ }
2201
+
2202
+ .pinokio-modal-input.pinokio-modal-input--error {
2203
+ border-color: #ff6b6b;
2204
+ box-shadow: 0 0 0 1px rgba(255, 107, 107, 0.25);
2205
+ }
2206
+
2207
+ .pinokio-fork-checkbox-row {
2208
+ display: flex;
2209
+ align-items: center;
2210
+ gap: 8px;
2211
+ font-size: 0.85rem;
2212
+ }
2213
+
2214
+ .pinokio-fork-checkbox-row label {
2215
+ cursor: pointer;
2216
+ }
2217
+
2218
+ .pinokio-fork-org-input {
2219
+ display: flex;
2220
+ flex-direction: column;
2221
+ gap: 6px;
2222
+ }
2223
+
2224
+ .pinokio-fork-org-input.hidden {
2225
+ display: none !important;
2226
+ }
2227
+
2228
+ .pinokio-fork-org-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-fork-org-hint {
2237
+ margin: 0;
2238
+ font-size: 0.75rem;
2239
+ color: rgba(255, 255, 255, 0.55);
2240
+ }
2241
+
2242
+ .pinokio-fork-dropdown-item,
2243
+ .pinokio-publish-dropdown-item {
2244
+ display: flex;
2245
+ flex-direction: column;
2246
+ align-items: flex-start;
2247
+ gap: 4px;
2248
+ text-align: left;
2249
+ }
2250
+
2251
+ .pinokio-fork-dropdown-title,
2252
+ .pinokio-publish-dropdown-title {
2253
+ font-weight: 600;
2254
+ font-size: 0.9rem;
2255
+ }
2256
+
2257
+ .pinokio-fork-dropdown-remote,
2258
+ .pinokio-publish-dropdown-remote {
2259
+ font-size: 0.75rem;
2260
+ color: rgba(255, 255, 255, 0.6);
2261
+ word-break: break-word;
2262
+ white-space: normal;
2263
+ overflow-wrap: anywhere;
2264
+ }
2265
+
2266
+ .pinokio-fork-dropdown-remote.empty,
2267
+ .pinokio-publish-dropdown-remote.empty {
2268
+ font-style: italic;
2269
+ color: rgba(255, 255, 255, 0.45);
2270
+ }
2271
+
2272
+ .fs-dropdown-item--disabled {
2273
+ opacity: 0.5;
2274
+ cursor: not-allowed;
2275
+ }
2276
+
2117
2277
  .pinokio-git-history-list {
2118
2278
  display: flex;
2119
2279
  flex-direction: column;
@@ -2526,13 +2686,16 @@ body.dark {
2526
2686
  font-size: 1rem;
2527
2687
  }
2528
2688
  */
2529
- #fs-push-btn {
2689
+ #fs-push-btn,
2690
+ #fs-fork-btn {
2530
2691
  min-width: 0;
2531
2692
  }
2532
- #fs-push-btn .fs-status-label {
2693
+ #fs-push-btn .fs-status-label,
2694
+ #fs-fork-btn .fs-status-label {
2533
2695
  font-size: 0;
2534
2696
  }
2535
- #fs-push-btn .fs-status-label i {
2697
+ #fs-push-btn .fs-status-label i,
2698
+ #fs-fork-btn .fs-status-label i {
2536
2699
  font-size: 1rem;
2537
2700
  }
2538
2701
  #fs-status .fs-status-btn .disk-usage,
@@ -2679,7 +2842,7 @@ body.dark {
2679
2842
  <% } %>
2680
2843
  <div class='container'>
2681
2844
  <% 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%>">
2845
+ <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
2846
  <a target="<%=src%>" href="<%=src%>" class='fs-status-btn frame-link' data-index="0" data-mode="refresh" data-type="n">
2684
2847
  <span class='fs-status-label'>
2685
2848
  <i class="fa-regular fa-folder-open"></i>
@@ -2733,12 +2896,24 @@ body.dark {
2733
2896
  </button>
2734
2897
  <div class='fs-dropdown-menu submenu hidden' id='fs-changes-menu'></div>
2735
2898
  </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>
2899
+ <div class='fs-status-dropdown git-fork'>
2900
+ <button id='fs-fork-btn' class='fs-status-btn revealer' data-group='#fs-fork-menu' type='button'>
2901
+ <span class='fs-status-label'>
2902
+ <i class="fa-solid fa-code-branch"></i>
2903
+ <span class='fs-status-title'>Fork</span>
2904
+ </span>
2905
+ </button>
2906
+ <div class='fs-dropdown-menu submenu hidden' id='fs-fork-menu'></div>
2907
+ </div>
2908
+ <div class='fs-status-dropdown git-publish'>
2909
+ <button id='fs-push-btn' class='fs-status-btn revealer' data-group='#fs-push-menu' type='button'>
2910
+ <span class='fs-status-label'>
2911
+ <i class="fa-brands fa-github"></i>
2912
+ <span class='fs-status-title'>Publish</span>
2913
+ </span>
2914
+ </button>
2915
+ <div class='fs-dropdown-menu submenu hidden' id='fs-push-menu'></div>
2916
+ </div>
2742
2917
  </div>
2743
2918
  <% } %>
2744
2919
  <main class='browserview'>
@@ -2772,17 +2947,20 @@ body.dark {
2772
2947
  let ignorePersistedSelection = pluginLaunchActive
2773
2948
  let lastForegroundSignature = null
2774
2949
  const iframe_onerror = (iframe) => {
2950
+ if (iframe && iframe.dataset && iframe.dataset.forceVisible === 'true') {
2951
+ return
2952
+ }
2775
2953
  let originalSrc = iframe.src
2776
2954
  iframe.onload = function() {
2777
2955
  try {
2778
2956
  // Try to access the iframe's document
2957
+ const iframeDoc = iframe.contentDocument || (iframe.contentWindow ? iframe.contentWindow.document : null)
2779
2958
  const currentSrc = iframe.src
2780
2959
  // Check if it's a chrome error page or empty
2781
2960
  if (currentSrc !== originalSrc &&
2782
2961
  (currentSrc.includes('chrome-error://') ||
2783
2962
  currentSrc === 'about:blank' ||
2784
2963
  currentSrc.includes('data:'))) {
2785
- iframeDoc.classList.add("hidden")
2786
2964
  Swal.fire({
2787
2965
  html: `<i class="fa-solid fa-circle-notch fa-spin"></i> Loading...`,
2788
2966
  customClass: {
@@ -2798,22 +2976,9 @@ body.dark {
2798
2976
  }, 3000)
2799
2977
  }
2800
2978
  } catch (e) {
2801
- iframe.classList.add("hidden")
2802
2979
  // Cross-origin restriction - assume it loaded successfully
2803
2980
  // 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);
2981
+ console.warn('Iframe load warning', e)
2817
2982
  }
2818
2983
  }
2819
2984
  }
@@ -2821,7 +2986,7 @@ body.dark {
2821
2986
  document.querySelectorAll(".menu-container .selected").forEach((el) => {
2822
2987
  el.classList.remove("selected")
2823
2988
  })
2824
- document.querySelectorAll("iframe").forEach((el) => {
2989
+ document.querySelectorAll("main.browserview iframe").forEach((el) => {
2825
2990
  el.classList.add("hidden")
2826
2991
  })
2827
2992
  let frame = document.createElement("iframe")
@@ -4588,7 +4753,7 @@ body.dark {
4588
4753
  if (!sourceWindow) {
4589
4754
  return null
4590
4755
  }
4591
- const frames = Array.from(document.querySelectorAll("iframe"))
4756
+ const frames = Array.from(document.querySelectorAll("main.browserview iframe"))
4592
4757
  for (const frame of frames) {
4593
4758
  if (frame.contentWindow === sourceWindow) {
4594
4759
  return frame.name || null
@@ -4829,7 +4994,7 @@ body.dark {
4829
4994
  <% } %>
4830
4995
 
4831
4996
  // hide all frames
4832
- document.querySelectorAll("iframe").forEach((el) => {
4997
+ document.querySelectorAll("main.browserview iframe").forEach((el) => {
4833
4998
  el.classList.add("hidden")
4834
4999
  })
4835
5000
 
@@ -5975,9 +6140,9 @@ body.dark {
5975
6140
  } else {
5976
6141
  global_selector = null
5977
6142
  }
5978
- const frameExists = Array.from(document.querySelectorAll("iframe")).some((frame) => {
5979
- return frame.name === rawName
5980
- })
6143
+ const frameExists = Array.from(document.querySelectorAll("main.browserview iframe")).some((frame) => {
6144
+ return frame.name === rawName
6145
+ })
5981
6146
  if (!frameExists) {
5982
6147
  create_iframe(rawName, event.data.launch.href)
5983
6148
  }
@@ -6082,6 +6247,13 @@ body.dark {
6082
6247
  const changesMenu = document.getElementById('fs-changes-menu')
6083
6248
  const changesBtn = document.getElementById('fs-changes-btn')
6084
6249
  const badgeElement = changesBtn ? changesBtn.querySelector('.badge') : null
6250
+ const forkBtn = document.getElementById('fs-fork-btn')
6251
+ const forkDropdownContainer = document.querySelector('#fs-status .git-fork')
6252
+ const forkMenu = document.getElementById('fs-fork-menu')
6253
+ const pushBtn = document.getElementById('fs-push-btn')
6254
+ const publishMenu = document.getElementById('fs-push-menu')
6255
+
6256
+ let latestGitIntegration = null
6085
6257
 
6086
6258
  const readDataAttr = (node, attr) => {
6087
6259
  if (!node) {
@@ -6097,6 +6269,9 @@ body.dark {
6097
6269
  const statusUri = readDataAttr(fsStatusEl, 'data-status-uri')
6098
6270
  const monitorUri = readDataAttr(fsStatusEl, 'data-uri')
6099
6271
  const workspaceName = readDataAttr(fsStatusEl, 'data-workspace')
6272
+ const historyUri = readDataAttr(fsStatusEl, 'data-history-uri')
6273
+ const defaultPushUri = readDataAttr(fsStatusEl, 'data-push-uri')
6274
+ const defaultForkUri = readDataAttr(fsStatusEl, 'data-fork-uri')
6100
6275
 
6101
6276
  const encodeRepoPath = (value) => {
6102
6277
  if (typeof value !== 'string' || value.length === 0) {
@@ -6105,6 +6280,191 @@ body.dark {
6105
6280
  return value.split('/').map(encodeURIComponent).join('/')
6106
6281
  }
6107
6282
 
6283
+ function escapeHtml(value) {
6284
+ if (value === null || value === undefined) {
6285
+ return ''
6286
+ }
6287
+ return String(value).replace(/[&<>"']/g, (match) => {
6288
+ switch (match) {
6289
+ case '&':
6290
+ return '&amp;'
6291
+ case '<':
6292
+ return '&lt;'
6293
+ case '>':
6294
+ return '&gt;'
6295
+ case '"':
6296
+ return '&quot;'
6297
+ case '\'':
6298
+ return '&#39;'
6299
+ default:
6300
+ return match
6301
+ }
6302
+ })
6303
+ }
6304
+
6305
+ function getRepoListSnapshot() {
6306
+ if (Array.isArray(lastRepoList) && lastRepoList.length > 0) {
6307
+ return lastRepoList.slice()
6308
+ }
6309
+ return Array.from(repoStatusCache.values())
6310
+ }
6311
+
6312
+ function parseRemoteSlug(remoteUrl) {
6313
+ if (!remoteUrl || typeof remoteUrl !== 'string') {
6314
+ return { owner: null, name: null, full: null }
6315
+ }
6316
+ const trimmed = remoteUrl.trim()
6317
+ if (!trimmed) {
6318
+ return { owner: null, name: null, full: null }
6319
+ }
6320
+ const withoutGit = trimmed.replace(/\.git$/i, '')
6321
+
6322
+ const parsePathSegments = (pathValue) => {
6323
+ if (!pathValue) {
6324
+ return []
6325
+ }
6326
+ const cleaned = pathValue.replace(/^\/+/, '')
6327
+ return cleaned.split('/').filter(Boolean)
6328
+ }
6329
+
6330
+ let pathSegment = ''
6331
+ if (withoutGit.startsWith('git@')) {
6332
+ const colonSplit = withoutGit.split(':')
6333
+ pathSegment = colonSplit.length > 1 ? colonSplit.slice(1).join(':') : ''
6334
+ } else {
6335
+ try {
6336
+ const prefixed = withoutGit.includes('://') ? withoutGit : `https://${withoutGit}`
6337
+ const url = new URL(prefixed)
6338
+ pathSegment = url.pathname
6339
+ } catch (error) {
6340
+ pathSegment = withoutGit
6341
+ }
6342
+ }
6343
+
6344
+ const segments = parsePathSegments(pathSegment)
6345
+ if (segments.length >= 2) {
6346
+ const owner = segments[segments.length - 2]
6347
+ const name = segments[segments.length - 1]
6348
+ return { owner, name, full: `${owner}/${name}` }
6349
+ }
6350
+
6351
+ if (segments.length === 1) {
6352
+ const name = segments[0]
6353
+ return { owner: null, name, full: name }
6354
+ }
6355
+
6356
+ return { owner: null, name: null, full: null }
6357
+ }
6358
+
6359
+ function deriveForkDefaultName(repo) {
6360
+ if (!repo) {
6361
+ return workspaceName || 'fork'
6362
+ }
6363
+ const remoteSlug = parseRemoteSlug(repo.url || '')
6364
+ if (remoteSlug && remoteSlug.name) {
6365
+ return remoteSlug.name
6366
+ }
6367
+ if (typeof repo.name === 'string' && repo.name.length > 0) {
6368
+ const segments = repo.name.split('/').filter(Boolean)
6369
+ if (segments.length > 0) {
6370
+ return segments[segments.length - 1]
6371
+ }
6372
+ }
6373
+ if (typeof repo.repoParam === 'string' && repo.repoParam.length > 0) {
6374
+ const repoSegments = repo.repoParam.split('/').filter(Boolean)
6375
+ if (repoSegments.length > 0) {
6376
+ return repoSegments[repoSegments.length - 1]
6377
+ }
6378
+ }
6379
+ return workspaceName || 'fork'
6380
+ }
6381
+
6382
+ function hasRemoteConfigured(repo) {
6383
+ return Boolean(repo && typeof repo.url === 'string' && repo.url.trim().length > 0)
6384
+ }
6385
+
6386
+ function resolveForkUri(repo) {
6387
+ if (repo && typeof repo.git_fork_url === 'string' && repo.git_fork_url.length > 0) {
6388
+ return repo.git_fork_url
6389
+ }
6390
+ return defaultForkUri || null
6391
+ }
6392
+
6393
+ function resolvePushUri(repo) {
6394
+ if (repo && typeof repo.git_push_url === 'string' && repo.git_push_url.length > 0) {
6395
+ return repo.git_push_url
6396
+ }
6397
+ return defaultPushUri || null
6398
+ }
6399
+
6400
+ function updateForkButton() {
6401
+ if (!forkBtn) {
6402
+ return
6403
+ }
6404
+ const labelEl = forkBtn.querySelector('.fs-status-title')
6405
+ const repos = getRepoListSnapshot()
6406
+ const forkTargets = Array.isArray(repos)
6407
+ ? repos.filter((repo) => hasRemoteConfigured(repo) && resolveForkUri(repo))
6408
+ : []
6409
+
6410
+ const detachHandlers = () => {
6411
+ forkBtn.removeEventListener('click', handlePushLogin)
6412
+ }
6413
+
6414
+ const enableDropdown = () => {
6415
+ forkBtn.classList.add('revealer')
6416
+ forkBtn.setAttribute('data-group', '#fs-fork-menu')
6417
+ }
6418
+
6419
+ const disableDropdown = () => {
6420
+ forkBtn.classList.remove('revealer')
6421
+ forkBtn.removeAttribute('data-group')
6422
+ closeStatusDropdowns()
6423
+ }
6424
+
6425
+ detachHandlers()
6426
+
6427
+ const isConnected = Boolean(latestGitIntegration && latestGitIntegration.connected)
6428
+
6429
+ renderForkDropdown(repos, {
6430
+ emptyMessage: isConnected
6431
+ ? 'No remotes available to fork'
6432
+ : 'Connect GitHub to enable forking',
6433
+ })
6434
+
6435
+ if (!isConnected) {
6436
+ disableDropdown()
6437
+ if (labelEl) {
6438
+ labelEl.textContent = 'Login'
6439
+ }
6440
+ forkBtn.disabled = false
6441
+ forkBtn.classList.remove('fs-status-btn--disabled')
6442
+ forkBtn.setAttribute('title', 'Connect GitHub to fork this workspace')
6443
+ forkBtn.addEventListener('click', handlePushLogin)
6444
+ return
6445
+ }
6446
+
6447
+ if (labelEl) {
6448
+ labelEl.textContent = 'Fork'
6449
+ }
6450
+
6451
+ forkBtn.disabled = false
6452
+ forkBtn.classList.remove('fs-status-btn--disabled')
6453
+ enableDropdown()
6454
+
6455
+ if (!Array.isArray(repos) || repos.length === 0) {
6456
+ forkBtn.setAttribute('title', 'No Git repositories detected')
6457
+ return
6458
+ }
6459
+
6460
+ if (forkTargets.length === 0) {
6461
+ forkBtn.setAttribute('title', 'No remotes available to fork')
6462
+ return
6463
+ }
6464
+
6465
+ forkBtn.removeAttribute('title')
6466
+ }
6467
+
6108
6468
  const updateCombinedBadge = (total) => {
6109
6469
  if (!badgeElement) {
6110
6470
  return
@@ -6136,6 +6496,143 @@ body.dark {
6136
6496
  changesMenu.append(messageEl)
6137
6497
  }
6138
6498
 
6499
+ const setForkMenuMessage = (message) => {
6500
+ if (!forkMenu) {
6501
+ return
6502
+ }
6503
+ const messageEl = document.createElement('div')
6504
+ messageEl.className = 'fs-dropdown-empty'
6505
+ messageEl.textContent = message
6506
+ forkMenu.innerHTML = ''
6507
+ forkMenu.append(messageEl)
6508
+ }
6509
+
6510
+ const setPublishMenuMessage = (message) => {
6511
+ if (!publishMenu) {
6512
+ return
6513
+ }
6514
+ const messageEl = document.createElement('div')
6515
+ messageEl.className = 'fs-dropdown-empty'
6516
+ messageEl.textContent = message
6517
+ publishMenu.innerHTML = ''
6518
+ publishMenu.append(messageEl)
6519
+ }
6520
+
6521
+ const renderForkDropdown = (repos, options = {}) => {
6522
+ if (!forkMenu) {
6523
+ return
6524
+ }
6525
+
6526
+ const opts = typeof options === 'object' && options !== null ? options : {}
6527
+ const list = Array.isArray(repos) ? repos : []
6528
+
6529
+ forkMenu.innerHTML = ''
6530
+
6531
+ if (list.length === 0) {
6532
+ setForkMenuMessage(opts.emptyMessage || 'No repositories available')
6533
+ return
6534
+ }
6535
+
6536
+ const fragment = document.createDocumentFragment()
6537
+
6538
+ list.forEach((repo) => {
6539
+ const key = repo && typeof repo.repoParam === 'string' ? repo.repoParam : ''
6540
+ const name = repo && repo.name ? repo.name : (key || 'Repository')
6541
+ const remoteUrl = repo && repo.url ? repo.url : ''
6542
+ const forkUri = resolveForkUri(repo)
6543
+ const hasRemote = hasRemoteConfigured(repo)
6544
+
6545
+ const item = document.createElement('button')
6546
+ item.type = 'button'
6547
+ item.className = 'fs-dropdown-item pinokio-fork-dropdown-item'
6548
+ item.dataset.repo = key
6549
+
6550
+ const remoteClass = remoteUrl
6551
+ ? 'pinokio-fork-dropdown-remote'
6552
+ : 'pinokio-fork-dropdown-remote empty'
6553
+ const remoteDisplay = remoteUrl ? escapeHtml(remoteUrl) : 'No remote detected'
6554
+
6555
+ item.innerHTML = `
6556
+ <div class="pinokio-fork-dropdown-title">${escapeHtml(name)}</div>
6557
+ <div class="${remoteClass}">${remoteDisplay}</div>
6558
+ `
6559
+
6560
+ if (!hasRemote || !forkUri) {
6561
+ item.disabled = true
6562
+ item.classList.add('fs-dropdown-item--disabled')
6563
+ } else {
6564
+ item.addEventListener('click', (event) => {
6565
+ event.preventDefault()
6566
+ event.stopPropagation()
6567
+ closeStatusDropdowns()
6568
+ showForkModalForRepo(key)
6569
+ })
6570
+ }
6571
+
6572
+ fragment.appendChild(item)
6573
+ })
6574
+
6575
+ forkMenu.appendChild(fragment)
6576
+ }
6577
+
6578
+ const renderPublishDropdown = (repos, options = {}) => {
6579
+ if (!publishMenu) {
6580
+ return
6581
+ }
6582
+
6583
+ const opts = typeof options === 'object' && options !== null ? options : {}
6584
+ const list = Array.isArray(repos) ? repos : []
6585
+
6586
+ publishMenu.innerHTML = ''
6587
+
6588
+ if (list.length === 0) {
6589
+ setPublishMenuMessage(opts.emptyMessage || 'No repositories available')
6590
+ return
6591
+ }
6592
+
6593
+ const fragment = document.createDocumentFragment()
6594
+
6595
+ list.forEach((repo) => {
6596
+ const key = repo && typeof repo.repoParam === 'string' ? repo.repoParam : ''
6597
+ const name = repo && repo.name ? repo.name : (key || 'Repository')
6598
+ const remoteUrl = repo && repo.url ? repo.url : ''
6599
+ const pushUri = resolvePushUri(repo)
6600
+ const hasRemote = hasRemoteConfigured(repo)
6601
+
6602
+ const item = document.createElement('button')
6603
+ item.type = 'button'
6604
+ item.className = 'fs-dropdown-item pinokio-publish-dropdown-item'
6605
+ item.dataset.repo = key
6606
+ item.dataset.name = name
6607
+
6608
+ const remoteClass = remoteUrl
6609
+ ? 'pinokio-publish-dropdown-remote'
6610
+ : 'pinokio-publish-dropdown-remote empty'
6611
+ const remoteDisplay = remoteUrl ? escapeHtml(remoteUrl) : 'No remote detected'
6612
+
6613
+ item.innerHTML = `
6614
+ <div class="pinokio-publish-dropdown-title">${escapeHtml(name)}</div>
6615
+ <div class="${remoteClass}">${remoteDisplay}</div>
6616
+ `
6617
+
6618
+ if (!hasRemote || !pushUri) {
6619
+ item.disabled = true
6620
+ item.classList.add('fs-dropdown-item--disabled')
6621
+ } else {
6622
+ item.addEventListener('click', (event) => {
6623
+ event.preventDefault()
6624
+ event.stopPropagation()
6625
+ closeStatusDropdowns()
6626
+ showPublishModal({ repoParam: key, repoName: name })
6627
+ })
6628
+ }
6629
+
6630
+ fragment.appendChild(item)
6631
+ })
6632
+
6633
+ publishMenu.appendChild(fragment)
6634
+ }
6635
+
6139
6636
  const attachRepoDropdownHandlers = () => {
6140
6637
  if (!changesMenu) {
6141
6638
  return
@@ -6233,6 +6730,8 @@ body.dark {
6233
6730
  changes: Array.isArray(data.changes) ? data.changes : [],
6234
6731
  git_commit_url: data.git_commit_url || null,
6235
6732
  git_history_url: workspaceName ? `/info/git/HEAD/${encodeRepoPath(workspaceName)}` : readDataAttr(fsStatusEl, 'data-history-uri'),
6733
+ git_fork_url: defaultForkUri,
6734
+ git_push_url: defaultPushUri,
6236
6735
  url: null,
6237
6736
  }
6238
6737
 
@@ -6246,12 +6745,14 @@ body.dark {
6246
6745
  renderChangesDropdown(lastRepoList)
6247
6746
  updateCombinedBadge(fallbackRepo.changeCount)
6248
6747
  updatePublishButton()
6748
+ updateForkButton()
6249
6749
  return true
6250
6750
  } catch (error) {
6251
6751
  console.error('check_git fallback error:', error)
6252
6752
  setChangesMenuMessage('Unable to load repositories')
6253
6753
  updateCombinedBadge(0)
6254
6754
  updateChangesButtonState(false)
6755
+ updateForkButton()
6255
6756
  return false
6256
6757
  }
6257
6758
  }
@@ -6316,6 +6817,7 @@ body.dark {
6316
6817
  : sortedRepos.reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6317
6818
  updateCombinedBadge(total)
6318
6819
  updateChangesButtonState(true)
6820
+ updateForkButton()
6319
6821
  }
6320
6822
 
6321
6823
  updatePublishButton()
@@ -6526,6 +7028,12 @@ body.dark {
6526
7028
  updatedRepo.git_commit_url = freshData.git_commit_url || null
6527
7029
  updatedRepo.name = updatedRepo.name || repoDisplayName || repoParam
6528
7030
  updatedRepo.git_history_url = updatedRepo.git_history_url || (repoData && repoData.git_history_url) || (repoParam ? `/info/git/HEAD/${encodeRepoPath(repoParam)}` : null)
7031
+ if (!updatedRepo.git_fork_url) {
7032
+ updatedRepo.git_fork_url = repoData && repoData.git_fork_url ? repoData.git_fork_url : resolveForkUri(updatedRepo)
7033
+ }
7034
+ if (!updatedRepo.git_push_url) {
7035
+ updatedRepo.git_push_url = repoData && repoData.git_push_url ? repoData.git_push_url : resolvePushUri(updatedRepo)
7036
+ }
6529
7037
  repoStatusCache.set(repoParam, updatedRepo)
6530
7038
  repoData = updatedRepo
6531
7039
  const listEntry = lastRepoList.find((entry) => entry.repoParam === repoParam)
@@ -6536,6 +7044,12 @@ body.dark {
6536
7044
  if (updatedRepo.git_history_url) {
6537
7045
  listEntry.git_history_url = updatedRepo.git_history_url
6538
7046
  }
7047
+ if (!listEntry.git_fork_url && updatedRepo.git_fork_url) {
7048
+ listEntry.git_fork_url = updatedRepo.git_fork_url
7049
+ }
7050
+ if (!listEntry.git_push_url && updatedRepo.git_push_url) {
7051
+ listEntry.git_push_url = updatedRepo.git_push_url
7052
+ }
6539
7053
  }
6540
7054
  const total = Array.from(repoStatusCache.values()).reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6541
7055
  updateCombinedBadge(total)
@@ -7030,6 +7544,12 @@ body.dark {
7030
7544
  closeBtn.addEventListener('click', () => Swal.close())
7031
7545
  }
7032
7546
 
7547
+ if (iframe) {
7548
+ iframe.dataset.forceVisible = 'true'
7549
+ iframe.classList.remove('hidden')
7550
+ iframe.removeAttribute('hidden')
7551
+ }
7552
+
7033
7553
  disconnectHandler = (event) => {
7034
7554
  if (!iframe || event.source !== iframe.contentWindow) return
7035
7555
  if (!event.data || event.data.type !== 'pinokio:socket-closed') return
@@ -7055,16 +7575,62 @@ body.dark {
7055
7575
  }
7056
7576
  }
7057
7577
 
7058
- const showPublishModal = () => {
7059
- const pushUri = document.querySelector('#fs-status').getAttribute('data-push-uri')
7578
+ const showPublishModal = (target) => {
7579
+ let repoParam = null
7580
+ let repoLabel = null
7581
+
7582
+ if (typeof target === 'string') {
7583
+ repoParam = target
7584
+ } else if (target && typeof target === 'object') {
7585
+ if (typeof target.repoParam === 'string') {
7586
+ repoParam = target.repoParam
7587
+ }
7588
+ if (typeof target.repoName === 'string' && target.repoName.trim().length > 0) {
7589
+ repoLabel = target.repoName.trim()
7590
+ }
7591
+ }
7592
+
7593
+ if (!repoParam) {
7594
+ repoParam = activeRepoKey || null
7595
+ }
7596
+
7597
+ let repoData = null
7598
+ if (repoParam) {
7599
+ repoData = repoStatusCache.get(repoParam) || null
7600
+ if (!repoData && lastRepoList.length > 0) {
7601
+ const fallbackMatch = lastRepoList.find((repo) => repo.repoParam === repoParam)
7602
+ if (fallbackMatch) {
7603
+ repoData = fallbackMatch
7604
+ }
7605
+ }
7606
+ }
7607
+
7608
+ if (!repoLabel && repoData && typeof repoData.name === 'string') {
7609
+ repoLabel = repoData.name
7610
+ }
7611
+ if (!repoLabel && repoParam) {
7612
+ repoLabel = repoParam
7613
+ }
7614
+ if (!repoLabel) {
7615
+ repoLabel = workspaceName || 'Repository'
7616
+ }
7617
+
7618
+ const pushUri = resolvePushUri(repoData)
7060
7619
  if (!pushUri) {
7061
7620
  Swal.fire({
7062
7621
  title: 'Error',
7063
- text: 'Publish URL not available.',
7622
+ text: 'Publish URL not available for this repository.',
7064
7623
  icon: 'error'
7065
7624
  })
7066
7625
  return
7067
7626
  }
7627
+
7628
+ const remoteDisplay = repoData && repoData.url ? escapeHtml(repoData.url) : null
7629
+ const subtitle = `${escapeHtml(repoLabel)} — review your latest changes before publishing.`
7630
+ const remoteHtml = remoteDisplay ? `<div class="pinokio-fork-item-url">${remoteDisplay}</div>` : ''
7631
+ const timestampedUri = pushUri.includes('?')
7632
+ ? `${pushUri}&ts=${Date.now()}`
7633
+ : `${pushUri}?ts=${Date.now()}`
7068
7634
 
7069
7635
  const modalHtml = `
7070
7636
  <div class="pinokio-modal-surface">
@@ -7072,11 +7638,12 @@ body.dark {
7072
7638
  <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
7073
7639
  <div class="pinokio-modal-heading">
7074
7640
  <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>
7641
+ <div class="pinokio-modal-subtitle">${subtitle}</div>
7642
+ ${remoteHtml}
7076
7643
  </div>
7077
7644
  </div>
7078
7645
  <div class="pinokio-modal-body pinokio-modal-body--iframe">
7079
- <iframe src="${pushUri}"></iframe>
7646
+ <iframe src="${timestampedUri}"></iframe>
7080
7647
  </div>
7081
7648
  <div class="pinokio-modal-footer pinokio-modal-footer--publish" data-publish-footer>
7082
7649
  <button type="button" class="pinokio-publish-close-btn" data-publish-close>Close</button>
@@ -7329,65 +7896,384 @@ body.dark {
7329
7896
  focusConfirm: false
7330
7897
  })
7331
7898
  }
7899
+
7900
+ async function showForkModalForRepo(repoParam) {
7901
+ const key = typeof repoParam === 'string' && repoParam.length > 0 ? repoParam : (activeRepoKey || null)
7902
+ if (!key) {
7903
+ Swal.fire({
7904
+ title: 'No repository selected',
7905
+ text: 'Select a repository to fork from the dropdown.',
7906
+ icon: 'info'
7907
+ })
7908
+ return
7909
+ }
7910
+
7911
+ let repo = repoStatusCache.get(key)
7912
+ if (!repo) {
7913
+ await check_git()
7914
+ repo = repoStatusCache.get(key)
7915
+ }
7916
+
7917
+ if (!repo) {
7918
+ Swal.fire({
7919
+ title: 'Repository unavailable',
7920
+ text: 'The selected repository could not be found. Refresh Git status and try again.',
7921
+ icon: 'error'
7922
+ })
7923
+ return
7924
+ }
7925
+
7926
+ if (!hasRemoteConfigured(repo)) {
7927
+ Swal.fire({
7928
+ title: 'Remote required',
7929
+ text: 'This repository does not have a remote configured, so it cannot be forked.',
7930
+ icon: 'info'
7931
+ })
7932
+ return
7933
+ }
7934
+
7935
+ const forkUri = resolveForkUri(repo)
7936
+ if (!forkUri) {
7937
+ Swal.fire({
7938
+ title: 'Fork script unavailable',
7939
+ text: 'Unable to locate the fork script for this repository.',
7940
+ icon: 'error'
7941
+ })
7942
+ return
7943
+ }
7944
+
7945
+ const defaultName = deriveForkDefaultName(repo)
7946
+ const remoteDisplay = repo.url ? escapeHtml(repo.url) : 'No remote detected'
7947
+ const remoteClass = repo.url ? 'pinokio-fork-item-url' : 'pinokio-fork-item-url empty'
7948
+ const nameInputId = 'pinokio-fork-name-input'
7949
+ const orgToggleId = 'pinokio-fork-org-toggle'
7950
+ const orgInputId = 'pinokio-fork-org-input'
7951
+
7952
+ const modalHtml = `
7953
+ <div class="pinokio-modal-surface">
7954
+ <div class="pinokio-modal-header">
7955
+ <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
7956
+ <div class="pinokio-modal-heading">
7957
+ <div class="pinokio-modal-title">Fork repository</div>
7958
+ <div class="pinokio-modal-subtitle">${escapeHtml(repo.name || key)}</div>
7959
+ </div>
7960
+ </div>
7961
+ <div class="pinokio-modal-body">
7962
+ <div class="pinokio-fork-modal">
7963
+ <div class="pinokio-fork-item" data-disabled="false">
7964
+ <div class="pinokio-fork-item-url ${remoteClass}">${remoteDisplay}</div>
7965
+ <div class="pinokio-fork-name-input">
7966
+ <label for="${nameInputId}">Fork name</label>
7967
+ <input id="${nameInputId}" class="pinokio-modal-input" value="${escapeHtml(defaultName || '')}">
7968
+ </div>
7969
+ <div class="pinokio-fork-checkbox-row">
7970
+ <input type="checkbox" id="${orgToggleId}" data-fork-org-toggle>
7971
+ <label for="${orgToggleId}">Fork into organization</label>
7972
+ </div>
7973
+ <div class="pinokio-fork-org-input hidden" data-fork-org-section>
7974
+ <label for="${orgInputId}">Organization</label>
7975
+ <input id="${orgInputId}" class="pinokio-modal-input" placeholder="my-org">
7976
+ <p class="pinokio-fork-org-hint">Provide an organization where you have permission to create repositories.</p>
7977
+ </div>
7978
+ </div>
7979
+ </div>
7980
+ </div>
7981
+ </div>
7982
+ `
7983
+
7984
+ Swal.fire({
7985
+ html: modalHtml,
7986
+ customClass: {
7987
+ popup: 'pinokio-modern-modal',
7988
+ htmlContainer: 'pinokio-modern-html',
7989
+ closeButton: 'pinokio-modern-close',
7990
+ confirmButton: 'pinokio-modern-confirm',
7991
+ cancelButton: 'pinokio-modern-cancel'
7992
+ },
7993
+ backdrop: 'rgba(9,11,15,0.65)',
7994
+ width: 'min(520px, 90vw)',
7995
+ buttonsStyling: false,
7996
+ showCloseButton: true,
7997
+ showCancelButton: true,
7998
+ showConfirmButton: true,
7999
+ confirmButtonText: 'Fork on GitHub',
8000
+ cancelButtonText: 'Cancel',
8001
+ focusConfirm: false,
8002
+ didOpen: (popup) => {
8003
+ const input = popup.querySelector(`#${nameInputId}`)
8004
+ if (input) {
8005
+ input.addEventListener('input', () => {
8006
+ input.classList.remove('pinokio-modal-input--error')
8007
+ })
8008
+ input.focus()
8009
+ input.select()
8010
+ }
8011
+ const orgToggle = popup.querySelector('#' + orgToggleId)
8012
+ const orgSection = popup.querySelector('[data-fork-org-section]')
8013
+ const orgInput = popup.querySelector('#' + orgInputId)
8014
+ const syncOrgSection = () => {
8015
+ if (!orgSection) {
8016
+ return
8017
+ }
8018
+ if (orgToggle && orgToggle.checked) {
8019
+ orgSection.classList.remove('hidden')
8020
+ if (orgInput) {
8021
+ requestAnimationFrame(() => orgInput.focus())
8022
+ }
8023
+ } else {
8024
+ orgSection.classList.add('hidden')
8025
+ if (orgInput) {
8026
+ orgInput.classList.remove('pinokio-modal-input--error')
8027
+ }
8028
+ }
8029
+ }
8030
+ if (orgToggle) {
8031
+ orgToggle.addEventListener('change', syncOrgSection)
8032
+ syncOrgSection()
8033
+ }
8034
+ if (orgInput) {
8035
+ orgInput.addEventListener('input', () => {
8036
+ orgInput.classList.remove('pinokio-modal-input--error')
8037
+ })
8038
+ }
8039
+ },
8040
+ preConfirm: () => {
8041
+ const input = document.getElementById(nameInputId)
8042
+ const forkName = input ? input.value.trim() : ''
8043
+ if (!forkName) {
8044
+ if (input) {
8045
+ input.classList.add('pinokio-modal-input--error')
8046
+ setTimeout(() => input.focus(), 0)
8047
+ }
8048
+ Swal.showValidationMessage('Enter a repository name for the fork')
8049
+ return false
8050
+ }
8051
+ const orgToggle = document.getElementById(orgToggleId)
8052
+ const orgInput = document.getElementById(orgInputId)
8053
+ let orgValue = null
8054
+ if (orgToggle && orgToggle.checked) {
8055
+ orgValue = orgInput ? orgInput.value.trim() : ''
8056
+ if (!orgValue) {
8057
+ if (orgInput) {
8058
+ orgInput.classList.add('pinokio-modal-input--error')
8059
+ setTimeout(() => orgInput.focus(), 0)
8060
+ }
8061
+ Swal.showValidationMessage('Enter the organization name or uncheck the option')
8062
+ return false
8063
+ }
8064
+ }
8065
+ return {
8066
+ job: {
8067
+ repoParam: key,
8068
+ repoName: repo.name || key,
8069
+ forkUri,
8070
+ forkName,
8071
+ remoteUrl: repo.url || '',
8072
+ org: orgValue,
8073
+ }
8074
+ }
8075
+ }
8076
+ }).then((result) => {
8077
+ if (result.isConfirmed && result.value && result.value.job) {
8078
+ showForkShellModal(result.value.job)
8079
+ }
8080
+ })
8081
+ }
8082
+
8083
+ function showForkShellModal(job) {
8084
+ if (!job || !job.forkUri) {
8085
+ return
8086
+ }
8087
+
8088
+ const lifecycle = createPublishModalLifecycle()
8089
+ const forkNameValue = typeof job.forkName === 'string' && job.forkName.trim().length > 0
8090
+ ? job.forkName.trim()
8091
+ : deriveForkDefaultName({ name: job.repoName, url: job.remoteUrl })
8092
+ const queryParts = [`name=${encodeURIComponent(forkNameValue)}`]
8093
+ if (job.org) {
8094
+ queryParts.push(`org=${encodeURIComponent(job.org)}`)
8095
+ }
8096
+ queryParts.push(`ts=${Date.now()}`)
8097
+ const separator = job.forkUri.includes('?') ? '&' : '?'
8098
+ const finalUri = `${job.forkUri}${separator}${queryParts.join('&')}`
8099
+
8100
+ const remoteDisplay = job.remoteUrl ? escapeHtml(job.remoteUrl) : 'No remote detected'
8101
+ const titleDisplay = job.repoName ? escapeHtml(job.repoName) : 'Repository'
8102
+ const forkDisplay = escapeHtml(forkNameValue)
8103
+ const subtitleParts = [titleDisplay, forkDisplay]
8104
+ if (job.org) {
8105
+ subtitleParts.push(`Org: ${escapeHtml(job.org)}`)
8106
+ }
8107
+ const subtitleHtml = subtitleParts.join(' · ')
8108
+ Swal.fire({
8109
+ html: `
8110
+ <div class="pinokio-modal-surface">
8111
+ <div class="pinokio-modal-header">
8112
+ <div class="pinokio-modal-icon"><i class="fa-brands fa-github"></i></div>
8113
+ <div class="pinokio-modal-heading">
8114
+ <div class="pinokio-modal-title">Fork on GitHub</div>
8115
+ <div class="pinokio-modal-subtitle">${subtitleHtml}</div>
8116
+ <div class="pinokio-fork-item-url">${remoteDisplay}</div>
8117
+ </div>
8118
+ </div>
8119
+ <div class="pinokio-modal-body pinokio-modal-body--iframe">
8120
+ <iframe src="${finalUri}"></iframe>
8121
+ </div>
8122
+ <div class="pinokio-modal-footer pinokio-modal-footer--publish" data-publish-footer>
8123
+ <button type="button" class="pinokio-publish-close-btn" data-publish-close>Close</button>
8124
+ </div>
8125
+ </div>
8126
+ `,
8127
+ customClass: {
8128
+ popup: 'pinokio-modern-modal',
8129
+ htmlContainer: 'pinokio-modern-html',
8130
+ closeButton: 'pinokio-modern-close'
8131
+ },
8132
+ backdrop: 'rgba(9,11,15,0.65)',
8133
+ width: 'min(760px, 94vw)',
8134
+ buttonsStyling: false,
8135
+ showConfirmButton: false,
8136
+ showCloseButton: true,
8137
+ focusConfirm: false,
8138
+ didOpen: lifecycle.didOpen,
8139
+ willClose: lifecycle.willClose
8140
+ }).then(() => {
8141
+ setTimeout(() => {
8142
+ updateForkButton()
8143
+ check_git()
8144
+ }, 250)
8145
+ })
8146
+ }
7332
8147
 
7333
- const handlePushLogin = () => {
8148
+ function handlePushLogin() {
7334
8149
  window.location.href = '/github'
7335
8150
  }
7336
8151
 
7337
8152
  // Function to update publish/create button
7338
8153
  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
8154
+ if (!pushBtn) {
8155
+ latestGitIntegration = null
8156
+ updateForkButton()
8157
+ return
8158
+ }
8159
+
8160
+ const labelEl = pushBtn.querySelector('.fs-status-title')
8161
+ const setLabel = (text) => {
8162
+ if (labelEl) {
8163
+ labelEl.textContent = text
8164
+ }
8165
+ }
8166
+ const detachHandlers = () => {
8167
+ pushBtn.removeEventListener('click', showPublishModal)
8168
+ pushBtn.removeEventListener('click', showCreateModal)
8169
+ pushBtn.removeEventListener('click', handlePushLogin)
8170
+ }
8171
+ const enableDropdown = () => {
8172
+ pushBtn.classList.add('revealer')
8173
+ pushBtn.setAttribute('data-group', '#fs-push-menu')
8174
+ }
8175
+ const disableDropdown = () => {
8176
+ pushBtn.classList.remove('revealer')
8177
+ pushBtn.removeAttribute('data-group')
8178
+ closeStatusDropdowns()
8179
+ }
8180
+
8181
+ const repos = getRepoListSnapshot()
8182
+ const publishTargets = Array.isArray(repos)
8183
+ ? repos.filter((repo) => hasRemoteConfigured(repo) && resolvePushUri(repo))
8184
+ : []
8185
+
8186
+ detachHandlers()
8187
+
8188
+ const syncForkButton = () => {
8189
+ updateForkButton()
8190
+ }
8191
+
8192
+ if (!historyUri) {
8193
+ setPublishMenuMessage('Git integration unavailable')
8194
+ setLabel('Publish')
8195
+ disableDropdown()
8196
+ pushBtn.disabled = publishTargets.length === 0
8197
+ if (pushBtn.disabled) {
8198
+ pushBtn.classList.add('fs-status-btn--disabled')
8199
+ } else {
8200
+ pushBtn.classList.remove('fs-status-btn--disabled')
8201
+ }
8202
+ pushBtn.setAttribute('title', 'Git integration is not available')
8203
+ latestGitIntegration = null
8204
+ syncForkButton()
8205
+ return
8206
+ }
8207
+
8208
+ if (!publishMenu) {
8209
+ setLabel('Publish')
8210
+ disableDropdown()
8211
+ } else if (!publishMenu.hasChildNodes()) {
8212
+ setPublishMenuMessage('Loading repositories...')
8213
+ }
7343
8214
 
7344
8215
  try {
7345
8216
  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
- }
7352
- }
7353
- const resetHandlers = () => {
7354
- pushBtn.removeEventListener('click', showPublishModal)
7355
- pushBtn.removeEventListener('click', showCreateModal)
7356
- pushBtn.removeEventListener('click', handlePushLogin)
8217
+ if (!response.ok) {
8218
+ throw new Error(`HTTP ${response.status}`)
7357
8219
  }
8220
+ const data = await response.json()
8221
+ latestGitIntegration = data
8222
+
8223
+ const isConnected = Boolean(data && data.connected)
8224
+ const emptyMessage = isConnected
8225
+ ? 'No Git repositories detected'
8226
+ : 'Connect GitHub to publish this workspace'
8227
+ renderPublishDropdown(repos, { emptyMessage })
7358
8228
 
7359
- // Check if GitHub is connected
7360
- if (!data.connected) {
7361
- // Not logged in - show "Login" button
8229
+ if (!isConnected) {
7362
8230
  setLabel('Login')
7363
- resetHandlers()
8231
+ pushBtn.disabled = false
8232
+ pushBtn.classList.remove('fs-status-btn--disabled')
8233
+ pushBtn.setAttribute('title', 'Connect GitHub to publish this workspace')
8234
+ disableDropdown()
7364
8235
  pushBtn.addEventListener('click', handlePushLogin)
8236
+ syncForkButton()
7365
8237
  return
7366
8238
  }
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
8239
+
8240
+ if (!Array.isArray(repos) || repos.length === 0) {
8241
+ setLabel('Publish')
8242
+ pushBtn.disabled = true
8243
+ pushBtn.classList.add('fs-status-btn--disabled')
8244
+ pushBtn.setAttribute('title', 'No Git repositories detected')
8245
+ disableDropdown()
8246
+ syncForkButton()
8247
+ return
8248
+ }
8249
+
8250
+ pushBtn.disabled = false
8251
+ pushBtn.classList.remove('fs-status-btn--disabled')
8252
+
8253
+ if (publishTargets.length === 0) {
7371
8254
  setLabel('Create')
7372
- resetHandlers()
8255
+ pushBtn.setAttribute('title', 'Create a GitHub repository for this project')
8256
+ disableDropdown()
7373
8257
  pushBtn.addEventListener('click', showCreateModal)
7374
- } else {
7375
- // Has remotes - show "Publish" button
7376
- setLabel('Publish')
7377
- resetHandlers()
7378
- pushBtn.addEventListener('click', showPublishModal)
8258
+ syncForkButton()
8259
+ return
7379
8260
  }
8261
+
8262
+ setLabel('Publish')
8263
+ pushBtn.removeAttribute('title')
8264
+ enableDropdown()
8265
+ syncForkButton()
7380
8266
  } catch (error) {
7381
8267
  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)
8268
+ setPublishMenuMessage('Git integration unavailable')
8269
+ setLabel('Login')
8270
+ pushBtn.disabled = false
8271
+ pushBtn.classList.remove('fs-status-btn--disabled')
8272
+ pushBtn.setAttribute('title', 'Connect GitHub to publish this workspace')
8273
+ disableDropdown()
7390
8274
  pushBtn.addEventListener('click', handlePushLogin)
8275
+ latestGitIntegration = null
8276
+ updateForkButton()
7391
8277
  }
7392
8278
  }
7393
8279