pinokiod 3.169.0 → 3.180.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.
@@ -280,5 +280,101 @@ module.exports = function registerFileRoutes(app, { kernel, getTheme, exists })
280
280
  });
281
281
  }));
282
282
 
283
+ router.post('/api/files/delete', asyncHandler(async (req, res) => {
284
+ const { workspace, path: relativePath, root: rootParam } = req.body || {};
285
+ if (typeof relativePath !== 'string') {
286
+ throw createHttpError(400, 'Path must be provided');
287
+ }
288
+
289
+ const resolved = await resolveWorkspacePath(workspace, relativePath, rootParam);
290
+ const { absolutePath, relativePosix, workspaceSlug } = resolved;
291
+
292
+ if (!relativePosix) {
293
+ throw createHttpError(400, 'Cannot delete workspace root');
294
+ }
295
+
296
+ const stats = await fs.promises.stat(absolutePath).catch(() => null);
297
+ if (!stats) {
298
+ throw createHttpError(404, 'Path not found');
299
+ }
300
+
301
+ let removedType;
302
+ if (stats.isDirectory()) {
303
+ await fs.promises.rm(absolutePath, { recursive: true, force: true });
304
+ removedType = 'directory';
305
+ } else if (stats.isFile()) {
306
+ await fs.promises.unlink(absolutePath);
307
+ removedType = 'file';
308
+ } else {
309
+ throw createHttpError(400, 'Unsupported file type');
310
+ }
311
+
312
+ res.json({
313
+ workspace: workspaceSlug,
314
+ path: relativePosix,
315
+ type: removedType,
316
+ success: true,
317
+ });
318
+ }));
319
+
320
+ router.post('/api/files/rename', asyncHandler(async (req, res) => {
321
+ const { workspace, path: relativePath, name: newName, root: rootParam } = req.body || {};
322
+ if (typeof relativePath !== 'string' || relativePath.length === 0) {
323
+ throw createHttpError(400, 'Path must be provided');
324
+ }
325
+ if (typeof newName !== 'string' || newName.trim().length === 0) {
326
+ throw createHttpError(400, 'New name must be provided');
327
+ }
328
+
329
+ const sanitizedName = newName.trim();
330
+ if (sanitizedName.includes('/') || sanitizedName.includes('\\')) {
331
+ throw createHttpError(400, 'Name cannot contain path separators');
332
+ }
333
+
334
+ const resolved = await resolveWorkspacePath(workspace, relativePath, rootParam);
335
+ const { absolutePath, relativePosix, workspaceSlug, workspaceRoot } = resolved;
336
+
337
+ if (!relativePosix) {
338
+ throw createHttpError(400, 'Cannot rename workspace root');
339
+ }
340
+
341
+ const sourceStats = await fs.promises.stat(absolutePath).catch(() => null);
342
+ if (!sourceStats) {
343
+ throw createHttpError(404, 'Source path not found');
344
+ }
345
+
346
+ const parentSegments = resolved.relativeSegments.slice(0, -1);
347
+ const sourceName = resolved.relativeSegments[resolved.relativeSegments.length - 1];
348
+ if (sourceName === sanitizedName) {
349
+ res.json({
350
+ workspace: workspaceSlug,
351
+ path: relativePosix,
352
+ target: relativePosix,
353
+ success: true,
354
+ unchanged: true,
355
+ });
356
+ return;
357
+ }
358
+
359
+ const targetAbsolute = path.resolve(workspaceRoot, ...parentSegments, sanitizedName);
360
+ const relativeTargetSegments = sanitizeSegments([...parentSegments, sanitizedName].join('/'));
361
+ const relativeTarget = relativeTargetSegments.join('/');
362
+ const collision = await fs.promises.stat(targetAbsolute).catch(() => null);
363
+ if (collision) {
364
+ throw createHttpError(409, 'A file or folder with that name already exists');
365
+ }
366
+
367
+ await fs.promises.rename(absolutePath, targetAbsolute);
368
+
369
+ const targetStats = await fs.promises.stat(targetAbsolute);
370
+ res.json({
371
+ workspace: workspaceSlug,
372
+ path: relativePosix,
373
+ target: relativeTarget,
374
+ type: targetStats.isDirectory() ? 'directory' : targetStats.isFile() ? 'file' : 'other',
375
+ success: true,
376
+ });
377
+ }));
378
+
283
379
  app.use(router);
