pinokiod 7.3.10 → 7.3.11

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.
Files changed (51) hide show
  1. package/kernel/bin/brew.js +12 -2
  2. package/kernel/bin/caddy.js +24 -20
  3. package/kernel/bin/huggingface.js +2 -2
  4. package/kernel/bin/setup.js +2 -3
  5. package/kernel/bin/uv.js +13 -6
  6. package/kernel/connect/index.js +5 -1
  7. package/kernel/connect/providers/huggingface/index.js +213 -75
  8. package/kernel/environment.js +16 -1
  9. package/kernel/router/localhost_home_router.js +7 -0
  10. package/kernel/shell.js +1 -5
  11. package/kernel/util.js +1 -0
  12. package/package.json +1 -1
  13. package/server/index.js +75 -33
  14. package/server/public/common.js +45 -71
  15. package/server/public/install.js +20 -2
  16. package/server/public/layout.js +1 -1
  17. package/server/public/style.css +1432 -511
  18. package/server/public/tab-link-popover.css +162 -18
  19. package/server/public/tab-link-popover.js +230 -21
  20. package/server/public/task-launcher.css +121 -84
  21. package/server/public/terminal-settings.js +121 -45
  22. package/server/public/universal-launcher.css +42 -33
  23. package/server/public/urldropdown.css +284 -0
  24. package/server/views/app.ejs +1595 -308
  25. package/server/views/autolaunch.ejs +4 -5
  26. package/server/views/checkpoints.ejs +223 -50
  27. package/server/views/connect/huggingface.ejs +406 -325
  28. package/server/views/connect.ejs +0 -1
  29. package/server/views/github.ejs +277 -324
  30. package/server/views/index.ejs +53 -1
  31. package/server/views/install.ejs +119 -19
  32. package/server/views/logs.ejs +10 -9
  33. package/server/views/net.ejs +341 -64
  34. package/server/views/network.ejs +85 -63
  35. package/server/views/partials/main_sidebar.ejs +237 -12
  36. package/server/views/plugins.ejs +141 -3
  37. package/server/views/settings.ejs +103 -7
  38. package/server/views/setup.ejs +0 -5
  39. package/server/views/skills.ejs +0 -1
  40. package/server/views/task_list.ejs +0 -1
  41. package/server/views/terminal.ejs +142 -1
  42. package/server/views/terminals.ejs +346 -6
  43. package/server/views/tools.ejs +828 -1691
  44. package/test/caddy-install.test.js +53 -0
  45. package/test/connect-setup.test.js +16 -0
  46. package/test/github-connection.test.js +1 -1
  47. package/test/huggingface-bin.test.js +4 -4
  48. package/test/huggingface-connect.test.js +73 -0
  49. package/test/main-sidebar.test.js +31 -0
  50. package/test/shell-run-template.test.js +5 -1
  51. package/test/uv-bin.test.js +29 -0
