pinokiod 8.0.18 → 8.0.20
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.
- package/package.json +1 -1
- package/server/index.js +6 -1
- package/server/public/common.js +26 -1
- package/server/public/logs.js +12 -2
- package/server/views/app.ejs +153 -19
- package/server/views/explore.ejs +2 -2
- package/server/views/partials/home_server_popover.ejs +3 -1
- package/server/views/partials/home_server_popover_assets.ejs +24 -22
- package/system/plugin/opencode/icon.png +0 -0
- package/system/plugin/opencode/pinokio.js +57 -0
- package/system/plugin/opencode-auto/icon.png +0 -0
- package/system/plugin/opencode-auto/pinokio.js +14 -0
- package/test/explore-lan-install-navigation.test.js +145 -0
- package/test/home-server-popover-ui.test.js +109 -0
- package/test/launch-settings-ui.test.js +52 -19
- package/test/opencode-plugin.test.js +45 -0
- package/test/plugin-sources.test.js +1 -1
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -7632,7 +7632,7 @@ class Server {
|
|
|
7632
7632
|
const category = classifyPluginMenuItem(pluginItem)
|
|
7633
7633
|
const title = pluginItem?.title || pluginItem?.text || pluginItem?.name || "Plugin"
|
|
7634
7634
|
const hasInstalledCheck = PluginSources.isInstalledCheck(pluginItem?.installed)
|
|
7635
|
-
const installed =
|
|
7635
|
+
const installed = null
|
|
7636
7636
|
return {
|
|
7637
7637
|
index,
|
|
7638
7638
|
title,
|
|
@@ -8262,6 +8262,11 @@ class Server {
|
|
|
8262
8262
|
res.status(404).send("Plugin not found.")
|
|
8263
8263
|
return
|
|
8264
8264
|
}
|
|
8265
|
+
if (plugin.hasInstalledCheck) {
|
|
8266
|
+
const validation = await contentValidation.validatePluginByPath(plugin.pluginPath)
|
|
8267
|
+
const config = validation.valid && validation.context ? validation.context.config : null
|
|
8268
|
+
plugin.installed = config ? await evaluatePluginInstalled(config) : null
|
|
8269
|
+
}
|
|
8265
8270
|
const [apps, shareState, sidebarContext] = await Promise.all([
|
|
8266
8271
|
collectPluginApps(),
|
|
8267
8272
|
buildPluginShareState(plugin),
|
package/server/public/common.js
CHANGED
|
@@ -3013,7 +3013,15 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
|
|
|
3013
3013
|
return;
|
|
3014
3014
|
}
|
|
3015
3015
|
}
|
|
3016
|
-
|
|
3016
|
+
let frame = document.activeElement;
|
|
3017
|
+
if (
|
|
3018
|
+
(!frame || frame.tagName !== 'IFRAME') &&
|
|
3019
|
+
new URLSearchParams(window.location.search).get('mode') === 'explore' &&
|
|
3020
|
+
event.origin === 'https://pinokio.co'
|
|
3021
|
+
) {
|
|
3022
|
+
const exploreFrame = document.querySelector('main > iframe');
|
|
3023
|
+
if (exploreFrame && event.source === exploreFrame.contentWindow) frame = exploreFrame;
|
|
3024
|
+
}
|
|
3017
3025
|
if (!frame || frame.tagName !== 'IFRAME') {
|
|
3018
3026
|
try {
|
|
3019
3027
|
console.warn('[pinokio:navigate] no active iframe');
|
|
@@ -3029,6 +3037,23 @@ if (typeof window !== 'undefined' && !window.__pinokioNavigateListenerInstalled)
|
|
|
3029
3037
|
} catch (_) {}
|
|
3030
3038
|
return;
|
|
3031
3039
|
}
|
|
3040
|
+
const localInstall =
|
|
3041
|
+
target.origin === 'http://localhost:42000' &&
|
|
3042
|
+
(
|
|
3043
|
+
(target.pathname === '/pinokio/download' && Boolean(target.searchParams.get('uri'))) ||
|
|
3044
|
+
(
|
|
3045
|
+
target.pathname === '/checkpoints' &&
|
|
3046
|
+
Boolean(target.searchParams.get('registry')) &&
|
|
3047
|
+
/^(?:sha256:)?[0-9a-f]{64}$/i.test(target.searchParams.get('hash') || '')
|
|
3048
|
+
)
|
|
3049
|
+
);
|
|
3050
|
+
const lanExplore =
|
|
3051
|
+
new URLSearchParams(window.location.search).get('mode') === 'explore' &&
|
|
3052
|
+
!['localhost', '127.0.0.1', '::1', '[::1]'].includes(window.location.hostname) &&
|
|
3053
|
+
event.origin === 'https://pinokio.co' && event.source === frame.contentWindow;
|
|
3054
|
+
if (lanExplore && localInstall) {
|
|
3055
|
+
target = new URL(`${target.pathname}${target.search}${target.hash}`, window.location.origin);
|
|
3056
|
+
}
|
|
3032
3057
|
if (target.origin !== window.location.origin) {
|
|
3033
3058
|
try {
|
|
3034
3059
|
console.warn('[pinokio:navigate] blocked origin', target.origin);
|
package/server/public/logs.js
CHANGED
|
@@ -590,7 +590,17 @@
|
|
|
590
590
|
return `${detailUrl}${separator}next=install`
|
|
591
591
|
}
|
|
592
592
|
}
|
|
593
|
-
redirectToPluginInstallIfNeeded(tool) {
|
|
593
|
+
async redirectToPluginInstallIfNeeded(tool) {
|
|
594
|
+
if (tool && tool.hasInstall === true && tool.hasInstalledCheck === true && typeof tool.installed !== 'boolean') {
|
|
595
|
+
const pluginPath = typeof tool.pluginPath === 'string' ? tool.pluginPath.trim() : ''
|
|
596
|
+
if (pluginPath) {
|
|
597
|
+
try {
|
|
598
|
+
const response = await fetch(`/api/plugin/install-state?path=${encodeURIComponent(pluginPath)}`)
|
|
599
|
+
const state = response.ok ? await response.json() : null
|
|
600
|
+
tool.installed = state && typeof state.installed === 'boolean' ? state.installed : null
|
|
601
|
+
} catch (_) {}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
594
604
|
const href = this.pluginInstallHref(tool)
|
|
595
605
|
if (!href) {
|
|
596
606
|
return false
|
|
@@ -992,7 +1002,7 @@
|
|
|
992
1002
|
return
|
|
993
1003
|
}
|
|
994
1004
|
error.textContent = ''
|
|
995
|
-
if (this.redirectToPluginInstallIfNeeded(tool)) {
|
|
1005
|
+
if (await this.redirectToPluginInstallIfNeeded(tool)) {
|
|
996
1006
|
close()
|
|
997
1007
|
return
|
|
998
1008
|
}
|
package/server/views/app.ejs
CHANGED
|
@@ -544,6 +544,7 @@ body.dark .snapshot-comment {
|
|
|
544
544
|
color: royalblue;
|
|
545
545
|
}
|
|
546
546
|
#sidebar-toggle {
|
|
547
|
+
--sidebar-edge-hint-rgb: 37, 99, 235;
|
|
547
548
|
color: #0f172a;
|
|
548
549
|
position: relative;
|
|
549
550
|
width: 40px;
|
|
@@ -554,6 +555,7 @@ body.dark .snapshot-comment {
|
|
|
554
555
|
transition: background-color 140ms cubic-bezier(0.25, 1, 0.5, 1), box-shadow 140ms cubic-bezier(0.25, 1, 0.5, 1), color 140ms cubic-bezier(0.25, 1, 0.5, 1);
|
|
555
556
|
}
|
|
556
557
|
body.dark #sidebar-toggle {
|
|
558
|
+
--sidebar-edge-hint-rgb: 96, 165, 250;
|
|
557
559
|
color: rgba(248, 250, 252, 0.92);
|
|
558
560
|
}
|
|
559
561
|
#sidebar-toggle[aria-expanded="true"] {
|
|
@@ -570,6 +572,32 @@ body.dark #sidebar-toggle[aria-expanded="true"] {
|
|
|
570
572
|
display: inline-flex;
|
|
571
573
|
align-items: center;
|
|
572
574
|
justify-content: center;
|
|
575
|
+
transition: filter 140ms cubic-bezier(0.25, 1, 0.5, 1), transform 140ms cubic-bezier(0.25, 1, 0.5, 1);
|
|
576
|
+
}
|
|
577
|
+
#sidebar-toggle.sidebar-edge-hint {
|
|
578
|
+
color: rgb(30, 64, 175);
|
|
579
|
+
background: rgba(var(--sidebar-edge-hint-rgb), 0.09);
|
|
580
|
+
box-shadow:
|
|
581
|
+
0 0 0 1px rgba(var(--sidebar-edge-hint-rgb), 0.42),
|
|
582
|
+
0 0 8px rgba(var(--sidebar-edge-hint-rgb), 0.28);
|
|
583
|
+
}
|
|
584
|
+
body.dark #sidebar-toggle.sidebar-edge-hint {
|
|
585
|
+
color: rgb(219, 234, 254);
|
|
586
|
+
background: rgba(var(--sidebar-edge-hint-rgb), 0.12);
|
|
587
|
+
box-shadow:
|
|
588
|
+
0 0 0 1px rgba(147, 197, 253, 0.55),
|
|
589
|
+
0 0 10px rgba(var(--sidebar-edge-hint-rgb), 0.38);
|
|
590
|
+
}
|
|
591
|
+
#sidebar-toggle.sidebar-edge-hint .sidebar-toggle-glyph {
|
|
592
|
+
transform: scale(1.03);
|
|
593
|
+
filter:
|
|
594
|
+
drop-shadow(0 0 1px rgba(var(--sidebar-edge-hint-rgb), 0.82))
|
|
595
|
+
drop-shadow(0 0 5px rgba(var(--sidebar-edge-hint-rgb), 0.55));
|
|
596
|
+
}
|
|
597
|
+
@media (prefers-reduced-motion: reduce) {
|
|
598
|
+
.sidebar-toggle-glyph {
|
|
599
|
+
transition: none;
|
|
600
|
+
}
|
|
573
601
|
}
|
|
574
602
|
.sidebar-toggle-frame {
|
|
575
603
|
position: relative;
|
|
@@ -2664,6 +2692,65 @@ body.dark .appcanvas.panel-open .menu-actions #ask-ai-tab.selected {
|
|
|
2664
2692
|
body.dark .appcanvas.vertical .menu-actions {
|
|
2665
2693
|
border-top: none;
|
|
2666
2694
|
}
|
|
2695
|
+
.appcanvas.vertical .sidebar-hover-preference {
|
|
2696
|
+
width: 100%;
|
|
2697
|
+
padding-top: 5px;
|
|
2698
|
+
border-top: 1px solid var(--pinokio-sidebar-separator);
|
|
2699
|
+
}
|
|
2700
|
+
.appcanvas.vertical .sidebar-hover-toggle,
|
|
2701
|
+
.appcanvas.vertical .sidebar-hover-toggle .tab {
|
|
2702
|
+
width: 100%;
|
|
2703
|
+
}
|
|
2704
|
+
.sidebar-hover-switch {
|
|
2705
|
+
display: inline-flex;
|
|
2706
|
+
align-items: center;
|
|
2707
|
+
width: 30px;
|
|
2708
|
+
height: 16px;
|
|
2709
|
+
margin-left: auto;
|
|
2710
|
+
padding: 2px;
|
|
2711
|
+
border: 1px solid rgba(100, 116, 139, 0.32);
|
|
2712
|
+
border-radius: 999px;
|
|
2713
|
+
background: rgba(148, 163, 184, 0.24);
|
|
2714
|
+
box-sizing: border-box;
|
|
2715
|
+
flex: 0 0 auto;
|
|
2716
|
+
transition: background 140ms ease, border-color 140ms ease;
|
|
2717
|
+
}
|
|
2718
|
+
.sidebar-hover-switch-thumb {
|
|
2719
|
+
width: 10px;
|
|
2720
|
+
height: 10px;
|
|
2721
|
+
border-radius: 50%;
|
|
2722
|
+
background: rgba(71, 85, 105, 0.82);
|
|
2723
|
+
transform: translateX(0);
|
|
2724
|
+
transition: transform 140ms cubic-bezier(0.25, 1, 0.5, 1), background 140ms ease;
|
|
2725
|
+
}
|
|
2726
|
+
.sidebar-hover-toggle[aria-checked="true"] .sidebar-hover-switch {
|
|
2727
|
+
border-color: rgba(15, 23, 42, 0.68);
|
|
2728
|
+
background: rgba(15, 23, 42, 0.82);
|
|
2729
|
+
}
|
|
2730
|
+
.sidebar-hover-toggle[aria-checked="true"] .sidebar-hover-switch-thumb {
|
|
2731
|
+
background: rgba(248, 250, 252, 0.96);
|
|
2732
|
+
transform: translateX(14px);
|
|
2733
|
+
}
|
|
2734
|
+
body.dark .sidebar-hover-switch {
|
|
2735
|
+
border-color: rgba(148, 163, 184, 0.32);
|
|
2736
|
+
background: rgba(148, 163, 184, 0.18);
|
|
2737
|
+
}
|
|
2738
|
+
body.dark .sidebar-hover-switch-thumb {
|
|
2739
|
+
background: rgba(203, 213, 225, 0.76);
|
|
2740
|
+
}
|
|
2741
|
+
body.dark .sidebar-hover-toggle[aria-checked="true"] .sidebar-hover-switch {
|
|
2742
|
+
border-color: rgba(226, 232, 240, 0.62);
|
|
2743
|
+
background: rgba(226, 232, 240, 0.72);
|
|
2744
|
+
}
|
|
2745
|
+
body.dark .sidebar-hover-toggle[aria-checked="true"] .sidebar-hover-switch-thumb {
|
|
2746
|
+
background: rgba(15, 23, 42, 0.9);
|
|
2747
|
+
}
|
|
2748
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2749
|
+
.sidebar-hover-switch,
|
|
2750
|
+
.sidebar-hover-switch-thumb {
|
|
2751
|
+
transition: none;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2667
2754
|
.appcanvas.vertical > aside .menu-actions .header-item + .header-item {
|
|
2668
2755
|
box-shadow: none !important;
|
|
2669
2756
|
}
|
|
@@ -8346,6 +8433,17 @@ body.dark .pinokio-custom-terminal-header {
|
|
|
8346
8433
|
<% if (type === 'browse') { %>
|
|
8347
8434
|
<%- include('./partials/fs_status') %>
|
|
8348
8435
|
<% } %>
|
|
8436
|
+
<% if (type === 'run') { %>
|
|
8437
|
+
<div class="sidebar-hover-preference">
|
|
8438
|
+
<button type="button" class="btn header-item sidebar-hover-toggle" role="switch" aria-checked="true" data-app-sidebar-show-on-hover data-tippy-content="Show the sidebar when hovering over the left edge">
|
|
8439
|
+
<div class="tab">
|
|
8440
|
+
<i class="fa-solid fa-eye menu-action-leading-icon" aria-hidden="true"></i>
|
|
8441
|
+
<div class="display">Show on hover</div>
|
|
8442
|
+
<span class="sidebar-hover-switch" aria-hidden="true"><span class="sidebar-hover-switch-thumb"></span></span>
|
|
8443
|
+
</div>
|
|
8444
|
+
</button>
|
|
8445
|
+
</div>
|
|
8446
|
+
<% } %>
|
|
8349
8447
|
<button type='button' id='logs-toggle' class="btn header-item" data-tippy-content="Get help with logs" aria-expanded="false" aria-controls="app-logs-drawer">
|
|
8350
8448
|
<div class='tab'>
|
|
8351
8449
|
<i class="fa-solid fa-circle-question menu-action-leading-icon"></i>
|
|
@@ -11463,7 +11561,7 @@ const rerenderMenuSection = (container, html) => {
|
|
|
11463
11561
|
: "Protection beta is off for this app. You can change it later from the Run tab."
|
|
11464
11562
|
})
|
|
11465
11563
|
}
|
|
11466
|
-
maybeShowProtectionInstallToast()
|
|
11564
|
+
// maybeShowProtectionInstallToast()
|
|
11467
11565
|
const buildPluginLaunchFromQuery = () => {
|
|
11468
11566
|
try {
|
|
11469
11567
|
const pageUrl = new URL(window.location.href)
|
|
@@ -16498,6 +16596,7 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16498
16596
|
const aside = document.getElementById("app-sidebar")
|
|
16499
16597
|
const toggle = document.getElementById("sidebar-toggle")
|
|
16500
16598
|
const peekTrigger = document.querySelector("[data-app-sidebar-peek-trigger]")
|
|
16599
|
+
const showOnHoverToggle = document.querySelector("[data-app-sidebar-show-on-hover]")
|
|
16501
16600
|
if (!appcanvas || !aside || !toggle) {
|
|
16502
16601
|
if (toggle) {
|
|
16503
16602
|
toggle.classList.add("hidden")
|
|
@@ -16509,6 +16608,21 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16509
16608
|
const storageKey = workspaceName
|
|
16510
16609
|
? `pinokio.sidebar-collapsed:${workspaceName}`
|
|
16511
16610
|
: `pinokio.sidebar-collapsed:${location.pathname}`
|
|
16611
|
+
const showOnHoverStorageKey = "pinokio.sidebar-show-on-hover"
|
|
16612
|
+
const legacyAutoShowStorageKey = "pinokio.sidebar-auto-show"
|
|
16613
|
+
let showOnHoverEnabled = true
|
|
16614
|
+
try {
|
|
16615
|
+
const storedShowOnHover = window.localStorage.getItem(showOnHoverStorageKey)
|
|
16616
|
+
const storedPreference = storedShowOnHover === null
|
|
16617
|
+
? window.localStorage.getItem(legacyAutoShowStorageKey)
|
|
16618
|
+
: storedShowOnHover
|
|
16619
|
+
showOnHoverEnabled = storedPreference !== "0"
|
|
16620
|
+
} catch (_) {}
|
|
16621
|
+
|
|
16622
|
+
const syncShowOnHoverToggle = () => {
|
|
16623
|
+
if (!showOnHoverToggle) return
|
|
16624
|
+
showOnHoverToggle.setAttribute("aria-checked", showOnHoverEnabled ? "true" : "false")
|
|
16625
|
+
}
|
|
16512
16626
|
|
|
16513
16627
|
const updateToggle = (collapsed) => {
|
|
16514
16628
|
const expanded = !collapsed
|
|
@@ -16524,20 +16638,10 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16524
16638
|
}
|
|
16525
16639
|
|
|
16526
16640
|
const desktopPeekMq = window.matchMedia("(min-width: 769px) and (hover: hover) and (pointer: fine)")
|
|
16527
|
-
const shouldAutoPeekOnIdle = <%- JSON.stringify(type === "run") %>
|
|
16528
16641
|
let peekCloseTimer = 0
|
|
16529
16642
|
|
|
16530
16643
|
const isCollapsed = () => appcanvas.classList.contains("sidebar-collapsed")
|
|
16531
16644
|
const isPeeking = () => appcanvas.classList.contains("sidebar-peeking")
|
|
16532
|
-
const hasVisibleBrowserSurface = () => {
|
|
16533
|
-
const browserview = document.querySelector("main.browserview")
|
|
16534
|
-
if (!browserview) return false
|
|
16535
|
-
if (browserview.querySelector("iframe:not(.hidden)")) return true
|
|
16536
|
-
const networkStatus = document.getElementById("browserview-network-status")
|
|
16537
|
-
if (networkStatus && !networkStatus.hidden) return true
|
|
16538
|
-
const launchStatus = document.querySelector("[data-launch-requirements-status]")
|
|
16539
|
-
return Boolean(launchStatus && !launchStatus.hidden)
|
|
16540
|
-
}
|
|
16541
16645
|
const syncSidebarVisibility = () => {
|
|
16542
16646
|
const peeking = isPeeking()
|
|
16543
16647
|
aside.setAttribute("aria-hidden", isCollapsed() && !peeking ? "true" : "false")
|
|
@@ -16549,9 +16653,24 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16549
16653
|
const setPeeking = (peeking) => {
|
|
16550
16654
|
const next = !!peeking && canPeek()
|
|
16551
16655
|
window.clearTimeout(peekCloseTimer)
|
|
16656
|
+
if (next) {
|
|
16657
|
+
toggle.classList.remove("sidebar-edge-hint")
|
|
16658
|
+
}
|
|
16552
16659
|
appcanvas.classList.toggle("sidebar-peeking", next)
|
|
16553
16660
|
syncSidebarVisibility()
|
|
16554
16661
|
}
|
|
16662
|
+
const setShowOnHoverEnabled = (enabled, options = {}) => {
|
|
16663
|
+
showOnHoverEnabled = !!enabled
|
|
16664
|
+
syncShowOnHoverToggle()
|
|
16665
|
+
if (showOnHoverEnabled) {
|
|
16666
|
+
toggle.classList.remove("sidebar-edge-hint")
|
|
16667
|
+
}
|
|
16668
|
+
if (options.persist !== false) {
|
|
16669
|
+
try {
|
|
16670
|
+
window.localStorage.setItem(showOnHoverStorageKey, showOnHoverEnabled ? "1" : "0")
|
|
16671
|
+
} catch (_) {}
|
|
16672
|
+
}
|
|
16673
|
+
}
|
|
16555
16674
|
const shouldKeepPeekOpen = () => {
|
|
16556
16675
|
const active = document.activeElement
|
|
16557
16676
|
return (
|
|
@@ -16592,13 +16711,9 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16592
16711
|
initialCollapsed = window.localStorage.getItem(storageKey) === "1"
|
|
16593
16712
|
} catch (_) {}
|
|
16594
16713
|
setCollapsed(initialCollapsed, { persist: false })
|
|
16714
|
+
syncShowOnHoverToggle()
|
|
16595
16715
|
requestAnimationFrame(() => {
|
|
16596
16716
|
appcanvas.classList.add("sidebar-motion-ready")
|
|
16597
|
-
requestAnimationFrame(() => {
|
|
16598
|
-
if (shouldAutoPeekOnIdle && initialCollapsed && !hasVisibleBrowserSurface()) {
|
|
16599
|
-
setPeeking(true)
|
|
16600
|
-
}
|
|
16601
|
-
})
|
|
16602
16717
|
})
|
|
16603
16718
|
|
|
16604
16719
|
toggle.addEventListener("click", (event) => {
|
|
@@ -16606,11 +16721,28 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16606
16721
|
setCollapsed(!appcanvas.classList.contains("sidebar-collapsed"))
|
|
16607
16722
|
})
|
|
16608
16723
|
|
|
16724
|
+
if (showOnHoverToggle) {
|
|
16725
|
+
showOnHoverToggle.addEventListener("click", (event) => {
|
|
16726
|
+
event.preventDefault()
|
|
16727
|
+
event.stopPropagation()
|
|
16728
|
+
setShowOnHoverEnabled(!showOnHoverEnabled)
|
|
16729
|
+
})
|
|
16730
|
+
}
|
|
16731
|
+
|
|
16609
16732
|
if (peekTrigger) {
|
|
16610
|
-
peekTrigger.addEventListener("pointerenter", () =>
|
|
16733
|
+
peekTrigger.addEventListener("pointerenter", () => {
|
|
16734
|
+
if (showOnHoverEnabled) {
|
|
16735
|
+
setPeeking(true)
|
|
16736
|
+
} else {
|
|
16737
|
+
toggle.classList.add("sidebar-edge-hint")
|
|
16738
|
+
}
|
|
16739
|
+
})
|
|
16611
16740
|
peekTrigger.addEventListener("focus", () => setPeeking(true))
|
|
16612
16741
|
peekTrigger.addEventListener("click", () => setPeeking(true))
|
|
16613
|
-
peekTrigger.addEventListener("pointerleave",
|
|
16742
|
+
peekTrigger.addEventListener("pointerleave", () => {
|
|
16743
|
+
toggle.classList.remove("sidebar-edge-hint")
|
|
16744
|
+
closePeekSoon()
|
|
16745
|
+
})
|
|
16614
16746
|
peekTrigger.addEventListener("blur", closePeekSoon)
|
|
16615
16747
|
aside.addEventListener("pointerenter", () => {
|
|
16616
16748
|
if (isPeeking()) {
|
|
@@ -16659,8 +16791,10 @@ const rerenderMenuSection = (container, html) => {
|
|
|
16659
16791
|
window.PinokioSidebar = {
|
|
16660
16792
|
setCollapsed,
|
|
16661
16793
|
setPeeking,
|
|
16794
|
+
setShowOnHoverEnabled,
|
|
16662
16795
|
isCollapsed,
|
|
16663
|
-
isPeeking
|
|
16796
|
+
isPeeking,
|
|
16797
|
+
isShowOnHoverEnabled: () => showOnHoverEnabled
|
|
16664
16798
|
}
|
|
16665
16799
|
})()
|
|
16666
16800
|
</script>
|
package/server/views/explore.ejs
CHANGED
|
@@ -177,9 +177,9 @@ body.dark .explore-url {
|
|
|
177
177
|
<%- include('partials/home_server_popover_assets') %>
|
|
178
178
|
<main>
|
|
179
179
|
<% if (theme === "dark") { %>
|
|
180
|
-
<iframe name="
|
|
180
|
+
<iframe name="pinokio-explore" src="<%=discover_dark%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
|
|
181
181
|
<% } else { %>
|
|
182
|
-
<iframe name="
|
|
182
|
+
<iframe name="pinokio-explore" src="<%=discover_light%>" <%=agent === "electron" ? "no-referrer" : ""%> allow="fullscreen *;" allowfullscreen></iframe>
|
|
183
183
|
<% } %>
|
|
184
184
|
<%- include('partials/main_sidebar', { selected: 'explore' }) %>
|
|
185
185
|
</main>
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
</span>
|
|
17
17
|
</button>
|
|
18
18
|
<div class="home-server-popover__panel" data-home-server-panel role="dialog" aria-label="Home Server" hidden>
|
|
19
|
-
<div
|
|
19
|
+
<div data-home-server-content>
|
|
20
|
+
<div class="home-server-popover__loading">Checking Home Server...</div>
|
|
21
|
+
</div>
|
|
20
22
|
</div>
|
|
21
23
|
</div>
|
|
@@ -642,8 +642,10 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
642
642
|
root.dataset.homeServerReady = "1"
|
|
643
643
|
const trigger = root.querySelector("[data-home-server-trigger]")
|
|
644
644
|
const panel = root.querySelector("[data-home-server-panel]")
|
|
645
|
+
const panelContent = root.querySelector("[data-home-server-content]")
|
|
645
646
|
let payload = null
|
|
646
647
|
let pollTimer = null
|
|
648
|
+
let qrReturnUrl = ""
|
|
647
649
|
const setTriggerHintEnabled = (enabled) => {
|
|
648
650
|
if (!trigger) {
|
|
649
651
|
return
|
|
@@ -699,11 +701,12 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
699
701
|
existing.remove()
|
|
700
702
|
}
|
|
701
703
|
}
|
|
702
|
-
const showQrPreview = (url
|
|
704
|
+
const showQrPreview = (url) => {
|
|
703
705
|
if (!panel || !url) {
|
|
704
706
|
return
|
|
705
707
|
}
|
|
706
708
|
removeQrPreview()
|
|
709
|
+
qrReturnUrl = url
|
|
707
710
|
const safeUrl = escapeHtml(url)
|
|
708
711
|
const qrUrl = `/qr?data=${encodeURIComponent(url)}&s=7&m=0`
|
|
709
712
|
panel.insertAdjacentHTML("beforeend", `
|
|
@@ -719,11 +722,6 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
719
722
|
</div>
|
|
720
723
|
<div class="home-server-popover__qr-url" title="${safeUrl}">${safeUrl}</div>
|
|
721
724
|
</div>`)
|
|
722
|
-
const preview = panel.querySelector("[data-home-server-qr-preview]")
|
|
723
|
-
if (preview && sourceButton) {
|
|
724
|
-
preview.dataset.returnFocus = "true"
|
|
725
|
-
root._homeServerQrReturnFocus = sourceButton
|
|
726
|
-
}
|
|
727
725
|
const closeButton = panel.querySelector("[data-home-server-qr-close]")
|
|
728
726
|
if (closeButton && typeof closeButton.focus === "function") {
|
|
729
727
|
closeButton.focus()
|
|
@@ -1002,19 +1000,18 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1002
1000
|
document.dispatchEvent(new CustomEvent("pinokio:home-server-status", {
|
|
1003
1001
|
detail: { status, payload: data || { status } }
|
|
1004
1002
|
}))
|
|
1005
|
-
if (!
|
|
1003
|
+
if (!panelContent) {
|
|
1006
1004
|
return
|
|
1007
1005
|
}
|
|
1008
1006
|
if (status === "on") {
|
|
1009
|
-
|
|
1007
|
+
panelContent.innerHTML = renderOn(data)
|
|
1010
1008
|
} else if (status === "starting") {
|
|
1011
|
-
|
|
1009
|
+
panelContent.innerHTML = renderStarting(data)
|
|
1012
1010
|
} else if (status === "setup") {
|
|
1013
|
-
|
|
1011
|
+
panelContent.innerHTML = renderSetup(data)
|
|
1014
1012
|
} else {
|
|
1015
|
-
|
|
1013
|
+
panelContent.innerHTML = renderOff(data)
|
|
1016
1014
|
}
|
|
1017
|
-
root._homeServerQrReturnFocus = null
|
|
1018
1015
|
}
|
|
1019
1016
|
const fetchState = async () => {
|
|
1020
1017
|
const response = await fetch("/info/home-server", { cache: "no-store" })
|
|
@@ -1072,7 +1069,7 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1072
1069
|
if (attempts >= 60) {
|
|
1073
1070
|
stopPolling()
|
|
1074
1071
|
setStatus("error")
|
|
1075
|
-
|
|
1072
|
+
panelContent.innerHTML = renderError()
|
|
1076
1073
|
}
|
|
1077
1074
|
}
|
|
1078
1075
|
}, 1500)
|
|
@@ -1111,11 +1108,11 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1111
1108
|
}
|
|
1112
1109
|
} catch (err) {
|
|
1113
1110
|
setStatus("error")
|
|
1114
|
-
|
|
1111
|
+
panelContent.innerHTML = renderError(err && err.message ? err.message : "")
|
|
1115
1112
|
}
|
|
1116
1113
|
}
|
|
1117
1114
|
const openPanel = async () => {
|
|
1118
|
-
if (!panel || !trigger) {
|
|
1115
|
+
if (!panel || !panelContent || !trigger) {
|
|
1119
1116
|
return
|
|
1120
1117
|
}
|
|
1121
1118
|
panel.hidden = false
|
|
@@ -1123,14 +1120,14 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1123
1120
|
trigger.setAttribute("aria-expanded", "true")
|
|
1124
1121
|
setTriggerHintEnabled(false)
|
|
1125
1122
|
if (!payload) {
|
|
1126
|
-
|
|
1123
|
+
panelContent.innerHTML = `<div class="home-server-popover__loading">Checking Home Server...</div>`
|
|
1127
1124
|
}
|
|
1128
1125
|
try {
|
|
1129
1126
|
await fetchState()
|
|
1130
1127
|
refreshPolling(payload)
|
|
1131
1128
|
} catch (err) {
|
|
1132
1129
|
setStatus("error")
|
|
1133
|
-
|
|
1130
|
+
panelContent.innerHTML = renderError(err && err.message ? err.message : "")
|
|
1134
1131
|
}
|
|
1135
1132
|
}
|
|
1136
1133
|
const closePanel = () => {
|
|
@@ -1139,7 +1136,7 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1139
1136
|
}
|
|
1140
1137
|
panel.hidden = true
|
|
1141
1138
|
removeQrPreview()
|
|
1142
|
-
|
|
1139
|
+
qrReturnUrl = ""
|
|
1143
1140
|
root.dataset.homeServerOpen = "false"
|
|
1144
1141
|
trigger.setAttribute("aria-expanded", "false")
|
|
1145
1142
|
setTriggerHintEnabled(true)
|
|
@@ -1192,17 +1189,22 @@ body.dark .home-server-popover__qr-close:focus-visible {
|
|
|
1192
1189
|
if (qrButton) {
|
|
1193
1190
|
event.preventDefault()
|
|
1194
1191
|
event.stopPropagation()
|
|
1195
|
-
showQrPreview(qrButton.getAttribute("data-home-server-qr") || ""
|
|
1192
|
+
showQrPreview(qrButton.getAttribute("data-home-server-qr") || "")
|
|
1196
1193
|
return
|
|
1197
1194
|
}
|
|
1198
1195
|
const qrCloseButton = event.target.closest("[data-home-server-qr-close]")
|
|
1199
1196
|
if (qrCloseButton) {
|
|
1200
1197
|
event.preventDefault()
|
|
1201
1198
|
event.stopPropagation()
|
|
1199
|
+
const returnUrl = qrReturnUrl
|
|
1202
1200
|
removeQrPreview()
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1201
|
+
qrReturnUrl = ""
|
|
1202
|
+
const focusTarget = panelContent
|
|
1203
|
+
? Array.from(panelContent.querySelectorAll("[data-home-server-qr]")).find((button) => {
|
|
1204
|
+
return button.getAttribute("data-home-server-qr") === returnUrl
|
|
1205
|
+
})
|
|
1206
|
+
: null
|
|
1207
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
1206
1208
|
focusTarget.focus()
|
|
1207
1209
|
}
|
|
1208
1210
|
return
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
title: "Opencode",
|
|
3
|
+
icon: "icon.png",
|
|
4
|
+
description: "The AI coding agent built for the terminal.",
|
|
5
|
+
link: "https://opencode.ai/",
|
|
6
|
+
path: "plugin",
|
|
7
|
+
async installed(kernel) {
|
|
8
|
+
return Boolean(kernel.which("opencode"))
|
|
9
|
+
},
|
|
10
|
+
install: [{
|
|
11
|
+
method: "shell.run",
|
|
12
|
+
params: {
|
|
13
|
+
message: "npm install -g opencode-ai@latest"
|
|
14
|
+
}
|
|
15
|
+
}],
|
|
16
|
+
uninstall: [{
|
|
17
|
+
method: "shell.run",
|
|
18
|
+
params: {
|
|
19
|
+
message: "npm uninstall -g opencode-ai"
|
|
20
|
+
}
|
|
21
|
+
}, {
|
|
22
|
+
method: "fs.rm",
|
|
23
|
+
params: {
|
|
24
|
+
path: "."
|
|
25
|
+
}
|
|
26
|
+
}],
|
|
27
|
+
update: [{
|
|
28
|
+
method: "shell.run",
|
|
29
|
+
params: {
|
|
30
|
+
message: "git pull",
|
|
31
|
+
}
|
|
32
|
+
}, {
|
|
33
|
+
method: "shell.run",
|
|
34
|
+
params: {
|
|
35
|
+
message: "npm install -g opencode-ai@latest"
|
|
36
|
+
}
|
|
37
|
+
}],
|
|
38
|
+
run: [{
|
|
39
|
+
when: "{{platform === 'win32'}}",
|
|
40
|
+
method: "shell.run",
|
|
41
|
+
params: {
|
|
42
|
+
shell: "bash",
|
|
43
|
+
message: "opencode",
|
|
44
|
+
path: "{{args.cwd}}",
|
|
45
|
+
input: true
|
|
46
|
+
}
|
|
47
|
+
}, {
|
|
48
|
+
when: "{{platform !== 'win32'}}",
|
|
49
|
+
id: "run",
|
|
50
|
+
method: "shell.run",
|
|
51
|
+
params: {
|
|
52
|
+
message: "opencode",
|
|
53
|
+
path: "{{args.cwd}}",
|
|
54
|
+
input: true
|
|
55
|
+
}
|
|
56
|
+
}]
|
|
57
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const opencode = require("../opencode/pinokio")
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
...opencode,
|
|
5
|
+
title: "Opencode Auto",
|
|
6
|
+
description: "OpenCode with permission prompts automatically approved unless explicitly denied.",
|
|
7
|
+
run: opencode.run.map((step) => ({
|
|
8
|
+
...step,
|
|
9
|
+
params: {
|
|
10
|
+
...step.params,
|
|
11
|
+
message: "opencode --auto",
|
|
12
|
+
},
|
|
13
|
+
})),
|
|
14
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const path = require("node:path")
|
|
4
|
+
const test = require("node:test")
|
|
5
|
+
const { JSDOM, VirtualConsole } = require("jsdom")
|
|
6
|
+
|
|
7
|
+
const commonScriptPath = path.resolve(__dirname, "../server/public/common.js")
|
|
8
|
+
const exploreTemplatePath = path.resolve(__dirname, "../server/views/explore.ejs")
|
|
9
|
+
|
|
10
|
+
async function createHarness(url = "http://192.168.1.50:42000/home?mode=explore") {
|
|
11
|
+
const source = await fs.readFile(commonScriptPath, "utf8")
|
|
12
|
+
const virtualConsole = new VirtualConsole()
|
|
13
|
+
virtualConsole.on("jsdomError", (error) => {
|
|
14
|
+
throw error
|
|
15
|
+
})
|
|
16
|
+
const dom = new JSDOM("<!doctype html><body><main><iframe></iframe></main></body>", {
|
|
17
|
+
runScripts: "dangerously",
|
|
18
|
+
pretendToBeVisual: true,
|
|
19
|
+
url,
|
|
20
|
+
virtualConsole,
|
|
21
|
+
beforeParse(window) {
|
|
22
|
+
window.fetch = async () => ({ ok: true, json: async () => ({}) })
|
|
23
|
+
window.matchMedia = () => ({
|
|
24
|
+
matches: false,
|
|
25
|
+
addEventListener() {},
|
|
26
|
+
removeEventListener() {},
|
|
27
|
+
})
|
|
28
|
+
window.ResizeObserver = class {
|
|
29
|
+
observe() {}
|
|
30
|
+
disconnect() {}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
const script = dom.window.document.createElement("script")
|
|
35
|
+
script.textContent = source
|
|
36
|
+
dom.window.document.body.appendChild(script)
|
|
37
|
+
const frame = dom.window.document.querySelector("iframe")
|
|
38
|
+
frame.src = "https://pinokio.co/?embed=1&theme=dark"
|
|
39
|
+
frame.focus()
|
|
40
|
+
assert.equal(dom.window.document.activeElement, frame)
|
|
41
|
+
return { dom, frame }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function navigate(dom, frame, target, options = {}) {
|
|
45
|
+
dom.window.dispatchEvent(new dom.window.MessageEvent("message", {
|
|
46
|
+
data: { e: "pinokio:navigate", url: target },
|
|
47
|
+
origin: options.origin || "https://pinokio.co",
|
|
48
|
+
source: options.source === undefined ? frame.contentWindow : options.source,
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("Explore uses a stable frame identity instead of the launcher schema", async () => {
|
|
53
|
+
const source = await fs.readFile(exploreTemplatePath, "utf8")
|
|
54
|
+
|
|
55
|
+
assert.equal(source.match(/name="pinokio-explore"/g)?.length, 2)
|
|
56
|
+
assert.doesNotMatch(source, /name="<%=schema%>"/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("LAN Explore rebases registry download installs onto the Home Server origin", async (t) => {
|
|
60
|
+
const { dom, frame } = await createHarness()
|
|
61
|
+
t.after(() => dom.window.close())
|
|
62
|
+
|
|
63
|
+
navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev")
|
|
64
|
+
|
|
65
|
+
assert.equal(
|
|
66
|
+
frame.src,
|
|
67
|
+
"http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api&branch=dev",
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("LAN Explore handles registry installs when mobile Safari leaves the parent body focused", async (t) => {
|
|
72
|
+
const { dom, frame } = await createHarness()
|
|
73
|
+
t.after(() => dom.window.close())
|
|
74
|
+
dom.window.document.body.tabIndex = -1
|
|
75
|
+
dom.window.document.body.focus()
|
|
76
|
+
assert.equal(dom.window.document.activeElement, dom.window.document.body)
|
|
77
|
+
|
|
78
|
+
navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api")
|
|
79
|
+
|
|
80
|
+
assert.equal(
|
|
81
|
+
frame.src,
|
|
82
|
+
"http://192.168.1.50:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo&path=api",
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("LAN Explore rebases valid checkpoint installs onto the Home Server origin", async (t) => {
|
|
87
|
+
const { dom, frame } = await createHarness()
|
|
88
|
+
t.after(() => dom.window.close())
|
|
89
|
+
const hash = `sha256%3A${"a".repeat(64)}`
|
|
90
|
+
|
|
91
|
+
navigate(dom, frame, `http://localhost:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`)
|
|
92
|
+
|
|
93
|
+
assert.equal(
|
|
94
|
+
frame.src,
|
|
95
|
+
`http://192.168.1.50:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=${hash}&path=api`,
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const blockedCases = [
|
|
100
|
+
["unrelated local route", "http://localhost:42000/tasker?vars=url", {}],
|
|
101
|
+
["download without a URI", "http://localhost:42000/pinokio/download", {}],
|
|
102
|
+
["checkpoint with an invalid hash", "http://localhost:42000/checkpoints?registry=https%3A%2F%2Fapi.pinokio.co&hash=abc", {}],
|
|
103
|
+
["different message origin", "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo", { origin: "https://example.com" }],
|
|
104
|
+
["different message source", "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo", { source: null }],
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for (const [label, target, options] of blockedCases) {
|
|
108
|
+
test(`LAN Explore leaves ${label} blocked`, async (t) => {
|
|
109
|
+
const { dom, frame } = await createHarness()
|
|
110
|
+
t.after(() => dom.window.close())
|
|
111
|
+
const original = frame.src
|
|
112
|
+
|
|
113
|
+
navigate(dom, frame, target, options)
|
|
114
|
+
|
|
115
|
+
assert.equal(frame.src, original)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test("the additive branch does not run outside Explore", async (t) => {
|
|
120
|
+
const { dom, frame } = await createHarness("http://192.168.1.50:42000/home")
|
|
121
|
+
t.after(() => dom.window.close())
|
|
122
|
+
const original = frame.src
|
|
123
|
+
|
|
124
|
+
navigate(dom, frame, "http://localhost:42000/pinokio/download?uri=https%3A%2F%2Fgithub.com%2Facme%2Fdemo")
|
|
125
|
+
|
|
126
|
+
assert.equal(frame.src, original)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("localhost keeps the existing same-origin navigation behavior", async (t) => {
|
|
130
|
+
const { dom, frame } = await createHarness("http://localhost:42000/home?mode=explore")
|
|
131
|
+
t.after(() => dom.window.close())
|
|
132
|
+
|
|
133
|
+
navigate(dom, frame, "http://localhost:42000/existing-local-route?value=1")
|
|
134
|
+
|
|
135
|
+
assert.equal(frame.src, "http://localhost:42000/existing-local-route?value=1")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("LAN keeps the existing same-origin navigation behavior", async (t) => {
|
|
139
|
+
const { dom, frame } = await createHarness()
|
|
140
|
+
t.after(() => dom.window.close())
|
|
141
|
+
|
|
142
|
+
navigate(dom, frame, "http://192.168.1.50:42000/existing-lan-route?value=1")
|
|
143
|
+
|
|
144
|
+
assert.equal(frame.src, "http://192.168.1.50:42000/existing-lan-route?value=1")
|
|
145
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const path = require("node:path")
|
|
4
|
+
const test = require("node:test")
|
|
5
|
+
const { JSDOM, VirtualConsole } = require("jsdom")
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(__dirname, "..")
|
|
8
|
+
const popoverPath = path.join(root, "server/views/partials/home_server_popover.ejs")
|
|
9
|
+
const assetsPath = path.join(root, "server/views/partials/home_server_popover_assets.ejs")
|
|
10
|
+
|
|
11
|
+
async function waitFor(predicate, message) {
|
|
12
|
+
const deadline = Date.now() + 1000
|
|
13
|
+
while (Date.now() < deadline) {
|
|
14
|
+
if (predicate()) return
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
16
|
+
}
|
|
17
|
+
assert.fail(`Timed out waiting for ${message}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createHarness() {
|
|
21
|
+
const [popover, assets] = await Promise.all([
|
|
22
|
+
fs.readFile(popoverPath, "utf8"),
|
|
23
|
+
fs.readFile(assetsPath, "utf8")
|
|
24
|
+
])
|
|
25
|
+
const scriptStart = assets.indexOf("<script>")
|
|
26
|
+
const scriptEnd = assets.lastIndexOf("</script>")
|
|
27
|
+
assert.notEqual(scriptStart, -1)
|
|
28
|
+
assert.notEqual(scriptEnd, -1)
|
|
29
|
+
const script = assets.slice(scriptStart + "<script>".length, scriptEnd)
|
|
30
|
+
|
|
31
|
+
const state = {
|
|
32
|
+
interval: null,
|
|
33
|
+
payload: {
|
|
34
|
+
status: "on",
|
|
35
|
+
apps: [{
|
|
36
|
+
id: "maestro",
|
|
37
|
+
name: "Maestro",
|
|
38
|
+
url: "",
|
|
39
|
+
state: "starting"
|
|
40
|
+
}],
|
|
41
|
+
routes: [{
|
|
42
|
+
name: "Syncthing",
|
|
43
|
+
url: "http://192.168.1.10:42006"
|
|
44
|
+
}]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const virtualConsole = new VirtualConsole()
|
|
49
|
+
virtualConsole.on("jsdomError", (error) => {
|
|
50
|
+
throw error
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const dom = new JSDOM(`<!doctype html><body>${popover}<script>${script}</script></body>`, {
|
|
54
|
+
url: "http://127.0.0.1:42000/",
|
|
55
|
+
runScripts: "dangerously",
|
|
56
|
+
pretendToBeVisual: true,
|
|
57
|
+
virtualConsole,
|
|
58
|
+
beforeParse(window) {
|
|
59
|
+
window.fetch = async () => ({
|
|
60
|
+
ok: true,
|
|
61
|
+
json: async () => JSON.parse(JSON.stringify(state.payload))
|
|
62
|
+
})
|
|
63
|
+
window.setInterval = (callback, delay) => {
|
|
64
|
+
state.interval = { callback, delay }
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
window.clearInterval = () => {
|
|
68
|
+
state.interval = null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await waitFor(
|
|
74
|
+
() => dom.window.document.querySelector('[data-home-server-qr="http://192.168.1.10:42006"]'),
|
|
75
|
+
"initial Home Server content"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return { dom, state }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("Home Server polling refreshes content without closing an open QR preview", async (t) => {
|
|
82
|
+
const { dom, state } = await createHarness()
|
|
83
|
+
t.after(() => dom.window.close())
|
|
84
|
+
const { document } = dom.window
|
|
85
|
+
const panel = document.querySelector("[data-home-server-panel]")
|
|
86
|
+
const panelContent = document.querySelector("[data-home-server-content]")
|
|
87
|
+
const trigger = document.querySelector("[data-home-server-trigger]")
|
|
88
|
+
|
|
89
|
+
trigger.click()
|
|
90
|
+
await waitFor(() => state.interval, "Home Server polling")
|
|
91
|
+
|
|
92
|
+
const qrButton = panelContent.querySelector('[data-home-server-qr="http://192.168.1.10:42006"]')
|
|
93
|
+
qrButton.click()
|
|
94
|
+
const preview = panel.querySelector("[data-home-server-qr-preview]")
|
|
95
|
+
assert.ok(preview)
|
|
96
|
+
assert.equal(document.activeElement, preview.querySelector("[data-home-server-qr-close]"))
|
|
97
|
+
|
|
98
|
+
state.payload.routes[0].name = "Syncthing refreshed"
|
|
99
|
+
await state.interval.callback()
|
|
100
|
+
|
|
101
|
+
assert.equal(panel.querySelector("[data-home-server-qr-preview]"), preview)
|
|
102
|
+
assert.equal(qrButton.isConnected, false)
|
|
103
|
+
assert.match(panelContent.textContent, /Syncthing refreshed/)
|
|
104
|
+
|
|
105
|
+
preview.querySelector("[data-home-server-qr-close]").click()
|
|
106
|
+
const refreshedQrButton = panelContent.querySelector('[data-home-server-qr="http://192.168.1.10:42006"]')
|
|
107
|
+
assert.equal(panel.querySelector("[data-home-server-qr-preview]"), null)
|
|
108
|
+
assert.equal(document.activeElement, refreshedQrButton)
|
|
109
|
+
})
|
|
@@ -856,17 +856,21 @@ test("static guard: open without launching disables page-load script frame selec
|
|
|
856
856
|
assert.match(appView, /if \(!target && !automaticSelectionDisabled && preselected && preselected !== devTab\)/)
|
|
857
857
|
})
|
|
858
858
|
|
|
859
|
-
test("static guard: collapsed
|
|
859
|
+
test("static guard: collapsed app sidebar exposes a hover preference", async () => {
|
|
860
860
|
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
861
861
|
|
|
862
|
-
assert.match(appView, /
|
|
863
|
-
assert.match(appView,
|
|
864
|
-
assert.match(appView, /
|
|
862
|
+
assert.match(appView, /data-app-sidebar-show-on-hover/)
|
|
863
|
+
assert.match(appView, />Show on hover</)
|
|
864
|
+
assert.match(appView, /Show the sidebar when hovering over the left edge/)
|
|
865
|
+
assert.match(appView, /#sidebar-toggle\.sidebar-edge-hint \.sidebar-toggle-glyph/)
|
|
866
|
+
assert.match(appView, /peekTrigger\.addEventListener\("pointerenter", \(\) => \{[\s\S]*if \(showOnHoverEnabled\) \{[\s\S]*setPeeking\(true\)[\s\S]*\} else \{[\s\S]*toggle\.classList\.add\("sidebar-edge-hint"\)/)
|
|
867
|
+
assert.match(appView, /peekTrigger\.addEventListener\("pointerleave", \(\) => \{[\s\S]*toggle\.classList\.remove\("sidebar-edge-hint"\)[\s\S]*closePeekSoon\(\)/)
|
|
868
|
+
assert.doesNotMatch(appView, /shouldAutoShowOnEmptyRunView|hasVisibleBrowserSurface/)
|
|
865
869
|
assert.match(appView, /setCollapsed\(initialCollapsed, \{ persist: false \}\)/)
|
|
866
870
|
assert.match(appView, /aside\.addEventListener\("click", \(event\) => \{[\s\S]*target\.closest\("\.reveal, \.revealer, \[data-app-autolaunch-button\]"\)[\s\S]*window\.setTimeout\(\(\) => setPeeking\(false\), 0\)/)
|
|
867
871
|
})
|
|
868
872
|
|
|
869
|
-
test("app sidebar
|
|
873
|
+
test("app sidebar hover preference gates hover without closing the active peek", async () => {
|
|
870
874
|
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
871
875
|
const start = appView.indexOf('<script>\n(() => {\n const appcanvas = document.querySelector(".appcanvas")')
|
|
872
876
|
const end = appView.indexOf('</script>\n<script src="/tab-idle-notifier.js"></script>', start)
|
|
@@ -876,23 +880,20 @@ test("app sidebar auto-peeks on collapsed idle run page and dismisses after clic
|
|
|
876
880
|
const sidebarScript = appView
|
|
877
881
|
.slice(start + "<script>\n".length, end)
|
|
878
882
|
.replace(/<%- JSON\.stringify\(typeof name === "string" \? name : ""\) %>/g, '"test-app"')
|
|
879
|
-
.replace(/<%- JSON\.stringify\(type === "run"\) %>/g, "true")
|
|
880
883
|
|
|
881
|
-
const createDom = (
|
|
884
|
+
const createDom = () => {
|
|
882
885
|
return new JSDOM(`<!doctype html>
|
|
883
886
|
<button id="sidebar-toggle"></button>
|
|
884
887
|
<div class="appcanvas vertical">
|
|
885
888
|
<button type="button" data-app-sidebar-peek-trigger></button>
|
|
886
889
|
<aside id="app-sidebar">
|
|
890
|
+
<button type="button" role="switch" aria-checked="true" data-app-sidebar-show-on-hover>Show on hover</button>
|
|
887
891
|
<button type="button" class="reveal" id="sidebar-reveal">Downloads</button>
|
|
888
892
|
<button type="button" class="revealer" id="sidebar-revealer">Changes</button>
|
|
889
893
|
<button type="button" data-app-autolaunch-button id="sidebar-autolaunch">Autolaunch</button>
|
|
890
894
|
<button type="button" id="sidebar-action">Start</button>
|
|
891
895
|
</aside>
|
|
892
|
-
|
|
893
|
-
</div>
|
|
894
|
-
<div id="browserview-network-status" hidden></div>
|
|
895
|
-
<div data-launch-requirements-status hidden></div>`, {
|
|
896
|
+
</div>`, {
|
|
896
897
|
url: "http://127.0.0.1:42000/v/test-app",
|
|
897
898
|
runScripts: "outside-only",
|
|
898
899
|
pretendToBeVisual: true,
|
|
@@ -908,6 +909,8 @@ test("app sidebar auto-peeks on collapsed idle run page and dismisses after clic
|
|
|
908
909
|
|
|
909
910
|
const wait = (ms = 75) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
910
911
|
const storageKey = "pinokio.sidebar-collapsed:test-app"
|
|
912
|
+
const showOnHoverStorageKey = "pinokio.sidebar-show-on-hover"
|
|
913
|
+
const legacyAutoShowStorageKey = "pinokio.sidebar-auto-show"
|
|
911
914
|
|
|
912
915
|
const dom = createDom()
|
|
913
916
|
dom.window.localStorage.setItem(storageKey, "1")
|
|
@@ -916,10 +919,17 @@ test("app sidebar auto-peeks on collapsed idle run page and dismisses after clic
|
|
|
916
919
|
|
|
917
920
|
const appcanvas = dom.window.document.querySelector(".appcanvas")
|
|
918
921
|
const aside = dom.window.document.getElementById("app-sidebar")
|
|
922
|
+
const peekTrigger = dom.window.document.querySelector("[data-app-sidebar-peek-trigger]")
|
|
923
|
+
const sidebarToggle = dom.window.document.getElementById("sidebar-toggle")
|
|
919
924
|
assert.equal(appcanvas.classList.contains("sidebar-collapsed"), true)
|
|
925
|
+
assert.equal(appcanvas.classList.contains("sidebar-peeking"), false)
|
|
926
|
+
assert.equal(aside.getAttribute("aria-hidden"), "true")
|
|
927
|
+
assert.equal(dom.window.localStorage.getItem(storageKey), "1")
|
|
928
|
+
|
|
929
|
+
peekTrigger.dispatchEvent(new dom.window.Event("pointerenter"))
|
|
920
930
|
assert.equal(appcanvas.classList.contains("sidebar-peeking"), true)
|
|
931
|
+
assert.equal(sidebarToggle.classList.contains("sidebar-edge-hint"), false)
|
|
921
932
|
assert.equal(aside.getAttribute("aria-hidden"), "false")
|
|
922
|
-
assert.equal(dom.window.localStorage.getItem(storageKey), "1")
|
|
923
933
|
|
|
924
934
|
aside.querySelector("#sidebar-reveal").dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }))
|
|
925
935
|
await wait(5)
|
|
@@ -940,15 +950,38 @@ test("app sidebar auto-peeks on collapsed idle run page and dismisses after clic
|
|
|
940
950
|
assert.equal(appcanvas.classList.contains("sidebar-peeking"), false)
|
|
941
951
|
assert.equal(dom.window.localStorage.getItem(storageKey), "1")
|
|
942
952
|
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
953
|
+
const disabledDom = createDom()
|
|
954
|
+
disabledDom.window.localStorage.setItem(storageKey, "1")
|
|
955
|
+
disabledDom.window.localStorage.setItem(legacyAutoShowStorageKey, "0")
|
|
956
|
+
disabledDom.window.eval(sidebarScript)
|
|
946
957
|
await wait()
|
|
947
958
|
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
959
|
+
const disabledCanvas = disabledDom.window.document.querySelector(".appcanvas")
|
|
960
|
+
const disabledPeekTrigger = disabledDom.window.document.querySelector("[data-app-sidebar-peek-trigger]")
|
|
961
|
+
const showOnHoverToggle = disabledDom.window.document.querySelector("[data-app-sidebar-show-on-hover]")
|
|
962
|
+
const disabledSidebarToggle = disabledDom.window.document.getElementById("sidebar-toggle")
|
|
963
|
+
assert.equal(disabledCanvas.classList.contains("sidebar-collapsed"), true)
|
|
964
|
+
assert.equal(disabledCanvas.classList.contains("sidebar-peeking"), false)
|
|
965
|
+
assert.equal(showOnHoverToggle.getAttribute("aria-checked"), "false")
|
|
966
|
+
|
|
967
|
+
disabledPeekTrigger.dispatchEvent(new disabledDom.window.Event("pointerenter"))
|
|
968
|
+
assert.equal(disabledCanvas.classList.contains("sidebar-peeking"), false)
|
|
969
|
+
assert.equal(disabledSidebarToggle.classList.contains("sidebar-edge-hint"), true)
|
|
970
|
+
|
|
971
|
+
disabledPeekTrigger.dispatchEvent(new disabledDom.window.Event("pointerleave"))
|
|
972
|
+
assert.equal(disabledSidebarToggle.classList.contains("sidebar-edge-hint"), false)
|
|
973
|
+
|
|
974
|
+
disabledPeekTrigger.click()
|
|
975
|
+
assert.equal(disabledCanvas.classList.contains("sidebar-peeking"), true)
|
|
976
|
+
assert.equal(disabledSidebarToggle.classList.contains("sidebar-edge-hint"), false)
|
|
977
|
+
showOnHoverToggle.click()
|
|
978
|
+
assert.equal(showOnHoverToggle.getAttribute("aria-checked"), "true")
|
|
979
|
+
assert.equal(disabledDom.window.localStorage.getItem(showOnHoverStorageKey), "1")
|
|
980
|
+
|
|
981
|
+
showOnHoverToggle.click()
|
|
982
|
+
assert.equal(showOnHoverToggle.getAttribute("aria-checked"), "false")
|
|
983
|
+
assert.equal(disabledDom.window.localStorage.getItem(showOnHoverStorageKey), "0")
|
|
984
|
+
assert.equal(disabledCanvas.classList.contains("sidebar-peeking"), true)
|
|
952
985
|
})
|
|
953
986
|
|
|
954
987
|
test("app identity popover exposes launcher metadata and remote browser link", async () => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const test = require("node:test")
|
|
3
|
+
|
|
4
|
+
const opencode = require("../system/plugin/opencode/pinokio")
|
|
5
|
+
const opencodeAuto = require("../system/plugin/opencode-auto/pinokio")
|
|
6
|
+
|
|
7
|
+
test("OpenCode installed status follows executable availability", async () => {
|
|
8
|
+
const lookups = []
|
|
9
|
+
const kernel = {
|
|
10
|
+
which(name) {
|
|
11
|
+
lookups.push(name)
|
|
12
|
+
return null
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
assert.equal(await opencode.installed(kernel, {}), false)
|
|
17
|
+
|
|
18
|
+
kernel.which = (name) => {
|
|
19
|
+
lookups.push(name)
|
|
20
|
+
return "/pinokio/bin/npm/bin/opencode"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assert.equal(await opencode.installed(kernel, {}), true)
|
|
24
|
+
assert.deepEqual(lookups, ["opencode", "opencode"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("OpenCode Auto shares the managed lifecycle and launches in auto mode", () => {
|
|
28
|
+
for (const action of ["install", "uninstall", "update", "installed"]) {
|
|
29
|
+
assert.equal(opencodeAuto[action], opencode[action])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assert.equal(opencodeAuto.run.length, opencode.run.length)
|
|
33
|
+
for (let index = 0; index < opencodeAuto.run.length; index += 1) {
|
|
34
|
+
const normalStep = opencode.run[index]
|
|
35
|
+
const autoStep = opencodeAuto.run[index]
|
|
36
|
+
|
|
37
|
+
assert.equal(autoStep.when, normalStep.when)
|
|
38
|
+
assert.equal(autoStep.id, normalStep.id)
|
|
39
|
+
assert.equal(autoStep.method, normalStep.method)
|
|
40
|
+
assert.equal(autoStep.params.path, normalStep.params.path)
|
|
41
|
+
assert.equal(autoStep.params.input, normalStep.params.input)
|
|
42
|
+
assert.equal(autoStep.params.shell, normalStep.params.shell)
|
|
43
|
+
assert.equal(autoStep.params.message, "opencode --auto")
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -208,7 +208,7 @@ module.exports = {
|
|
|
208
208
|
const systemPlugins = menu.filter((item) => item.source === "system")
|
|
209
209
|
const localPlugins = menu.filter((item) => item.source === "local")
|
|
210
210
|
|
|
211
|
-
assert.strictEqual(systemPlugins.length,
|
|
211
|
+
assert.strictEqual(systemPlugins.length, 12)
|
|
212
212
|
assert.ok(systemPlugins.every((item) => item.system === true))
|
|
213
213
|
assert.ok(systemPlugins.every((item) => item.href.startsWith("/pinokio/run/plugin/")))
|
|
214
214
|
assert.ok(systemPlugins.every((item) => item.image.startsWith("/pinokio/asset/plugin/")))
|