284
380
  };
@@ -1759,6 +1759,10 @@ body.dark .swal2-html-container label {
1759
1759
  --pinokio-custom-commit-close-color: rgba(71, 85, 105, 0.75);
1760
1760
  --pinokio-custom-commit-close-hover-bg: rgba(148, 163, 184, 0.18);
1761
1761
  --pinokio-custom-commit-close-hover-color: #0f172a;
1762
+ --pinokio-custom-commit-action-bg: rgba(59, 130, 246, 0.12);
1763
+ --pinokio-custom-commit-action-border: rgba(59, 130, 246, 0.35);
1764
+ --pinokio-custom-commit-action-color: #1d4ed8;
1765
+ --pinokio-custom-commit-action-hover: rgba(59, 130, 246, 0.18);
1762
1766
  --pinokio-commit-item-bg: rgba(226, 232, 240, 0.6);
1763
1767
  --pinokio-commit-item-hover-bg: rgba(59, 130, 246, 0.12);
1764
1768
  --pinokio-commit-item-text: #0f172a;
@@ -1837,6 +1841,10 @@ body.dark {
1837
1841
  --pinokio-custom-commit-close-color: rgba(226, 232, 240, 0.7);
1838
1842
  --pinokio-custom-commit-close-hover-bg: rgba(148, 163, 184, 0.18);
1839
1843
  --pinokio-custom-commit-close-hover-color: #f8fafc;
1844
+ --pinokio-custom-commit-action-bg: rgba(56, 189, 248, 0.12);
1845
+ --pinokio-custom-commit-action-border: rgba(56, 189, 248, 0.35);
1846
+ --pinokio-custom-commit-action-color: #7dd3fc;
1847
+ --pinokio-custom-commit-action-hover: rgba(56, 189, 248, 0.18);
1840
1848
  --pinokio-commit-item-bg: rgba(15, 23, 42, 0.35);
1841
1849
  --pinokio-commit-item-hover-bg: rgba(37, 99, 235, 0.16);
1842
1850
  --pinokio-commit-item-text: #f8fafc;
@@ -2173,6 +2181,57 @@ body.dark {
2173
2181
  flex-wrap: wrap;
2174
2182
  }
2175
2183
 
2184
+ .pinokio-history-latest-banner {
2185
+ display: flex;
2186
+ justify-content: space-between;
2187
+ align-items: center;
2188
+ padding: 12px 20px;
2189
+ margin: 12px 20px 0;
2190
+ background: rgba(59, 130, 246, 0.08);
2191
+ border: 1px solid rgba(59, 130, 246, 0.18);
2192
+ border-radius: 10px;
2193
+ color: var(--pinokio-modal-title-color);
2194
+ }
2195
+
2196
+ .pinokio-history-latest-text {
2197
+ font-size: 13px;
2198
+ font-weight: 600;
2199
+ }
2200
+
2201
+ .pinokio-history-latest-btn {
2202
+ display: inline-flex;
2203
+ align-items: center;
2204
+ gap: 6px;
2205
+ background: rgba(59, 130, 246, 0.18);
2206
+ border: 1px solid rgba(59, 130, 246, 0.35);
2207
+ color: #1d4ed8;
2208
+ padding: 6px 14px;
2209
+ border-radius: 6px;
2210
+ font-weight: 600;
2211
+ font-size: 13px;
2212
+ cursor: pointer;
2213
+ transition: background 0.15s ease, border 0.15s ease, transform 0.15s ease;
2214
+ }
2215
+
2216
+ .pinokio-history-latest-btn:hover {
2217
+ background: rgba(59, 130, 246, 0.24);
2218
+ transform: translateY(-1px);
2219
+ }
2220
+
2221
+ .pinokio-history-latest-btn:focus-visible {
2222
+ outline: 2px solid rgba(59, 130, 246, 0.45);
2223
+ outline-offset: 3px;
2224
+ }
2225
+
2226
+ .pinokio-history-latest-banner--disabled {
2227
+ opacity: 0.6;
2228
+ }
2229
+
2230
+ .pinokio-history-latest-banner--disabled .pinokio-history-latest-btn {
2231
+ pointer-events: none;
2232
+ cursor: not-allowed;
2233
+ }
2234
+
2176
2235
  .pinokio-pill {
2177
2236
  display: inline-flex;
2178
2237
  align-items: center;
@@ -2553,6 +2612,104 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2553
2612
  flex: 1;
2554
2613
  overflow: hidden;
2555
2614
  padding: 24px;
2615
+ display: flex;
2616
+ flex-direction: column;
2617
+ gap: 16px;
2618
+ }
2619
+
2620
+ .pinokio-git-commit-actions {
2621
+ display: flex;
2622
+ justify-content: flex-end;
2623
+ }
2624
+
2625
+ .pinokio-git-commit-switch-btn {
2626
+ display: inline-flex;
2627
+ align-items: center;
2628
+ gap: 6px;
2629
+ background: var(--pinokio-custom-commit-action-bg);
2630
+ border: 1px solid var(--pinokio-custom-commit-action-border);
2631
+ color: var(--pinokio-custom-commit-action-color);
2632
+ padding: 6px 14px;
2633
+ border-radius: 6px;
2634
+ font-weight: 600;
2635
+ font-size: 13px;
2636
+ cursor: pointer;
2637
+ transition: background 0.15s ease, border 0.15s ease, transform 0.15s ease;
2638
+ }
2639
+
2640
+ .pinokio-git-commit-switch-btn:hover {
2641
+ background: var(--pinokio-custom-commit-action-hover);
2642
+ transform: translateY(-1px);
2643
+ }
2644
+
2645
+ .pinokio-git-commit-switch-btn:focus-visible {
2646
+ outline: 2px solid var(--pinokio-custom-commit-action-color);
2647
+ outline-offset: 3px;
2648
+ }
2649
+
2650
+ .pinokio-custom-terminal-overlay {
2651
+ position: fixed;
2652
+ inset: 0;
2653
+ background: rgba(9, 11, 15, 0.7);
2654
+ display: flex;
2655
+ align-items: center;
2656
+ justify-content: center;
2657
+ z-index: 999999;
2658
+ }
2659
+
2660
+ .pinokio-custom-terminal-modal {
2661
+ width: min(900px, 92vw);
2662
+ height: min(620px, 88vh);
2663
+ background: var(--pinokio-modal-surface-bg, #101522);
2664
+ color: var(--pinokio-modal-title-color);
2665
+ border-radius: 18px;
2666
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.55);
2667
+ display: flex;
2668
+ flex-direction: column;
2669
+ overflow: hidden;
2670
+ border: 1px solid rgba(76, 137, 251, 0.2);
2671
+ }
2672
+
2673
+ .pinokio-custom-terminal-header {
2674
+ display: flex;
2675
+ justify-content: space-between;
2676
+ align-items: center;
2677
+ padding: 16px 20px;
2678
+ background: rgba(30, 41, 59, 0.8);
2679
+ border-bottom: 1px solid rgba(76, 137, 251, 0.25);
2680
+ }
2681
+
2682
+ .pinokio-custom-terminal-header h3 {
2683
+ margin: 0;
2684
+ font-size: 15px;
2685
+ font-weight: 600;
2686
+ }
2687
+
2688
+ .pinokio-custom-terminal-close {
2689
+ background: none;
2690
+ border: none;
2691
+ font-size: 26px;
2692
+ cursor: pointer;
2693
+ padding: 6px 10px;
2694
+ border-radius: 12px;
2695
+ color: rgba(226, 232, 240, 0.7);
2696
+ transition: background 0.2s ease, color 0.2s ease;
2697
+ }
2698
+
2699
+ .pinokio-custom-terminal-close:hover {
2700
+ background: rgba(148, 163, 184, 0.18);
2701
+ color: #f8fafc;
2702
+ }
2703
+
2704
+ .pinokio-custom-terminal-body {
2705
+ flex: 1;
2706
+ background: #0b1120;
2707
+ }
2708
+
2709
+ .pinokio-custom-terminal-body iframe {
2710
+ width: 100%;
2711
+ height: 100%;
2712
+ border: none;
2556
2713
  }
2557
2714
 
2558
2715
  .pinokio-git-commit-item {
@@ -2848,10 +3005,12 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2848
3005
  #fs-fork-btn .fs-status-label i {
2849
3006
  font-size: 1rem;
2850
3007
  }
2851
- #fs-status .fs-status-btn .disk-usage,
2852
- #fs-status .fs-status-btn .badge {
3008
+ #fs-status .fs-status-btn .disk-usage {
2853
3009
  display: none;
2854
3010
  }
3011
+ #fs-status .git-changes .badge {
3012
+ display: inline-flex;
3013
+ }
2855
3014
  }
