pinokiod 3.102.0 → 3.103.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.
@@ -1313,7 +1313,7 @@ body.dark #fs-status {
1313
1313
  border-radius: 4px;
1314
1314
  display: flex !important;
1315
1315
  align-items: center;
1316
- gap: 6px;
1316
+ gap: 4px;
1317
1317
  font-size: 12px;
1318
1318
  line-height: 1.2;
1319
1319
  color: #24292f;
@@ -1351,6 +1351,13 @@ body.dark #fs-status {
1351
1351
  min-width: 0;
1352
1352
  }
1353
1353
 
1354
+ .fs-status-btn.fs-status-btn--disabled,
1355
+ .fs-status-btn:disabled {
1356
+ opacity: 0.45;
1357
+ cursor: default;
1358
+ pointer-events: none;
1359
+ }
1360
+
1354
1361
  .fs-status-dropdown {
1355
1362
  position: relative;
1356
1363
  }
@@ -1441,6 +1448,79 @@ body.dark #fs-status .fs-dropdown-menu .frame-link:hover {
1441
1448
  background: rgba(148, 163, 184, 0.15);
1442
1449
  }
1443
1450
 
1451
+ #fs-status .git-changes {
1452
+ margin-left: auto;
1453
+ }
1454
+
1455
+ #fs-changes-menu .fs-dropdown-empty {
1456
+ padding: 8px 14px;
1457
+ font-size: 12px;
1458
+ color: rgba(31, 41, 55, 0.66);
1459
+ }
1460
+
1461
+ #fs-changes-menu {
1462
+ left: auto;
1463
+ right: 0;
1464
+ width: max-content;
1465
+ max-width: min(460px, calc(100vw - 32px));
1466
+ max-height: min(70vh, 420px);
1467
+ overflow: auto;
1468
+ }
1469
+
1470
+ #fs-changes-menu .fs-dropdown-item {
1471
+ align-items: flex-start;
1472
+ white-space: normal;
1473
+ }
1474
+
1475
+ body.dark #fs-changes-menu .fs-dropdown-empty {
1476
+ color: rgba(226, 232, 240, 0.7);
1477
+ }
1478
+
1479
+ #fs-changes-menu .git-changes-item-label {
1480
+ display: flex;
1481
+ align-items: center;
1482
+ gap: 6px;
1483
+ min-width: 0;
1484
+ white-space: normal;
1485
+ word-break: break-word;
1486
+ }
1487
+
1488
+ #fs-changes-menu .git-changes-item-count {
1489
+ margin-left: auto;
1490
+ font-size: 12px;
1491
+ font-weight: 600;
1492
+ color: #1f2937;
1493
+ }
1494
+
1495
+ #fs-changes-menu .git-changes-item-count.git-changes-item-count--dirty {
1496
+ color: #2563eb;
1497
+ }
1498
+
1499
+ #fs-changes-menu .git-changes-item-count.git-changes-item-count--clean {
1500
+ font-weight: 500;
1501
+ color: rgba(31, 41, 55, 0.6);
1502
+ }
1503
+
1504
+ #fs-changes-menu .git-changes-item.git-changes-item--active {
1505
+ background: rgba(37, 99, 235, 0.08);
1506
+ }
1507
+
1508
+ body.dark #fs-changes-menu .git-changes-item-count {
1509
+ color: #e2e8f0;
1510
+ }
1511
+
1512
+ body.dark #fs-changes-menu .git-changes-item-count.git-changes-item-count--dirty {
1513
+ color: #60a5fa;
1514
+ }
1515
+
1516
+ body.dark #fs-changes-menu .git-changes-item-count.git-changes-item-count--clean {
1517
+ color: rgba(148, 163, 184, 0.6);
1518
+ }
1519
+
1520
+ body.dark #fs-changes-menu .git-changes-item.git-changes-item--active {
1521
+ background: rgba(96, 165, 250, 0.15);
1522
+ }
1523
+
1444
1524
  #fs-status .app-info {
1445
1525
  display: flex;
1446
1526
  align-items: center;
