pinokiod 3.181.0 → 3.183.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/kernel/util.js +15 -2
  2. package/package.json +1 -1
  3. package/server/index.js +111 -12
  4. package/server/public/common.js +433 -240
  5. package/server/public/files-app/app.css +64 -0
  6. package/server/public/files-app/app.js +87 -0
  7. package/server/public/install.js +8 -1
  8. package/server/public/layout.js +11 -1
  9. package/server/public/sound/beep.mp3 +0 -0
  10. package/server/public/sound/bell.mp3 +0 -0
  11. package/server/public/sound/bright-ring.mp3 +0 -0
  12. package/server/public/sound/clap.mp3 +0 -0
  13. package/server/public/sound/deep-ring.mp3 +0 -0
  14. package/server/public/sound/gasp.mp3 +0 -0
  15. package/server/public/sound/hehe.mp3 +0 -0
  16. package/server/public/sound/levelup.mp3 +0 -0
  17. package/server/public/sound/light-pop.mp3 +0 -0
  18. package/server/public/sound/light-ring.mp3 +0 -0
  19. package/server/public/sound/meow.mp3 +0 -0
  20. package/server/public/sound/piano.mp3 +0 -0
  21. package/server/public/sound/pop.mp3 +0 -0
  22. package/server/public/sound/uhoh.mp3 +0 -0
  23. package/server/public/sound/whistle.mp3 +0 -0
  24. package/server/public/style.css +173 -2
  25. package/server/public/tab-idle-notifier.js +697 -4
  26. package/server/public/terminal-settings.js +1131 -0
  27. package/server/public/urldropdown.css +28 -1
  28. package/server/views/{terminals.ejs → agents.ejs} +98 -30
  29. package/server/views/app.ejs +1 -0
  30. package/server/views/bootstrap.ejs +8 -0
  31. package/server/views/connect/x.ejs +1 -0
  32. package/server/views/connect.ejs +2 -1
  33. package/server/views/container.ejs +1 -0
  34. package/server/views/d.ejs +172 -18
  35. package/server/views/download.ejs +1 -0
  36. package/server/views/editor.ejs +8 -0
  37. package/server/views/explore.ejs +1 -0
  38. package/server/views/file_browser.ejs +4 -0
  39. package/server/views/form.ejs +1 -0
  40. package/server/views/frame.ejs +1 -0
  41. package/server/views/github.ejs +1 -0
  42. package/server/views/help.ejs +1 -0
  43. package/server/views/index.ejs +2 -1
  44. package/server/views/init/index.ejs +10 -1
  45. package/server/views/install.ejs +8 -0
  46. package/server/views/mini.ejs +1 -0
  47. package/server/views/net.ejs +2 -1
  48. package/server/views/network.ejs +2 -1
  49. package/server/views/pro.ejs +8 -0
  50. package/server/views/prototype/index.ejs +9 -0
  51. package/server/views/review.ejs +1 -0
  52. package/server/views/screenshots.ejs +2 -2
  53. package/server/views/settings.ejs +2 -2
  54. package/server/views/setup.ejs +1 -0
  55. package/server/views/setup_home.ejs +1 -0
  56. package/server/views/shell.ejs +8 -0
  57. package/server/views/terminal.ejs +8 -0
  58. package/server/views/tools.ejs +2 -2
package/kernel/util.js CHANGED
@@ -664,7 +664,18 @@ function push(params) {
664
664
  if (HTTP_URL_REGEX.test(trimmed)) {
665
665
  clientSoundUrl = trimmed
666
666
  } else {
667
- console.warn(`Ignoring notification sound (expected http/https URL): ${trimmed}`)
667
+ let normalised = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
668
+ let decoded = normalised
669
+ try {
670
+ decoded = decodeURIComponent(normalised)
671
+ } catch (_) {
672
+ // Ignore decode errors; fall back to original string
673
+ }
674
+ if (decoded.startsWith('/sound/') && !decoded.includes('..')) {
675
+ clientSoundUrl = normalised
676
+ } else {
677
+ console.warn(`Ignoring notification sound (expected http/https URL or /sound asset): ${trimmed}`)
678
+ }
668
679
  }
669
680
  }
670
681
 