2856
3015
  /*
2857
3016
  @media only screen and (max-width: 800px) {
@@ -2904,7 +3063,8 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2904
3063
  <button class='btn2' id='screenshot' data-tippy-content="take a screenshot"><i class="fa-solid fa-camera"></i></button>
2905
3064
  <div class='sep'></div>
2906
3065
  <div class='mode-selector'>
2907
- <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Community</div></a>
3066
+ <a class="btn2 <%=type === 'review' ? 'selected' : ''%>" href="<%=review_tab%>"><div><i class="fa-regular fa-message"></i></div><div class='caption'>Forum</div></a>
3067
+ <a class="btn2 <%=type === 'files' ? 'selected' : ''%>" href="<%=files_tab%>"><div><i class="fa-solid fa-file-lines"></i></div><div class='caption'>Files</div></a>
2908
3068
  <a class="btn2 <%=type === 'browse' ? 'selected' : ''%>" href="<%=dev_tab%>"><div><i class="fa-solid fa-code"></i></div><div class='caption'>Dev</div></a>
2909
3069
  <a class="btn2 <%=type === 'run' ? 'selected' : ''%>" href="<%=run_tab%>"><div><i class="fa-solid fa-circle-play"></i></div><div class='caption'>Run</div></a>
2910
3070
  </div>
@@ -2931,6 +3091,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2931
3091
  </div>
2932
3092
  </div>
2933
3093
  <% } else { %>
3094
+ <% if (type !== 'files') { %>
2934
3095
  <aside class='active'>
2935
3096
  <!--
2936
3097
  <div class='header-top header-item'>
@@ -2963,15 +3124,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
2963
3124
  <div class='loader'><i class='fa-solid fa-angle-right'></i></div>
2964
3125
  -->
2965
3126
  </a>
2966
- <a id='editortab' data-mode="refresh" target="<%=editor_tab%>" href="<%=editor_tab%>" class="btn header-item frame-link edit-tab" data-index="11">
2967
- <div class='tab'>
2968
- <i class="fa-solid fa-file-lines"></i>
2969
- <div class='display'>Files</div>
2970
- <div class='tab-metric'>
2971
- <span class='disk-usage tab-metric__value' data-path="/">--</span>
2972
- </div>
2973
- </div>
2974
- </a>
3127
+
2975
3128
  <div class="dynamic <%=type==='run' ? '' : 'selected'%>">
2976
3129
  <div class='submenu'>
2977
3130
  <% if (plugin_menu) { %>
@@ -3006,6 +3159,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3006
3159
  </div>
3007
3160
  </div>
3008
3161
  </aside>
3162
+ <% } %>
3009
3163
  <% if (type === "run") { %>
3010
3164
  <div class='appcanvas_filler'></div>
3011
3165
  <% } %>
@@ -3024,6 +3178,62 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3024
3178
  </div>
3025
3179
  </div>
3026
3180
  -->
3181
+ <div class='fs-status-dropdown fs-open-explorer'>
3182
+ <button class='fs-status-btn' data-filepath="<%=path%>" type='button'>
3183
+ <span class='fs-status-label'>
3184
+ <i class="fa-solid fa-folder-open"></i>
3185
+ <span class='fs-status-title'>Open in File Explorer</span>
3186
+ </span>
3187
+ </button>
3188
+ </div>
3189
+ <div class='fs-status-dropdown git-changes'>
3190
+ <button id='fs-changes-btn' class='fs-status-btn revealer' data-group='#fs-changes-menu' type='button'>
3191
+ <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
3192
+ <div class='badge'></div>
3193
+ </button>
3194
+ <div class='fs-dropdown-menu submenu hidden' id='fs-changes-menu'></div>
3195
+ </div>
3196
+ <div class='fs-status-dropdown git-fork'>
3197
+ <button id='fs-fork-btn' class='fs-status-btn revealer' data-group='#fs-fork-menu' type='button'>
3198
+ <span class='fs-status-label'>
3199
+ <i class="fa-solid fa-code-branch"></i>
3200
+ <span class='fs-status-title'>Fork</span>
3201
+ </span>
3202
+ </button>
3203
+ <div class='fs-dropdown-menu submenu hidden' id='fs-fork-menu'></div>
3204
+ </div>
3205
+ <div class='fs-status-dropdown git-publish'>
3206
+ <button id='fs-push-btn' class='fs-status-btn revealer' data-group='#fs-push-menu' type='button'>
3207
+ <span class='fs-status-label'>
3208
+ <i class="fa-brands fa-github"></i>
3209
+ <span class='fs-status-title'>Publish</span>
3210
+ </span>
3211
+ </button>
3212
+ <div class='fs-dropdown-menu submenu hidden' id='fs-push-menu'></div>
3213
+ </div>
3214
+ </div>
3215
+ <% } else if (type === 'files') { %>
3216
+ <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%>">
3217
+ <!--
3218
+ <div class='fs-status-dropdown nested-menu git blue'>
3219
+ <button type='button' class='fs-status-btn frame-link reveal'>
3220
+ <span class='fs-status-label'>
3221
+ <i class="fa-brands fa-git-alt"></i>
3222
+ Git
3223
+ </span>
3224
+ </button>
3225
+ <div class='fs-dropdown-menu submenu hidden' id='git-repos'>
3226
+ </div>
3227
+ </div>
3228
+ -->
3229
+ <div class='fs-status-dropdown fs-open-explorer'>
3230
+ <button class='fs-status-btn' data-filepath="<%=path%>" type='button'>
3231
+ <span class='fs-status-label'>
3232
+ <i class="fa-solid fa-folder-open"></i>
3233
+ <span class='fs-status-title'>Open in File Explorer</span>
3234
+ </span>
3235
+ </button>
3236
+ </div>
3027
3237
  <div class='fs-status-dropdown git-changes'>
3028
3238
  <button id='fs-changes-btn' class='fs-status-btn revealer' data-group='#fs-changes-menu' type='button'>
3029
3239
  <span class='fs-status-label'><i class="fa-solid fa-code-compare"></i> Changes</span>
@@ -3052,6 +3262,9 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
3052
3262
  </div>
3053
3263
  <% } %>
3054
3264
  <main class='browserview'>
3265
+ <% if (type === 'files') { %>
3266
+ <iframe class='selected' src="<%=editor_tab%>"></iframe>
3267
+ <% } %>
3055
3268
  </main>
3056
3269
  </div>
3057
3270
  </div>
@@ -6517,7 +6730,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
6517
6730
  }
6518
6731
  });
6519
6732
  */
