sourcemonkey 0.0.0-stage → 0.5.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 (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,203 @@
1
+ import { renderGlyph, setGlyphButton, setGlyphButtonBusy, } from '../glyph.js';
2
+ import { formatLogTimestamp } from '../log-format.js';
3
+ import { installHomeLinkHandler } from '../page-links.js';
4
+ import { formatBytes } from '../../script-display.js';
5
+ import { isIconCacheKey, listCachedIcons, } from '../site-icon.js';
6
+ import { deleteAllCachedIcons, deleteCachedIcon, refreshCachedIcon } from '../worker-client.js';
7
+ const summaryEl = document.getElementById('icons-summary');
8
+ const emptyEl = document.getElementById('icons-empty');
9
+ const tableEl = document.getElementById('icons-table');
10
+ const tbodyEl = document.getElementById('icons-tbody');
11
+ const deleteAllBtn = document.getElementById('delete-all');
12
+ const REFRESH_LABELS = {
13
+ idle: 'Refresh cached icon',
14
+ busy: 'Refreshing…',
15
+ };
16
+ const refreshingKeys = new Set();
17
+ let renderGeneration = 0;
18
+ // The log's local-time spelling, without its milliseconds: cache
19
+ // timestamps are days apart, so the tail is noise here.
20
+ function formatTimestamp(ms) {
21
+ if (Number.isNaN(new Date(ms).getTime()))
22
+ return '—';
23
+ return formatLogTimestamp(ms).replace(/\.\d{3}$/, '');
24
+ }
25
+ // Storage failures on this page have no surface of their own; an
26
+ // unhandled rejection would land in chrome://extensions/?errors, where
27
+ // it reads as SourceMonkey being broken.
28
+ function report(what) {
29
+ return (err) => console.info(`[SourceMonkey] debug icons: ${what} failed`, err);
30
+ }
31
+ function formatIso(ms) {
32
+ const d = new Date(ms);
33
+ return Number.isNaN(d.getTime()) ? '' : d.toISOString();
34
+ }
35
+ function kindLabel(kind) {
36
+ return kind === 'url' ? '@icon' : kind;
37
+ }
38
+ function renderRow(row) {
39
+ const tr = document.createElement('tr');
40
+ tr.dataset.storageKey = row.storageKey;
41
+ // 1. Icon
42
+ const iconTd = document.createElement('td');
43
+ iconTd.className = 'col-icon';
44
+ if (row.entry.dataUrl) {
45
+ const img = document.createElement('img');
46
+ img.src = row.entry.dataUrl;
47
+ img.alt = '';
48
+ iconTd.appendChild(img);
49
+ }
50
+ else {
51
+ iconTd.appendChild(renderGlyph(document, '#sm-globe', 'site-icon-globe'));
52
+ }
53
+ tr.appendChild(iconTd);
54
+ // 2. Site / Target
55
+ const targetTd = document.createElement('td');
56
+ targetTd.className = 'col-target';
57
+ const link = document.createElement('a');
58
+ link.href = row.target;
59
+ link.target = '_blank';
60
+ link.rel = 'noopener noreferrer';
61
+ link.textContent = row.target;
62
+ link.title = row.storageKey;
63
+ targetTd.appendChild(link);
64
+ tr.appendChild(targetTd);
65
+ // 3. Scope
66
+ const kindTd = document.createElement('td');
67
+ const chip = document.createElement('span');
68
+ chip.className = `kind-chip kind-${row.kind}`;
69
+ chip.textContent = kindLabel(row.kind);
70
+ chip.title = row.storageKey;
71
+ kindTd.appendChild(chip);
72
+ tr.appendChild(kindTd);
73
+ // 4. Details
74
+ const detailsTd = document.createElement('td');
75
+ const parts = [];
76
+ if (row.entry.dataUrl) {
77
+ const mime = row.mimeType ?? 'image';
78
+ const size = row.byteLength !== null ? formatBytes(row.byteLength) : '';
79
+ parts.push(size ? `${mime} (${size})` : mime);
80
+ }
81
+ else {
82
+ parts.push('none (miss)');
83
+ }
84
+ if (row.entry.v !== undefined) {
85
+ parts.push(`v${row.entry.v}`);
86
+ }
87
+ detailsTd.textContent = parts.join(' · ');
88
+ const badge = document.createElement('span');
89
+ if (!row.isFresh) {
90
+ badge.className = 'status-badge status-stale';
91
+ badge.textContent = 'expired';
92
+ }
93
+ else if (row.requiredVersion !== null && row.entry.v !== row.requiredVersion) {
94
+ badge.className = 'status-badge status-stale';
95
+ badge.textContent = 'stale schema';
96
+ const have = row.entry.v === undefined ? 'none' : `v${row.entry.v}`;
97
+ badge.title = [
98
+ `Schema version: ${have}`,
99
+ `Current version: v${row.requiredVersion}`,
100
+ 'Ignored and refetched on the next render.',
101
+ ].join('\n');
102
+ }
103
+ else if (!row.isUsable) {
104
+ badge.className = 'status-badge status-stale';
105
+ badge.textContent = 'unpaintable';
106
+ badge.title = [
107
+ 'The cached value is not an icon we would paint.',
108
+ 'Ignored and refetched on the next render.',
109
+ ].join('\n');
110
+ }
111
+ else if (row.entry.dataUrl === null) {
112
+ badge.className = 'status-badge status-miss';
113
+ badge.textContent = 'miss';
114
+ }
115
+ else {
116
+ badge.className = 'status-badge status-ok';
117
+ badge.textContent = 'ok';
118
+ }
119
+ detailsTd.appendChild(badge);
120
+ tr.appendChild(detailsTd);
121
+ // 5. Fetched timestamp
122
+ const fetchedTd = document.createElement('td');
123
+ fetchedTd.className = 'col-time';
124
+ fetchedTd.textContent = formatTimestamp(row.entry.fetchedAt);
125
+ fetchedTd.title = formatIso(row.entry.fetchedAt);
126
+ tr.appendChild(fetchedTd);
127
+ // 6. Expiry timestamp
128
+ const expiresTd = document.createElement('td');
129
+ expiresTd.className = 'col-time';
130
+ expiresTd.textContent = formatTimestamp(row.expiresAt);
131
+ expiresTd.title = formatIso(row.expiresAt);
132
+ tr.appendChild(expiresTd);
133
+ // 7. Actions (Refresh + Delete)
134
+ const actionsTd = document.createElement('td');
135
+ actionsTd.className = 'col-actions';
136
+ const wrap = document.createElement('div');
137
+ wrap.className = 'row-actions';
138
+ const refreshBtn = document.createElement('button');
139
+ refreshBtn.type = 'button';
140
+ refreshBtn.className = 'refresh-icon';
141
+ setGlyphButton(refreshBtn, '#sm-refresh', REFRESH_LABELS.idle);
142
+ if (refreshingKeys.has(row.storageKey)) {
143
+ setGlyphButtonBusy(refreshBtn, true, REFRESH_LABELS);
144
+ }
145
+ // The storage write at the end of a refresh re-renders the table via
146
+ // `onChanged`, so this button is replaced rather than un-busied; the
147
+ // explicit render covers the refresh that stored nothing new.
148
+ refreshBtn.addEventListener('click', () => {
149
+ refreshingKeys.add(row.storageKey);
150
+ setGlyphButtonBusy(refreshBtn, true, REFRESH_LABELS);
151
+ void refreshCachedIcon(row.storageKey)
152
+ .catch(report('refresh'))
153
+ .finally(() => {
154
+ refreshingKeys.delete(row.storageKey);
155
+ void render();
156
+ });
157
+ });
158
+ const deleteBtn = document.createElement('button');
159
+ deleteBtn.type = 'button';
160
+ deleteBtn.className = 'delete-icon';
161
+ setGlyphButton(deleteBtn, '#sm-trash', 'Delete cached icon');
162
+ deleteBtn.addEventListener('click', () => {
163
+ deleteBtn.disabled = true;
164
+ void deleteCachedIcon(row.storageKey)
165
+ .catch(report('delete'))
166
+ .finally(() => {
167
+ void render();
168
+ });
169
+ });
170
+ wrap.append(refreshBtn, deleteBtn);
171
+ actionsTd.appendChild(wrap);
172
+ tr.appendChild(actionsTd);
173
+ return tr;
174
+ }
175
+ async function render() {
176
+ const gen = ++renderGeneration;
177
+ const rows = await listCachedIcons();
178
+ if (gen !== renderGeneration)
179
+ return;
180
+ const count = rows.length;
181
+ summaryEl.textContent = count === 1 ? '1 cached icon' : `${count} cached icons`;
182
+ deleteAllBtn.disabled = count === 0;
183
+ emptyEl.hidden = count > 0;
184
+ tableEl.hidden = count === 0;
185
+ tbodyEl.replaceChildren(...rows.map(renderRow));
186
+ }
187
+ installHomeLinkHandler();
188
+ deleteAllBtn.addEventListener('click', () => {
189
+ deleteAllBtn.disabled = true;
190
+ void deleteAllCachedIcons()
191
+ .catch(report('delete all'))
192
+ .finally(() => {
193
+ void render();
194
+ });
195
+ });
196
+ chrome.storage.onChanged.addListener((changes, areaName) => {
197
+ if (areaName !== 'local')
198
+ return;
199
+ if (Object.keys(changes).some(isIconCacheKey)) {
200
+ void render();
201
+ }
202
+ });
203
+ void render();
@@ -0,0 +1,365 @@
1
+ // The storage debug page (/debug/storage.html): where
2
+ // `chrome.storage.local` goes, by script, by stored file, or in GM
3
+ // values. Read-only. See docs/storage.md#storage-debug-page.
4
+ import { collectionAnchorId } from '../collection-display.js';
5
+ import { installHomeLinkHandler, namedPageUrl, wireNamedTabLink } from '../page-links.js';
6
+ import { timeStorageRead } from '../storage-scan.js';
7
+ import { describeError } from '../script-store.js';
8
+ import { appendWithSlashBreaks, viewerUrl } from '../../script-display.js';
9
+ import { buildStorageBreakdown, defaultSortDir, fileIdentity, sortFiles, sortScripts, sortValues, } from '../storage-breakdown.js';
10
+ const summaryEl = document.getElementById('storage-summary');
11
+ const emptyEl = document.getElementById('storage-empty');
12
+ const tableEl = document.getElementById('storage-table');
13
+ const viewButtons = [...document.querySelectorAll('.views button')];
14
+ installHomeLinkHandler();
15
+ // Each view keeps its own sort while the page is open.
16
+ const sorts = {
17
+ script: { key: 'size', dir: 'desc' },
18
+ file: { key: 'size', dir: 'desc' },
19
+ values: { key: 'size', dir: 'desc' },
20
+ };
21
+ let breakdown = null;
22
+ let loadError = '';
23
+ // The view is the URL's fragment, so a reload keeps it.
24
+ function currentView() {
25
+ const hash = location.hash.slice(1);
26
+ return hash === 'file' || hash === 'values' ? hash : 'script';
27
+ }
28
+ for (const button of viewButtons) {
29
+ button.addEventListener('click', () => {
30
+ const view = button.dataset.view;
31
+ history.replaceState(null, '', view === 'script' ? location.pathname : `#${view}`);
32
+ render();
33
+ });
34
+ }
35
+ addEventListener('hashchange', render);
36
+ // ---------- Formatting ----------
37
+ const numberFormat = new Intl.NumberFormat();
38
+ function plural(n, one, many = `${one}s`) {
39
+ return `${numberFormat.format(n)} ${n === 1 ? one : many}`;
40
+ }
41
+ // Whole KB, as on the Options page; a size too small to show as 1 KB
42
+ // still reads as something rather than 0.
43
+ function formatKb(bytes) {
44
+ const kb = Math.round(bytes / 1024);
45
+ if (kb === 0 && bytes > 0)
46
+ return '< 1 KB';
47
+ return `${numberFormat.format(kb)} KB`;
48
+ }
49
+ const KIND_LABELS = {
50
+ script: 'script',
51
+ metadata: 'metadata',
52
+ require: '@require',
53
+ resource: '@resource',
54
+ 'smdev-text': 'sm-dev script text',
55
+ 'smdev-file': 'sm-dev pushed file',
56
+ };
57
+ // ---------- Cells ----------
58
+ function span(className, text) {
59
+ const el = document.createElement('span');
60
+ el.className = className;
61
+ el.textContent = text;
62
+ return el;
63
+ }
64
+ function td(className, ...children) {
65
+ const cell = document.createElement('td');
66
+ if (className)
67
+ cell.className = className;
68
+ // Spaces between the parts, so a kind, a name and a location wrap
69
+ // as words.
70
+ children.forEach((child, i) => {
71
+ if (i > 0)
72
+ cell.append(' ');
73
+ cell.append(child);
74
+ });
75
+ return cell;
76
+ }
77
+ // A name cell running down beside the group's item lines, so a long
78
+ // name wraps alongside them rather than pushing them down.
79
+ function spanGroup(cell, items) {
80
+ cell.rowSpan = 1 + items;
81
+ return cell;
82
+ }
83
+ function sizeCell(bytes) {
84
+ const cell = td('num', formatKb(bytes));
85
+ cell.title = `${numberFormat.format(bytes)} bytes`;
86
+ return cell;
87
+ }
88
+ // Blank for a file only one script uses.
89
+ function sharedCell(shared) {
90
+ return td('num', shared > 1 ? numberFormat.format(shared) : '');
91
+ }
92
+ function collectionCell(ref) {
93
+ if (!ref.collectionId)
94
+ return td(undefined, span('muted', 'No collection'));
95
+ const a = document.createElement('a');
96
+ // Break after slashes, as the collections page does: a name is often a path.
97
+ appendWithSlashBreaks(a, ref.collectionName);
98
+ const hash = collectionAnchorId(ref.collectionId);
99
+ wireNamedTabLink(a, 'collections', hash);
100
+ // Its page-relative href would resolve inside debug/, beside this page.
101
+ a.href = chrome.runtime.getURL(namedPageUrl('collections', hash));
102
+ return td(undefined, a);
103
+ }
104
+ function scriptCell(ref) {
105
+ const a = document.createElement('a');
106
+ appendWithSlashBreaks(a, ref.scriptName);
107
+ a.href = `../${viewerUrl(ref.scriptId)}`;
108
+ a.target = '_blank';
109
+ a.rel = 'noopener';
110
+ return td(undefined, a);
111
+ }
112
+ // The parts naming an item: its kind, `@resource` name and location,
113
+ // whichever it has and the caller wants.
114
+ function itemParts(item) {
115
+ const parts = [];
116
+ if (item.kind)
117
+ parts.push(span('kind', KIND_LABELS[item.kind]));
118
+ if (item.name)
119
+ parts.push(span('loc', item.name));
120
+ if (item.location) {
121
+ const loc = span('loc', '');
122
+ appendWithSlashBreaks(loc, item.location);
123
+ parts.push(loc);
124
+ }
125
+ if (item.missing)
126
+ parts.push(span('muted', MISSING));
127
+ return parts;
128
+ }
129
+ // A file scripts point at that isn't in storage (see `missing` in
130
+ // storage-breakdown.ts).
131
+ const MISSING = '(missing from storage)';
132
+ function row(className, cells) {
133
+ const tr = document.createElement('tr');
134
+ tr.className = className;
135
+ tr.append(...cells);
136
+ return tr;
137
+ }
138
+ function group(rows) {
139
+ const tbody = document.createElement('tbody');
140
+ tbody.className = 'group';
141
+ tbody.append(...rows);
142
+ return tbody;
143
+ }
144
+ function head(view, columns) {
145
+ const colgroup = document.createElement('colgroup');
146
+ for (const c of columns) {
147
+ const col = document.createElement('col');
148
+ if (c.width)
149
+ col.className = c.width;
150
+ colgroup.append(col);
151
+ }
152
+ const thead = document.createElement('thead');
153
+ const tr = document.createElement('tr');
154
+ const sort = sorts[view];
155
+ for (const c of columns) {
156
+ const th = document.createElement('th');
157
+ th.textContent = c.label;
158
+ const classes = [c.num ? 'num' : '', c.sort ? 'sortable' : ''].filter(Boolean);
159
+ if (classes.length)
160
+ th.className = classes.join(' ');
161
+ if (c.sort) {
162
+ const key = c.sort;
163
+ th.dataset.sort = key;
164
+ if (sort.key === key) {
165
+ th.append(span('sort-arrow', sort.dir === 'desc' ? '▼' : '▲'));
166
+ th.setAttribute('aria-sort', sort.dir === 'desc' ? 'descending' : 'ascending');
167
+ }
168
+ th.addEventListener('click', () => {
169
+ sorts[view] = sort.key === key
170
+ ? { key, dir: sort.dir === 'desc' ? 'asc' : 'desc' }
171
+ : { key, dir: defaultSortDir(key) };
172
+ render();
173
+ });
174
+ }
175
+ tr.append(th);
176
+ }
177
+ thead.append(tr);
178
+ return [colgroup, thead];
179
+ }
180
+ function scriptTable(b) {
181
+ const { key, dir } = sorts.script;
182
+ return [
183
+ ...head('script', [
184
+ { label: 'Collection', sort: 'collection', width: 'col-collection' },
185
+ { label: 'Script', sort: 'script', width: 'col-name' },
186
+ { label: 'File' },
187
+ { label: 'Size', sort: 'size', width: 'col-size', num: true },
188
+ { label: 'Shared copies', sort: 'shared', width: 'col-shared', num: true },
189
+ ]),
190
+ ...sortScripts(b.scripts, key, dir).map((s) => group([
191
+ row('group-head', [
192
+ spanGroup(collectionCell(s), s.items.length), spanGroup(scriptCell(s), s.items.length),
193
+ td('total-label', 'Total'), sizeCell(s.bytes), td(),
194
+ ]),
195
+ ...s.items.map((item) => row('group-item', [
196
+ td('item', ...itemParts(item)), sizeCell(item.bytes), sharedCell(item.shared),
197
+ ])),
198
+ ])),
199
+ ];
200
+ }
201
+ function fileGroup(file) {
202
+ const id = fileIdentity(file);
203
+ // The heading names what every use agrees on; each use's line names
204
+ // the rest.
205
+ const heading = id.kind === null
206
+ ? [span('muted', 'Multiple locations')]
207
+ : [
208
+ ...itemParts({
209
+ kind: id.kind,
210
+ name: id.sameName ? id.name : undefined,
211
+ location: id.sameLocation ? id.location : undefined,
212
+ }),
213
+ ...(id.sameLocation ? [] : [span('muted', 'multiple locations')]),
214
+ ];
215
+ if (file.missing)
216
+ heading.push(span('muted', MISSING));
217
+ const own = (use) => id.kind === null ? itemParts(use) : itemParts({
218
+ name: id.sameName ? undefined : use.name,
219
+ location: id.sameLocation ? undefined : use.location,
220
+ });
221
+ const useCells = (use) => [collectionCell(use), scriptCell(use)].map((cell) => {
222
+ cell.classList.add('use');
223
+ return cell;
224
+ });
225
+ // When the heading says it all, the first use goes on the heading's
226
+ // line rather than leaving a gap above the uses, and the heading runs
227
+ // down beside the rest, so a long one wraps without pushing them down.
228
+ const [first, ...rest] = file.uses;
229
+ const lead = first && file.uses.every((use) => own(use).length === 0) ? first : undefined;
230
+ if (lead) {
231
+ return group([
232
+ row('group-head', [
233
+ spanGroup(td(undefined, ...heading), rest.length), ...useCells(lead),
234
+ sizeCell(file.bytes), sharedCell(file.uses.length),
235
+ ]),
236
+ ...rest.map((use) => row('group-item', [...useCells(use), td(), td()])),
237
+ ]);
238
+ }
239
+ return group([
240
+ row('group-head', [td(undefined, ...heading), td(), td(), sizeCell(file.bytes), sharedCell(file.uses.length)]),
241
+ ...file.uses.map((use) => row('group-item', [
242
+ td('item', ...own(use)), ...useCells(use), td(), td(),
243
+ ])),
244
+ ]);
245
+ }
246
+ function fileTable(b) {
247
+ const { key, dir } = sorts.file;
248
+ const parts = [
249
+ ...head('file', [
250
+ { label: 'File' },
251
+ { label: 'Collection', width: 'col-collection' },
252
+ { label: 'Script', width: 'col-name' },
253
+ { label: 'Size', sort: 'size', width: 'col-size', num: true },
254
+ { label: 'Shared copies', sort: 'shared', width: 'col-shared', num: true },
255
+ ]),
256
+ // Only those two headings sort here.
257
+ ...sortFiles(b.files, key === 'shared' ? 'shared' : 'size', dir).map(fileGroup),
258
+ ];
259
+ // Always last: it belongs to no collection or script to sort by.
260
+ if (b.unused.length > 0) {
261
+ const unused = [...b.unused].sort((x, y) => y.bytes - x.bytes);
262
+ parts.push(group([
263
+ row('group-head', [
264
+ td(undefined, span('muted', 'Not used by any script')), td(), td(),
265
+ sizeCell(unused.reduce((sum, f) => sum + f.bytes, 0)), td(),
266
+ ]),
267
+ ...unused.map((f) => row('group-item', [
268
+ td('item', span('loc', f.key)), td(), td(), sizeCell(f.bytes), td(),
269
+ ])),
270
+ ]));
271
+ }
272
+ return parts;
273
+ }
274
+ function valuesTable(b) {
275
+ const { key, dir } = sorts.values;
276
+ return [
277
+ ...head('values', [
278
+ { label: 'Collection', sort: 'collection', width: 'col-values-name' },
279
+ { label: 'Script', sort: 'script' },
280
+ { label: 'Size', sort: 'size', width: 'col-size', num: true },
281
+ { label: 'Values', sort: 'count', width: 'col-count', num: true },
282
+ ]),
283
+ ...sortValues(b.values, key, dir).map((v) => group([
284
+ row('group-head', v.script
285
+ ? [collectionCell(v.script), scriptCell(v.script), sizeCell(v.bytes), td('num', numberFormat.format(v.count))]
286
+ : [
287
+ td(undefined, span('muted', 'Removed script')), td(undefined, span('loc', v.scriptId)),
288
+ sizeCell(v.bytes), td('num', numberFormat.format(v.count)),
289
+ ]),
290
+ ])),
291
+ ];
292
+ }
293
+ // ---------- Render ----------
294
+ function summary(view, b) {
295
+ if (view === 'script') {
296
+ // Scripts whose collection row is missing ('') aren't one more.
297
+ const collections = new Set(b.scripts.map((s) => s.collectionId).filter(Boolean)).size;
298
+ return `${plural(b.scripts.length, 'script')} in ${plural(collections, 'collection')}. `
299
+ + 'Shared files are counted once in each script that references them.';
300
+ }
301
+ if (view === 'file') {
302
+ // A missing file isn't stored.
303
+ const all = [...b.files.filter((f) => !f.missing), ...b.unused];
304
+ const bytes = all.reduce((sum, f) => sum + f.bytes, 0);
305
+ return `${plural(all.length, 'stored file')}, ${formatKb(bytes)}. Each shared file counts once.`;
306
+ }
307
+ const count = b.values.reduce((sum, v) => sum + v.count, 0);
308
+ const bytes = b.values.reduce((sum, v) => sum + v.bytes, 0);
309
+ return `${plural(count, 'value')} in ${plural(b.values.length, 'script')}, ${formatKb(bytes)}.`;
310
+ }
311
+ const EMPTY = {
312
+ script: 'No scripts in storage.',
313
+ file: 'No stored files.',
314
+ values: 'No scripts have stored values.',
315
+ };
316
+ function render() {
317
+ const view = currentView();
318
+ for (const button of viewButtons)
319
+ button.setAttribute('aria-pressed', String(button.dataset.view === view));
320
+ if (!breakdown) {
321
+ summaryEl.textContent = loadError;
322
+ emptyEl.hidden = true;
323
+ tableEl.hidden = true;
324
+ return;
325
+ }
326
+ const b = breakdown;
327
+ const isEmpty = view === 'script' ? b.scripts.length === 0
328
+ : view === 'file' ? b.files.length + b.unused.length === 0
329
+ : b.values.length === 0;
330
+ summaryEl.textContent = isEmpty ? '' : summary(view, b);
331
+ emptyEl.textContent = EMPTY[view];
332
+ emptyEl.hidden = !isEmpty;
333
+ tableEl.hidden = isEmpty;
334
+ tableEl.replaceChildren(...(isEmpty ? [] : view === 'script' ? scriptTable(b) : view === 'file' ? fileTable(b) : valuesTable(b)));
335
+ }
336
+ // Each load reads every row, so a burst of writes (a refresh, a script
337
+ // spamming GM_setValue) is coalesced into one reload a second at most.
338
+ // Loads can overlap; only the newest one may draw.
339
+ let latest = 0;
340
+ let pending = false;
341
+ async function load() {
342
+ pending = false;
343
+ const mine = ++latest;
344
+ try {
345
+ const rows = await timeStorageRead('storage debug page', () => chrome.storage.local.get(null));
346
+ if (mine !== latest)
347
+ return;
348
+ breakdown = buildStorageBreakdown(rows);
349
+ }
350
+ catch (err) {
351
+ if (mine !== latest)
352
+ return;
353
+ breakdown = null;
354
+ loadError = `Could not read storage: ${describeError(err)}`;
355
+ }
356
+ render();
357
+ }
358
+ chrome.storage.onChanged.addListener((_changes, areaName) => {
359
+ if (areaName !== 'local' || pending)
360
+ return;
361
+ pending = true;
362
+ setTimeout(() => void load(), 1000);
363
+ });
364
+ render();
365
+ void load();
@@ -0,0 +1,77 @@
1
+ // Lets external pages (anywhere on the web) "navigate into" the
2
+ // extension. Plain web pages can't link to chrome-extension:// URLs,
3
+ // so we publish a stub docs/redirect-page.md on GitHub, watch for
4
+ // navigations to it, and rewrite the tab to the requested extension
5
+ // page.
6
+ //
7
+ // Source URL:
8
+ // https://github.com/jshute96/SourceMonkey/blob/main/docs/redirect-page.md
9
+ // ?page=<allowed>&<other-params>
10
+ // Target URL:
11
+ // chrome-extension://<id>/<page>?<other-params>
12
+ //
13
+ // `page` is allowlisted. Anything else short-circuits and leaves the
14
+ // GitHub page alone — the doc itself explains how to install the
15
+ // extension, so a non-redirect outcome is the "no extension" fallback.
16
+ //
17
+ // chrome.runtime.getURL() builds the full chrome-extension:// URL at
18
+ // runtime, so this works whether the extension was loaded unpacked
19
+ // (ID pinned via manifest "key") or installed from the Web Store
20
+ // (different ID).
21
+ import { CAPTURED_PARAM } from './install-capture.js';
22
+ const REDIRECT_URL = 'https://github.com/jshute96/SourceMonkey/blob/main/docs/redirect-page.md';
23
+ const ALLOWED_PAGES = new Set(['collections.html', 'options.html', 'userscript.html']);
24
+ // Pure: maps a source URL to its rewrite target, or null when the URL
25
+ // is not a deep link we should act on. Exported for unit tests.
26
+ export function buildDeepLinkTarget(srcUrl) {
27
+ let u;
28
+ try {
29
+ u = new URL(srcUrl);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ // Reject userinfo (user:pass@host) for defence in depth — origin
35
+ // strips it, so the equality check would otherwise accept it.
36
+ if (u.username !== '' || u.password !== '')
37
+ return null;
38
+ if (`${u.origin}${u.pathname}` !== REDIRECT_URL)
39
+ return null;
40
+ const page = u.searchParams.get('page');
41
+ if (!page || !ALLOWED_PAGES.has(page))
42
+ return null;
43
+ const passthrough = new URLSearchParams(u.searchParams);
44
+ passthrough.delete('page');
45
+ // `captured` tells the viewer "the install capture opened this",
46
+ // which lets it navigate the tab away on its own. That claim is only
47
+ // the service worker's to make — this is the one way a web page
48
+ // could put a param onto an extension page, so it does not carry.
49
+ passthrough.delete(CAPTURED_PARAM);
50
+ // Note: passthrough.toString() uses application/x-www-form-urlencoded
51
+ // encoding (spaces → `+`, literal `+` → `%2B`). End-to-end loss-less
52
+ // for consumers that decode via URLSearchParams; consumers that
53
+ // hand-parse the query should be aware of this round-trip.
54
+ const qs = passthrough.toString();
55
+ return chrome.runtime.getURL(page) + (qs ? `?${qs}` : '');
56
+ }
57
+ export function installDeepLinkRedirect() {
58
+ const wn = chrome.webNavigation;
59
+ if (!wn) {
60
+ console.info('[SourceMonkey] chrome.webNavigation not available; deep-link redirect disabled');
61
+ return;
62
+ }
63
+ wn.onBeforeNavigate.addListener((d) => {
64
+ if (d.frameId !== 0)
65
+ return;
66
+ const target = buildDeepLinkTarget(d.url);
67
+ if (!target)
68
+ return;
69
+ // Tab may have closed between the event firing and our update
70
+ // landing; ignore the resulting rejection.
71
+ chrome.tabs.update(d.tabId, { url: target }).catch(() => { });
72
+ },
73
+ // Cheap pre-filter so we don't run on every navigation. The
74
+ // listener does the authoritative check; this just narrows the
75
+ // event volume.
76
+ { url: [{ urlPrefix: REDIRECT_URL }] });
77
+ }