pinokiod 7.5.38 → 7.5.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.38",
3
+ "version": "7.5.40",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16183,10 +16183,20 @@ const rerenderMenuSection = (container, html) => {
16183
16183
  }
16184
16184
 
16185
16185
  const desktopPeekMq = window.matchMedia("(min-width: 769px) and (hover: hover) and (pointer: fine)")
16186
+ const shouldAutoPeekOnIdle = <%- JSON.stringify(type === "run") %>
16186
16187
  let peekCloseTimer = 0
16187
16188
 
16188
16189
  const isCollapsed = () => appcanvas.classList.contains("sidebar-collapsed")
16189
16190
  const isPeeking = () => appcanvas.classList.contains("sidebar-peeking")
16191
+ const hasVisibleBrowserSurface = () => {
16192
+ const browserview = document.querySelector("main.browserview")
16193
+ if (!browserview) return false
16194
+ if (browserview.querySelector("iframe:not(.hidden)")) return true
16195
+ const networkStatus = document.getElementById("browserview-network-status")
16196
+ if (networkStatus && !networkStatus.hidden) return true
16197
+ const launchStatus = document.querySelector("[data-launch-requirements-status]")
16198
+ return Boolean(launchStatus && !launchStatus.hidden)
16199
+ }
16190
16200
  const syncSidebarVisibility = () => {
16191
16201
  const peeking = isPeeking()
16192
16202
  aside.setAttribute("aria-hidden", isCollapsed() && !peeking ? "true" : "false")
@@ -16243,6 +16253,11 @@ const rerenderMenuSection = (container, html) => {
16243
16253
  setCollapsed(initialCollapsed, { persist: false })
16244
16254
  requestAnimationFrame(() => {
16245
16255
  appcanvas.classList.add("sidebar-motion-ready")
16256
+ requestAnimationFrame(() => {
16257
+ if (shouldAutoPeekOnIdle && initialCollapsed && !hasVisibleBrowserSurface()) {
16258
+ setPeeking(true)
16259
+ }
16260
+ })
16246
16261
  })
16247
16262
 
16248
16263
  toggle.addEventListener("click", (event) => {
@@ -16268,6 +16283,10 @@ const rerenderMenuSection = (container, html) => {
16268
16283
  })
16269
16284
  aside.addEventListener("pointerleave", closePeekSoon)
16270
16285
  aside.addEventListener("focusout", closePeekSoon)
16286
+ aside.addEventListener("click", () => {
16287
+ if (!isPeeking()) return
16288
+ window.setTimeout(() => setPeeking(false), 0)
16289
+ })
16271
16290
  document.addEventListener("pointerdown", (event) => {
16272
16291
  if (!isPeeking()) return
16273
16292
  const target = event.target
@@ -865,7 +865,7 @@ body.dark .menu-btns .toggle-star.is-starred {
865
865
  padding: 0;
866
866
  border: 1px solid rgba(15, 23, 42, 0.14);
867
867
  border-radius: 8px;
868
- background: rgba(255, 255, 255, 0.98);
868
+ background: var(--home-page-nav-bg, #ffffff);
869
869
  color: rgba(15, 23, 42, 0.94);
870
870
  box-shadow: 0 24px 70px rgba(15, 23, 42, 0.24);
871
871
  }
@@ -886,7 +886,7 @@ body.dark .menu-btns .toggle-star.is-starred {
886
886
  }
887
887
  body.dark .home-actions-dialog {
888
888
  border-color: rgba(255, 255, 255, 0.12);
889
- background: rgba(18, 20, 25, 0.98);
889
+ background: var(--home-page-nav-bg, #1b1c1d);
890
890
  color: rgba(248, 250, 252, 0.94);
891
891
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55);
892
892
  }
@@ -1125,6 +1125,35 @@ body.dark .home-mode-command {
1125
1125
  font-size: 10px;
1126
1126
  opacity: 0.6;
1127
1127
  }
1128
+ .home-run-command.has-block-label,
1129
+ .home-run-menu-summary.has-block-label {
1130
+ min-height: 52px;
1131
+ padding-top: 8px;
1132
+ padding-bottom: 8px;
1133
+ }
1134
+ .home-run-menu-label {
1135
+ display: flex;
1136
+ align-items: center;
1137
+ gap: 9px;
1138
+ }
1139
+ .home-run-menu-label.has-block-content {
1140
+ align-items: flex-start;
1141
+ white-space: normal;
1142
+ line-height: 1.18;
1143
+ }
1144
+ .home-run-menu-label.has-block-content > i:first-child {
1145
+ margin-top: 1px;
1146
+ }
1147
+ .home-run-menu-label.has-block-content > div {
1148
+ min-width: 0;
1149
+ display: flex;
1150
+ flex-direction: column;
1151
+ gap: 2px;
1152
+ }
1153
+ .home-run-menu-label.has-block-content > div > strong,
1154
+ .home-run-menu-label.has-block-content > div > div {
1155
+ line-height: 1.18;
1156
+ }
1128
1157
  .home-run-stop {
1129
1158
  width: 38px;
1130
1159
  min-height: 38px;
@@ -1,3 +1,15 @@
1
+ <%
2
+ const homeActionsAppId = item && (item.uri || item.path || item.name) ? String(item.uri || item.path || item.name) : ""
3
+ const homeActionsSettingsHref = homeActionsAppId
4
+ ? `/v/${encodeURIComponent(homeActionsAppId)}?${new URLSearchParams({
5
+ pinokio_home_select: JSON.stringify({
6
+ target: "env-settings",
7
+ hrefAttr: `/env/api/${homeActionsAppId}?host=app`,
8
+ textValue: "Settings"
9
+ })
10
+ }).toString()}`
11
+ : ""
12
+ %>
1
13
  <dialog id='<%= id %>' class='home-actions-dialog' aria-labelledby='<%= id %>-title'>
2
14
  <div class='home-actions-shell'>
3
15
  <div class='home-actions-header'>
@@ -49,6 +61,12 @@
49
61
  <span class='home-actions-count'><%= item.terminal_online_count %> online</span>
50
62
  <% } %>
51
63
  </button>
64
+ <% if (homeActionsSettingsHref) { %>
65
+ <a class='home-mode-command' href='<%= homeActionsSettingsHref %>'>
66
+ <i class='fa-solid fa-gear' aria-hidden='true'></i>
67
+ <span>Settings</span>
68
+ </a>
69
+ <% } %>
52
70
  <button type='button' class='home-mode-command' data-filepath='<%= item.filepath %>'>
53
71
  <i class='fa-solid fa-folder-open' aria-hidden='true'></i>
54
72
  <span>Open in file explorer</span>
@@ -9,6 +9,7 @@ const stripHtml = (value) => String(value ?? "")
9
9
  .replace(/<[^>]*>/g, " ")
10
10
  .replace(/\s+/g, " ")
11
11
  .trim()
12
+ const hasBlockContent = (value) => /<\s*(div|p|section|article|ul|ol|li|h[1-6])(?:\s|>)/i.test(String(value ?? ""))
12
13
  const buildHomeSelectionPayload = (menuItem, index) => {
13
14
  const dataFilepath = (menuItem.fs || menuItem.command) ? (menuItem.href || "") : ""
14
15
  const payload = {
@@ -49,9 +50,10 @@ const buildHomeSelectionUrl = (menuItem, index) => {
49
50
  %>
50
51
  <% menu.forEach((item, index) => { %>
51
52
  <% if (item && item.menu) { %>
53
+ <% const itemBtnHasBlock = hasBlockContent(item.btn) %>
52
54
  <details class='home-run-menu-group' open>
53
- <summary class='home-run-menu-summary'>
54
- <span class='home-run-menu-label'><%- item.btn %></span>
55
+ <summary class='home-run-menu-summary <%= itemBtnHasBlock ? "has-block-label" : "" %>'>
56
+ <div class='home-run-menu-label <%= itemBtnHasBlock ? "has-block-content" : "" %>'><%- item.btn %></div>
55
57
  <i class='fa-solid fa-chevron-down' aria-hidden='true'></i>
56
58
  </summary>
57
59
  <div class='home-run-menu-children'>
@@ -60,13 +62,14 @@ const buildHomeSelectionUrl = (menuItem, index) => {
60
62
  </details>
61
63
  <% } else if (item && item.btn) { %>
62
64
  <% const openUrl = buildHomeSelectionUrl(item, index) %>
65
+ <% const itemBtnHasBlock = hasBlockContent(item.btn) %>
63
66
  <div class='home-run-menu-row <%= item.running ? "is-running" : "" %>'>
64
67
  <a
65
- class='home-run-command'
68
+ class='home-run-command <%= itemBtnHasBlock ? "has-block-label" : "" %>'
66
69
  href='<%= openUrl %>'
67
70
  <% if (item.confirm) { %>data-confirm="<%- escapeAttr(item.confirm) %>"<% } %>
68
71
  >
69
- <span class='home-run-menu-label'><%- item.btn %></span>
72
+ <div class='home-run-menu-label <%= itemBtnHasBlock ? "has-block-content" : "" %>'><%- item.btn %></div>
70
73
  <% if (item.running) { %><span class='home-run-menu-status'>Running</span><% } %>
71
74
  <i class='fa-solid fa-chevron-right' aria-hidden='true'></i>
72
75
  </a>
@@ -2,6 +2,8 @@ const assert = require("node:assert/strict")
2
2
  const fs = require("node:fs/promises")
3
3
  const path = require("node:path")
4
4
  const test = require("node:test")
5
+ const ejs = require("ejs")
6
+ const { JSDOM } = require("jsdom")
5
7
 
6
8
  const root = path.resolve(__dirname, "..")
7
9
 
@@ -370,6 +372,11 @@ test("static guard: open without launching is not wired to launch requirement st
370
372
  assert.match(homeActionModal, /Open without launching/)
371
373
  assert.match(homeActionModal, /class='home-mode-command home-browse' data-src='<%= item\.view_url %>'/)
372
374
  assert.match(homeActionModal, /Open dev mode/)
375
+ assert.match(homeActionModal, /const homeActionsAppId = item && \(item\.uri \|\| item\.path \|\| item\.name\)/)
376
+ assert.match(homeActionModal, /pinokio_home_select: JSON\.stringify/)
377
+ assert.match(homeActionModal, /target: "env-settings"/)
378
+ assert.match(homeActionModal, /href='<%= homeActionsSettingsHref %>'/)
379
+ assert.match(homeActionModal, /Settings/)
373
380
  assert.doesNotMatch(homeActionModal, /home-actions-tabs|home-actions-tab|role='tab'|role='tabpanel'/)
374
381
  assert.doesNotMatch(homeActionModal, /Open files mode/)
375
382
  assert.doesNotMatch(homeActionModal, /launch.requirements|data-launch-requirements|frame-link/)
@@ -378,6 +385,66 @@ test("static guard: open without launching is not wired to launch requirement st
378
385
  assert.doesNotMatch(statusClient, /saved requirement/)
379
386
  })
380
387
 
388
+ test("home actions modal renders an app settings link", async () => {
389
+ const html = await ejs.renderFile(path.resolve(root, "server/views/partials/home_action_modal.ejs"), {
390
+ id: "home-actions-test",
391
+ item: {
392
+ icon: "",
393
+ name: "Test App",
394
+ filepath: "/tmp/pinokio/api/test app.git",
395
+ uri: "test app.git",
396
+ menu: [],
397
+ view_url: "/v/test%20app.git",
398
+ dev_url: "/p/test%20app.git/dev",
399
+ url: "/p/test%20app.git",
400
+ terminal_online_count: 0,
401
+ running: false
402
+ }
403
+ })
404
+
405
+ const hrefMatch = html.match(/href='([^']+)'/)
406
+ assert.ok(hrefMatch)
407
+ const href = new URL(hrefMatch[1], "http://127.0.0.1")
408
+ assert.equal(href.pathname, "/v/test%20app.git")
409
+ const payload = JSON.parse(href.searchParams.get("pinokio_home_select"))
410
+ assert.deepEqual(payload, {
411
+ target: "env-settings",
412
+ hrefAttr: "/env/api/test app.git?host=app",
413
+ textValue: "Settings"
414
+ })
415
+ assert.match(html, />\s*Settings\s*</)
416
+ })
417
+
418
+ test("home actions drawer background follows the home page surface", async () => {
419
+ const homeView = await fs.readFile(path.resolve(root, "server/views/index.ejs"), "utf8")
420
+
421
+ assert.match(homeView, /body\.is-home \{[\s\S]*--home-page-nav-bg: #ffffff;[\s\S]*background: #ffffff;/)
422
+ assert.match(homeView, /body\.dark\.is-home \{[\s\S]*--home-page-nav-bg: #1b1c1d;[\s\S]*background: #1b1c1d;/)
423
+ assert.match(homeView, /\.home-actions-dialog \{[\s\S]*background: var\(--home-page-nav-bg, #ffffff\);/)
424
+ assert.match(homeView, /body\.dark \.home-actions-dialog \{[\s\S]*background: var\(--home-page-nav-bg, #1b1c1d\);/)
425
+ assert.doesNotMatch(homeView, /body\.dark \.home-actions-dialog \{[\s\S]*background: rgba\(18, 20, 25, 0\.98\);/)
426
+ })
427
+
428
+ test("home run menu gives block labels valid padded layout", async () => {
429
+ const homeView = await fs.readFile(path.resolve(root, "server/views/index.ejs"), "utf8")
430
+ const html = await ejs.renderFile(path.resolve(root, "server/views/partials/home_run_menu.ejs"), {
431
+ app: {
432
+ view_url: "/v/test-app"
433
+ },
434
+ menu: [{
435
+ icon: "fa-regular fa-circle-xmark",
436
+ btn: '<i class="fa-regular fa-circle-xmark"></i><div><strong>Reset</strong><div>Revert to pre-install state</div></div>',
437
+ href: "reset.js"
438
+ }]
439
+ })
440
+
441
+ assert.match(html, /class='home-run-command has-block-label'/)
442
+ assert.match(html, /<div class='home-run-menu-label has-block-content'>/)
443
+ assert.doesNotMatch(html, /<span class='home-run-menu-label/)
444
+ assert.match(homeView, /\.home-run-command\.has-block-label,[\s\S]*\.home-run-menu-summary\.has-block-label \{[\s\S]*padding-top: 8px;[\s\S]*padding-bottom: 8px;/)
445
+ assert.match(homeView, /\.home-run-menu-label\.has-block-content \{[\s\S]*white-space: normal;/)
446
+ })
447
+
381
448
  test("static guard: open without launching disables page-load script frame selection", async () => {
382
449
  const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
383
450
 
@@ -392,6 +459,84 @@ test("static guard: open without launching disables page-load script frame selec
392
459
  assert.match(appView, /if \(!target && !automaticSelectionDisabled && preselected && preselected !== devTab\)/)
393
460
  })
394
461
 
462
+ test("static guard: collapsed idle run page peeks without persisting sidebar state", async () => {
463
+ const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
464
+
465
+ assert.match(appView, /const shouldAutoPeekOnIdle = <%- JSON\.stringify\(type === "run"\) %>/)
466
+ assert.match(appView, /const hasVisibleBrowserSurface = \(\) => \{[\s\S]*browserview\.querySelector\("iframe:not\(\.hidden\)"\)[\s\S]*browserview-network-status[\s\S]*data-launch-requirements-status/)
467
+ assert.match(appView, /if \(shouldAutoPeekOnIdle && initialCollapsed && !hasVisibleBrowserSurface\(\)\) \{[\s\S]*setPeeking\(true\)/)
468
+ assert.match(appView, /setCollapsed\(initialCollapsed, \{ persist: false \}\)/)
469
+ assert.match(appView, /aside\.addEventListener\("click", \(\) => \{[\s\S]*window\.setTimeout\(\(\) => setPeeking\(false\), 0\)/)
470
+ })
471
+
472
+ test("app sidebar auto-peeks on collapsed idle run page and dismisses after click", async () => {
473
+ const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
474
+ const start = appView.indexOf('<script>\n(() => {\n const appcanvas = document.querySelector(".appcanvas")')
475
+ const end = appView.indexOf('</script>\n<script src="/tab-idle-notifier.js"></script>', start)
476
+ assert.notEqual(start, -1)
477
+ assert.notEqual(end, -1)
478
+
479
+ const sidebarScript = appView
480
+ .slice(start + "<script>\n".length, end)
481
+ .replace(/<%- JSON\.stringify\(typeof name === "string" \? name : ""\) %>/g, '"test-app"')
482
+ .replace(/<%- JSON\.stringify\(type === "run"\) %>/g, "true")
483
+
484
+ const createDom = (browserSurface = "") => {
485
+ return new JSDOM(`<!doctype html>
486
+ <button id="sidebar-toggle"></button>
487
+ <div class="appcanvas vertical">
488
+ <button type="button" data-app-sidebar-peek-trigger></button>
489
+ <aside id="app-sidebar"><button type="button" id="sidebar-action">Start</button></aside>
490
+ <main class="browserview">${browserSurface}</main>
491
+ </div>
492
+ <div id="browserview-network-status" hidden></div>
493
+ <div data-launch-requirements-status hidden></div>`, {
494
+ url: "http://127.0.0.1:42000/v/test-app",
495
+ runScripts: "outside-only",
496
+ pretendToBeVisual: true,
497
+ beforeParse(window) {
498
+ window.matchMedia = () => ({
499
+ matches: true,
500
+ addEventListener() {},
501
+ removeEventListener() {}
502
+ })
503
+ }
504
+ })
505
+ }
506
+
507
+ const wait = (ms = 75) => new Promise((resolve) => setTimeout(resolve, ms))
508
+ const storageKey = "pinokio.sidebar-collapsed:test-app"
509
+
510
+ const dom = createDom()
511
+ dom.window.localStorage.setItem(storageKey, "1")
512
+ dom.window.eval(sidebarScript)
513
+ await wait()
514
+
515
+ const appcanvas = dom.window.document.querySelector(".appcanvas")
516
+ const aside = dom.window.document.getElementById("app-sidebar")
517
+ assert.equal(appcanvas.classList.contains("sidebar-collapsed"), true)
518
+ assert.equal(appcanvas.classList.contains("sidebar-peeking"), true)
519
+ assert.equal(aside.getAttribute("aria-hidden"), "false")
520
+ assert.equal(dom.window.localStorage.getItem(storageKey), "1")
521
+
522
+ aside.querySelector("#sidebar-action").dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }))
523
+ await wait(5)
524
+
525
+ assert.equal(appcanvas.classList.contains("sidebar-collapsed"), true)
526
+ assert.equal(appcanvas.classList.contains("sidebar-peeking"), false)
527
+ assert.equal(dom.window.localStorage.getItem(storageKey), "1")
528
+
529
+ const activeDom = createDom('<iframe src="about:blank"></iframe>')
530
+ activeDom.window.localStorage.setItem(storageKey, "1")
531
+ activeDom.window.eval(sidebarScript)
532
+ await wait()
533
+
534
+ const activeCanvas = activeDom.window.document.querySelector(".appcanvas")
535
+ assert.equal(activeCanvas.classList.contains("sidebar-collapsed"), true)
536
+ assert.equal(activeCanvas.classList.contains("sidebar-peeking"), false)
537
+ assert.equal(activeDom.window.localStorage.getItem(storageKey), "1")
538
+ })
539
+
395
540
  test("static guard: home run command selection opens without launching before selecting command", async () => {
396
541
  const homeRunMenu = await fs.readFile(path.resolve(root, "server/views/partials/home_run_menu.ejs"), "utf8")
397
542
 
@@ -949,6 +949,81 @@ test('Windows cmd suppresses echo only for explicit conda shell.run commands', (
949
949
  }, command), { command })
950
950
  })
951
951
 
952
+ test('Windows cmd array shell.run marks only the conda launch quiet', async () => {
953
+ const { context, root } = createContext('win32')
954
+ const records = []
955
+ const originalPrompt = Shell.prototype.prompt
956
+ const originalExec = Shell.prototype.exec
957
+ Shell.prototype.prompt = async () => 'PINOKIO_PROMPT'
958
+ Shell.prototype.exec = async function (params) {
959
+ this.platform = 'win32'
960
+ this.shell = 'cmd.exe'
961
+ const originalParams = {
962
+ input: params && params.input,
963
+ message: params && params.message,
964
+ }
965
+ const activatedParams = await this.activate(params)
966
+ const command = this.build(activatedParams)
967
+ records.push({
968
+ message: originalParams.message,
969
+ prepared: this.prepareCommandExecution(originalParams, command),
970
+ })
971
+ return command
972
+ }
973
+
974
+ try {
975
+ const kernel = {
976
+ homedir: root,
977
+ platform: 'win32',
978
+ bracketedPasteSupport: { 'cmd.exe': false },
979
+ envs: {},
980
+ exists: async () => false,
981
+ which: () => null,
982
+ path: (...parts) => path.join(root, ...parts),
983
+ bin: {
984
+ envs: (env = {}) => env,
985
+ path: (...parts) => path.join(root, 'bin', ...parts),
986
+ activationCommands: () => [],
987
+ },
988
+ api: {
989
+ resolvePath: (_cwd, target) => target,
990
+ running: {},
991
+ },
992
+ git: {
993
+ repos: async () => [],
994
+ restoreNewReposForActiveSnapshot: async () => {},
995
+ },
996
+ template: {
997
+ render: (value) => value,
998
+ },
999
+ }
1000
+ kernel.shell = new Shells(kernel)
1001
+
1002
+ await kernel.shell.run({
1003
+ path: context.appPath,
1004
+ message: [
1005
+ 'echo before',
1006
+ 'conda install -y -c conda-forge ffmpeg',
1007
+ 'python main.py',
1008
+ ],
1009
+ }, {}, () => {})
1010
+
1011
+ assert.deepEqual(records.map((record) => record.message), [
1012
+ 'echo before',
1013
+ 'conda install -y -c conda-forge ffmpeg',
1014
+ 'python main.py',
1015
+ ])
1016
+ assert.deepEqual(records.map((record) => !!record.prepared.quietCmd), [
1017
+ false,
1018
+ true,
1019
+ false,
1020
+ ])
1021
+ } finally {
1022
+ Shell.prototype.prompt = originalPrompt
1023
+ Shell.prototype.exec = originalExec
1024
+ }
1025
+ })
1026
+
952
1027
  test('rewrite warning is scoped to each user-visible shell flow', async () => {
953
1028
  CondaRuntimeGuard.resetNoticeSessionsForTest()
954
1029
  const { context, events, root } = createContext()