6520
- <% if (type === "browse") { %>
6733
+ <% if (type === "browse" || type === "files") { %>
6521
6734
  const repoStatusCache = new Map()
6522
6735
  let lastRepoList = []
6523
6736
  let currentChanges = []
@@ -7143,10 +7356,43 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7143
7356
  const showIframeView = (src) => {
7144
7357
  const iframeMarkup = `<iframe src="${src}" frameborder="0"></iframe>`
7145
7358
  const diffBody = document.querySelector('.pinokio-modal-body--diff')
7359
+ let iframeContainer = null
7146
7360
  if (diffBody) {
7147
7361
  diffBody.classList.remove('pinokio-modal-body--diff')
7148
7362
  diffBody.classList.add('pinokio-modal-body--iframe')
7149
7363
  diffBody.innerHTML = iframeMarkup
7364
+ iframeContainer = diffBody
7365
+ } else {
7366
+ const activeSwal = Swal.isVisible()
7367
+ const fallbackHtml = `
7368
+ <div class="pinokio-modal-surface pinokio-modal-surface--iframe">
7369
+ <div class="pinokio-modal-body pinokio-modal-body--iframe"></div>
7370
+ </div>
7371
+ `
7372
+ const launchPromise = Swal.fire({
7373
+ html: fallbackHtml,
7374
+ customClass: {
7375
+ popup: 'pinokio-modern-modal',
7376
+ htmlContainer: 'pinokio-modern-html',
7377
+ closeButton: 'pinokio-modern-close'
7378
+ },
7379
+ backdrop: 'rgba(9,11,15,0.65)',
7380
+ width: 'min(720px, 90vw)',
7381
+ showConfirmButton: false,
7382
+ showCloseButton: true,
7383
+ buttonsStyling: false,
7384
+ focusConfirm: false,
7385
+ })
7386
+ if (!activeSwal) {
7387
+ launchPromise.then(() => {})
7388
+ }
7389
+ const container = Swal.getHtmlContainer()
7390
+ if (container) {
7391
+ iframeContainer = container.querySelector('.pinokio-modal-body--iframe')
7392
+ if (iframeContainer) {
7393
+ iframeContainer.innerHTML = iframeMarkup
7394
+ }
7395
+ }
7150
7396
  }
7151
7397
  const commitFooter = document.querySelector('.pinokio-modal-footer--commit')
7152
7398
  if (commitFooter && commitFooter.parentNode) {
@@ -7616,7 +7862,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7616
7862
  }
7617
7863
 
7618
7864
  const historyData = await response.json()
7619
- displayGitHistory(historyData, { repoName: repoName || null, repoParam: repoParam || null })
7865
+ displayGitHistory(historyData, { repoName: repoName || null, repoParam: repoParam || null, repoData })
7620
7866
  } catch (error) {
7621
7867
  console.error('Failed to load git history:', error)
7622
7868
  Swal.fire({
@@ -7629,6 +7875,7 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7629
7875
 
7630
7876
  const displayGitHistory = (historyData, options = {}) => {
7631
7877
  const repoName = options && typeof options === 'object' ? options.repoName : null
7878
+ const repoData = options && typeof options === 'object' ? options.repoData : null
7632
7879
  const commits = historyData.log || []
7633
7880
  const remote = historyData.remote || ''
7634
7881
  const currentRef = historyData.ref || 'HEAD'
@@ -7659,6 +7906,14 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7659
7906
  </div>
7660
7907
  </div>
7661
7908
  ${metaBadges.length ? `<div class="pinokio-history-meta">${metaBadges.join('')}</div>` : ''}
7909
+ <div class="pinokio-history-latest-banner" data-history-latest-banner>
7910
+ <div class="pinokio-history-latest-text">
7911
+ Currently viewing ${escapeHtml(currentRef)}
7912
+ </div>
7913
+ <button type="button" class="pinokio-history-latest-btn" data-history-return-head>
7914
+ <i class="fa-solid fa-arrow-rotate-left"></i> Return to newest commit
7915
+ </button>
7916
+ </div>
7662
7917
  <div class="pinokio-modal-body pinokio-modal-body--history">
7663
7918
  ${commits.length === 0 ?
7664
7919
  '<div class="pinokio-history-empty">No commits found</div>' :
@@ -7689,8 +7944,53 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7689
7944
  if (commitInfo) {
7690
7945
  await showCommitDiffModal(commitInfo)
7691
7946
  }
7692
- })
7947
+ })
7693
7948
  })
7949
+
7950
+ const returnBtn = document.querySelector('[data-history-return-head]')
7951
+ const banner = document.querySelector('[data-history-latest-banner]')
7952
+ if (returnBtn && typeof showIframeView === 'function') {
7953
+ const repoParam = options && typeof options === 'object' ? options.repoParam : null
7954
+ let checkoutCwd = null
7955
+ if (historyData && typeof historyData.dir === 'string' && historyData.dir.length > 0) {
7956
+ checkoutCwd = historyData.dir
7957
+ } else if (repoParam) {
7958
+ checkoutCwd = repoParam
7959
+ }
7960
+
7961
+ const branchEntries = Array.isArray(historyData.branches) ? historyData.branches : []
7962
+ let checkoutTarget = null
7963
+
7964
+ if (repoData && typeof repoData.branch === 'string' && repoData.branch.length > 0 && repoData.branch !== currentRef) {
7965
+ checkoutTarget = repoData.branch
7966
+ }
7967
+ if (!checkoutTarget) {
7968
+ const nonCurrent = branchEntries.find((entry) => entry && typeof entry.branch === 'string' && entry.branch.length > 0 && entry.branch !== currentRef)
7969
+ if (nonCurrent) {
7970
+ checkoutTarget = nonCurrent.branch
7971
+ }
7972
+ }
7973
+ if (!checkoutTarget && branchEntries.length > 0) {
7974
+ checkoutTarget = branchEntries[0].branch
7975
+ }
7976
+
7977
+ if (checkoutCwd && checkoutTarget) {
7978
+ const checkoutUrl = `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(checkoutCwd)}&commit=${encodeURIComponent(checkoutTarget)}&callback_target=parent&callback=$location.href`
7979
+ returnBtn.addEventListener('click', () => {
7980
+ openCheckoutTerminal(checkoutUrl)
7981
+ })
7982
+ } else {
7983
+ returnBtn.disabled = true
7984
+ if (banner) {
7985
+ banner.classList.add('pinokio-history-latest-banner--disabled')
7986
+ }
7987
+ }
7988
+ } else if (banner) {
7989
+ const parent = banner.parentNode
7990
+ if (parent) {
7991
+ parent.removeChild(banner)
7992
+ }
7993
+ }
7694
7994
  }
