pinokiod 7.3.0 → 7.3.3

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 (125) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
  7. package/kernel/bin/conda.js +15 -5
  8. package/kernel/bin/git.js +9 -10
  9. package/kernel/bin/huggingface.js +1 -1
  10. package/kernel/bin/index.js +5 -2
  11. package/kernel/bin/zip.js +9 -1
  12. package/kernel/connect/providers/github/README.md +5 -4
  13. package/kernel/environment.js +195 -92
  14. package/kernel/git.js +98 -19
  15. package/kernel/gitconfig_template +7 -0
  16. package/kernel/gpu/amd.js +72 -0
  17. package/kernel/gpu/apple.js +8 -0
  18. package/kernel/gpu/common.js +12 -0
  19. package/kernel/gpu/intel.js +47 -0
  20. package/kernel/gpu/nvidia.js +8 -0
  21. package/kernel/index.js +11 -1
  22. package/kernel/managed_skills.js +871 -0
  23. package/kernel/plugin.js +6 -58
  24. package/kernel/plugin_sources.js +316 -0
  25. package/kernel/resource_usage/gpu.js +349 -0
  26. package/kernel/resource_usage/index.js +322 -0
  27. package/kernel/resource_usage/macos_footprint.js +197 -0
  28. package/kernel/resource_usage/preferences.js +92 -0
  29. package/kernel/resource_usage/process_tree.js +303 -0
  30. package/kernel/scripts/git/create +4 -4
  31. package/kernel/scripts/git/fork +7 -8
  32. package/kernel/shell.js +23 -2
  33. package/kernel/shells.js +41 -0
  34. package/kernel/sysinfo.js +62 -9
  35. package/kernel/util.js +60 -0
  36. package/package.json +1 -1
  37. package/server/index.js +984 -156
  38. package/server/lib/app_log_report.js +543 -0
  39. package/server/lib/content_validation.js +55 -33
  40. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  41. package/server/lib/terminal_session_helpers.js +0 -3
  42. package/server/public/common.js +77 -31
  43. package/server/public/create-launcher.js +4 -32
  44. package/server/public/logs.js +1428 -0
  45. package/server/public/nav.js +7 -0
  46. package/server/public/plugin-detail.js +93 -10
  47. package/server/public/privacy_filter_worker.js +391 -0
  48. package/server/public/style.css +1104 -154
  49. package/server/public/task-launcher.js +8 -29
  50. package/server/public/universal-launcher.css +8 -6
  51. package/server/public/universal-launcher.js +3 -27
  52. package/server/routes/apps.js +195 -1
  53. package/server/views/app.ejs +3041 -717
  54. package/server/views/autolaunch.ejs +917 -0
  55. package/server/views/bootstrap.ejs +7 -1
  56. package/server/views/d.ejs +408 -65
  57. package/server/views/editor.ejs +85 -19
  58. package/server/views/index.ejs +661 -111
  59. package/server/views/init/index.ejs +1 -1
  60. package/server/views/install.ejs +1 -1
  61. package/server/views/logs.ejs +164 -86
  62. package/server/views/net.ejs +7 -1
  63. package/server/views/partials/d_terminal_column.ejs +2 -2
  64. package/server/views/partials/d_terminal_options.ejs +0 -8
  65. package/server/views/partials/fs_status.ejs +47 -0
  66. package/server/views/partials/home_action_modal.ejs +86 -0
  67. package/server/views/partials/home_run_menu.ejs +87 -0
  68. package/server/views/partials/main_sidebar.ejs +2 -0
  69. package/server/views/partials/menu.ejs +1 -1
  70. package/server/views/plugin_detail.ejs +19 -4
  71. package/server/views/plugins.ejs +201 -3
  72. package/server/views/pre.ejs +1 -1
  73. package/server/views/pro.ejs +1 -1
  74. package/server/views/shell.ejs +40 -18
  75. package/server/views/skills.ejs +506 -0
  76. package/server/views/terminal.ejs +45 -19
  77. package/spec/INSTRUCTION_SYNC.md +20 -10
  78. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  79. package/system/plugin/antigravity-cli/common.js +155 -0
  80. package/system/plugin/antigravity-cli/install.js +272 -0
  81. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  82. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  83. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  84. package/system/plugin/claude/claude.png +0 -0
  85. package/system/plugin/claude/pinokio.js +47 -0
  86. package/system/plugin/claude-auto/claude.png +0 -0
  87. package/system/plugin/claude-auto/pinokio.js +58 -0
  88. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  89. package/system/plugin/claude-desktop/pinokio.js +23 -0
  90. package/system/plugin/codex/openai.webp +0 -0
  91. package/system/plugin/codex/pinokio.js +42 -0
  92. package/system/plugin/codex-auto/openai.webp +0 -0
  93. package/system/plugin/codex-auto/pinokio.js +49 -0
  94. package/system/plugin/codex-desktop/icon.png +0 -0
  95. package/system/plugin/codex-desktop/pinokio.js +23 -0
  96. package/system/plugin/crush/crush.png +0 -0
  97. package/system/plugin/crush/pinokio.js +15 -0
  98. package/system/plugin/cursor/cursor.jpeg +0 -0
  99. package/system/plugin/cursor/pinokio.js +23 -0
  100. package/system/plugin/qwen/pinokio.js +34 -0
  101. package/system/plugin/qwen/qwen.png +0 -0
  102. package/system/plugin/vscode/pinokio.js +20 -0
  103. package/system/plugin/vscode/vscode.png +0 -0
  104. package/system/plugin/windsurf/pinokio.js +23 -0
  105. package/system/plugin/windsurf/windsurf.png +0 -0
  106. package/test/antigravity-cli-plugin.test.js +185 -0
  107. package/test/app-api.test.js +239 -0
  108. package/test/app-log-report.test.js +67 -0
  109. package/test/environment-cache-preflight.test.js +98 -0
  110. package/test/git-bin.test.js +59 -0
  111. package/test/git-defaults.test.js +97 -0
  112. package/test/github-api.test.js +158 -0
  113. package/test/github-connection.test.js +117 -0
  114. package/test/huggingface-bin.test.js +25 -0
  115. package/test/managed-skills.test.js +351 -0
  116. package/test/plugin-action-functions.test.js +337 -0
  117. package/test/plugin-dev-iframe.test.js +17 -0
  118. package/test/plugin-sources.test.js +203 -0
  119. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  120. package/test/process-wait.test.js +169 -0
  121. package/test/script-api.test.js +97 -0
  122. package/test/shell-api.test.js +134 -0
  123. package/test/shell-run-template.test.js +209 -0
  124. package/test/storage-api.test.js +137 -0
  125. package/test/uri-api.test.js +100 -0
