pinokiod 7.5.19 → 7.5.21
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/kernel/bin/index.js +0 -5
- package/kernel/bin/setup.js +0 -1
- package/kernel/index.js +0 -3
- package/package.json +1 -1
- package/server/public/common.js +1 -1
- package/server/public/style.css +34 -0
- package/server/public/urldropdown.css +272 -0
- package/server/public/urldropdown.js +44 -12
- package/server/views/app.ejs +190 -146
- package/server/views/autolaunch.ejs +10 -1
- package/server/views/columns.ejs +13 -1
- package/server/views/d.ejs +54 -137
- package/server/views/index.ejs +14 -6
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +4 -0
- package/server/views/partials/d_column_subtitle.ejs +10 -0
- package/server/views/partials/d_menu_column.ejs +18 -0
- package/server/views/partials/d_terminal_column.ejs +2 -11
- package/server/views/partials/d_terminal_options.ejs +1 -18
- package/server/views/partials/d_tool_tab.ejs +32 -0
- package/server/views/partials/main_sidebar.ejs +3 -0
- package/server/views/rows.ejs +13 -1
- package/system/plugin/antigravity-cli/common.js +10 -16
- package/test/antigravity-cli-plugin.test.js +26 -19
- package/test/home-autolaunch-live-ui.test.js +70 -0
- package/test/launch-requirements-browser.test.js +259 -3
- package/test/plugin-dev-iframe.test.js +67 -1
- package/test/plugin-sources.test.js +1 -1
- package/test/url-split-modal.test.js +107 -0
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +0 -15
- package/system/plugin/qwen/pinokio.js +0 -34
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/windsurf/pinokio.js +0 -23
- package/system/plugin/windsurf/windsurf.png +0 -0
|
@@ -27,6 +27,7 @@ const playwright = loadPlaywright()
|
|
|
27
27
|
|
|
28
28
|
const root = path.resolve(__dirname, "..")
|
|
29
29
|
const appViewPath = path.resolve(root, "server/views/app.ejs")
|
|
30
|
+
const autolaunchViewPath = path.resolve(root, "server/views/autolaunch.ejs")
|
|
30
31
|
const statusClientPath = path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs")
|
|
31
32
|
const homeViewPath = path.resolve(root, "server/views/index.ejs")
|
|
32
33
|
|
|
@@ -55,6 +56,19 @@ async function renderAppAutolaunchScript(initialState) {
|
|
|
55
56
|
})
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
async function renderGlobalAutolaunchScript(state) {
|
|
60
|
+
const view = await fs.readFile(autolaunchViewPath, "utf8")
|
|
61
|
+
const start = view.indexOf("<script>\nlet autolaunchApps =")
|
|
62
|
+
assert.notEqual(start, -1)
|
|
63
|
+
const end = view.indexOf("\n</script>", start)
|
|
64
|
+
assert.notEqual(end, -1)
|
|
65
|
+
return ejs.render(view.slice(start + "<script>".length, end), {
|
|
66
|
+
appsJson: JSON.stringify([state.targetApp])
|
|
67
|
+
}, {
|
|
68
|
+
filename: autolaunchViewPath
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
async function renderLaunchRequirementsStatusClient(enabled) {
|
|
59
73
|
const template = await fs.readFile(statusClientPath, "utf8")
|
|
60
74
|
return ejs.render(template, {
|
|
@@ -89,6 +103,9 @@ function html(body, script = "") {
|
|
|
89
103
|
body { font-family: sans-serif; }
|
|
90
104
|
.app-autolaunch-modal { position: fixed; inset: 20px; background: white; border: 1px solid #ccc; padding: 16px; }
|
|
91
105
|
.app-autolaunch-script-option, .app-autolaunch-dependency-option, .app-autolaunch-dependency-row { display: block; margin: 6px 0; }
|
|
106
|
+
.transformed-sidebar-fixture { display: flex; width: 100%; min-height: 520px; }
|
|
107
|
+
.transformed-sidebar-fixture > aside { width: 280px; flex: 0 0 280px; transform: translateX(0); }
|
|
108
|
+
.transformed-sidebar-fixture > main { flex: 1 1 auto; }
|
|
92
109
|
.launch-requirements-card { width: 560px; margin: 40px auto; }
|
|
93
110
|
.launch-requirements-row { display: grid; grid-template-columns: 32px 1fr auto; gap: 8px; align-items: center; }
|
|
94
111
|
</style>
|
|
@@ -100,9 +117,8 @@ function html(body, script = "") {
|
|
|
100
117
|
</html>`
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return html(`
|
|
120
|
+
function appAutolaunchMarkup(initialApp) {
|
|
121
|
+
return `
|
|
106
122
|
<div class="app-autolaunch" data-app-autolaunch data-app-id="target">
|
|
107
123
|
<button type="button" class="app-autolaunch-row" data-app-autolaunch-button data-enabled="${initialApp.autolaunch_enabled ? "true" : "false"}" aria-haspopup="dialog" aria-expanded="false">
|
|
108
124
|
<span class="app-autolaunch-label">Autolaunch</span>
|
|
@@ -122,6 +138,37 @@ async function appFixtureHtml(initialApp) {
|
|
|
122
138
|
<div data-app-autolaunch-feedback></div>
|
|
123
139
|
</div>
|
|
124
140
|
</div>
|
|
141
|
+
`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function appFixtureHtml(initialApp) {
|
|
145
|
+
const script = await renderAppAutolaunchScript(initialApp)
|
|
146
|
+
return html(appAutolaunchMarkup(initialApp), script)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function transformedSidebarAppFixtureHtml(initialApp) {
|
|
150
|
+
const script = await renderAppAutolaunchScript(initialApp)
|
|
151
|
+
return html(`
|
|
152
|
+
<div class="appcanvas transformed-sidebar-fixture">
|
|
153
|
+
<aside>
|
|
154
|
+
${appAutolaunchMarkup(initialApp)}
|
|
155
|
+
</aside>
|
|
156
|
+
<main></main>
|
|
157
|
+
</div>
|
|
158
|
+
`, script)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function globalAutolaunchFixtureHtml(state) {
|
|
162
|
+
const script = await renderGlobalAutolaunchScript(state)
|
|
163
|
+
return html(`
|
|
164
|
+
<main class="autolaunch-page">
|
|
165
|
+
<strong data-enabled-count>0</strong>
|
|
166
|
+
<strong data-app-count>1</strong>
|
|
167
|
+
<button type="button" data-disable-all disabled>Disable startup</button>
|
|
168
|
+
<input type="search" data-app-search>
|
|
169
|
+
<div data-app-list></div>
|
|
170
|
+
<section data-detail></section>
|
|
171
|
+
</main>
|
|
125
172
|
`, script)
|
|
126
173
|
}
|
|
127
174
|
|
|
@@ -201,6 +248,8 @@ function readBody(req) {
|
|
|
201
248
|
async function startFixtureServer(state) {
|
|
202
249
|
const pages = {
|
|
203
250
|
app: await appFixtureHtml(state.targetApp),
|
|
251
|
+
transformedApp: await transformedSidebarAppFixtureHtml(state.targetApp),
|
|
252
|
+
autolaunch: await globalAutolaunchFixtureHtml(state),
|
|
204
253
|
openWithoutLaunching: await openWithoutLaunchingHtml(),
|
|
205
254
|
status: await statusFixtureHtml(),
|
|
206
255
|
home: await homeFixtureHtml()
|
|
@@ -213,6 +262,16 @@ async function startFixtureServer(state) {
|
|
|
213
262
|
res.end(pages.app)
|
|
214
263
|
return
|
|
215
264
|
}
|
|
265
|
+
if (req.method === "GET" && url.pathname === "/app-transformed-sidebar-fixture") {
|
|
266
|
+
res.writeHead(200, { "Content-Type": "text/html" })
|
|
267
|
+
res.end(pages.transformedApp)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (req.method === "GET" && url.pathname === "/autolaunch-fixture") {
|
|
271
|
+
res.writeHead(200, { "Content-Type": "text/html" })
|
|
272
|
+
res.end(pages.autolaunch)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
216
275
|
if (req.method === "GET" && url.pathname === "/open-without-launching") {
|
|
217
276
|
res.writeHead(200, { "Content-Type": "text/html" })
|
|
218
277
|
res.end(pages.openWithoutLaunching)
|
|
@@ -392,6 +451,15 @@ async function textContent(page, selector) {
|
|
|
392
451
|
return page.locator(selector).first().textContent()
|
|
393
452
|
}
|
|
394
453
|
|
|
454
|
+
async function waitForState(predicate, message, timeoutMs = 2000) {
|
|
455
|
+
const started = Date.now()
|
|
456
|
+
while (Date.now() - started < timeoutMs) {
|
|
457
|
+
if (predicate()) return
|
|
458
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
459
|
+
}
|
|
460
|
+
assert.ok(predicate(), message)
|
|
461
|
+
}
|
|
462
|
+
|
|
395
463
|
browserTest("browser: selecting a launch script from empty state persists and stays off", async () => {
|
|
396
464
|
const state = baseState({ targetScript: "" })
|
|
397
465
|
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
@@ -414,6 +482,194 @@ browserTest("browser: selecting a launch script from empty state persists and st
|
|
|
414
482
|
})
|
|
415
483
|
})
|
|
416
484
|
|
|
485
|
+
browserTest("browser: app autolaunch modal is viewport sized from transformed sidebar", async () => {
|
|
486
|
+
const state = baseState({ targetScript: "" })
|
|
487
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
488
|
+
await page.setViewportSize({ width: 1000, height: 700 })
|
|
489
|
+
await page.goto(`${baseUrl}/app-transformed-sidebar-fixture`)
|
|
490
|
+
await page.click("[data-app-autolaunch-button]")
|
|
491
|
+
await page.waitForSelector("[data-app-autolaunch-modal]:not(.hidden)")
|
|
492
|
+
|
|
493
|
+
const rect = await page.locator("[data-app-autolaunch-modal]").evaluate((modal) => {
|
|
494
|
+
const box = modal.getBoundingClientRect()
|
|
495
|
+
return {
|
|
496
|
+
left: box.left,
|
|
497
|
+
top: box.top,
|
|
498
|
+
right: box.right,
|
|
499
|
+
bottom: box.bottom,
|
|
500
|
+
width: box.width,
|
|
501
|
+
height: box.height,
|
|
502
|
+
parentTag: modal.parentElement ? modal.parentElement.tagName : ""
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
assert.equal(rect.parentTag, "BODY")
|
|
507
|
+
assert.ok(rect.width > 900, `modal width should span the viewport, got ${rect.width}`)
|
|
508
|
+
assert.ok(Math.abs(rect.left - 20) <= 1, `modal should use viewport left inset, got ${rect.left}`)
|
|
509
|
+
assert.ok(Math.abs(rect.right - 980) <= 1, `modal should use viewport right inset, got ${rect.right}`)
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
browserTest("browser: startup toggle from empty selection saves the single eligible default", async () => {
|
|
514
|
+
const state = baseState({ targetScript: "" })
|
|
515
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
516
|
+
await page.goto(`${baseUrl}/app-fixture`)
|
|
517
|
+
await page.click("[data-app-autolaunch-button]")
|
|
518
|
+
await page.waitForSelector('input[name="app-autolaunch-script"][value="start.js"]')
|
|
519
|
+
|
|
520
|
+
assert.equal(await page.isChecked('input[name="app-autolaunch-script"][value="start.js"]'), false)
|
|
521
|
+
|
|
522
|
+
await page.click("[data-app-autolaunch-switch]")
|
|
523
|
+
await waitForState(
|
|
524
|
+
() => state.autolaunchPosts.length === 1,
|
|
525
|
+
"Start with Pinokio should save the single eligible default"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
assert.deepEqual(state.autolaunchPosts[0], {
|
|
529
|
+
app: "target",
|
|
530
|
+
script: "start.js",
|
|
531
|
+
enabled: true
|
|
532
|
+
})
|
|
533
|
+
assert.equal(state.launchRequirementsGets, 0)
|
|
534
|
+
assert.equal(await page.isChecked('input[name="app-autolaunch-script"][value="start.js"]'), true)
|
|
535
|
+
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "ON")
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
browserTest("browser: startup toggle from empty selection warns when no eligible default exists", async () => {
|
|
540
|
+
const cases = [{
|
|
541
|
+
name: "no default",
|
|
542
|
+
menu: [{ script: "start.js", label: "Start" }],
|
|
543
|
+
other: [{ script: "target.custom.js", label: "Custom" }]
|
|
544
|
+
}, {
|
|
545
|
+
name: "multiple defaults",
|
|
546
|
+
menu: [
|
|
547
|
+
{ script: "start.js", label: "Start", menu_default: true },
|
|
548
|
+
{ script: "target.custom.js", label: "Custom", menu_default: true }
|
|
549
|
+
],
|
|
550
|
+
other: []
|
|
551
|
+
}, {
|
|
552
|
+
name: "params default",
|
|
553
|
+
menu: [{ script: "start.js", label: "Start", menu_default: true, has_params: true }],
|
|
554
|
+
other: [{ script: "target.custom.js", label: "Custom" }]
|
|
555
|
+
}]
|
|
556
|
+
|
|
557
|
+
for (const scenario of cases) {
|
|
558
|
+
const state = baseState({ targetScript: "" })
|
|
559
|
+
state.candidates.target.menu = scenario.menu
|
|
560
|
+
state.candidates.target.other = scenario.other
|
|
561
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
562
|
+
await page.goto(`${baseUrl}/app-fixture`)
|
|
563
|
+
await page.click("[data-app-autolaunch-button]")
|
|
564
|
+
await page.waitForSelector("[data-app-autolaunch-switch]")
|
|
565
|
+
|
|
566
|
+
await page.click("[data-app-autolaunch-switch]")
|
|
567
|
+
await page.waitForFunction(() => {
|
|
568
|
+
const feedback = document.querySelector("[data-app-autolaunch-feedback]")
|
|
569
|
+
return feedback && /Choose .*launch script/i.test(feedback.textContent || "")
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
assert.equal(state.autolaunchPosts.length, 0, scenario.name)
|
|
573
|
+
assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "OFF")
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
browserTest("browser: global startup toggle from empty selection saves the single eligible default", async () => {
|
|
579
|
+
const state = baseState({ targetScript: "" })
|
|
580
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
581
|
+
await page.goto(`${baseUrl}/autolaunch-fixture?app=target`)
|
|
582
|
+
await page.waitForSelector('input[name="autolaunch-script"][value="start.js"]')
|
|
583
|
+
|
|
584
|
+
assert.equal(await page.isChecked('input[name="autolaunch-script"][value="start.js"]'), false)
|
|
585
|
+
|
|
586
|
+
await page.click("[data-autolaunch-switch]")
|
|
587
|
+
await waitForState(
|
|
588
|
+
() => state.autolaunchPosts.length === 1,
|
|
589
|
+
"Global Start with Pinokio should save the single eligible default"
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
assert.deepEqual(state.autolaunchPosts[0], {
|
|
593
|
+
app: "target",
|
|
594
|
+
script: "start.js",
|
|
595
|
+
enabled: true
|
|
596
|
+
})
|
|
597
|
+
assert.equal(state.launchRequirementsGets, 0)
|
|
598
|
+
await page.waitForFunction(() => {
|
|
599
|
+
const input = document.querySelector('input[name="autolaunch-script"][value="start.js"]')
|
|
600
|
+
const enabledCount = document.querySelector("[data-enabled-count]")
|
|
601
|
+
return input && input.checked && enabledCount && enabledCount.textContent.trim() === "1"
|
|
602
|
+
})
|
|
603
|
+
assert.equal(await page.isChecked('input[name="autolaunch-script"][value="start.js"]'), true)
|
|
604
|
+
assert.equal((await textContent(page, "[data-enabled-count]")).trim(), "1")
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
browserTest("browser: global startup toggle from empty selection warns when no eligible default exists", async () => {
|
|
609
|
+
const cases = [{
|
|
610
|
+
name: "no default",
|
|
611
|
+
menu: [{ script: "start.js", label: "Start" }],
|
|
612
|
+
other: [{ script: "target.custom.js", label: "Custom" }]
|
|
613
|
+
}, {
|
|
614
|
+
name: "multiple defaults",
|
|
615
|
+
menu: [
|
|
616
|
+
{ script: "start.js", label: "Start", menu_default: true },
|
|
617
|
+
{ script: "target.custom.js", label: "Custom", menu_default: true }
|
|
618
|
+
],
|
|
619
|
+
other: []
|
|
620
|
+
}, {
|
|
621
|
+
name: "params default",
|
|
622
|
+
menu: [{ script: "start.js", label: "Start", menu_default: true, has_params: true }],
|
|
623
|
+
other: [{ script: "target.custom.js", label: "Custom" }]
|
|
624
|
+
}]
|
|
625
|
+
|
|
626
|
+
for (const scenario of cases) {
|
|
627
|
+
const state = baseState({ targetScript: "" })
|
|
628
|
+
state.candidates.target.menu = scenario.menu
|
|
629
|
+
state.candidates.target.other = scenario.other
|
|
630
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
631
|
+
await page.goto(`${baseUrl}/autolaunch-fixture?app=target`)
|
|
632
|
+
await page.waitForSelector("[data-manual-script]")
|
|
633
|
+
|
|
634
|
+
await page.click("[data-autolaunch-switch]")
|
|
635
|
+
await page.waitForFunction(() => {
|
|
636
|
+
const feedback = document.querySelector("[data-feedback]")
|
|
637
|
+
return feedback && /Choose .*launch script/i.test(feedback.textContent || "")
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
assert.equal(state.autolaunchPosts.length, 0, scenario.name)
|
|
641
|
+
assert.equal((await textContent(page, "[data-enabled-count]")).trim(), "0")
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
browserTest("browser: global startup toggle uses pending manual script before eligible default", async () => {
|
|
647
|
+
const state = baseState({ targetScript: "" })
|
|
648
|
+
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
649
|
+
await page.goto(`${baseUrl}/autolaunch-fixture?app=target`)
|
|
650
|
+
await page.waitForSelector("[data-manual-script]")
|
|
651
|
+
|
|
652
|
+
await page.fill("[data-manual-script]", "target.custom.js")
|
|
653
|
+
await page.click("[data-autolaunch-switch]")
|
|
654
|
+
await waitForState(
|
|
655
|
+
() => state.autolaunchPosts.length === 1,
|
|
656
|
+
"Global Start with Pinokio should save the pending manual script"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
assert.deepEqual(state.autolaunchPosts[0], {
|
|
660
|
+
app: "target",
|
|
661
|
+
script: "target.custom.js",
|
|
662
|
+
enabled: true
|
|
663
|
+
})
|
|
664
|
+
assert.equal(await page.isChecked('input[name="autolaunch-script"][value="start.js"]'), false)
|
|
665
|
+
await page.waitForFunction(() => {
|
|
666
|
+
const enabledCount = document.querySelector("[data-enabled-count]")
|
|
667
|
+
return enabledCount && enabledCount.textContent.trim() === "1"
|
|
668
|
+
})
|
|
669
|
+
assert.equal((await textContent(page, "[data-enabled-count]")).trim(), "1")
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
|
|
417
673
|
browserTest("browser: adding a requirement with an existing script still requires script confirmation", async () => {
|
|
418
674
|
const state = baseState({ targetScript: "target.custom.js", helperScript: "start.js" })
|
|
419
675
|
await withBrowser(state, async ({ page, baseUrl }) => {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const assert = require("assert")
|
|
2
|
+
const ejs = require("ejs")
|
|
2
3
|
const fs = require("node:fs")
|
|
3
4
|
const path = require("node:path")
|
|
4
5
|
const test = require("node:test")
|
|
5
6
|
|
|
7
|
+
const viewPath = path.join(__dirname, "..", "server", "views", "d.ejs")
|
|
8
|
+
|
|
6
9
|
test("dev iframe checks plugin install state before posting a launch request", () => {
|
|
7
|
-
const view = fs.readFileSync(
|
|
10
|
+
const view = fs.readFileSync(viewPath, "utf8")
|
|
8
11
|
const launchStart = view.indexOf("const launchTab = async (tab) =>")
|
|
9
12
|
const redirectIndex = view.indexOf("if (await redirectToPluginInstallIfNeeded(href))", launchStart)
|
|
10
13
|
const postMessageIndex = view.indexOf("window.parent.postMessage", launchStart)
|
|
@@ -15,3 +18,66 @@ test("dev iframe checks plugin install state before posting a launch request", (
|
|
|
15
18
|
assert.ok(view.includes("window.top.location.href = redirectHref"))
|
|
16
19
|
assert.ok(view.includes("/api/plugin/install-state?path="))
|
|
17
20
|
})
|
|
21
|
+
|
|
22
|
+
test("dev iframe renders plugin apps before project shell and shows description metadata", async () => {
|
|
23
|
+
const html = await ejs.renderFile(viewPath, {
|
|
24
|
+
filepath: "/tmp/pinokio-dev-view",
|
|
25
|
+
retry: false,
|
|
26
|
+
theme: "light",
|
|
27
|
+
agent: "browser",
|
|
28
|
+
dynamic: [{
|
|
29
|
+
icon: "fa-solid fa-terminal",
|
|
30
|
+
title: "Terminals",
|
|
31
|
+
subtitle: "Open a project shell, with or without Python activated.",
|
|
32
|
+
menu: [{
|
|
33
|
+
icon: "fa-solid fa-terminal",
|
|
34
|
+
title: "Bash",
|
|
35
|
+
subtitle: "Plain shell plus detected Python environments",
|
|
36
|
+
menu: [{
|
|
37
|
+
icon: "fa-solid fa-terminal",
|
|
38
|
+
title: "Shell",
|
|
39
|
+
subtitle: "Open a plain Bash shell",
|
|
40
|
+
href: "/shell/dev.bash.plain?path=%2Ftmp%2Fpinokio-dev-view"
|
|
41
|
+
}]
|
|
42
|
+
}]
|
|
43
|
+
}, {
|
|
44
|
+
icon: "fa-solid fa-plug-circle-bolt",
|
|
45
|
+
title: "Terminal Apps",
|
|
46
|
+
subtitle: "Terminal apps provided by your installed",
|
|
47
|
+
subtitle_link_href: "/plugins",
|
|
48
|
+
subtitle_link_label: "plugins",
|
|
49
|
+
menu: [{
|
|
50
|
+
icon: "fa-solid fa-terminal",
|
|
51
|
+
title: "Agent CLI",
|
|
52
|
+
description: "Runs the agent inside this workspace.",
|
|
53
|
+
href: "/run/plugin/agent"
|
|
54
|
+
}]
|
|
55
|
+
}, {
|
|
56
|
+
icon: "fa-solid fa-arrow-up-right-from-square",
|
|
57
|
+
title: "Desktop Apps",
|
|
58
|
+
subtitle: "Desktop apps provided by your installed",
|
|
59
|
+
subtitle_link_href: "/plugins",
|
|
60
|
+
subtitle_link_label: "plugins",
|
|
61
|
+
menu: [{
|
|
62
|
+
icon: "fa-solid fa-code",
|
|
63
|
+
title: "Editor",
|
|
64
|
+
metadata: {
|
|
65
|
+
description: "Opens the workspace in a desktop editor."
|
|
66
|
+
},
|
|
67
|
+
href: "/run/plugin/editor"
|
|
68
|
+
}]
|
|
69
|
+
}],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const terminalAppsIndex = html.indexOf("Terminal Apps")
|
|
73
|
+
const desktopAppsIndex = html.indexOf("Desktop Apps")
|
|
74
|
+
const projectShellIndex = html.indexOf("Project Shell")
|
|
75
|
+
|
|
76
|
+
assert.ok(terminalAppsIndex !== -1)
|
|
77
|
+
assert.ok(desktopAppsIndex !== -1)
|
|
78
|
+
assert.ok(projectShellIndex !== -1)
|
|
79
|
+
assert.ok(terminalAppsIndex < projectShellIndex)
|
|
80
|
+
assert.ok(desktopAppsIndex < projectShellIndex)
|
|
81
|
+
assert.ok(html.includes("Runs the agent inside this workspace."))
|
|
82
|
+
assert.ok(html.includes("Opens the workspace in a desktop editor."))
|
|
83
|
+
})
|
|
@@ -189,7 +189,7 @@ module.exports = {
|
|
|
189
189
|
const systemPlugins = menu.filter((item) => item.source === "system")
|
|
190
190
|
const localPlugins = menu.filter((item) => item.source === "local")
|
|
191
191
|
|
|
192
|
-
assert.strictEqual(systemPlugins.length,
|
|
192
|
+
assert.strictEqual(systemPlugins.length, 10)
|
|
193
193
|
assert.ok(systemPlugins.every((item) => item.system === true))
|
|
194
194
|
assert.ok(systemPlugins.every((item) => item.href.startsWith("/pinokio/run/plugin/")))
|
|
195
195
|
assert.ok(systemPlugins.every((item) => item.image.startsWith("/pinokio/asset/plugin/")))
|
|
@@ -0,0 +1,107 @@
|
|
|
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 dropdownScriptPath = path.resolve(root, "server/public/urldropdown.js")
|
|
9
|
+
|
|
10
|
+
function jsonResponse(payload) {
|
|
11
|
+
return {
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
statusText: "OK",
|
|
15
|
+
json: async () => payload
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function waitFor(predicate, message = "condition") {
|
|
20
|
+
const deadline = Date.now() + 2000
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
if (predicate()) return
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
24
|
+
}
|
|
25
|
+
assert.fail(`Timed out waiting for ${message}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function createDropdownDom() {
|
|
29
|
+
const script = await fs.readFile(dropdownScriptPath, "utf8")
|
|
30
|
+
const virtualConsole = new VirtualConsole()
|
|
31
|
+
virtualConsole.on("jsdomError", (error) => {
|
|
32
|
+
throw error
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const dom = new JSDOM(`<!doctype html>
|
|
36
|
+
<html>
|
|
37
|
+
<head><title>Current app</title></head>
|
|
38
|
+
<body>
|
|
39
|
+
<form class="urlbar"><input type="url"></form>
|
|
40
|
+
<div id="url-dropdown"></div>
|
|
41
|
+
<script>${script}</script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>`, {
|
|
44
|
+
url: "http://127.0.0.1:42000/p/current",
|
|
45
|
+
runScripts: "dangerously",
|
|
46
|
+
resources: "usable",
|
|
47
|
+
pretendToBeVisual: true,
|
|
48
|
+
virtualConsole,
|
|
49
|
+
beforeParse(window) {
|
|
50
|
+
window.fetch = async (url) => {
|
|
51
|
+
if (String(url).includes("/info/apps")) {
|
|
52
|
+
return jsonResponse({
|
|
53
|
+
apps: [
|
|
54
|
+
{ name: "MMAudio", title: "MMAudio" },
|
|
55
|
+
{ name: "AllTalk-TTS", title: "AllTalk-TTS v2" }
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
if (String(url).includes("/info/procs")) {
|
|
60
|
+
return jsonResponse({ info: [] })
|
|
61
|
+
}
|
|
62
|
+
return jsonResponse({})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const { window } = dom
|
|
68
|
+
await new Promise((resolve) => window.addEventListener("DOMContentLoaded", resolve, { once: true }))
|
|
69
|
+
window.initUrlDropdown()
|
|
70
|
+
return dom
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test("split URL modal uses inline submit and omits Files actions", async () => {
|
|
74
|
+
const dom = await createDropdownDom()
|
|
75
|
+
const { window } = dom
|
|
76
|
+
const { document } = window
|
|
77
|
+
|
|
78
|
+
const selection = window.PinokioUrlDropdown.openSplitModal({
|
|
79
|
+
title: "Split Into Columns",
|
|
80
|
+
description: "Choose a running process or use the current tab URL for the new pane.",
|
|
81
|
+
confirmLabel: "Open in pane"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await waitFor(() => document.querySelector(".url-modal-content.split-mode .url-mode-button"), "split modal app rows")
|
|
85
|
+
|
|
86
|
+
const overlay = document.getElementById("url-modal-overlay")
|
|
87
|
+
const content = document.querySelector(".url-modal-content")
|
|
88
|
+
const inlineSubmit = document.querySelector(".url-modal-inline-submit")
|
|
89
|
+
const actions = document.querySelector(".url-modal-actions")
|
|
90
|
+
const modeLabels = Array.from(document.querySelectorAll(".url-modal-content.split-mode .url-mode-button span"))
|
|
91
|
+
.map((node) => node.textContent)
|
|
92
|
+
|
|
93
|
+
assert.equal(overlay.classList.contains("split-mode"), true)
|
|
94
|
+
assert.equal(content.classList.contains("split-mode"), true)
|
|
95
|
+
assert.equal(inlineSubmit.textContent, "Open in pane")
|
|
96
|
+
assert.ok(actions)
|
|
97
|
+
assert.deepEqual([...new Set(modeLabels)].sort(), ["Dev", "Run"])
|
|
98
|
+
assert.doesNotMatch(content.textContent, /\bFiles\b/)
|
|
99
|
+
|
|
100
|
+
const input = document.querySelector(".url-modal-input")
|
|
101
|
+
input.value = "http://localhost:7860"
|
|
102
|
+
input.dispatchEvent(new window.Event("input", { bubbles: true }))
|
|
103
|
+
assert.equal(inlineSubmit.disabled, false)
|
|
104
|
+
inlineSubmit.click()
|
|
105
|
+
|
|
106
|
+
assert.equal(await selection, "/container?url=http%3A%2F%2Flocalhost%3A7860")
|
|
107
|
+
})
|
|
Binary file
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
title: "Crush",
|
|
3
|
-
icon: "crush.png",
|
|
4
|
-
link: "https://github.com/charmbracelet/crush",
|
|
5
|
-
run: [{
|
|
6
|
-
id: "run",
|
|
7
|
-
method: "shell.run",
|
|
8
|
-
params: {
|
|
9
|
-
message: "npx -y @charmland/crush@latest",
|
|
10
|
-
path: "{{args.cwd}}",
|
|
11
|
-
buffer: 1024,
|
|
12
|
-
input: true
|
|
13
|
-
}
|
|
14
|
-
}]
|
|
15
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
title: "Qwen Code",
|
|
3
|
-
link: "https://github.com/QwenLM/qwen-code",
|
|
4
|
-
icon: "qwen.png",
|
|
5
|
-
env: [{
|
|
6
|
-
key: "OPENAI_API_KEY",
|
|
7
|
-
default: "OPENAI_API_KEY"
|
|
8
|
-
}, {
|
|
9
|
-
key: "OPENAI_BASE_URL",
|
|
10
|
-
description: "use the OpenAI API compatible api endpoint",
|
|
11
|
-
default: "http://localhost:1234/v1"
|
|
12
|
-
}, {
|
|
13
|
-
key: "OPENAI_MODEL",
|
|
14
|
-
description: "the openai compatible model",
|
|
15
|
-
default: "mradermacher/Bootes-Qwen3_Coder-Reasoning-i1-GGUF"
|
|
16
|
-
}],
|
|
17
|
-
run: [{
|
|
18
|
-
id: "run",
|
|
19
|
-
method: "shell.run",
|
|
20
|
-
params: {
|
|
21
|
-
message: {
|
|
22
|
-
_: [
|
|
23
|
-
"npx",
|
|
24
|
-
"-y",
|
|
25
|
-
"@qwen-code/qwen-code@latest"
|
|
26
|
-
],
|
|
27
|
-
"prompt-interactive": "{{args.prompt || undefined}}"
|
|
28
|
-
},
|
|
29
|
-
path: "{{args.cwd}}",
|
|
30
|
-
buffer: 1024,
|
|
31
|
-
input: true
|
|
32
|
-
}
|
|
33
|
-
}]
|
|
34
|
-
}
|
|
Binary file
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
title: "Windsurf",
|
|
3
|
-
link: "https://windsurf.com/",
|
|
4
|
-
icon: "windsurf.png",
|
|
5
|
-
description: "The AI Code Editor",
|
|
6
|
-
launch_type: "desktop",
|
|
7
|
-
run: [{
|
|
8
|
-
method: "uri.open",
|
|
9
|
-
params: {
|
|
10
|
-
uri: "windsurf://cascade/newChat",
|
|
11
|
-
params: {
|
|
12
|
-
prompt: "{{args.prompt || ''}}",
|
|
13
|
-
folder: "{{args.cwd || ''}}"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}, {
|
|
17
|
-
method: "process.wait",
|
|
18
|
-
params: {
|
|
19
|
-
title: "Launched",
|
|
20
|
-
description: "Click the stop button to stop watching file changes"
|
|
21
|
-
}
|
|
22
|
-
}]
|
|
23
|
-
}
|
|
Binary file
|