7695
7995
  })
7696
7996
  }
@@ -7731,6 +8031,20 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7731
8031
  alert('No file changes detected.')
7732
8032
  return
7733
8033
  }
8034
+
8035
+ const commitOid = typeof diffData.oid === 'string' ? diffData.oid : (Array.isArray(changes) && typeof changes[0]?.ref === 'string' ? changes[0].ref : null)
8036
+ const commitCheckoutCwd = (() => {
8037
+ const url = typeof diffData.git_commit_url === 'string' ? new URL(diffData.git_commit_url, window.location.origin) : null
8038
+ if (!url) {
8039
+ return null
8040
+ }
8041
+ const cwdParam = url.searchParams.get('cwd')
8042
+ return cwdParam && cwdParam.length > 0 ? cwdParam : null
8043
+ })()
8044
+
8045
+ const checkoutUrl = commitOid && commitCheckoutCwd
8046
+ ? `/run/scripts/git/checkout.json?cwd=${encodeURIComponent(commitCheckoutCwd)}&commit=${encodeURIComponent(commitOid)}&callback_target=parent&callback=$location.href`
8047
+ : null
7734
8048
 
7735
8049
  // Create custom overlay modal
7736
8050
  const overlay = document.createElement('div')
@@ -7755,6 +8069,13 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7755
8069
  <div class="pinokio-git-diff-empty-state">Select a file to view its changes</div>