@@ -159,7 +159,13 @@ document.addEventListener("DOMContentLoaded", () => {
159
159
  }
160
160
  const fitAddon = new FitAddon.FitAddon();
161
161
  term.loadAddon(fitAddon);
162
- term.loadAddon(new WebLinksAddon.WebLinksAddon());
162
+ <% if (agent === "electron") { %>
163
+ term.loadAddon(new WebLinksAddon.WebLinksAddon((event, uri) => {
164
+ window.open(uri, "_blank", "browser")
165
+ }));
166
+ <% } else { %>
167
+ term.loadAddon(new WebLinksAddon.WebLinksAddon());
168
+ <% } %>
163
169
  term.open(document.querySelector("#terminal"))
164
170
  const terminalContainer = document.querySelector("#terminal")
165
171
  window.PinokioTouch.bindTerminalFocus(term, terminalContainer)
@@ -161,8 +161,7 @@ body.dark .tab:hover .disclosure-indicator,
161
161
  body.dark .tab:focus-visible .disclosure-indicator {
162
162
  color: rgba(255, 255, 255, 0.9);
163
163
  }
164
- .tab-action-link,
165
- .ai-perm-link {
164
+ .tab-action-link {
166
165
  border: none;
167
166
  background: none;
168
167
  display: inline-flex;
@@ -177,20 +176,15 @@ body.dark .tab:focus-visible .disclosure-indicator {
177
176
  flex: 0 0 auto;
178
177
  }
179
178
  .tab-action-link:hover,
180
- .tab-action-link:focus-visible,
181
- .ai-perm-link:hover,
182
- .ai-perm-link:focus-visible {
179
+ .tab-action-link:focus-visible {
183
180
  background: rgba(0, 0, 0, 0.08);
184
181
  color: rgba(0, 0, 0, 0.85);
185
182
  }
186
- body.dark .tab-action-link,
187
- body.dark .ai-perm-link {
183
+ body.dark .tab-action-link {
188
184
  color: rgba(255, 255, 255, 0.75);
189
185
  }
190
186
  body.dark .tab-action-link:hover,
191
- body.dark .tab-action-link:focus-visible,
192
- body.dark .ai-perm-link:hover,
193
- body.dark .ai-perm-link:focus-visible {
187
+ body.dark .tab-action-link:focus-visible {
194
188
  background: rgba(255, 255, 255, 0.12);
195
189
  color: rgba(255, 255, 255, 0.95);
196
190
  }
@@ -532,6 +526,316 @@ body.dark #update-spec {
532
526
  background: rgba(0,0,0,0.8);
533
527
  color: white;
534
528
  }
529
+
530
+ /* Compact one-column tool launcher. Keep the existing .tab/data-* contracts intact. */
531
+ main {
532
+ padding: 0 16px 32px;
533
+ }
534
+
535
+ form.search {
536
+ width: 100%;
537
+ max-width: 1040px;
538
+ margin: 14px auto;
539
+ padding: 0;
540
+ background: transparent;
541
+ }
542
+
543
+ body.dark form.search {
544
+ background: transparent;
545
+ }
546
+
547
+ form.search input[type=search] {
548
+ width: 100%;
549
+ min-height: 42px;
550
+ box-sizing: border-box;
551
+ padding: 0 14px;
552
+ color: rgba(0, 0, 0, 0.88);
553
+ background: white;
554
+ border: 1px solid rgba(0, 0, 0, 0.08);
555
+ border-radius: 8px;
556
+ outline: none;
557
+ }
558
+
559
+ form.search input[type=search]:focus {
560
+ border-color: rgba(29, 78, 216, 0.45);
561
+ box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.1);
562
+ }
563
+
564
+ body.dark form.search input[type=search] {
565
+ color: white;
566
+ background: rgba(255, 255, 255, 0.055);
567
+ border-color: rgba(255, 255, 255, 0.1);
568
+ }
569
+
570
+ .menu-grid,
571
+ .menu-grid.menu-grid--triptych {
572
+ display: flex;
573
+ flex-direction: column;
574
+ grid-template-columns: none;
575
+ gap: 14px;
576
+ width: 100%;
577
+ max-width: 1040px;
578
+ margin: 0 auto;
579
+ }
580
+
581
+ main > .menu-container {
582
+ max-width: 1040px;
583
+ margin: 14px auto 0;
584
+ }
585
+
586
+ .menu-container,
587
+ .menu-column {
588
+ width: 100%;
589
+ min-width: 0;
590
+ box-sizing: border-box;
591
+ overflow: hidden;
592
+ background: rgba(255, 255, 255, 0.96);
593
+ border: 1px solid rgba(0, 0, 0, 0.08);
594
+ border-radius: 8px;
595
+ }
596
+
597
+ body.dark .menu-container,
598
+ body.dark .menu-column {
599
+ background: rgba(255, 255, 255, 0.035);
600
+ border-color: rgba(255, 255, 255, 0.1);
601
+ }
602
+
603
+ .tab-header,
604
+ .menu-column .tab-header,
605
+ body.dark .tab-header,
606
+ body.dark .menu-column .tab-header {
607
+ padding: 14px 16px 8px;
608
+ background: transparent;
609
+ font-weight: 400;
610
+ }
611
+
612
+ .tab-header h3 {
613
+ display: flex;
614
+ align-items: center;
615
+ gap: 8px;
616
+ margin: 0;
617
+ color: rgba(0, 0, 0, 0.52);
618
+ font-size: 12px;
619
+ font-weight: 500;
620
+ letter-spacing: 0.04em;
621
+ text-transform: uppercase;
622
+ opacity: 1;
623
+ }
624
+
625
+ .tab-header h3 i {
626
+ color: rgba(0, 0, 0, 0.44);
627
+ }
628
+
629
+ body.dark .tab-header h3 {
630
+ color: rgba(255, 255, 255, 0.62);
631
+ }
632
+
633
+ body.dark .tab-header h3 i {
634
+ color: rgba(255, 255, 255, 0.52);
635
+ }
636
+
637
+ .column-subtitle {
638
+ min-height: 0;
639
+ padding: 0 16px 12px;
640
+ color: rgba(0, 0, 0, 0.58);
641
+ font-size: 12px;
642
+ line-height: 1.35;
643
+ opacity: 1;
644
+ }
645
+
646
+ body.dark .column-subtitle {
647
+ color: rgba(255, 255, 255, 0.72);
648
+ }
649
+
650
+ .tab-content,
651
+ .menu-column .tab-content {
652
+ display: flex;
653
+ flex-direction: column;
654
+ flex-wrap: nowrap;
655
+ gap: 0;
656
+ min-width: 0;
657
+ padding-top: 0;
658
+ }
659
+
660
+ .tab {
661
+ min-height: 40px;
662
+ gap: 10px;
663
+ padding: 5px 12px;
664
+ font-size: 13px;
665
+ font-weight: 400;
666
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
667
+ transition: background 0.16s ease, color 0.16s ease;
668
+ }
669
+
670
+ .tab:hover,
671
+ .tab:focus-visible {
672
+ background: rgba(0, 0, 0, 0.035);
673
+ }
674
+
675
+ .tab:focus-visible {
676
+ outline: 2px solid rgba(29, 78, 216, 0.75);
677
+ outline-offset: -2px;
678
+ }
679
+
680
+ .tab.selected {
681
+ background: rgba(0, 0, 0, 0.045);
682
+ }
683
+
684
+ body.dark .tab {
685
+ background: transparent;
686
+ border-top-color: rgba(255, 255, 255, 0.08);
687
+ }
688
+
689
+ body.dark .tab:hover,
690
+ body.dark .tab:focus-visible,
691
+ body.dark .tab.selected {
692
+ background: rgba(255, 255, 255, 0.07);
693
+ }
694
+
695
+ .tab h2 {
696
+ font-size: 13px;
697
+ font-weight: 450;
698
+ }
699
+
700
+ .tab-title {
701
+ line-height: 1.2;
702
+ }
703
+
704
+ .tab-subtitle {
705
+ line-height: 1.2;
706
+ }
707
+
708
+ .tab-copy {
709
+ gap: 0;
710
+ }
711
+
712
+ .subtitle {
713
+ color: rgba(0, 0, 0, 0.54);
714
+ font-size: 11px;
715
+ opacity: 1;
716
+ }
717
+
718
+ body.dark .subtitle {
719
+ color: rgba(255, 255, 255, 0.58);
720
+ }
721
+
722
+ .tab i.img,
723
+ .tab img {
724
+ flex: 0 0 26px;
725
+ width: 26px;
726
+ height: 26px;
727
+ border-radius: 6px;
728
+ border: 1px solid rgba(0, 0, 0, 0.08);
729
+ }
730
+
731
+ .tab i.img {
732
+ font-size: 13px;
733
+ }
734
+
735
+ .tab-actions {
736
+ opacity: 0.68;
737
+ transition: opacity 0.16s ease;
738
+ }
739
+
740
+ .tab:hover .tab-actions,
741
+ .tab:focus-visible .tab-actions,
742
+ .tab.is-starting .tab-actions {
743
+ opacity: 1;
744
+ }
745
+
746
+ .tab-action-link {
747
+ width: 24px;
748
+ height: 24px;
749
+ border-radius: 6px;
750
+ }
751
+
752
+ .tab .disclosure-indicator {
753
+ width: 20px;
754
+ height: 20px;
755
+ color: rgba(0, 0, 0, 0.34);
756
+ }
757
+
758
+ .tab:hover .disclosure-indicator,
759
+ .tab:focus-visible .disclosure-indicator {
760
+ color: rgba(0, 0, 0, 0.66);
761
+ }
762
+
763
+ .terminal-shell-group {
764
+ gap: 0;
765
+ }
766
+
767
+ .terminal-shell-heading {
768
+ gap: 8px;
769
+ padding: 8px 14px 6px;
770
+ color: rgba(0, 0, 0, 0.48);
771
+ }
772
+
773
+ body.dark .terminal-shell-heading {
774
+ color: rgba(255, 255, 255, 0.62);
775
+ }
776
+
777
+ .terminal-shell-heading i.img,
778
+ .terminal-shell-heading img {
779
+ flex: 0 0 22px;
780
+ width: 22px;
781
+ height: 22px;
782
+ border-radius: 6px;
783
+ border: 1px solid rgba(0, 0, 0, 0.08);
784
+ }
785
+
786
+ .terminal-shell-heading i.img {
787
+ font-size: 12px;
788
+ }
789
+
790
+ .terminal-shell-heading .tab-title {
791
+ font-size: 12px;
792
+ font-weight: 500;
793
+ }
794
+
795
+ .terminal-shell-heading .tab-subtitle {
796
+ display: none;
797
+ }
798
+
799
+ .user-terminal--single .terminal-shell-heading {
800
+ display: none;
801
+ }
802
+
803
+ .terminal-shell-options {
804
+ gap: 0;
805
+ margin-left: 0;
806
+ padding-left: 0;
807
+ border-left: none;
808
+ }
809
+
810
+ body.dark .terminal-shell-options {
811
+ border-left: none;
812
+ }
813
+
814
+ .terminal-option-tab {
815
+ padding-left: 14px;
816
+ }
817
+
818
+ @media (max-width: 700px) {
819
+ main {
820
+ padding: 0 10px 24px;
821
+ }
822
+
823
+ .tab {
824
+ min-height: 40px;
825
+ padding-inline: 10px;
826
+ }
827
+
828
+ .tab i.img,
829
+ .tab img {
830
+ flex-basis: 26px;
831
+ width: 26px;
832
+ height: 26px;
833
+ }
834
+
835
+ .column-subtitle {
836
+ padding-inline: 14px;
837
+ }
838
+ }
535
839
  </style>
536
840
  <script src="/hotkeys.min.js"></script>
537
841
  <script src="/sweetalert2.js"></script>
@@ -548,7 +852,7 @@ body.dark #update-spec {
548
852
  <header>
549
853
  <h1>Build</h1>
550
854
  <div class='flexible'></div>
551
- <a class='btn' href="https://github.com/pinokiocomputer/code/issues" target="_blank"><i class="fa-solid fa-up-right-from-square"></i> Request a tool</a>
855
+ <a class='btn' href="/plugins" target="_blank"><i class="fa-solid fa-up-right-from-square"></i> Request a tool</a>
552
856
  <a class='btn' id='git-pull'><i class="fa-solid fa-rotate"></i> Check for new tools</a>
553
857
  <div class='pulling hidden'>
554
858
  <div class='loading'>
@@ -615,13 +919,8 @@ body.dark #update-spec {
615
919
  <% } %>
616
920
  </div>
617
921
  <div class='tab-actions'>
618
- <% if (i.link) { %>
619
- <button class="tab-action-link" type="button" data-doc-link="true" data-href="<%= i.link %>" title="Open docs" data-tippy-content="Open docs" aria-label="Open docs">
620
- <i class="fa-solid fa-circle-info"></i>
621
- </button>
622
- <% } %>
623
- <button class="ai-perm-link" type="button" data-ai-consent="<%= i.href %>" title="AI permissions" data-tippy-content="AI permissions" aria-label="AI permissions">
624
- <i class="fa-solid fa-shield-halved"></i>
922
+ <button class="tab-action-link" type="button" data-plugin-settings="true" data-href="<%= i.href %>" title="Manage plugin" data-tippy-content="Manage plugin" aria-label="Manage plugin">
923
+ <i class="fa-solid fa-gear"></i>
625
924
  </button>
626
925
  <div class='disclosure-indicator' aria-hidden="true">
627
926
  <i class="fa-solid fa-chevron-right"></i>
@@ -663,13 +962,8 @@ body.dark #update-spec {
663
962
  <% } %>
664
963
  </div>
665
964
  <div class='tab-actions'>
666
- <% if (i.link) { %>
667
- <button class="tab-action-link" type="button" data-doc-link="true" data-href="<%= i.link %>" title="Open docs" data-tippy-content="Open docs" aria-label="Open docs">
668
- <i class="fa-solid fa-circle-info"></i>
669
- </button>
670
- <% } %>
671
- <button class="ai-perm-link" type="button" data-ai-consent="<%= i.href %>" title="AI permissions" data-tippy-content="AI permissions" aria-label="AI permissions">
672
- <i class="fa-solid fa-shield-halved"></i>
965
+ <button class="tab-action-link" type="button" data-plugin-settings="true" data-href="<%= i.href %>" title="Manage plugin" data-tippy-content="Manage plugin" aria-label="Manage plugin">
966
+ <i class="fa-solid fa-gear"></i>
673
967
  </button>
674
968
  <div class='disclosure-indicator' aria-hidden="true">
675
969
  <i class="fa-solid fa-chevron-right"></i>
@@ -703,13 +997,8 @@ body.dark #update-spec {
703
997
  <% } %>
704
998
  </div>
705
999
  <div class='tab-actions'>
706
- <% if (i.link) { %>
707
- <button class="tab-action-link" type="button" data-doc-link="true" data-href="<%= i.link %>" title="Open docs" data-tippy-content="Open docs" aria-label="Open docs">
708
- <i class="fa-solid fa-circle-info"></i>
709
- </button>
710
- <% } %>
711
- <button class="ai-perm-link" type="button" data-ai-consent="<%= i.href %>" title="AI permissions" data-tippy-content="AI permissions" aria-label="AI permissions">
712
- <i class="fa-solid fa-shield-halved"></i>
1000
+ <button class="tab-action-link" type="button" data-plugin-settings="true" data-href="<%= i.href %>" title="Manage plugin" data-tippy-content="Manage plugin" aria-label="Manage plugin">
1001
+ <i class="fa-solid fa-gear"></i>
713
1002
  </button>
714
1003
  <div class='disclosure-indicator' aria-hidden="true">
715
1004
  <i class="fa-solid fa-chevron-right"></i>
@@ -923,6 +1212,7 @@ const appendWorkspaceCwd = (value, cwd = workspaceCwd) => {
923
1212
  try {
924
1213
  const parsed = new URL(raw, window.location.origin)
925
1214
  const isPluginLauncher = parsed.pathname.startsWith("/run/plugin/")
1215
+ || parsed.pathname.startsWith("/pinokio/run/plugin/")
926
1216
  || (parsed.pathname.startsWith("/run/api/") && /\/pinokio\.js$/i.test(parsed.pathname))
927
1217
  if (parsed.origin !== window.location.origin || !isPluginLauncher) {
928
1218
  return value
@@ -944,27 +1234,79 @@ const appendSessionParam = (value, session) => {
944
1234
  const delimiter = value.includes('?') ? '&' : '?'
945
1235
  return `${value}${delimiter}session=${session}`
946
1236
  }
947
- const normalizeConsentSource = (value) => {
948
- if (!value || typeof value !== "string") return ""
949
- return value.trim().replace(/\\/g, "/").replace(/\/+$/, "")
1237
+ const pluginInstallPreflightCache = new Map()
1238
+ const normalizePluginPathFromLaunchHref = (rawHref) => {
1239
+ if (!rawHref || typeof rawHref !== "string") {
1240
+ return ""
1241
+ }
1242
+ try {
1243
+ const parsed = new URL(rawHref, window.location.origin)
1244
+ if (parsed.origin !== window.location.origin) {
1245
+ return ""
1246
+ }
1247
+ if (parsed.pathname.startsWith("/pinokio/run/plugin/")) {
1248
+ return parsed.pathname
1249
+ }
1250
+ if (parsed.pathname.startsWith("/run/plugin/") || (parsed.pathname.startsWith("/run/api/") && /\/pinokio\.js$/i.test(parsed.pathname))) {
1251
+ return parsed.pathname.replace(/^\/run/, "")
1252
+ }
1253
+ } catch (_) {}
1254
+ return ""
950
1255
  }
951
- const revokeConsentForHref = (href) => {
1256
+ const pluginInstallHrefFromState = (state, fallbackPath) => {
1257
+ const detailUrl = state && typeof state.detailUrl === "string" && state.detailUrl
1258
+ ? state.detailUrl
1259
+ : `/plugin?path=${encodeURIComponent(fallbackPath)}`
952
1260
  try {
953
- if (!href || typeof href !== "string") return { ok: false }
954
- const store = window.localStorage
955
- if (!store) return { ok: false }
956
- const url = new URL(href, window.location.origin)
957
- const cwd = url.searchParams.get("cwd")
958
- if (!cwd) return { ok: false }
959
- const folder = normalizeConsentSource(cwd)
960
- if (!folder) return { ok: false }
961
- const key = `pinokio:ai-consent:${encodeURIComponent(folder)}`
962
- const existed = store.getItem(key) !== null
963
- store.removeItem(key)
964
- return { ok: true, folder, existed }
1261
+ const parsed = new URL(detailUrl, window.location.origin)
1262
+ parsed.searchParams.set("next", "install")
1263
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
965
1264
  } catch (_) {
966
- return { ok: false }
1265
+ const separator = detailUrl.includes("?") ? "&" : "?"
1266
+ return `${detailUrl}${separator}next=install`
1267
+ }
1268
+ }
1269
+ const getPluginInstallRedirect = async (rawHref) => {
1270
+ const pluginPath = normalizePluginPathFromLaunchHref(rawHref)
1271
+ if (!pluginPath) {
1272
+ return ""
1273
+ }
1274
+ if (pluginInstallPreflightCache.has(pluginPath)) {
1275
+ return pluginInstallPreflightCache.get(pluginPath)
1276
+ }
1277
+ let redirectHref = ""
1278
+ try {
1279
+ const response = await fetch(`/api/plugin/install-state?path=${encodeURIComponent(pluginPath)}`, {
1280
+ cache: "no-store"
1281
+ })
1282
+ const state = await response.json().catch(() => ({}))
1283
+ if (response.ok && state && state.ok && state.hasInstall && state.hasInstalledCheck && state.installed === false) {
1284
+ redirectHref = pluginInstallHrefFromState(state, pluginPath)
1285
+ }
1286
+ } catch (_) {}
1287
+ pluginInstallPreflightCache.set(pluginPath, redirectHref)
1288
+ return redirectHref
1289
+ }
1290
+ const redirectToPluginInstallIfNeeded = async (rawHref) => {
1291
+ const redirectHref = await getPluginInstallRedirect(rawHref)
1292
+ if (!redirectHref) {
1293
+ return false
967
1294
  }
1295
+ window.top.location.href = redirectHref
1296
+ return true
1297
+ }
1298
+ const pluginSettingsHrefFromLaunchHref = (rawHref) => {
1299
+ const launchHref = appendWorkspaceCwd(rawHref || "")
1300
+ const pluginPath = normalizePluginPathFromLaunchHref(launchHref)
1301
+ if (!pluginPath) {
1302
+ return ""
1303
+ }
1304
+ const parsed = new URL("/plugin", window.location.origin)
1305
+ parsed.searchParams.set("path", pluginPath)
1306
+ if (workspaceCwd) {
1307
+ parsed.searchParams.set("cwd", workspaceCwd)
1308
+ }
1309
+ return `${parsed.pathname}${parsed.search}`
968
1310
  }
969
1311
  const START_ICON_HTML = '<i class="fa-solid fa-chevron-right"></i>'
970
1312
  const STARTING_ICON_HTML = '<i class="fa-solid fa-circle-notch fa-spin"></i>'
@@ -1053,7 +1395,7 @@ const toggleTerminalGroup = async (tab) => {
1053
1395
  }
1054
1396
  }
1055
1397
  const pendingLaunchTabs = new Map()
1056
- const launchTab = (tab) => {
1398
+ const launchTab = async (tab) => {
1057
1399
  if (!tab) {
1058
1400
  return false
1059
1401
  }
@@ -1070,11 +1412,16 @@ const launchTab = (tab) => {
1070
1412
  }
1071
1413
 
1072
1414
  const session = generateSession()
1415
+ const href = appendSessionParam(appendWorkspaceCwd(baseHref), session)
1416
+ const target = appendSessionParam(appendWorkspaceCwd(baseTarget), session)
1417
+
1418
+ if (await redirectToPluginInstallIfNeeded(href)) {
1419
+ return true
1420
+ }
1421
+
1073
1422
  pendingLaunchTabs.set(session, tab)
1074
1423
  setTabLaunchState(tab, 'starting')
1075
1424
 
1076
- const href = appendSessionParam(appendWorkspaceCwd(baseHref), session)
1077
- const target = appendSessionParam(appendWorkspaceCwd(baseTarget), session)
1078
1425
  const labelNode = tab.querySelector('.tab-title')
1079
1426
  const label = labelNode ? labelNode.textContent.trim() : ""
1080
1427
 
@@ -1108,18 +1455,14 @@ document.querySelector("form input[type=search]").addEventListener("input", (e)
1108
1455
  renderSearch()
1109
1456
  })
1110
1457
  document.querySelector("form input[type=search]").focus()
1111
- document.addEventListener("click", (e) => {
1112
- const permTarget = e.target.closest('[data-ai-consent]')
1113
- if (permTarget) {
1458
+ document.addEventListener("click", async (e) => {
1459
+ const settingsTarget = e.target.closest('[data-plugin-settings][data-href]')
1460
+ if (settingsTarget) {
1114
1461
  e.preventDefault()
1115
1462
  e.stopPropagation()
1116
- const href = appendWorkspaceCwd(permTarget.getAttribute('data-ai-consent') || '')
1117
- const result = revokeConsentForHref(href)
1118
- if (result && result.ok) {
1119
- const folderLabel = result.folder || 'this folder'
1120
- alert(`Cleared AI permission for ${folderLabel}. You will be prompted again next time.`)
1121
- } else {
1122
- alert('Unable to change AI permission for this item.')
1463
+ const settingsHref = pluginSettingsHrefFromLaunchHref(settingsTarget.getAttribute('data-href') || '')
1464
+ if (settingsHref) {
1465
+ window.top.location.href = settingsHref
1123
1466
  }
1124
1467
  return
1125
1468
  }
@@ -1145,13 +1488,13 @@ document.addEventListener("click", (e) => {
1145
1488
  return
1146
1489
  }
1147
1490
 
1148
- if (launchTab(tab)) {
1491
+ if (await launchTab(tab)) {
1149
1492
  e.preventDefault()
1150
1493
  e.stopPropagation()
1151
1494
  }
1152
1495
  })
1153
1496
 
1154
- document.addEventListener('keydown', (e) => {
1497
+ document.addEventListener('keydown', async (e) => {
1155
1498
  if (!['Enter', ' ', 'Spacebar'].includes(e.key)) {
1156
1499
  return
1157
1500
  }
@@ -1174,7 +1517,7 @@ document.addEventListener('keydown', (e) => {
1174
1517
  return
1175
1518
  }
1176
1519
 
1177
- const launched = launchTab(tab)
1520
+ const launched = await launchTab(tab)
1178
1521
  if (launched) {
1179
1522
  e.preventDefault()
1180
1523
  e.stopPropagation()