pinokiod 8.0.39 → 8.0.41

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 (37) hide show
  1. package/kernel/api/hf/index.js +2 -2
  2. package/kernel/api/index.js +12 -3
  3. package/kernel/connect/index.js +2 -2
  4. package/kernel/connect/providers/huggingface/index.js +14 -5
  5. package/kernel/index.js +14 -2
  6. package/kernel/shell.js +3 -2
  7. package/kernel/shells.js +6 -0
  8. package/kernel/vault/constants.js +13 -0
  9. package/kernel/vault/hash_worker.js +9 -2
  10. package/kernel/vault/index.js +1856 -316
  11. package/kernel/vault/registry.js +526 -52
  12. package/kernel/vault/snapshot.js +29 -0
  13. package/kernel/vault/sweeper.js +480 -210
  14. package/kernel/vault/walker.js +142 -0
  15. package/package.json +1 -1
  16. package/server/index.js +79 -90
  17. package/server/lib/privacy_filter_cache.js +4 -1
  18. package/server/public/storage-size.js +12 -0
  19. package/server/public/style.css +13 -0
  20. package/server/public/tab-link-popover.js +3 -0
  21. package/server/public/vault-nav.js +96 -0
  22. package/server/public/vault.css +1079 -0
  23. package/server/public/vault.js +1835 -0
  24. package/server/views/app.ejs +93 -16
  25. package/server/views/connect/huggingface.ejs +7 -9
  26. package/server/views/partials/main_sidebar.ejs +6 -1
  27. package/server/views/partials/vault_workspace.ejs +55 -0
  28. package/server/views/vault.ejs +5 -1532
  29. package/server/views/vault_app.ejs +22 -0
  30. package/test/hf-api.test.js +4 -2
  31. package/test/huggingface-connect.test.js +7 -11
  32. package/test/huggingface-token-validity.test.js +135 -0
  33. package/test/plugin-action-functions.test.js +19 -0
  34. package/test/privacy-filter-cache.test.js +1 -0
  35. package/test/vault-engine.test.js +1276 -26
  36. package/test/vault-sweep.test.js +1043 -35
  37. package/test/vault-ui.test.js +2184 -65