@@ -1508,15 +1588,17 @@ body.dark .fs-status-btn:hover {
1508
1588
  .fs-status-btn .badge {
1509
1589
  background: #dc3545;
1510
1590
  color: white;
1511
- padding: 4px 8px;
1512
- border-radius: 12px;
1591
+ padding: 2px 5px;
1592
+ border-radius: 2px;
1513
1593
  font-size: 11px;
1514
1594
  font-weight: 600;
1515
1595
  text-align: center;
1596
+ /*
1516
1597
  position: absolute;
1517
1598
  top: 0;
1518
1599
  right: 0;
1519
1600
  transform:translate(25%, -50%);
1601
+ */
1520
1602
  display: inline-flex;
1521
1603
  align-items: center;
1522
1604
  justify-content: center;
@@ -2596,7 +2678,7 @@ body.dark {
2596
2678
  <% } %>
2597
2679
  <div class='container'>
2598
2680
  <% if (type === "browse") { %>
2599
- <div id='fs-status' data-create-uri="<%=git_create_url%>" data-history-uri="<%=git_history_url%>" data-uri="<%=git_monitor_url%>" data-push-uri="<%=git_push_url%>">
2681
+ <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%>">
2600
2682
  <a target="<%=src%>" href="<%=src%>" class='fs-status-btn frame-link' data-index="0" data-mode="refresh" data-type="n">
2601
2683
  <span class='fs-status-label'>
2602
2684
  <i class="fa-regular fa-folder-open"></i>
@@ -2618,6 +2700,7 @@ body.dark {
2618
2700
  <button type='button' class='fs-dropdown-item' id='delete' data-name="<%=name%>"><i class="fa-solid fa-trash-can"></i> Delete</button>
2619
2701
  </div>
2620
2702
  </div>
2703
+ <!--
2621
2704
  <div class='fs-status-dropdown nested-menu git blue'>
2622
2705
  <button type='button' class='fs-status-btn frame-link reveal'>
2623
2706
  <span class='fs-status-label'>
@@ -2628,6 +2711,7 @@ body.dark {
2628
2711
  <div class='fs-dropdown-menu submenu hidden' id='git-repos'>
2629
2712
  </div>
2630
2713
  </div>
2714
+ -->
2631
2715
  <div class='fs-status-dropdown'>
2632
2716
  <button type='button' class='fs-status-btn revealer' data-group='#fs-settings-menu'>
2633
2717
  <span class='fs-status-label'><i class='fa-solid fa-gear'></i> Settings</span>
@@ -2641,13 +2725,13 @@ body.dark {
2641
2725
  </a>
2642
2726
  </div>
2643
2727
  </div>
2644
- <button id='fs-changes-btn' class='fs-status-btn'>
2645
- <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
2646
- <div class='badge'>loading...</div>
2647
- </button>
2648
- <button id='fs-history-btn' class='fs-status-btn'>
2649
- <span class='fs-status-label'><i class="fa-solid fa-clock-rotate-left"></i> History</span>
2650
- </button>
2728
+ <div class='fs-status-dropdown git-changes'>
2729
+ <button id='fs-changes-btn' class='fs-status-btn revealer' data-group='#fs-changes-menu' type='button'>
2730
+ <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
2731
+ <div class='badge'></div>
2732
+ </button>
2733
+ <div class='fs-dropdown-menu submenu hidden' id='fs-changes-menu'></div>
2734
+ </div>
2651
2735
  <button id='fs-push-btn' class='fs-status-btn'>
2652
2736
  <span class='fs-status-label'>
2653
2737
  <i class="fa-brands fa-github"></i>
@@ -2854,6 +2938,82 @@ body.dark {
2854
2938
  }
2855
2939
  }
2856
2940
 
2941
+ const ensureHttpDirectoryUrl = (value) => {
2942
+ try {
2943
+ const parsed = new URL(value)
2944
+ if (parsed.protocol.toLowerCase() !== "http:") {
2945
+ return value
2946
+ }
2947
+ let pathname = parsed.pathname || "/"
2948
+ const lastSegment = pathname.split("/").pop() || ""
2949
+ const hasExtension = lastSegment.includes(".")
2950
+ if (!hasExtension && !pathname.endsWith("/")) {
2951
+ pathname = `${pathname}/`
2952
+ parsed.pathname = pathname
2953
+ }
2954
+ parsed.hash = parsed.hash || ""
2955
+ parsed.search = parsed.search || ""
2956
+ return parsed.toString()
2957
+ } catch (_) {
2958
+ return value
2959
+ }
2960
+ }
2961
+
2962
+ const isLocalHostLike = (hostname) => {
2963
+ if (!hostname) {
2964
+ return false
2965
+ }
2966
+ const hostLower = hostname.toLowerCase()
2967
+ if (hostLower === location.hostname.toLowerCase()) {
2968
+ return true
2969
+ }
2970
+ if (hostLower === "localhost" || hostLower === "0.0.0.0") {
2971
+ return true
2972
+ }
2973
+ if (hostLower.startsWith("127.")) {
2974
+ return true
2975
+ }
2976
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostLower)) {
2977
+ return true
2978
+ }
2979
+ return false
2980
+ }
2981
+
2982
+ const extractProjectSlug = (node) => {
2983
+ if (!node) {
2984
+ return ""
2985
+ }
2986
+ const candidates = []
2987
+ const targetFull = node.getAttribute("data-target-full")
2988
+ if (typeof targetFull === "string" && targetFull.length > 0) {
2989
+ candidates.push(targetFull)
2990
+ }
2991
+ const dataHref = node.getAttribute("href")
2992
+ if (typeof dataHref === "string" && dataHref.length > 0) {
2993
+ candidates.push(dataHref)
2994
+ }
2995
+ try {
2996
+ const absolute = new URL(node.href, location.origin)
2997
+ candidates.push(absolute.pathname)
2998
+ } catch (_) {
2999
+ // ignore
3000
+ }
3001
+ for (const candidate of candidates) {
3002
+ if (typeof candidate !== "string" || candidate.length === 0) {
3003
+ continue
3004
+ }
3005
+ const assetMatch = candidate.match(/\/asset\/api\/([^\/?#]+)/i)
3006
+ if (assetMatch && assetMatch[1]) {
3007
+ return assetMatch[1]
3008
+ }
3009
+ const pageMatch = candidate.match(/\/p\/([^\/?#]+)/i)
3010
+ if (pageMatch && pageMatch[1]) {
3011
+ return pageMatch[1]
3012
+ }
3013
+ }
3014
+ return ""
3015
+ }
3016
+
2857
3017
  const formatDisplayUrl = (value) => {
2858
3018
  try {
2859
3019
  const parsed = new URL(value, location.origin)
@@ -3344,13 +3504,17 @@ body.dark {
3344
3504
  }
3345
3505
 
3346
3506
  const baseHref = link.href
3507
+ const projectSlug = extractProjectSlug(link).toLowerCase()
3347
3508
  const entries = []
3348
3509
  const entryByUrl = new Map()
3349
3510
  const addEntry = (type, label, url) => {
3350
3511
  if (!url) {
3351
3512
  return
3352
3513
  }
3353
- const canonical = canonicalizeUrl(url)
3514
+ let canonical = canonicalizeUrl(url)
3515
+ if (canonical && type === "http") {
3516
+ canonical = ensureHttpDirectoryUrl(canonical)
3517
+ }
3354
3518
  if (!canonical) {
3355
3519
  return
3356
3520
  }
@@ -3360,13 +3524,6 @@ body.dark {
3360
3524
  const originLower = parsed.origin.toLowerCase()
3361
3525
  if (originLower === location.origin.toLowerCase()) {
3362
3526
  skip = true
3363
- } else {
3364
- const hostLower = parsed.hostname.toLowerCase()
3365
- const port = parsed.port || (parsed.protocol === "http:" ? "80" : parsed.protocol === "https:" ? "443" : "")
3366
- const localHosts = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"])
3367
- if (parsed.protocol === "http:" && port === "42000" && localHosts.has(hostLower)) {
3368
- skip = true
3369
- }
3370
3527
  }
3371
3528
  } catch (_) {
3372
3529
  // ignore parse failures but do not skip by default
@@ -3404,6 +3561,26 @@ body.dark {
3404
3561
  httpsCandidates.add(canonicalizeUrl(baseHref))
3405
3562
  }
3406
3563
 
3564
+ if (projectSlug) {
3565
+ try {
3566
+ const baseUrl = new URL(baseHref, location.origin)
3567
+ let pathname = baseUrl.pathname || "/"
3568
+ if (pathname.endsWith("/index.html")) {
3569
+ pathname = pathname.slice(0, -"/index.html".length)
3570
+ }
3571
+ if (!pathname.endsWith("/")) {
3572
+ pathname = `${pathname}/`
3573
+ }
3574
+ const normalizedPath = pathname.toLowerCase()
3575
+ if (normalizedPath.includes(`/asset/api/${projectSlug}`)) {
3576
+ const fallbackHttp = `http://127.0.0.1:42000${pathname}`
3577
+ httpCandidates.add(canonicalizeUrl(fallbackHttp))
3578
+ }
3579
+ } catch (_) {
3580
+ // ignore fallback errors
3581
+ }
3582
+ }
3583
+
3407
3584
  const scriptKeys = collectScriptKeys(link)
3408
3585
  if (scriptKeys.length > 0) {
3409
3586
  const localInfo = await ensureLocalMemory()
@@ -3437,9 +3614,29 @@ body.dark {
3437
3614
  })
3438
3615
  }
3439
3616
 
3440
- const httpList = Array.from(httpCandidates).sort()
3441
3617
  const httpsList = Array.from(httpsCandidates).sort()
3442
3618
 
3619
+ if (httpsList.length > 0) {
3620
+ httpsList.forEach((url) => {
3621
+ try {
3622
+ const parsed = new URL(url)
3623
+ if (parsed.protocol.toLowerCase() !== "https:") {
3624
+ return
3625
+ }
3626
+ if (!parsed.port || parsed.port !== "42000") {
3627
+ return
3628
+ }
3629
+ const hostPort = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
3630
+ const httpUrl = `http://${hostPort}${parsed.pathname || "/"}${parsed.search || ""}`
3631
+ httpCandidates.add(canonicalizeUrl(httpUrl))
3632
+ } catch (_) {
3633
+ // ignore failures
3634
+ }
3635
+ })
3636
+ }
3637
+
3638
+ const httpList = Array.from(httpCandidates).sort()
3639
+
3443
3640
  httpList.forEach((url) => {
3444
3641
  addEntry("http", "HTTP", url)
3445
3642
  })
@@ -3512,12 +3709,12 @@ body.dark {
3512
3709
  return
3513
3710
  }
3514
3711
 
3712
+ let sameOrigin = false
3713
+ let canonicalBase = canonicalizeUrl(link.href)
3515
3714
  try {
3516
- const linkOrigin = new URL(link.href, location.href).origin
3517
- if (linkOrigin === location.origin) {
3518
- hideTabLinkPopover({ immediate: true })
3519
- return
3520
- }
3715
+ const linkUrl = new URL(link.href, location.href)
3716
+ sameOrigin = linkUrl.origin === location.origin
3717
+ canonicalBase = canonicalizeUrl(linkUrl.href)
3521
3718
  } catch (_) {
3522
3719
  hideTabLinkPopover({ immediate: true })
3523
3720
  return
@@ -3558,6 +3755,59 @@ body.dark {
3558
3755
  return
3559
3756
  }
3560
3757
 
3758
+ if (sameOrigin) {
3759
+ const slug = extractProjectSlug(link).toLowerCase()
3760
+ if (slug) {
3761
+ entries = entries.filter((entry) => {
3762
+ if (!entry || !entry.url) {
3763
+ return false
3764
+ }
3765
+ if (entry.url === canonicalBase) {
3766
+ return true
3767
+ }
3768
+ try {
3769
+ const parsed = new URL(entry.url)
3770
+ const hostLower = parsed.hostname ? parsed.hostname.toLowerCase() : ""
3771
+ if (isLocalHostLike(hostLower)) {
3772
+ if (entry.type === "http") {
3773
+ const pathLower = parsed.pathname ? parsed.pathname.toLowerCase() : ""
3774
+ if (pathLower.includes(`/asset/api/${slug}`) || pathLower.includes(`/p/${slug}`)) {
3775
+ return true
3776
+ }
3777
+ }
3778
+ return false
3779
+ }
3780
+ const pathLower = parsed.pathname ? parsed.pathname.toLowerCase() : ""
3781
+ if (pathLower.includes(`/asset/api/${slug}`)) {
3782
+ return true
3783
+ }
3784
+ if (pathLower.includes(`/p/${slug}`)) {
3785
+ return true
3786
+ }
3787
+ if (hostLower.split(".").some((part) => part === slug)) {
3788
+ return true
3789
+ }
3790
+ } catch (_) {
3791
+ return false
3792
+ }
3793
+ return false
3794
+ })
3795
+ } else {
3796
+ entries = entries.filter((entry) => entry.url === canonicalBase)
3797
+ }
3798
+
3799
+ const hasAlternate = entries.some((entry) => entry.url !== canonicalBase)
3800
+ if (!hasAlternate) {
3801
+ hideTabLinkPopover({ immediate: true })
3802
+ return
3803
+ }
3804
+ }
3805
+
3806
+ if (!entries || entries.length === 0) {
3807
+ hideTabLinkPopover({ immediate: true })
3808
+ return
3809
+ }
3810
+
3561
3811
  const popover = ensureTabLinkPopoverEl()
3562
3812
  popover.innerHTML = ""
3563
3813
 
@@ -5180,6 +5430,14 @@ body.dark {
5180
5430
  target = e.target.classList.contains("revealer") ? e.target : e.target.closest(".revealer")
5181
5431
 
5182
5432
  if (target) {
5433
+ if (target.classList.contains('fs-status-btn--disabled') || target.disabled) {
5434
+ e.preventDefault()
5435
+ e.stopPropagation()
5436
+ return
5437
+ }
5438
+ if (target.id === 'fs-changes-btn') {
5439
+ check_git()
5440
+ }
5183
5441
  e.preventDefault()
5184
5442
  e.stopPropagation()
5185
5443
  const group = target.getAttribute("data-group")
@@ -5612,12 +5870,14 @@ body.dark {
5612
5870
  restoreAllTabStates()
5613
5871
  <% } else { %>
5614
5872
  try_dynamic()
5873
+ /*
5615
5874
  const repos = await fetch("<%=repos%>").then((res) => {
5616
5875
  return res.text()
5617
5876
  })
5618
5877
  if (document.querySelector("#git-repos")) {
5619
5878
  document.querySelector("#git-repos").innerHTML = repos
5620
5879
  }
5880
+ */
5621
5881
  <% } %>
5622
5882
 
5623
5883
 
@@ -5725,6 +5985,7 @@ body.dark {
5725
5985
  refresh_du("logs")
5726
5986
  renderSelection({ force: true })
5727
5987
  <% if (type !== 'run') { %>
5988
+ /*
5728
5989
  fetch("<%=repos%>").then((res) => {
5729
5990
  return res.text()
5730
5991
  }).then((repos) => {
@@ -5733,6 +5994,7 @@ body.dark {
5733
5994
  }
5734
5995
  })
5735
5996
  refresh()
5997
+ */
5736
5998
  <% } %>
5737
5999
  <% if (plugin_menu) { %>
5738
6000
  // document.querySelector(".dynamic .reveal").click()
@@ -5772,31 +6034,266 @@ body.dark {
5772
6034
  });
5773
6035
  */
5774
6036
  <% if (type === "browse") { %>
6037
+ const repoStatusCache = new Map()
6038
+ let lastRepoList = []
5775
6039
  let currentChanges = []
5776
6040
  let gitCommitUrl = null
5777
-
5778
- const check_git = () => {
5779
- fetch("<%=git_monitor_url%>").then((res) => {
5780
- return res.json()
5781
- }).then((res) => {
5782
- currentChanges = res.changes || []
5783
- gitCommitUrl = res.git_commit_url || null
5784
- const changesBtn = document.querySelector("#fs-changes-btn")
5785
- const badgeElement = document.querySelector("#fs-changes-btn .badge")
5786
-
5787
- if (res.changes && res.changes.length > 0) {
5788
- // Show changes button and update badge
5789
- changesBtn.style.display = 'block'
5790
- badgeElement.innerHTML = `${res.changes.length}`
5791
- } else {
5792
- // Hide changes button when no changes
5793
- changesBtn.style.display = 'none'
5794
- badgeElement.innerHTML = ''
6041
+ let activeRepoKey = null
6042
+ let gitStatusRequest = null
6043
+
6044
+ const fsStatusEl = document.querySelector('#fs-status')
6045
+ const changesDropdownContainer = document.querySelector('#fs-status .git-changes')
6046
+ const changesMenu = document.getElementById('fs-changes-menu')
6047
+ const changesBtn = document.getElementById('fs-changes-btn')
6048
+ const badgeElement = changesBtn ? changesBtn.querySelector('.badge') : null
6049
+
6050
+ const readDataAttr = (node, attr) => {
6051
+ if (!node) {
6052
+ return null
6053
+ }
6054
+ const value = node.getAttribute(attr)
6055
+ if (!value || value === 'null' || value === 'undefined') {
6056
+ return null
6057
+ }
6058
+ return value
6059
+ }
6060
+
6061
+ const statusUri = readDataAttr(fsStatusEl, 'data-status-uri')
6062
+ const monitorUri = readDataAttr(fsStatusEl, 'data-uri')
6063
+ const workspaceName = readDataAttr(fsStatusEl, 'data-workspace')
6064
+
6065
+ const encodeRepoPath = (value) => {
6066
+ if (typeof value !== 'string' || value.length === 0) {
6067
+ return ''
6068
+ }
6069
+ return value.split('/').map(encodeURIComponent).join('/')
6070
+ }
6071
+
6072
+ const updateCombinedBadge = (total) => {
6073
+ if (!badgeElement) {
6074
+ return
6075
+ }
6076
+ badgeElement.textContent = total > 0 ? String(total) : ''
6077
+ }
6078
+
6079
+ const updateChangesButtonState = (hasRepos) => {
6080
+ if (!changesBtn) {
6081
+ return
6082
+ }
6083
+ if (hasRepos) {
6084
+ changesBtn.disabled = false
6085
+ changesBtn.classList.remove('fs-status-btn--disabled')
6086
+ } else {
6087
+ changesBtn.disabled = true
6088
+ changesBtn.classList.add('fs-status-btn--disabled')
6089
+ }
6090
+ }
6091
+
6092
+ const setChangesMenuMessage = (message) => {
6093
+ if (!changesMenu) {
6094
+ return
6095
+ }
6096
+ const messageEl = document.createElement('div')
6097
+ messageEl.className = 'fs-dropdown-empty'
6098
+ messageEl.textContent = message
6099
+ changesMenu.innerHTML = ''
6100
+ changesMenu.append(messageEl)
6101
+ }
6102
+
6103
+ const attachRepoDropdownHandlers = () => {
6104
+ if (!changesMenu) {
6105
+ return
6106
+ }
6107
+ const items = changesMenu.querySelectorAll('.git-changes-item')
6108
+ items.forEach((item) => {
6109
+ item.addEventListener('click', async (event) => {
6110
+ event.preventDefault()
6111
+ event.stopPropagation()
6112
+ const repoKey = item.dataset.repo
6113
+ const repoName = item.dataset.name
6114
+ if (repoKey) {
6115
+ activeRepoKey = repoKey
6116
+ }
6117
+ items.forEach((node) => {
6118
+ if (node === item) {
6119
+ node.classList.add('git-changes-item--active')
6120
+ } else {
6121
+ node.classList.remove('git-changes-item--active')
6122
+ }
6123
+ })
6124
+ closeStatusDropdowns()
6125
+ try {
6126
+ await showGitDiffModal({ repoParam: repoKey, repoName })
6127
+ } catch (err) {
6128
+ console.error('Failed to open diff modal:', err)
6129
+ }
6130
+ })
6131
+ })
6132
+ }
6133
+
6134
+ const renderChangesDropdown = (repos) => {
6135
+ if (!changesMenu) {
6136
+ return
6137
+ }
6138
+
6139
+ if (!Array.isArray(repos) || repos.length === 0) {
6140
+ setChangesMenuMessage('No Git repositories detected')
6141
+ updateChangesButtonState(false)
6142
+ return
6143
+ }
6144
+
6145
+ const fragment = document.createDocumentFragment()
6146
+ repos.forEach((repo) => {
6147
+ const button = document.createElement('button')
6148
+ button.type = 'button'
6149
+ button.className = 'fs-dropdown-item git-changes-item'
6150
+ button.dataset.repo = repo.repoParam
6151
+ button.dataset.name = repo.name
6152
+ if (repo.main) {
6153
+ button.dataset.main = 'true'
5795
6154
  }
5796
- updatePublishButton()
5797
- }).catch((error) => {
5798
- console.error('check_git error:', error)
6155
+ if (repo.repoParam === activeRepoKey) {
6156
+ button.classList.add('git-changes-item--active')
6157
+ }
6158
+
6159
+ button.innerHTML = `
6160
+ <span class="git-changes-item-label">
6161
+ <i class="fa-solid fa-code-branch"></i>
6162
+ <span>${repo.name}</span>
6163
+ </span>
6164
+ <span class="git-changes-item-count ${repo.changeCount > 0 ? 'git-changes-item-count--dirty' : 'git-changes-item-count--clean'}">
6165
+ ${repo.changeCount > 0 ? repo.changeCount : 'Clean'}
6166
+ </span>
6167
+ `
6168
+
6169
+ fragment.append(button)
5799
6170
  })
6171
+
6172
+ changesMenu.innerHTML = ''
6173
+ changesMenu.append(fragment)
6174
+ updateChangesButtonState(true)
6175
+ attachRepoDropdownHandlers()
6176
+ }
6177
+
6178
+ const updateFromLegacyMonitor = async () => {
6179
+ if (!monitorUri) {
6180
+ setChangesMenuMessage('Git status data unavailable')
6181
+ updateCombinedBadge(0)
6182
+ updateChangesButtonState(false)
6183
+ return false
6184
+ }
6185
+ try {
6186
+ const response = await fetch(monitorUri)
6187
+ if (!response.ok) {
6188
+ throw new Error(`HTTP ${response.status}`)
6189
+ }
6190
+ const data = await response.json()
6191
+ const repoKey = workspaceName || ''
6192
+ const fallbackRepo = {
6193
+ name: workspaceName || 'Current workspace',
6194
+ main: true,
6195
+ repoParam: repoKey,
6196
+ changeCount: Array.isArray(data.changes) ? data.changes.length : 0,
6197
+ changes: Array.isArray(data.changes) ? data.changes : [],
6198
+ git_commit_url: data.git_commit_url || null,
6199
+ git_history_url: workspaceName ? `/info/git/HEAD/${encodeRepoPath(workspaceName)}` : readDataAttr(fsStatusEl, 'data-history-uri'),
6200
+ url: null,
6201
+ }
6202
+
6203
+ repoStatusCache.clear()
6204
+ repoStatusCache.set(fallbackRepo.repoParam, fallbackRepo)
6205
+ lastRepoList = [fallbackRepo]
6206
+ activeRepoKey = fallbackRepo.repoParam
6207
+ currentChanges = fallbackRepo.changes
6208
+ gitCommitUrl = fallbackRepo.git_commit_url
6209
+
6210
+ renderChangesDropdown(lastRepoList)
6211
+ updateCombinedBadge(fallbackRepo.changeCount)
6212
+ updatePublishButton()
6213
+ return true
6214
+ } catch (error) {
6215
+ console.error('check_git fallback error:', error)
6216
+ setChangesMenuMessage('Unable to load repositories')
6217
+ updateCombinedBadge(0)
6218
+ updateChangesButtonState(false)
6219
+ return false
6220
+ }
6221
+ }
6222
+
6223
+ const check_git = async () => {
6224
+ if (gitStatusRequest) {
6225
+ return gitStatusRequest
6226
+ }
6227
+
6228
+ gitStatusRequest = (async () => {
6229
+ if (repoStatusCache.size === 0) {
6230
+ setChangesMenuMessage(statusUri ? 'Loading repositories...' : 'Loading changes...')
6231
+ updateChangesButtonState(false)
6232
+ }
6233
+
6234
+ if (!statusUri) {
6235
+ await updateFromLegacyMonitor()
6236
+ return
6237
+ }
6238
+
6239
+ try {
6240
+ const response = await fetch(statusUri)
6241
+ if (!response.ok) {
6242
+ throw new Error(`HTTP ${response.status}`)
6243
+ }
6244
+ const data = await response.json()
6245
+ const repos = Array.isArray(data.repos) ? data.repos : []
6246
+
6247
+ repoStatusCache.clear()
6248
+ repos.forEach((repo) => {
6249
+ repoStatusCache.set(repo.repoParam, repo)
6250
+ })
6251
+
6252
+ const sortedRepos = [...repos].sort((a, b) => {
6253
+ if (a.main === b.main) {
6254
+ return a.name.localeCompare(b.name)
6255
+ }
6256
+ return a.main ? -1 : 1
6257
+ })
6258
+
6259
+ lastRepoList = sortedRepos
6260
+
6261
+ const existingActive = activeRepoKey
6262
+ ? sortedRepos.find((repo) => repo.repoParam === activeRepoKey)
6263
+ : null
6264
+ const fallbackRepo = sortedRepos.find((repo) => repo.main) || sortedRepos[0] || null
6265
+ const resolvedActive = existingActive || fallbackRepo
6266
+ activeRepoKey = resolvedActive ? resolvedActive.repoParam : null
6267
+ currentChanges = resolvedActive ? (resolvedActive.changes || []) : []
6268
+ gitCommitUrl = resolvedActive ? (resolvedActive.git_commit_url || null) : null
6269
+
6270
+ if (changesDropdownContainer) {
6271
+ changesDropdownContainer.style.display = ''
6272
+ }
6273
+
6274
+ if (sortedRepos.length === 0) {
6275
+ await updateFromLegacyMonitor()
6276
+ } else {
6277
+ renderChangesDropdown(sortedRepos)
6278
+ const total = typeof data.totalChanges === 'number'
6279
+ ? data.totalChanges
6280
+ : sortedRepos.reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6281
+ updateCombinedBadge(total)
6282
+ updateChangesButtonState(true)
6283
+ }
6284
+
6285
+ updatePublishButton()
6286
+ } catch (error) {
6287
+ console.error('check_git error:', error)
6288
+ await updateFromLegacyMonitor()
6289
+ }
6290
+ })()
6291
+
6292
+ try {
6293
+ await gitStatusRequest
6294
+ } finally {
6295
+ gitStatusRequest = null
6296
+ }
5800
6297
  }
5801
6298
 
5802
6299
  let messageListener = null
@@ -5908,43 +6405,135 @@ body.dark {
5908
6405
  return messages.join(', ')
5909
6406
  }
5910
6407
 
5911
- const showGitDiffModal = async (diffData = null, modalTitle = 'File Changes', showSaveButton = true) => {
5912
- let changes = diffData ? (diffData.changes || []) : currentChanges
5913
- let commitUrl = diffData ? (diffData.git_commit_url || null) : gitCommitUrl
5914
-
5915
- // If no diffData provided and currentChanges is empty, try to fetch fresh data
5916
- if (!diffData && changes.length === 0) {
6408
+ const showGitDiffModal = async (diffData = null, modalTitle = null, showSaveButton = true) => {
6409
+ const diffDataIsObject = diffData && typeof diffData === 'object' && !Array.isArray(diffData)
6410
+ let repoParam = diffDataIsObject ? (diffData.repoParam || diffData.repoKey || null) : null
6411
+ let repoDisplayName = diffDataIsObject ? (diffData.repoName || diffData.name || null) : null
6412
+
6413
+ if (!repoParam) {
6414
+ repoParam = activeRepoKey || null
6415
+ }
6416
+
6417
+ if (!repoParam && repoStatusCache.size > 0) {
6418
+ const firstEntry = repoStatusCache.values().next().value
6419
+ if (firstEntry) {
6420
+ repoParam = firstEntry.repoParam
6421
+ if (!repoDisplayName) {
6422
+ repoDisplayName = firstEntry.name
6423
+ }
6424
+ }
6425
+ }
6426
+
6427
+ if (!repoParam) {
6428
+ await check_git()
6429
+ const fallback = repoStatusCache.values().next().value
6430
+ if (fallback) {
6431
+ repoParam = fallback.repoParam
6432
+ repoDisplayName = repoDisplayName || fallback.name
6433
+ }
6434
+ }
6435
+
6436
+ if (!repoParam) {
6437
+ Swal.fire({
6438
+ title: 'No repositories',
6439
+ text: 'No Git repositories were detected for this workspace.',
6440
+ icon: 'info'
6441
+ })
6442
+ return
6443
+ }
6444
+
6445
+ let repoData = repoStatusCache.get(repoParam)
6446
+ if (!repoData) {
6447
+ await check_git()
6448
+ repoData = repoStatusCache.get(repoParam)
6449
+ }
6450
+
6451
+ if (!repoDisplayName && repoData) {
6452
+ repoDisplayName = repoData.name
6453
+ }
6454
+ if (!repoDisplayName) {
6455
+ repoDisplayName = repoParam
6456
+ }
6457
+
6458
+ let changes
6459
+ if (diffDataIsObject && Array.isArray(diffData.changes)) {
6460
+ changes = diffData.changes
6461
+ } else if (repoData && Array.isArray(repoData.changes)) {
6462
+ changes = repoData.changes
6463
+ } else {
6464
+ changes = currentChanges
6465
+ }
6466
+
6467
+ let commitUrl
6468
+ if (diffDataIsObject && diffData.git_commit_url) {
6469
+ commitUrl = diffData.git_commit_url
6470
+ } else if (repoData && repoData.git_commit_url) {
6471
+ commitUrl = repoData.git_commit_url
6472
+ } else {
6473
+ commitUrl = gitCommitUrl
6474
+ }
6475
+
6476
+ const shouldForceRefresh = Boolean(diffDataIsObject && diffData.forceRefresh)
6477
+
6478
+ if (!changes || changes.length === 0 || shouldForceRefresh) {
5917
6479
  try {
5918
- const response = await fetch("<%=git_monitor_url%>")
6480
+ const response = await fetch(`/gitcommit/HEAD/${encodeRepoPath(repoParam)}`)
6481
+ if (!response.ok) {
6482
+ throw new Error(`HTTP ${response.status}`)
6483
+ }
5919
6484
  const freshData = await response.json()
5920
6485
  changes = freshData.changes || []
5921
- commitUrl = freshData.git_commit_url || null
5922
- currentChanges = changes // Update the global variable
5923
- gitCommitUrl = commitUrl
6486
+ commitUrl = freshData.git_commit_url || commitUrl
6487
+ const updatedRepo = repoData || { repoParam }
6488
+ updatedRepo.changes = changes
6489
+ updatedRepo.changeCount = changes.length
6490
+ updatedRepo.git_commit_url = freshData.git_commit_url || null
6491
+ updatedRepo.name = updatedRepo.name || repoDisplayName || repoParam
6492
+ updatedRepo.git_history_url = updatedRepo.git_history_url || (repoData && repoData.git_history_url) || (repoParam ? `/info/git/HEAD/${encodeRepoPath(repoParam)}` : null)
6493
+ repoStatusCache.set(repoParam, updatedRepo)
6494
+ repoData = updatedRepo
6495
+ const listEntry = lastRepoList.find((entry) => entry.repoParam === repoParam)
6496
+ if (listEntry) {
6497
+ listEntry.changes = changes
6498
+ listEntry.changeCount = changes.length
6499
+ listEntry.git_commit_url = updatedRepo.git_commit_url
6500
+ if (updatedRepo.git_history_url) {
6501
+ listEntry.git_history_url = updatedRepo.git_history_url
6502
+ }
6503
+ }
6504
+ const total = Array.from(repoStatusCache.values()).reduce((sum, repo) => sum + (repo.changeCount || 0), 0)
6505
+ updateCombinedBadge(total)
6506
+ if (lastRepoList.length > 0) {
6507
+ renderChangesDropdown(lastRepoList)
6508
+ }
5924
6509
  } catch (error) {
5925
- console.error('Failed to fetch current changes:', error)
6510
+ console.error('Failed to fetch repo changes:', error)
5926
6511
  }
5927
6512
  }
5928
-
5929
-
5930
- if (changes.length === 0) {
5931
- Swal.fire({
5932
- html: `
5933
- <div class="pinokio-no-changes-icon"><i class="fa-solid fa-check"></i></div>
5934
- <div class="pinokio-no-changes-title">Workspace is clean</div>
5935
- <div class="pinokio-no-changes-body">There are currently no tracked file changes. Make edits or refresh to check again.</div>
5936
- <div class="pinokio-no-changes-hint">Tip: run your build or tests before committing.</div>
5937
- `,
5938
- customClass: {
5939
- popup: 'pinokio-no-changes-popup',
5940
- confirmButton: 'pinokio-no-changes-confirm'
5941
- },
5942
- confirmButtonText: 'Close',
5943
- backdrop: 'rgba(9,11,15,0.6)'
5944
- })
6513
+
6514
+ activeRepoKey = repoParam
6515
+ currentChanges = changes || []
6516
+ gitCommitUrl = commitUrl
6517
+
6518
+ const title = modalTitle || `File Changes${repoDisplayName ? ` — ${repoDisplayName}` : ''}`
6519
+
6520
+ if (lastRepoList.length > 0) {
6521
+ renderChangesDropdown(lastRepoList)
6522
+ }
6523
+
6524
+ if (!changes || changes.length === 0) {
6525
+ try {
6526
+ await showGitHistoryModal({
6527
+ repoParam,
6528
+ repoName: repoDisplayName,
6529
+ historyUrl: repoData && repoData.git_history_url ? repoData.git_history_url : null,
6530
+ })
6531
+ } catch (error) {
6532
+ console.error('Failed to open history for clean repository:', error)
6533
+ }
5945
6534
  return
5946
6535
  }
5947
-
6536
+
5948
6537
  const changeSummary = `${changes.length} file${changes.length === 1 ? '' : 's'} changed`
5949
6538
  const statusCounts = { added: 0, modified: 0, deleted: 0, renamed: 0 }
5950
6539
  changes.forEach(change => {
@@ -5975,7 +6564,7 @@ body.dark {
5975
6564
  <div class="pinokio-modal-header">
5976
6565
  <div class="pinokio-modal-icon"><i class="fa-solid fa-code-branch"></i></div>
5977
6566
  <div class="pinokio-modal-heading">
5978
- <div class="pinokio-modal-title">${modalTitle}</div>
6567
+ <div class="pinokio-modal-title">${title}</div>
5979
6568
  <div class="pinokio-modal-subtitle">${changeSummary}</div>
5980
6569
  </div>
5981
6570
  </div>
@@ -6110,9 +6699,54 @@ body.dark {
6110
6699
  container.innerHTML = diffHtml
6111
6700
  }
6112
6701
 
6113
- const showGitHistoryModal = async () => {
6114
- const historyUri = document.querySelector('#fs-status').getAttribute('data-history-uri')
6115
- if (!historyUri) {
6702
+ const showGitHistoryModal = async (options = {}) => {
6703
+ const opts = options && typeof options === 'object' ? options : {}
6704
+ const buildHistoryUrl = (param) => {
6705
+ if (!param) {
6706
+ return null
6707
+ }
6708
+ return `/info/git/HEAD/${encodeRepoPath(param)}`
6709
+ }
6710
+
6711
+ let repoParam = opts.repoParam ?? null
6712
+ let repoName = opts.repoName ?? null
6713
+ let historyUrl = opts.historyUrl ?? null
6714
+
6715
+ if (!repoParam) {
6716
+ repoParam = activeRepoKey || null
6717
+ }
6718
+
6719
+ let repoData = repoParam ? repoStatusCache.get(repoParam) : null
6720
+
6721
+ if (!repoName && repoData && repoData.name) {
6722
+ repoName = repoData.name
6723
+ }
6724
+
6725
+ if (!historyUrl && repoData && repoData.git_history_url) {
6726
+ historyUrl = repoData.git_history_url
6727
+ }
6728
+
6729
+ const fallbackHistoryUri = readDataAttr(fsStatusEl, 'data-history-uri')
6730
+
6731
+ if (!historyUrl) {
6732
+ if (repoParam) {
6733
+ historyUrl = buildHistoryUrl(repoParam)
6734
+ } else if (fallbackHistoryUri) {
6735
+ historyUrl = fallbackHistoryUri
6736
+ }
6737
+ } else if (repoParam && typeof historyUrl === 'string' && historyUrl.startsWith('/info/git/HEAD/')) {
6738
+ historyUrl = buildHistoryUrl(repoParam)
6739
+ }
6740
+
6741
+ if (!repoName) {
6742
+ if (repoParam) {
6743
+ repoName = repoParam
6744
+ } else if (workspaceName) {
6745
+ repoName = workspaceName
6746
+ }
6747
+ }
6748
+
6749
+ if (!historyUrl) {
6116
6750
  Swal.fire({
6117
6751
  title: 'Error',
6118
6752
  text: 'Git history URL not available.',
@@ -6120,16 +6754,15 @@ body.dark {
6120
6754
  })
6121
6755
  return
6122
6756
  }
6123
-
6757
+
6124
6758
  try {
6125
- const response = await fetch(historyUri)
6759
+ const response = await fetch(historyUrl)
6126
6760
  if (!response.ok) {
6127
6761
  throw new Error(`HTTP ${response.status}: ${response.statusText}`)
6128
6762
  }
6129
-
6763
+
6130
6764
  const historyData = await response.json()
6131
- displayGitHistory(historyData)
6132
-
6765
+ displayGitHistory(historyData, { repoName: repoName || null, repoParam: repoParam || null })
6133
6766
  } catch (error) {
6134
6767
  console.error('Failed to load git history:', error)
6135
6768
  Swal.fire({
@@ -6140,7 +6773,8 @@ body.dark {
6140
6773
  }
6141
6774
  }
6142
6775
 
6143
- const displayGitHistory = (historyData) => {
6776
+ const displayGitHistory = (historyData, options = {}) => {
6777
+ const repoName = options && typeof options === 'object' ? options.repoName : null
6144
6778
  const commits = historyData.log || []
6145
6779
  const remote = historyData.remote || ''
6146
6780
  const currentRef = historyData.ref || 'HEAD'
@@ -6159,12 +6793,14 @@ body.dark {
6159
6793
  metaBadges.push(`<span class="pinokio-pill"><i class="fa-solid fa-cloud-arrow-up"></i>${remote}</span>`)
6160
6794
  }
6161
6795
 
6796
+ const historyTitle = repoName ? `${repoName} history` : 'Repository history'
6797
+
6162
6798
  const historyHtml = `
6163
6799
  <div class="pinokio-modal-surface pinokio-modal-surface--history">
6164
6800
  <div class="pinokio-modal-header">
6165
6801
  <div class="pinokio-modal-icon"><i class="fa-solid fa-clock-rotate-left"></i></div>
6166
6802
  <div class="pinokio-modal-heading">
6167
- <div class="pinokio-modal-title">Repository history</div>
6803
+ <div class="pinokio-modal-title">${historyTitle}</div>
6168
6804
  <div class="pinokio-modal-subtitle">${subtitleText}</div>
6169
6805
  </div>
6170
6806
  </div>
@@ -6719,15 +7355,10 @@ body.dark {
6719
7355
  }
6720
7356
  }
6721
7357
 
6722
- // Add click handlers for the buttons
6723
- document.querySelector('#fs-changes-btn').addEventListener('click', () => showGitDiffModal())
6724
- document.querySelector('#fs-history-btn').addEventListener('click', showGitHistoryModal)
6725
-
6726
7358
  // Initialize the publish/create button
6727
7359
  updatePublishButton()
6728
7360
 
6729
7361
  check_git()
6730
- setInterval(check_git, 10000)
6731
7362
  <% } %>
6732
7363
 
6733
7364
  setInterval(() => {