package/server/index.js CHANGED
@@ -1089,7 +1089,7 @@ class Server {
1089
1089
  const doneMarker = "PINOKIO_GITHUB_LOGIN_DONE"
1090
1090
  const delimiter = this.kernel.platform === "win32" ? " && " : " ; "
1091
1091
  const verifyCommand = this.kernel.platform === "win32"
1092
- ? "cmd /C \"set GIT_TERMINAL_PROMPT=0&& set GCM_INTERACTIVE=never&& (echo protocol=https& echo host=github.com& echo.) | git credential fill >NUL\""
1092
+ ? "powershell.exe -NoProfile -Command \"$env:GIT_TERMINAL_PROMPT='0'; $env:GCM_INTERACTIVE='never'; @('protocol=https','host=github.com','') | git credential fill > $null; exit $LASTEXITCODE\""
1093
1093
  : "printf 'protocol=https\\nhost=github.com\\n\\n' | GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never git credential fill >/dev/null"
1094
1094
  const doneCommand = this.kernel.platform === "win32"
1095
1095
  ? "echo P^INOKIO_GITHUB_LOGIN_DONE"
@@ -4914,11 +4914,17 @@ class Server {
4914
4914
  }
4915
4915
  async composePeerAccessPayload() {
4916
4916
  let peer_access_points = []
4917
+ let peer_access_router_installed = false
4917
4918
  try {
4918
4919
  peer_access_points = await this.buildPeerAccessPoints()
4919
4920
  } catch (error) {
4920
4921
  peer_access_points = []
4921
4922
  }
4923
+ try {
4924
+ peer_access_router_installed = await this.kernel.network_installed()
4925
+ } catch (error) {
4926
+ peer_access_router_installed = false
4927
+ }
4922
4928
  let peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
4923
4929
  let peer_qr = null
4924
4930
  if (peer_access_points.length > 0) {
@@ -4929,7 +4935,7 @@ class Server {
4929
4935
  peer_qr = await QRCode.toDataURL(peer_url)
4930
4936
  } catch (_) {}
4931
4937
  }
4932
- return { peer_access_points, peer_url, peer_qr }
4938
+ return { peer_access_points, peer_url, peer_qr, peer_access_router_installed }
4933
4939
  }
4934
4940
 
4935
4941
  async ensureLogsRootDirectory() {
@@ -8092,6 +8098,8 @@ class Server {
8092
8098
  if (!requireSkillsHome(res)) {
8093
8099
  return
8094
8100
  }
8101
+ const peerAccess = await this.composePeerAccessPayload()
8102
+ const list = this.getPeers()
8095
8103
  let skills = []
8096
8104
  let indexError = ""
8097
8105
  try {
@@ -8100,8 +8108,11 @@ class Server {
8100
8108
  indexError = error && error.message ? error.message : "Failed to read managed skills index."
8101
8109
  }
8102
8110
  res.render("skills", {
8111
+ current_host: this.kernel.peer.host,
8112
+ ...peerAccess,
8103
8113
  theme: this.theme,
8104
8114
  agent: req.agent,
8115
+ list,
8105
8116
  skills,
8106
8117
  skillsRootPath: ManagedSkills.skillsRoot(this.kernel),
8107
8118
  indexPath: ManagedSkills.indexPath(this.kernel),
@@ -12380,49 +12391,53 @@ class Server {
12380
12391
  * GET /connect/discord
12381
12392
  */
12382
12393
  this.app.get("/connect/:provider", ex(async (req, res) => {
12394
+ const providerUsesDeviceLogin = req.params.provider === "huggingface"
12395
+ const setupMode = providerUsesDeviceLogin ? "connect" : "network"
12383
12396
 
12384
12397
  // check if all the connect related modules are installed
12385
12398
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
12386
- bin: this.kernel.bin.preset("connect"),
12399
+ bin: this.kernel.bin.preset(setupMode),
12387
12400
  })
12388
12401
  if (!requirements_pending && install_required) {
12389
12402
  console.log("REDIRECT", req.params.provider)
12390
- res.redirect("/setup/connect?callback=/connect/" + req.params.provider)
12403
+ res.redirect(`/setup/${setupMode}?callback=/connect/${req.params.provider}`)
12391
12404
  return
12392
12405
  }
12393
12406
 
12394
- let https_running = false
12395
- try {
12396
- let res = await axios.get(`http://127.0.0.1:2019/config/`, {
12397
- timeout: 2000
12398
- })
12399
- let test = /pinokio\.localhost/.test(JSON.stringify(res.data))
12400
- if (test) {
12401
- https_running = true
12407
+ if (!providerUsesDeviceLogin) {
12408
+ let https_running = false
12409
+ try {
12410
+ let res = await axios.get(`http://127.0.0.1:2019/config/`, {
12411
+ timeout: 2000
12412
+ })
12413
+ let test = /pinokio\.localhost/.test(JSON.stringify(res.data))
12414
+ if (test) {
12415
+ https_running = true
12416
+ }
12417
+ } catch (e) {
12418
+ console.log(e)
12402
12419
  }
12403
- } catch (e) {
12404
- console.log(e)
12405
- }
12406
- if (!https_running) {
12420
+ if (!https_running) {
12407
12421
  // res.json({ error: "pinokio.host not yet available" })
12408
- res.redirect("/setup/connect?callback=/connect/" + req.params.provider)
12409
- return
12410
- }
12422
+ res.redirect(`/setup/${setupMode}?callback=/connect/${req.params.provider}`)
12423
+ return
12424
+ }
12411
12425
 
12412
- // check if pinokio.localhost router is running
12413
- let router_running = false
12414
- let router = this.kernel.router.published()
12415
- for(let ip in router) {
12416
- let domains = router[ip]
12417
- if (domains.includes("pinokio.localhost")) {
12418
- router_running = true
12419
- break
12426
+ // check if pinokio.localhost router is running
12427
+ let router_running = false
12428
+ let router = this.kernel.router.published()
12429
+ for(let ip in router) {
12430
+ let domains = router[ip]
12431
+ if (domains.includes("pinokio.localhost")) {
12432
+ router_running = true
12433
+ break
12434
+ }
12420
12435
  }
12421
- }
12422
- if (!router_running) {
12436
+ if (!router_running) {
12423
12437
  // res.json({ error: "pinokio.localhost not yet available" })
12424
- res.redirect("/setup/connect?callback=/connect/" + req.params.provider)
12425
- return
12438
+ res.redirect(`/setup/${setupMode}?callback=/connect/${req.params.provider}`)
12439
+ return
12440
+ }
12426
12441
  }
12427
12442
 
12428
12443
 
@@ -12437,7 +12452,7 @@ class Server {
12437
12452
  const config = this.kernel.connect.config[req.params.provider]
12438
12453
  const isPinokioHost = req.hostname === 'pinokio.localhost'
12439
12454
  const renderProtocol = isPinokioHost ? 'https' : 'http'
12440
- res.render(`connect/index`, {
12455
+ res.render(providerUsesDeviceLogin ? `connect/huggingface` : `connect/index`, {
12441
12456
  protocol: renderProtocol,
12442
12457
  name: req.params.provider,
12443
12458
  config,
@@ -13516,7 +13531,7 @@ class Server {
13516
13531
  1. mode:ai => all
13517
13532
  2. mode:coding => conda, nodejs, git
13518
13533
  3. mode:network => conda, git, caddy
13519
- 4. mode:connect => conda, git, caddy
13534
+ 4. mode:connect => conda, git, huggingface
13520
13535
  */
13521
13536
 
13522
13537
  let bin = this.kernel.bin.preset(req.params.mode)
@@ -15626,6 +15641,32 @@ class Server {
15626
15641
  res.json({ https_active: false, router_info: [], rewrite_mapping: {}, router: {} })
15627
15642
  }
15628
15643
  }))
15644
+ this.app.get("/info/network-sharing", ex(async (req, res) => {
15645
+ res.set("Cache-Control", "no-store")
15646
+ const https_active = !!(this.kernel.peer && this.kernel.peer.https_active)
15647
+ let installed = false
15648
+ let running = false
15649
+ try {
15650
+ const caddy = this.kernel.bin && this.kernel.bin.mod ? this.kernel.bin.mod.caddy : null
15651
+ if (caddy && typeof caddy.installed === "function") {
15652
+ installed = !!(await caddy.installed())
15653
+ }
15654
+ } catch (err) {
15655
+ installed = false
15656
+ }
15657
+ try {
15658
+ await axios.get("http://127.0.0.1:2019/config/", { timeout: 750 })
15659
+ running = true
15660
+ } catch (err) {
15661
+ running = false
15662
+ }
15663
+ res.json({
15664
+ active: https_active,
15665
+ installed,
15666
+ running,
15667
+ available: https_active && installed
15668
+ })
15669
+ }))
15629
15670
  this.app.get("/qr", ex(async (req, res) => {
15630
15671
  try {
15631
15672
  const data = typeof req.query.data === 'string' ? req.query.data : ''
@@ -16042,6 +16083,7 @@ class Server {
16042
16083
  })
16043
16084
  }))
16044
16085
  this.app.get("/pinokio/install", ex((req, res) => {
16086
+ res.set("Cache-Control", "no-store")
16045
16087
  let requirements = req.session.requirements
16046
16088
  let callback = req.session.callback
16047
16089
  req.session.requirements = null
@@ -116,6 +116,14 @@ function escapePinokioStatusHtml(value) {
116
116
  .replace(/'/g, ''')
117
117
  }
118
118
 
119
+ const PINOKIO_WAIT_STATUS_SPINNER_HTML = `
120
+ <span class="pinokio-install-inline-status-spinner" aria-hidden="true">
121
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-1"></span>
122
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-2"></span>
123
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-3"></span>
124
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-4"></span>
125
+ </span>`
126
+
119
127
  function ensurePinokioWaitFooterStatus() {
120
128
  let status = document.getElementById(PINOKIO_WAIT_FOOTER_ID)
121
129
  if (status) {
@@ -148,9 +156,7 @@ function showPinokioWaitFooterStatus(data = {}) {
148
156
  status.className = 'pinokio-install-inline-status pinokio-process-wait-footer-status is-progress'
149
157
  status.innerHTML = `
150
158
  <div class="pinokio-install-inline-status-shell">
151
- <span class="pinokio-install-inline-status-icon" aria-hidden="true">
152
- <i class="fa-solid fa-circle-notch fa-spin"></i>
153
- </span>
159
+ ${PINOKIO_WAIT_STATUS_SPINNER_HTML}
154
160
  <div class="pinokio-install-inline-status-copy">
155
161
  <div class="pinokio-install-inline-status-title">${escapePinokioStatusHtml(title)}</div>
156
162
  ${description ? `<div class="pinokio-install-inline-status-detail">${escapePinokioStatusHtml(description)}</div>` : ''}
@@ -445,24 +451,28 @@ class ScreenCaptureModal {
445
451
  .modal-overlay{position:fixed;inset:0;padding:24px;display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;pointer-events:none;transition:opacity 160ms ease,visibility 0s linear 160ms;}
446
452
  .modal-overlay.is-visible{opacity:1;visibility:visible;pointer-events:auto;transition-delay:0s;}
447
453
  @media (prefers-reduced-motion: reduce){.modal-overlay{transition:none;}.capture-modal{animation:none;}}
448
- .capture-modal-overlay{background:rgba(15,23,42,0.45);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);}
449
- .capture-modal{width:min(360px,calc(100% - 32px));padding:28px 26px;border-radius:18px;background:rgba(255,255,255,0.92);border:1px solid rgba(15,23,42,0.08);box-shadow:0 30px 80px rgba(15,23,42,0.35);display:flex;flex-direction:column;gap:18px;text-align:center;}
450
- body.dark .capture-modal{background:rgba(15,23,42,0.9);border-color:rgba(148,163,184,0.24);color:rgba(226,232,240,0.96);box-shadow:0 34px 88px rgba(2,6,20,0.82);}
451
- .capture-modal-title{font-size:20px;font-weight:600;letter-spacing:-0.01em;color:rgba(15,23,42,0.92);}
454
+ .capture-modal-overlay{background:rgba(9,11,15,0.34);-webkit-backdrop-filter:none;backdrop-filter:none;}
455
+ body.dark .capture-modal-overlay{background:rgba(0,0,0,0.58);}
456
+ .capture-modal{width:min(360px,calc(100% - 32px));padding:20px;border-radius:8px;background:#fff;border:1px solid rgba(24,24,27,0.12);box-shadow:0 18px 50px rgba(15,23,42,0.16);display:flex;flex-direction:column;gap:14px;text-align:center;color:#18181b;}
457
+ body.dark .capture-modal{background:#1b1c1d;border-color:rgba(255,255,255,0.1);color:rgba(250,250,250,0.94);box-shadow:0 24px 72px rgba(0,0,0,0.42);}
458
+ .capture-modal-title{font-size:16px;line-height:1.25;font-weight:600;letter-spacing:0;color:inherit;}
452
459
  body.dark .capture-modal-title{color:inherit;}
453
- .capture-modal-description{font-size:14px;line-height:1.5;color:rgba(71,85,105,0.82);}
454
- body.dark .capture-modal-description{color:rgba(203,213,225,0.88);}
455
- .capture-modal-actions{display:flex;justify-content:center;gap:12px;}
456
- .capture-modal-button{padding:10px 20px;border-radius:999px;border:1px solid transparent;font-size:14px;font-weight:600;cursor:pointer;transition:background 0.18s ease,color 0.18s ease,box-shadow 0.18s ease,transform 0.12s ease;}
457
- .capture-modal-button.primary{background:linear-gradient(135deg,rgba(127,91,243,0.95),rgba(84,63,196,0.95));color:#fff;box-shadow:0 16px 36px rgba(111,76,242,0.3);}
458
- .capture-modal-button.primary:hover{box-shadow:0 20px 42px rgba(111,76,242,0.38);transform:translateY(-1px);}
459
- .capture-modal-button.secondary{background:rgba(148,163,184,0.18);color:rgba(15,23,42,0.78);}
460
- .capture-modal-button.secondary:hover{background:rgba(148,163,184,0.28);box-shadow:0 12px 28px rgba(15,23,42,0.12);}
461
- body.dark .capture-modal-button.secondary{background:rgba(148,163,184,0.2);color:rgba(226,232,240,0.92);}
460
+ .capture-modal-description{font-size:12px;line-height:1.45;color:#71717a;}
461
+ body.dark .capture-modal-description{color:rgba(229,231,235,0.62);}
462
+ .capture-modal-actions{display:flex;justify-content:center;gap:8px;}
463
+ .capture-modal-button{min-height:32px;padding:0 12px;border-radius:6px;border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;transition:background 0.14s ease,color 0.14s ease,border-color 0.14s ease;}
464
+ .capture-modal-button.primary{background:#18181b;color:#fff;border-color:#18181b;box-shadow:none;}
465
+ .capture-modal-button.primary:hover{background:#27272a;border-color:#27272a;box-shadow:none;transform:none;}
466
+ body.dark .capture-modal-button.primary{background:rgba(250,250,250,0.94);border-color:rgba(250,250,250,0.94);color:#1b1c1d;}
467
+ body.dark .capture-modal-button.primary:hover{background:#fff;border-color:#fff;}
468
+ .capture-modal-button.secondary{background:transparent;color:#52525b;border-color:rgba(24,24,27,0.12);}
469
+ .capture-modal-button.secondary:hover{background:rgba(24,24,27,0.06);border-color:rgba(24,24,27,0.2);box-shadow:none;color:#18181b;}
470
+ body.dark .capture-modal-button.secondary{background:transparent;border-color:rgba(255,255,255,0.1);color:rgba(229,231,235,0.7);}
471
+ body.dark .capture-modal-button.secondary:hover{background:rgba(255,255,255,0.07);border-color:rgba(255,255,255,0.18);color:rgba(250,250,250,0.94);}
462
472
  .capture-modal-button:active{transform:translateY(1px);}
463
- .capture-modal-button:focus-visible{outline:2px solid rgba(127,91,243,0.8);outline-offset:2px;}
473
+ .capture-modal-button:focus-visible{outline:2px solid rgba(24,24,27,0.28);outline-offset:2px;}
464
474
  .modal-overlay.is-visible .capture-modal{animation:captureModalPop 160ms ease-out;}
465
- @media (max-width: 640px){.modal-overlay{padding:16px;}.capture-modal{width:calc(100% - 24px);padding:24px 20px;}.capture-modal-actions{flex-direction:column;}.capture-modal-button{width:100%;}}
475
+ @media (max-width: 640px){.modal-overlay{padding:16px;}.capture-modal{width:calc(100% - 24px);padding:18px;}.capture-modal-actions{flex-direction:column;}.capture-modal-button{width:100%;}}
466
476
  @keyframes captureModalPop{from{opacity:0;transform:scale(0.97) translateY(12px);}to{opacity:1;transform:scale(1) translateY(0);}}
467
477
  `;
468
478
  document.head.appendChild(style);
@@ -2579,13 +2589,13 @@ if (typeof hotkeys === 'function') {
2579
2589
  }
2580
2590
  })();
2581
2591
 
2582
- // Mobile "Tap to connect" curtain to prime audio on the top-level page
2583
- (function initMobileConnectCurtain() {
2592
+ // Mobile first-interaction audio primer for notification sounds on the top-level page
2593
+ (function initMobileAudioPrimer() {
2584
2594
  if (typeof window === 'undefined' || typeof document === 'undefined') {
2585
2595
  return;
2586
2596
  }
2587
2597
  try {
2588
- if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
2598
+ if (window.__pinokioAudioPrimerInstalled || window.__pinokioAudioPrimerInstalling) {
2589
2599
  return;
2590
2600
  }
2591
2601
  } catch (_) {}
@@ -2597,7 +2607,7 @@ if (typeof hotkeys === 'function') {
2597
2607
  // cross-origin parent; just bail
2598
2608
  return;
2599
2609
  }
2600
- if (window.__pinokioConnectCurtainInstalled) {
2610
+ if (window.__pinokioAudioPrimerInstalled) {
2601
2611
  return;
2602
2612
  }
2603
2613
 
@@ -2653,37 +2663,6 @@ if (typeof hotkeys === 'function') {
2653
2663
  return uaMobile;
2654
2664
  };
2655
2665
 
2656
- const createCurtain = () => {
2657
- const style = document.createElement('style');
2658
- style.textContent = `
2659
- .pinokio-connect-curtain{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;background:rgba(15,23,42,0.35);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center}
2660
- .pinokio-connect-msg{user-select:none;-webkit-user-select:none;color:#fff;background:rgba(15,23,42,0.85);padding:14px 18px;border-radius:12px;font:500 15px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 16px 40px rgba(0,0,0,.38);text-align:center;max-width:200px}
2661
- .pinokio-connect-msg-title{font-weight:600;font-size:16px;margin-bottom:4px}
2662
- .pinokio-connect-msg-hint{font-size:13px;opacity:.72}
2663
- @media (max-width:768px){.pinokio-connect-msg{font-size:14px;padding:12px 16px}}
2664
- `;
2665
- document.head.appendChild(style);
2666
-
2667
- const overlay = document.createElement('div');
2668
- overlay.className = 'pinokio-connect-curtain';
2669
- overlay.setAttribute('role', 'button');
2670
- overlay.setAttribute('aria-label', 'Tap to connect');
2671
- overlay.tabIndex = 0;
2672
- const msg = document.createElement('div');
2673
- msg.className = 'pinokio-connect-msg';
2674
- const msgTitle = document.createElement('div');
2675
- msgTitle.className = 'pinokio-connect-msg-title';
2676
- msgTitle.textContent = 'Tap to connect';
2677
- const msgHint = document.createElement('div');
2678
- msgHint.className = 'pinokio-connect-msg-hint';
2679
- msgHint.textContent = 'To type into the terminal, use the "Input" button.';
2680
- msg.appendChild(msgTitle);
2681
- msg.appendChild(msgHint);
2682
- overlay.appendChild(msg);
2683
- window.__pinokioConnectCurtainInstalled = true;
2684
- return overlay;
2685
- };
2686
-
2687
2666
  const SOUND_PREF_STORAGE_KEY = 'pinokio:idle-sound';
2688
2667
  const primeAudio = async () => {
2689
2668
  // Determine whether the user picked a custom `/sound/...` clip.
@@ -2747,31 +2726,26 @@ if (typeof hotkeys === 'function') {
2747
2726
  if (!(forceParam || isLikelyMobile())) {
2748
2727
  return;
2749
2728
  }
2750
- if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
2729
+ if (window.__pinokioAudioPrimerInstalled || window.__pinokioAudioPrimerInstalling) {
2751
2730
  return;
2752
2731
  }
2753
- try { window.__pinokioConnectCurtainInstalling = true; } catch (_) {}
2754
- const overlay = createCurtain();
2732
+ try { window.__pinokioAudioPrimerInstalling = true; } catch (_) {}
2755
2733
  let handled = false;
2756
- const onTap = async (e) => {
2734
+ const cleanup = () => {
2735
+ document.removeEventListener('click', onFirstInteraction, true);
2736
+ document.removeEventListener('touchend', onFirstInteraction, true);
2737
+ document.removeEventListener('keydown', onFirstInteraction, true);
2738
+ };
2739
+ const onFirstInteraction = async () => {
2757
2740
  if (handled) return;
2758
2741
  handled = true;
2759
- try { e.preventDefault(); e.stopPropagation(); } catch (_) {}
2742
+ cleanup();
2760
2743
  try { await primeAudio(); } catch (_) {}
2761
- try { overlay.remove(); } catch (_) {}
2762
- try { window.__pinokioConnectCurtainInstalled = true; window.__pinokioConnectCurtainInstalling = false; } catch (_) {}
2744
+ try { window.__pinokioAudioPrimerInstalled = true; window.__pinokioAudioPrimerInstalling = false; } catch (_) {}
2763
2745
  };
2764
- overlay.addEventListener('click', onTap, { once: true, capture: true });
2765
- overlay.addEventListener('keydown', (e) => {
2766
- if (!e) {
2767
- return;
2768
- }
2769
- const key = e.key || '';
2770
- if (key === 'Enter' || key === ' ' || key === 'Spacebar') {
2771
- onTap(e);
2772
- }
2773
- }, { capture: true });
2774
- document.body.appendChild(overlay);
2746
+ document.addEventListener('click', onFirstInteraction, true);
2747
+ document.addEventListener('touchend', onFirstInteraction, true);
2748
+ document.addEventListener('keydown', onFirstInteraction, true);
2775
2749
  };
2776
2750
 
2777
2751
  if (document.readyState === 'loading') {
@@ -38,13 +38,14 @@ const installname = async (url, name, options) => {
38
38
  cancelButtonText: 'Cancel',
39
39
  confirmButtonText: 'Download',
40
40
  buttonsStyling: false,
41
- backdrop: 'rgba(9, 11, 15, 0.65)',
41
+ backdrop: true,
42
42
  width: 'min(460px, 92vw)',
43
43
  allowOutsideClick: () => !Swal.isLoading(),
44
44
  allowEscapeKey: true,
45
45
  showLoaderOnConfirm: true,
46
46
  loaderHtml: '<span class="pinokio-download-loader-spinner" aria-hidden="true"></span><span class="pinokio-download-loader-text">Downloading...</span>',
47
47
  customClass: {
48
+ container: 'pinokio-download-container',
48
49
  popup: 'pinokio-download-modal',
49
50
  htmlContainer: 'pinokio-download-html',
50
51
  inputLabel: 'pinokio-download-label',
@@ -101,6 +102,23 @@ const escapeInstallHtml = (value) => {
101
102
  .replace(/'/g, '&#39;')
102
103
  }
103
104
 
105
+ const INLINE_INSTALL_STATUS_SPINNER_HTML = `
106
+ <span class="pinokio-install-inline-status-spinner" aria-hidden="true">
107
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-1"></span>
108
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-2"></span>
109
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-3"></span>
110
+ <span class="pinokio-install-inline-status-spinner-dot pinokio-install-inline-status-spinner-dot-4"></span>
111
+ </span>`
112
+
113
+ const getInlineInstallStatusIconHtml = (iconClass) => {
114
+ const normalizedIconClass = String(iconClass || '')
115
+ const isProgressIcon = /\bfa-circle-notch\b/.test(normalizedIconClass) || /\bfa-spin\b/.test(normalizedIconClass)
116
+ if (isProgressIcon) {
117
+ return INLINE_INSTALL_STATUS_SPINNER_HTML
118
+ }
119
+ return `<span class="pinokio-install-inline-status-icon" aria-hidden="true"><i class="${escapeInstallHtml(normalizedIconClass)}"></i></span>`
120
+ }
121
+
104
122
  const ensureInlineInstallStatus = () => {
105
123
  let status = document.getElementById(INLINE_INSTALL_STATUS_ID)
106
124
  if (status) {
@@ -126,7 +144,7 @@ const setInlineInstallStatus = ({ state, title, detailHtml, iconClass }) => {
126
144
  status.className = `pinokio-install-inline-status is-${normalizedState}`
127
145
  status.innerHTML = `
128
146
  <div class="pinokio-install-inline-status-shell">
129
- <span class="pinokio-install-inline-status-icon" aria-hidden="true"><i class="${iconClass}"></i></span>
147
+ ${getInlineInstallStatusIconHtml(iconClass)}
130
148
  <div class="pinokio-install-inline-status-copy">
131
149
  <div class="pinokio-install-inline-status-title">${escapeInstallHtml(title || '')}</div>
132
150
  ${detailHtml ? `<div class="pinokio-install-inline-status-detail">${detailHtml}</div>` : ''}
@@ -801,7 +801,7 @@
801
801
  };
802
802
 
803
803
  window.PinokioLayout = api;
804
- // Mobile "Tap to connect" curtain is centralized in common.js to avoid duplicates
804
+ // Mobile notification audio priming is centralized in common.js to avoid duplicates
805
805
 
806
806
  // Top-level notification listener (indicator + optional chime) for mobile
807
807
  (function initTopLevelNotificationListener() {