@@ -700,13 +711,15 @@ function push(params) {
700
711
  const clientImage = resolvePublicAssetUrl(notifyParams.contentImage) || resolvePublicAssetUrl(notifyParams.image)
701
712
  const deviceId = (typeof notifyParams.device_id === 'string' && notifyParams.device_id.trim()) ? notifyParams.device_id.trim() : null
702
713
  const audience = (typeof notifyParams.audience === 'string' && notifyParams.audience.trim()) ? notifyParams.audience.trim() : null
714
+ const clientEventSound = requestedSound === false ? false : (clientSoundUrl || null)
715
+
703
716
  const eventPayload = {
704
717
  id: randomUUID(),
705
718
  title: notifyParams.title,
706
719
  subtitle: notifyParams.subtitle || null,
707
720
  message: notifyParams.message || '',
708
721
  image: clientImage,
709
- sound: clientSoundUrl || null,
722
+ sound: clientEventSound,
710
723
  timestamp: Date.now(),
711
724
  platform,
712
725
  device_id: deviceId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.181.0",
3
+ "version": "3.183.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -38,6 +38,7 @@ const ini = require('ini')
38
38
  const ejs = require('ejs');
39
39
 
40
40
  const DEFAULT_PORT = 42000
41
+ const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.webm'])
41
42
 
42
43
  const ex = fn => (req, res, next) => {
43
44
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -3444,8 +3445,8 @@ class Server {
3444
3445
  }
3445
3446
  terminal = {
3446
3447
  icon: "fa-solid fa-terminal",
3447
- title: "User Terminal",
3448
- subtitle: "Open the terminal in the browser",
3448
+ title: "Shell",
3449
+ subtitle: "Open an interactive terminal in the browser",
3449
3450
  menu: terminals
3450
3451
  }
3451
3452
  } else {
@@ -3912,6 +3913,46 @@ class Server {
3912
3913
  getTheme: () => this.theme,
3913
3914
  exists: (target) => this.exists(target),
3914
3915
  });
3916
+
3917
+ this.app.get('/pinokio/notification-sounds', ex(async (req, res) => {
3918
+ const soundRoot = path.resolve(__dirname, 'public', 'sound');
3919
+ let entries = [];
3920
+ try {
3921
+ const dirEntries = await fs.promises.readdir(soundRoot, { withFileTypes: true });
3922
+ entries = dirEntries
3923
+ .filter((entry) => entry.isFile())
3924
+ .map((entry) => entry.name)
3925
+ .filter((name) => NOTIFICATION_SOUND_EXTENSIONS.has(path.extname(name).toLowerCase()))
3926
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
3927
+ } catch (error) {
3928
+ if (error && error.code === 'ENOENT') {
3929
+ return res.json({ sounds: [] });
3930
+ }
3931
+ return res.status(500).json({
3932
+ error: 'Failed to enumerate notification sounds',
3933
+ details: error && error.message ? error.message : String(error || ''),
3934
+ });
3935
+ }
3936
+
3937
+ const normalizeLabel = (filename) => {
3938
+ const withoutExt = filename.replace(/\.[^.]+$/, '');
3939
+ return withoutExt
3940
+ .replace(/[-_]+/g, ' ')
3941
+ .replace(/\b\w/g, (char) => char.toUpperCase());
3942
+ };
3943
+
3944
+ const sounds = entries.map((filename) => {
3945
+ const encoded = filename.split('/').map(encodeURIComponent).join('/');
3946
+ return {
3947
+ id: filename,
3948
+ label: normalizeLabel(filename),
3949
+ url: `/sound/${encoded}`,
3950
+ filename,
3951
+ };
3952
+ });
3953
+
3954
+ res.json({ sounds });
3955
+ }));
3915
3956
  /*
3916
3957
  this.app.get("/asset/*", ex((req, res) => {
3917
3958
  let pathComponents = req.params[0].split("/")
@@ -3982,7 +4023,7 @@ class Server {
3982
4023
  list,
3983
4024
  })
3984
4025
  }))
3985
- this.app.get("/terminals", ex(async (req, res) => {
4026
+ this.app.get("/agents", ex(async (req, res) => {
3986
4027
  if (!this.kernel.plugin.config) {
3987
4028
  try {
3988
4029
  await this.kernel.plugin.init()
@@ -4059,7 +4100,7 @@ class Server {
4059
4100
  let peer_qr = null
4060
4101
  try { peer_qr = await QRCode.toDataURL(peer_url) } catch (_) {}
4061
4102
  const list = this.getPeers()
4062
- res.render("terminals", {
4103
+ res.render("agents", {
4063
4104
  current_host: this.kernel.peer.host,
4064
4105
  peer_url,
4065
4106
  peer_qr,
@@ -4072,8 +4113,25 @@ class Server {
4072
4113
  list,
4073
4114
  })
4074
4115
  }))
4116
+ this.app.get("/api/plugin/menu", ex(async (req, res) => {
4117
+ try {
4118
+ if (!this.kernel.plugin.config) {
4119
+ await this.kernel.plugin.init()
4120
+ }
4121
+ const pluginMenu = this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)
4122
+ ? this.kernel.plugin.config.menu
4123
+ : []
4124
+ res.json({ menu: pluginMenu })
4125
+ } catch (error) {
4126
+ console.warn('Failed to load plugin menu for create launcher modal', error)
4127
+ res.json({ menu: [] })
4128
+ }
4129
+ }))
4075
4130
  this.app.get("/plugins", (req, res) => {
4076
- res.redirect(301, "/terminals")
4131
+ res.redirect(301, "/agents")
4132
+ })
4133
+ this.app.get("/terminals", (req, res) => {
4134
+ res.redirect(301, "/agents")
4077
4135
  })
4078
4136
  this.app.get("/screenshots", ex(async (req, res) => {
4079
4137
  const peer_url = `http://${this.kernel.peer.host}:${DEFAULT_PORT}`
@@ -6491,6 +6549,40 @@ class Server {
6491
6549
  let exec_menus = []
6492
6550
  let shell_menus = []
6493
6551
  let href_menus = []
6552
+ const normalizeForSort = (value) => {
6553
+ if (typeof value !== 'string') {
6554
+ return ''
6555
+ }
6556
+ return value.trim().toLocaleLowerCase()
6557
+ }
6558
+ const compareMenuItems = (a = {}, b = {}) => {
6559
+ const titleDiff = normalizeForSort(a.title).localeCompare(normalizeForSort(b.title))
6560
+ if (titleDiff !== 0) {
6561
+ return titleDiff
6562
+ }
6563
+ const subtitleDiff = normalizeForSort(a.subtitle).localeCompare(normalizeForSort(b.subtitle))
6564
+ if (subtitleDiff !== 0) {
6565
+ return subtitleDiff
6566
+ }
6567
+ return normalizeForSort(a.href || a.link).localeCompare(normalizeForSort(b.href || b.link))
6568
+ }
6569
+ const sortMenuEntries = (menuArray) => {
6570
+ if (!Array.isArray(menuArray) || menuArray.length < 2) {
6571
+ return
6572
+ }
6573
+ menuArray.sort(compareMenuItems)
6574
+ }
6575
+ const sortNestedMenus = (menuArray) => {
6576
+ if (!Array.isArray(menuArray)) {
6577
+ return
6578
+ }
6579
+ sortMenuEntries(menuArray)
6580
+ for (const entry of menuArray) {
6581
+ if (entry && Array.isArray(entry.menu)) {
6582
+ sortNestedMenus(entry.menu)
6583
+ }
6584
+ }
6585
+ }
6494
6586
  if (plugin_menu.length > 0) {
6495
6587
  for(let item of plugin_menu) {
6496
6588
  // if shell.run method exists
@@ -6518,30 +6610,37 @@ class Server {
6518
6610
  href_menus.push(item)
6519
6611
  }
6520
6612
  }
6521
- exec_menus.sort((a, b) => { return a > b })
6522
- shell_menus.sort((a, b) => { return a > b })
6523
- href_menus.sort((a, b) => { return a > b })
6613
+ sortNestedMenus(exec_menus)
6614
+ sortNestedMenus(shell_menus)
6615
+ sortNestedMenus(href_menus)
6524
6616
  }
6525
6617
 
6526
6618
  // let terminal = await this.terminals(filepath)
6527
6619
  // let online_terminal = await this.getPluginGlobal(req, terminal, filepath)
6528
6620
  // console.log("online_terminal", online_terminal)
6529
6621
  terminal.menus = href_menus
6622
+ sortNestedMenus(terminal.menu)
6623
+ sortNestedMenus(terminal.menus)
6530
6624
  let dynamic = [
6531
6625
  terminal,
6532
6626
  {
6533
6627
  icon: "fa-solid fa-robot",
6534
- title: "AI Terminal",
6535
- subtitle: "Let AI work on this app",
6628
+ title: "Terminal Agents",
6629
+ subtitle: "Start a session in Pinokio",
6536
6630
  menu: shell_menus
6537
6631
  },
6538
6632
  {
6539
6633
  icon: "fa-solid fa-arrow-up-right-from-square",
6540
- title: "External apps",
6541
- subtitle: "Open this project in 3rd party apps",
6634
+ title: "IDE Agents",
6635
+ subtitle: "Open the project in external IDEs",
6542
6636
  menu: exec_menus
6543
6637
  },
6544
6638
  ]
6639
+ for (const item of dynamic) {
6640
+ if (item && Array.isArray(item.menu)) {
6641
+ sortNestedMenus(item.menu)
6642
+ }
6643
+ }
6545
6644
 
6546
6645
  let spec = ""
6547
6646
  try {