@@ -0,0 +1,142 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { ENTRY_BATCH_SIZE } = require('./constants')
4
+
5
+ const isMissingError = (error) => !!(error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
6
+ const isHandledError = (options, error, target) => !!(
7
+ !isMissingError(error) && options.onError && options.onError(error, target)
8
+ )
9
+
10
+ // One traversal primitive for Scan and Repair. It owns only directory
11
+ // discovery and symlink policy; callers decide how files are classified.
12
+ async function * walkBatches(root, options = {}) {
13
+ const resolvedRoot = path.resolve(root)
14
+ const concurrency = Math.max(1, Number(options.concurrency) || 1)
15
+ const entryBatchSize = Math.max(1, Number(options.entryBatchSize) || ENTRY_BATCH_SIZE)
16
+ const statConcurrency = Math.max(1, Number(options.statConcurrency) || 32)
17
+ const skipDirectory = options.skipDirectory || (() => false)
18
+ const strictErrors = !!options.strictErrors
19
+ const pending = [{ dir: resolvedRoot, followRootSymlink: true }]
20
+ const active = []
21
+ try {
22
+ while (pending.length || active.length) {
23
+ while (active.length < concurrency && pending.length) {
24
+ const next = pending.pop()
25
+ const dir = next.dir
26
+ try {
27
+ let st = await fs.promises.lstat(dir)
28
+ if (next.followRootSymlink && st.isSymbolicLink()) {
29
+ st = await fs.promises.stat(dir)
30
+ }
31
+ if (!st.isDirectory()) {
32
+ yield [{ dir, entries: null, files: [], discoveredDirs: 0, discoveredFiles: 0, firstChunk: true }]
33
+ continue
34
+ }
35
+ active.push({ dir, handle: await fs.promises.opendir(dir), firstChunk: true })
36
+ } catch (error) {
37
+ if (isMissingError(error) && dir === resolvedRoot && options.onRootMissing) {
38
+ options.onRootMissing(error, dir)
39
+ }
40
+ if (strictErrors && !isMissingError(error) && !isHandledError(options, error, dir)) throw error
41
+ yield [{ dir, entries: null, files: [], discoveredDirs: 0, discoveredFiles: 0, firstChunk: true }]
42
+ }
43
+ }
44
+ if (!active.length) continue
45
+ const batch = await Promise.all(active.map(async (task) => {
46
+ const entries = []
47
+ let done = false
48
+ try {
49
+ while (entries.length < entryBatchSize) {
50
+ const entry = await task.handle.read()
51
+ if (!entry) {
52
+ done = true
53
+ break
54
+ }
55
+ entries.push(entry)
56
+ }
57
+ } catch (error) {
58
+ if (isMissingError(error) && task.dir === resolvedRoot && options.onRootMissing) {
59
+ options.onRootMissing(error, task.dir)
60
+ }
61
+ if (strictErrors && !isMissingError(error) && !isHandledError(options, error, task.dir)) throw error
62
+ done = true
63
+ }
64
+ return {
65
+ task, dir: task.dir, entries, done, firstChunk: task.firstChunk,
66
+ files: [], discoveredDirs: 0, discoveredFiles: 0
67
+ }
68
+ }))
69
+ const unknown = []
70
+ for (const group of batch) {
71
+ group.task.firstChunk = false
72
+ for (const entry of group.entries) {
73
+ if (entry.isSymbolicLink()) continue
74
+ const full = path.resolve(group.dir, entry.name)
75
+ if (entry.isDirectory()) {
76
+ if (skipDirectory(full)) continue
77
+ pending.push({ dir: full, followRootSymlink: false })
78
+ group.discoveredDirs += 1
79
+ } else if (entry.isFile()) {
80
+ group.files.push({ path: full, entry })
81
+ group.discoveredFiles += 1
82
+ } else {
83
+ unknown.push({ group, entry, full })
84
+ }
85
+ }
86
+ }
87
+ if (unknown.length) {
88
+ const stats = await statMany(
89
+ unknown.map((item) => item.full),
90
+ statConcurrency,
91
+ null,
92
+ { followSymlinks: false, strictErrors, onError: options.onError }
93
+ )
94
+ for (let index = 0; index < unknown.length; index++) {
95
+ const item = unknown[index]
96
+ const st = stats[index]
97
+ if (!st || st.isSymbolicLink()) continue
98
+ if (st.isDirectory()) {
99
+ if (skipDirectory(item.full)) continue
100
+ pending.push({ dir: item.full, followRootSymlink: false })
101
+ item.group.discoveredDirs += 1
102
+ } else if (st.isFile()) {
103
+ item.group.files.push({ path: item.full, entry: item.entry })
104
+ item.group.discoveredFiles += 1
105
+ }
106
+ }
107
+ }
108
+ yield batch.map(({ task, done, ...group }) => group)
109
+ for (let index = active.length - 1; index >= 0; index--) {
110
+ if (!batch[index].done) continue
111
+ await active[index].handle.close().catch(() => {})
112
+ active.splice(index, 1)
113
+ }
114
+ }
115
+ } finally {
116
+ await Promise.all(active.map((task) => task.handle.close().catch(() => {})))
117
+ }
118
+ }
119
+
120
+ const statMany = async (paths, concurrency = 32, onSettled = null, options = {}) => {
121
+ const results = new Array(paths.length)
122
+ const readMetadata = options.followSymlinks === false ? fs.promises.lstat : fs.promises.stat
123
+ let cursor = 0
124
+ const workers = Array.from({ length: Math.min(concurrency, paths.length) }, async () => {
125
+ while (cursor < paths.length) {
126
+ const index = cursor++
127
+ try {
128
+ results[index] = await readMetadata(paths[index])
129
+ } catch (error) {
130
+ if (options.strictErrors && !isMissingError(error) &&
131
+ !isHandledError(options, error, paths[index])) throw error
132
+ results[index] = null
133
+ } finally {
134
+ if (onSettled) onSettled(index, results[index])
135
+ }
136
+ }
137
+ })
138
+ await Promise.all(workers)
139
+ return results
140
+ }
141
+
142
+ module.exports = { walkBatches, statMany }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.39",
3
+ "version": "8.0.41",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1990,6 +1990,9 @@ class Server {
1990
1990
  result.hasSnapshots = !!(options && options.hasSnapshots)
1991
1991
  result.pendingSnapshotId = options && options.pendingSnapshotId ? String(options.pendingSnapshotId) : null
1992
1992
  result.registryEnabled = registryEnabled
1993
+ const vault = this.kernel.vault
1994
+ if (vault && vault.ready) await vault.ready
1995
+ result.vault_enabled = !!(vault && vault.enabled)
1993
1996
  if (!registryEnabled) {
1994
1997
  result.pendingSnapshotId = null
1995
1998
  }
@@ -6664,6 +6667,10 @@ class Server {
6664
6667
 
6665
6668
  this.started = false
6666
6669
  this.app = express();
6670
+ this.app.use((req, res, next) => {
6671
+ res.locals.vaultEnabled = !!(this.kernel.vault && this.kernel.vault.enabled)
6672
+ next()
6673
+ })
6667
6674
  this.app.use(cors({
6668
6675
  origin: '*'
6669
6676
  }));
@@ -15732,21 +15739,55 @@ class Server {
15732
15739
  // }))
15733
15740
 
15734
15741
 
15735
- // Vault dashboard data: everything from memory, never a walk. The scan
15736
- // itself runs ONLY via the explicit scan action below.
15742
+ // Vault dashboard data: registry-backed with bounded per-blob stats,
15743
+ // never a discovery walk. Scans run ONLY via the explicit action below.
15744
+ this.app.get("/info/dedup/nav", ex(async (req, res) => {
15745
+ if (!privacyFilterCache.isSameOriginRequest(req)) {
15746
+ res.sendStatus(403)
15747
+ return
15748
+ }
15749
+ const vault = this.kernel.vault
15750
+ if (vault && vault.ready) await vault.ready
15751
+ if (!vault || !vault.enabled) {
15752
+ res.sendStatus(404)
15753
+ return
15754
+ }
15755
+ res.set("Cache-Control", "no-store")
15756
+ const appName = req.query && typeof req.query.app === "string"
15757
+ ? req.query.app
15758
+ : null
15759
+ res.json(appName
15760
+ ? vault.navigationStatusForApp(appName)
15761
+ : vault.navigationStatus())
15762
+ }))
15737
15763
  this.app.get("/info/dedup", ex(async (req, res) => {
15764
+ if (!privacyFilterCache.isSameOriginRequest(req)) {
15765
+ res.sendStatus(403)
15766
+ return
15767
+ }
15738
15768
  const vault = this.kernel.vault
15769
+ if (vault && vault.ready) await vault.ready
15739
15770
  if (!vault || !vault.enabled) {
15740
- res.json({ enabled: false })
15771
+ res.sendStatus(404)
15741
15772
  return
15742
15773
  }
15774
+ res.set("Cache-Control", "no-store")
15775
+ const scopeId = req.query && typeof req.query.scope_id === "string"
15776
+ ? req.query.scope_id
15777
+ : null
15743
15778
  if (req.query && req.query.progress === "1") {
15744
- res.json(vault.progressStatus())
15779
+ res.json(vault.progressStatus(scopeId))
15745
15780
  return
15746
15781
  }
15747
- res.json(await vault.status())
15782
+ res.json(await vault.status(scopeId))
15748
15783
  }))
15749
15784
  this.app.get("/vault", ex(async (req, res) => {
15785
+ const vault = this.kernel.vault
15786
+ if (vault && vault.ready) await vault.ready
15787
+ if (!vault || !vault.enabled) {
15788
+ res.sendStatus(404)
15789
+ return
15790
+ }
15750
15791
  const { install_required, requirements_pending } = await this.kernel.bin.check({
15751
15792
  bin: this.kernel.bin.preset("dev"),
15752
15793
  })
@@ -15754,100 +15795,48 @@ class Server {
15754
15795
  res.redirect(`/setup/dev?callback=${encodeURIComponent(req.originalUrl)}`)
15755
15796
  return
15756
15797
  }
15798
+ await vault.ensureInitialized()
15757
15799
  res.render("vault", { theme: this.theme, agent: req.agent })
15758
15800
  }))
15801
+ this.app.get("/vault/app/:name", ex(async (req, res) => {
15802
+ if (!privacyFilterCache.isSameOriginRequest(req)) {
15803
+ res.sendStatus(403)
15804
+ return
15805
+ }
15806
+ const vault = this.kernel.vault
15807
+ if (vault && vault.ready) await vault.ready
15808
+ if (!vault || !vault.enabled) {
15809
+ res.sendStatus(404)
15810
+ return
15811
+ }
15812
+ await vault.ensureInitialized()
15813
+ await vault.refreshSources()
15814
+ const source = vault.sources().find((item) =>
15815
+ item.kind === "app" && item.app === req.params.name && item.available)
15816
+ if (!source) {
15817
+ res.sendStatus(404)
15818
+ return
15819
+ }
15820
+ res.render("vault_app", {
15821
+ theme: this.theme,
15822
+ agent: req.agent,
15823
+ scope_id: source.id
15824
+ })
15825
+ }))
15759
15826
  this.app.post("/vault/action", ex(async (req, res) => {
15827
+ if (!privacyFilterCache.isSameOriginRequest(req)) {
15828
+ res.sendStatus(403)
15829
+ return
15830
+ }
15760
15831
  const vault = this.kernel.vault
15832
+ if (vault && vault.ready) await vault.ready
15761
15833
  if (!vault || !vault.enabled) {
15762
- res.json({ error: "vault is not enabled" })
15834
+ res.sendStatus(404)
15763
15835
  return
15764
15836
  }
15765
15837
  const body = req.body || {}
15766
15838
  try {
15767
- switch (body.action) {
15768
- case "add_source": {
15769
- const result = await vault.addExternalSource(body.path)
15770
- res.json({
15771
- created: result.created,
15772
- source: {
15773
- id: result.source.id,
15774
- label: result.source.label,
15775
- target_path: result.source.root,
15776
- shareable: !!result.source.shareable
15777
- }
15778
- })
15779
- return
15780
- }
15781
- case "deduplicate": {
15782
- const scopeId = typeof body.scope_id === "string" ? body.scope_id : null
15783
- if (scopeId) {
15784
- const source = vault.sources().find((item) => item.id === scopeId && item.kind !== "virtual")
15785
- if (!source) {
15786
- res.json({ error: "That location is no longer available. Scan again to refresh it." })
15787
- return
15788
- }
15789
- if (!source.shareable) {
15790
- res.json({ error: "This location is on a disk that cannot share space with this vault." })
15791
- return
15792
- }
15793
- if (source.kind === "app") {
15794
- const appRoot = this.kernel.path("api", source.app) + path.sep
15795
- const running = this.kernel.api && this.kernel.api.running ? this.kernel.api.running : {}
15796
- const busy = Object.keys(running).some((k) => running[k] && k.startsWith(appRoot))
15797
- if (busy) {
15798
- res.json({ error: "This app has a running script. Stop it first, then deduplicate." })
15799
- return
15800
- }
15801
- }
15802
- res.json(await vault.sweeper.convertPending(undefined, {
15803
- scope_id: scopeId, batch_id: `batch-${Date.now()}`
15804
- }))
15805
- return
15806
- }
15807
- const app = Object.prototype.hasOwnProperty.call(body, "app") ? (body.app || null) : undefined
15808
- if (app) {
15809
- const appRoot = this.kernel.path("api", app) + path.sep
15810
- const running = this.kernel.api && this.kernel.api.running ? this.kernel.api.running : {}
15811
- const busy = Object.keys(running).some((k) => running[k] && k.startsWith(appRoot))
15812
- if (busy) {
15813
- res.json({ error: "This app has a running script. Stop it first, then deduplicate." })
15814
- return
15815
- }
15816
- }
15817
- res.json(await vault.sweeper.convertPending(app, { batch_id: `batch-${Date.now()}` }))
15818
- return
15819
- }
15820
- case "reclaim":
15821
- res.json(await vault.reclaim(body.hash))
15822
- return
15823
- case "reclaim_all":
15824
- res.json(await vault.reclaimAll())
15825
- return
15826
- case "scan":
15827
- vault.sweeper.scan().catch(() => {})
15828
- res.json({ started: true })
15829
- return
15830
- case "repair":
15831
- case "rebuild":
15832
- if (vault.sweeper && vault.sweeper.state.active) {
15833
- res.json({ error: "Wait for the current scan to finish before repairing the index." })
15834
- return
15835
- }
15836
- await vault.rebuild()
15837
- res.json({ done: true })
15838
- return
15839
- case "undo":
15840
- res.json(await vault.undoBatch(body.batch_id))
15841
- return
15842
- case "detach":
15843
- res.json(await vault.detach(body.path))
15844
- return
15845
- case "reshare":
15846
- res.json(await vault.reshare(body.path))
15847
- return
15848
- default:
15849
- res.json({ error: "unknown action" })
15850
- }
15839
+ res.json(await vault.perform(body.action, body))
15851
15840
  } catch (e) {
15852
15841
  res.json({ error: e && e.message ? e.message : String(e) })
15853
15842
  }
@@ -66,7 +66,7 @@ function assertKnownAsset(relativePath) {
66
66
  return normalized
67
67
  }
68
68
 
69
- function isInstallRequestAllowed(req) {
69
+ function isSameOriginRequest(req) {
70
70
  const fetchSite = String(req && typeof req.get === 'function' ? req.get('Sec-Fetch-Site') || '' : '').trim().toLowerCase()
71
71
  if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
72
72
  return false
@@ -90,6 +90,8 @@ function isInstallRequestAllowed(req) {
90
90
  }
91
91
  }
92
92
 
93
+ const isInstallRequestAllowed = isSameOriginRequest
94
+
93
95
  function assetPath(kernel, relativePath) {
94
96
  const normalized = assertKnownAsset(relativePath)
95
97
  return path.resolve(modelRoot(kernel), ...normalized.split('/'))
@@ -280,6 +282,7 @@ module.exports = {
280
282
  normalizeDtype,
281
283
  cacheRoot,
282
284
  modelRoot,
285
+ isSameOriginRequest,
283
286
  isInstallRequestAllowed,
284
287
  status,
285
288
  ensure
@@ -0,0 +1,12 @@
1
+ (function (root) {
2
+ const base = 1000
3
+ const units = ["B", "KB", "MB", "GB", "TB"]
4
+
5
+ root.PinokioFormatStorageSize = (value) => {
6
+ const bytes = Number(value)
7
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
8
+ const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(base)))
9
+ const scaled = bytes / Math.pow(base, index)
10
+ return `${Number(scaled.toFixed(index ? 2 : 0))} ${units[index]}`
11
+ }
12
+ })(window)
@@ -1567,6 +1567,14 @@ body.main-sidebar-page aside.main-sidebar .tab.selected .main-sidebar-action-ico
1567
1567
  background: rgba(45, 180, 92, 0.14);
1568
1568
  color: #247a43;
1569
1569
  }
1570
+ .main-sidebar [data-main-sidebar-vault-tab][data-vault-state="new"]:not(.selected) {
1571
+ background: var(--pinokio-sidebar-tab-hover);
1572
+ }
1573
+ .main-sidebar .main-sidebar-status-badge[data-state="new"] {
1574
+ border: 1px solid rgba(71, 85, 105, 0.22);
1575
+ background: transparent;
1576
+ color: rgba(31, 41, 55, 0.72);
1577
+ }
1570
1578
  body.dark .main-sidebar .main-sidebar-status-badge {
1571
1579
  background: rgba(255, 255, 255, 0.08);
1572
1580
  color: rgba(229, 231, 235, 0.68);
@@ -1575,6 +1583,11 @@ body.dark .main-sidebar .main-sidebar-status-badge[data-state="on"] {
1575
1583
  background: rgba(74, 222, 128, 0.14);
1576
1584
  color: #86efac;
1577
1585
  }
1586
+ body.dark .main-sidebar .main-sidebar-status-badge[data-state="new"] {
1587
+ border-color: rgba(226, 232, 240, 0.22);
1588
+ background: transparent;
1589
+ color: rgba(248, 250, 252, 0.76);
1590
+ }
1578
1591
  .main-sidebar .tab.submenu {
1579
1592
  padding-left: 14px !important;
1580
1593
  }
@@ -25,6 +25,9 @@
25
25
  if (!link.hasAttribute("href")) {
26
26
  return false
27
27
  }
28
+ if (link.dataset.tabLinkPopover === "false") {
29
+ return false
30
+ }
28
31
  const href = link.getAttribute("href")
29
32
  return typeof href === "string" && href.trim().length > 0
30
33
  }
@@ -0,0 +1,96 @@
1
+ (() => {
2
+ if (window.__pinokioVaultNavigationStatusInit) return
3
+ window.__pinokioVaultNavigationStatusInit = true
4
+
5
+ const formatBytes = (value) => {
6
+ const bytes = Number(value)
7
+ if (!Number.isFinite(bytes) || bytes <= 0) return ""
8
+ const units = ["B", "KB", "MB", "GB", "TB"]
9
+ const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1000)))
10
+ const scaled = bytes / Math.pow(1000, index)
11
+ const digits = scaled >= 10 || index === 0 ? 0 : 1
12
+ return `${Number(scaled.toFixed(digits))} ${units[index]}`
13
+ }
14
+
15
+ const init = () => {
16
+ const tab = document.querySelector("[data-main-sidebar-vault-tab], [data-app-vault-tab]")
17
+ if (!tab) return
18
+ const appName = tab.dataset.appVaultName || ""
19
+ const appMode = !!appName
20
+ const status = tab.querySelector(appMode
21
+ ? "[data-app-vault-status]"
22
+ : "[data-main-sidebar-vault-status]")
23
+ const prompt = tab.querySelector("[data-app-vault-prompt]")
24
+ if (!status) return
25
+ let requestId = 0
26
+
27
+ const render = (data) => {
28
+ let state = ""
29
+ let label = ""
30
+ let title = "Save space"
31
+ if (data && data.scanning) {
32
+ state = "scanning"
33
+ label = "Scanning"
34
+ title = appMode ? "Save space, scanning this app" : "Save space, scanning"
35
+ } else if (!data || !data.any_scan) {
36
+ state = "new"
37
+ label = "NEW"
38
+ title = "Save space — find duplicate files and use less disk"
39
+ } else if (!data.scanned) {
40
+ state = "not-scanned"
41
+ label = "Not scanned"
42
+ title = appMode ? "Save space, this app has not been scanned" : "Save space, not scanned"
43
+ } else if (Number(data.pending_bytes) > 0) {
44
+ state = "pending"
45
+ label = `${data.incomplete ? "≥" : ""}${formatBytes(data.pending_bytes)}`
46
+ title = `Save space, ${data.incomplete ? "at least " : ""}${formatBytes(data.pending_bytes)} can be saved${appMode ? " in this app" : ""}`
47
+ }
48
+
49
+ tab.dataset.vaultState = state || "ready"
50
+ tab.setAttribute("aria-label", title)
51
+ tab.setAttribute("title", title)
52
+ if (prompt) prompt.hidden = state !== "new"
53
+ status.hidden = !label
54
+ status.textContent = label
55
+ if (state) status.dataset.state = state
56
+ else delete status.dataset.state
57
+ }
58
+
59
+ const load = () => {
60
+ const currentRequest = ++requestId
61
+ const query = appMode ? `?app=${encodeURIComponent(appName)}` : ""
62
+ fetch(`/info/dedup/nav${query}`, { cache: "no-store" })
63
+ .then((response) => {
64
+ if (!response.ok) throw new Error(`Save space status failed: ${response.status}`)
65
+ return response.json()
66
+ })
67
+ .then((data) => {
68
+ if (currentRequest === requestId) render(data)
69
+ })
70
+ .catch(() => {
71
+ if (currentRequest !== requestId) return
72
+ status.hidden = true
73
+ if (prompt) prompt.hidden = true
74
+ delete status.dataset.state
75
+ delete tab.dataset.vaultState
76
+ tab.removeAttribute("aria-label")
77
+ tab.removeAttribute("title")
78
+ })
79
+ }
80
+
81
+ document.addEventListener("pinokio:vault-status-changed", load)
82
+ window.addEventListener("message", (event) => {
83
+ if (event.origin === window.location.origin &&
84
+ event.data && event.data.type === "pinokio:vault-status-changed") {
85
+ load()
86
+ }
87
+ })
88
+ load()
89
+ }
90
+
91
+ if (document.readyState === "loading") {
92
+ document.addEventListener("DOMContentLoaded", init, { once: true })
93
+ } else {
94
+ init()
95
+ }
96
+ })()