7756
8070
  </div>
7757
8071
  </div>
8072
+ ${checkoutUrl ? `
8073
+ <div class="pinokio-git-commit-actions">
8074
+ <button type="button" class="pinokio-git-commit-switch-btn">
8075
+ <i class="fa-solid fa-clock-rotate-left"></i> Switch to this version
8076
+ </button>
8077
+ </div>
8078
+ ` : ''}
7758
8079
  </div>
7759
8080
  </div>
7760
8081
  `
@@ -7763,14 +8084,23 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7763
8084
 
7764
8085
  // Add close handler
7765
8086
  const closeBtn = overlay.querySelector('.pinokio-custom-commit-close')
7766
- closeBtn.addEventListener('click', () => {
7767
- document.body.removeChild(overlay)
7768
- })
8087
+ let escHandler = null
8088
+ const cleanupOverlay = () => {
8089
+ if (escHandler) {
8090
+ document.removeEventListener('keydown', escHandler)
8091
+ escHandler = null
8092
+ }
8093
+ if (overlay.parentNode) {
8094
+ overlay.parentNode.removeChild(overlay)
8095
+ }
8096
+ }
8097
+
8098
+ closeBtn.addEventListener('click', cleanupOverlay)
7769
8099
 
7770
8100
  // Close on overlay click
7771
8101
  overlay.addEventListener('click', (e) => {
7772
8102
  if (e.target === overlay) {
7773
- document.body.removeChild(overlay)
8103
+ cleanupOverlay()
7774
8104
  }
7775
8105
  })
7776
8106
 
@@ -7798,16 +8128,66 @@ body.dark .pinokio-fork-dropdown-remote, body.dark .pinokio-publish-dropdown-rem
7798
8128
  }
7799
8129
  })
7800
8130
  })
8131
+
8132
+ if (checkoutUrl) {
8133
+ const switchBtn = overlay.querySelector('.pinokio-git-commit-switch-btn')
8134
+ if (switchBtn) {
8135
+ switchBtn.addEventListener('click', () => {
8136
+ cleanupOverlay()
8137
+ showIframeView(checkoutUrl)
8138
+ })
8139
+ }
8140
+ }
7801
8141
 
7802
8142
  // Handle escape key
7803
- const escHandler = (e) => {
8143
+ escHandler = (e) => {
7804
8144
  if (e.key === 'Escape') {
7805
- document.body.removeChild(overlay)
7806
- document.removeEventListener('keydown', escHandler)
8145
+ cleanupOverlay()
7807
8146
  }
7808
8147
  }
7809
8148
  document.addEventListener('keydown', escHandler)
7810
8149
  }
8150
+
8151
+ const openCheckoutTerminal = (src) => {
8152
+ const overlay = document.createElement('div')
8153
+ overlay.className = 'pinokio-custom-terminal-overlay'
8154
+ overlay.innerHTML = `
8155
+ <div class="pinokio-custom-terminal-modal">
8156
+ <div class="pinokio-custom-terminal-header">
8157
+ <h3>Git Checkout</h3>
8158
+ <button class="pinokio-custom-terminal-close">×</button>
8159
+ </div>
8160
+ <div class="pinokio-custom-terminal-body">
8161
+ <iframe src="${src}" frameborder="0"></iframe>
8162
+ </div>
8163
+ </div>
8164
+ `
8165
+
8166
+ const cleanup = () => {
8167
+ document.removeEventListener('keydown', escHandler)
8168
+ if (overlay.parentNode) {
8169
+ overlay.parentNode.removeChild(overlay)
8170
+ }
8171
+ }
8172
+
8173
+ const closeBtn = overlay.querySelector('.pinokio-custom-terminal-close')
8174
+ closeBtn.addEventListener('click', cleanup)
8175
+
8176
+ overlay.addEventListener('click', (event) => {
8177
+ if (event.target === overlay) {
8178
+ cleanup()
8179
+ }
8180
+ })
8181
+
8182
+ const escHandler = (event) => {
8183
+ if (event.key === 'Escape') {
8184
+ cleanup()
8185
+ }
8186
+ }
8187
+
8188
+ document.addEventListener('keydown', escHandler)
8189
+ document.body.appendChild(overlay)
8190
+ }
7811
8191
 
7812
8192
  const createCommitItem = (commitData) => {
7813
8193
  const commit = commitData.commit