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,175 @@
1
+ "use strict";
2
+ // Sandboxed compile-only parse check. Loaded by `parse-sandbox.html`,
3
+ // which is declared under `sandbox.pages` in the manifest — the one
4
+ // context in an MV3 extension allowed to declare `'unsafe-eval'`.
5
+ //
6
+ // See the comment in `parse-sandbox.html` for why the compile has to
7
+ // happen in a sandbox at all, and why compiling untrusted source here
8
+ // is safe.
9
+ //
10
+ // This file deliberately imports nothing. tsc would emit an ES module
11
+ // for a file with imports, and a sandboxed page loads it as a classic
12
+ // `<script src>`. The message shapes below are mirrored (not shared)
13
+ // from `src/parse-check-protocol.ts`, which is the source of truth;
14
+ // they are small and change together.
15
+ // `new Function` compiles the body and returns; it never runs it. A
16
+ // SyntaxError anywhere in the text is the answer we want.
17
+ function checkOne(source) {
18
+ try {
19
+ // eslint-disable-next-line no-new-func
20
+ new Function(source);
21
+ return { status: 'ok' };
22
+ }
23
+ catch (err) {
24
+ const message = err instanceof Error ? err.message : String(err);
25
+ // ONLY a SyntaxError means "this source is broken". Everything
26
+ // else means "we couldn't tell", and the difference matters more
27
+ // than it looks:
28
+ //
29
+ // - A `RangeError: Maximum call stack size exceeded` on a
30
+ // pathologically nested source is the parser running out of
31
+ // budget HERE — inside a message handler inside a promise
32
+ // chain — which is not the budget `chrome.userScripts` will
33
+ // compile the real entry with. Reporting it as a syntax error
34
+ // would disable a script V8 is perfectly willing to run.
35
+ // - If the sandbox CSP ever stopped applying, `new Function`
36
+ // would throw `EvalError` and every script in the profile
37
+ // would be branded broken at once. That is the exact outcome
38
+ // the design says cannot happen.
39
+ if (!(err instanceof SyntaxError))
40
+ return { status: 'unknown', message };
41
+ return { status: 'syntax-error', message, probes: locate(source) };
42
+ }
43
+ }
44
+ // ---------- Finding out WHERE ----------
45
+ //
46
+ // `new Function` answers "does this parse?" and nothing else: no line,
47
+ // no column, not even a position in `err.stack`, which stops at
48
+ // `at new Function (<anonymous>)`. V8 does report a position when a
49
+ // classic `<script>` fails to parse — the window `error` event carries
50
+ // `lineno` and `colno` — so once the verdict is in we compile the same
51
+ // text a second way, purely to locate the error it already found.
52
+ //
53
+ // **Appending a `<script>` normally runs it.** What keeps this
54
+ // compile-only is the sentinel below, not luck; see `PROBE_SENTINEL`.
55
+ //
56
+ // Two probes, because neither framing is faithful on its own:
57
+ //
58
+ // - **wrapped** reproduces `new Function`'s semantics exactly — the
59
+ // text is a function body, so a top-level `return` is legal, as it
60
+ // is in the entry we actually inject. Its weakness is that a
61
+ // source with unbalanced braces swallows the wrapper's closing
62
+ // `}`, and the parse then runs on to a later, different error.
63
+ // - **raw** has no wrapper to swallow, so it places a stray or
64
+ // missing brace exactly — but it is a *Script*, where a top-level
65
+ // `return` is an error V8 would never have raised for real.
66
+ //
67
+ // The caller picks. See `selectParseLocation` in parse-location.ts.
68
+ function locate(source) {
69
+ try {
70
+ return {
71
+ wrapped: runProbe(`${WRAPPER_HEAD}${source}${WRAPPER_TAIL}${PROBE_SENTINEL}`, 1),
72
+ raw: runProbe(`${source}${PROBE_SENTINEL}`, 0),
73
+ };
74
+ }
75
+ catch {
76
+ // Locating is a nicety; the verdict is the product. Never let a
77
+ // DOM failure here turn a usable `syntax-error` into a throw.
78
+ return undefined;
79
+ }
80
+ }
81
+ // An unmatched OPENER, appended to whatever we compile, so the probe
82
+ // script can never parse — and therefore can never execute a single
83
+ // statement, since a parse failure aborts the whole script before
84
+ // evaluation begins.
85
+ //
86
+ // It has to be an opener. A closing `}` or `)` would be *closed by*
87
+ // whatever the source left dangling: appending `}` to a source ending
88
+ // in `if (1) {` produces a valid program, and it runs. An opener at
89
+ // end of input has nothing after it to close it.
90
+ //
91
+ // The leading newline matters too — without it a source whose last
92
+ // line is a `//` comment swallows the sentinel.
93
+ //
94
+ // V8 reports the FIRST error it hits, so a real error in the source is
95
+ // still the one reported; the sentinel only speaks when the source had
96
+ // nothing to say. A probe that comes back with NO error at all would
97
+ // therefore mean the sentinel failed to do its job and the source ran —
98
+ // so it reports no position, and `selectParseLocation` simply has one
99
+ // fewer probe to choose from.
100
+ const PROBE_SENTINEL = '\n(';
101
+ const WRAPPER_HEAD = 'function __smParseProbe__() {\n';
102
+ const WRAPPER_TAIL = '\n}';
103
+ // Compile `text` as a classic script and report where it failed.
104
+ // `lineOffset` is how many lines of our own framing precede the
105
+ // source, so the caller gets source coordinates back.
106
+ function runProbe(text, lineOffset) {
107
+ let hit;
108
+ const onError = (event) => {
109
+ hit = {
110
+ // `event.error.message` is the clean one; `event.message` has
111
+ // "Uncaught SyntaxError: " in front of it as well.
112
+ message: stripDomPrefix(event.error instanceof Error ? event.error.message : event.message),
113
+ line: event.lineno - lineOffset,
114
+ column: event.colno,
115
+ };
116
+ // Marks the error handled, which is what stops Chrome reporting it
117
+ // as an uncaught page error — and an uncaught error in an
118
+ // extension context is forwarded into chrome://extensions/?errors,
119
+ // making SourceMonkey look broken when it is working exactly as
120
+ // designed. Verified: without this the error surfaces, with it the
121
+ // console stays clean. See CLAUDE.md.
122
+ event.preventDefault();
123
+ };
124
+ window.addEventListener('error', onError);
125
+ const el = document.createElement('script');
126
+ el.textContent = text;
127
+ try {
128
+ // The parse — and the `error` event it fires — both happen
129
+ // synchronously inside this call, which is why `hit` is readable
130
+ // immediately after it and no async plumbing is needed.
131
+ document.body.append(el);
132
+ }
133
+ finally {
134
+ el.remove();
135
+ window.removeEventListener('error', onError);
136
+ }
137
+ return hit;
138
+ }
139
+ // Inserting the script means the SyntaxError arrives wrapped in the
140
+ // DOM method that raised it: "Failed to execute 'append' on 'Element':
141
+ // Unexpected token ';'". Strip it so the message we may end up showing
142
+ // is about the user's code and not about our insertion trick. Matched
143
+ // loosely rather than by exact string so a different insertion method
144
+ // (or wording) degrades to a cosmetic prefix rather than a wrong
145
+ // message — and `selectParseLocation` compares with `endsWith`, so an
146
+ // unstripped prefix still matches.
147
+ function stripDomPrefix(message) {
148
+ return message.replace(/^Failed to execute '[^']*' on '[^']*': /, '');
149
+ }
150
+ window.addEventListener('message', (event) => {
151
+ // Only our embedder may ask.
152
+ if (event.source !== window.parent)
153
+ return;
154
+ const data = event.data;
155
+ if (!data || data.type !== 'sm:parse-check')
156
+ return;
157
+ if (!Array.isArray(data.sources) || typeof data.id !== 'string')
158
+ return;
159
+ const verdicts = data.sources.map((s) => checkOne(typeof s === 'string' ? s : ''));
160
+ // Reply to the embedder only, and to nothing else. A sandboxed page
161
+ // has an opaque origin, so the target origin has to be '*' — which
162
+ // makes the `event.source` check the only thing standing between us
163
+ // and replying to an arbitrary window. Nothing can reach this page
164
+ // today (the manifest declares no `web_accessible_resources`, so no
165
+ // web page can frame or navigate to it), so this enforces the
166
+ // property rather than relying on it staying true.
167
+ window.parent.postMessage({ type: 'sm:parse-check-result', id: data.id, verdicts }, '*');
168
+ });
169
+ // Announce readiness. The embedder cannot detect it any other way: a
170
+ // sandboxed frame is opaque-origin, so its `contentDocument` is always
171
+ // null and `readyState` is unreadable from outside. Without this the
172
+ // embedder's only signal is the iframe's `load` event, and a listener
173
+ // attached even a moment late would miss it — leaving every request to
174
+ // time out silently for the rest of the session.
175
+ window.parent.postMessage({ type: 'sm:parse-sandbox-ready' }, '*');
@@ -0,0 +1,41 @@
1
+ // Which scripts' menu command lists the person opened or closed in a
2
+ // tab's popup, kept for the tab's lifetime: someone using one of a
3
+ // script's commands is likely to come back for another, so a list they
4
+ // opened stays open the next time the popup shows, across reloads and
5
+ // navigations in the tab.
6
+ //
7
+ // One `popupopen:<tabId>` row per tab in `chrome.storage.session`
8
+ // ({ [scriptId]: open }), which outlasts the worker going idle between
9
+ // popups and clears with the browser. Only the worker writes it (the
10
+ // popup asks, `POPUP_SET_COMMANDS_OPEN_MESSAGE`); background.ts's
11
+ // `tabs.onRemoved` drops a closed tab's row.
12
+ const KEY_PREFIX = 'popupopen:';
13
+ function rowKey(tabId) {
14
+ return `${KEY_PREFIX}${tabId}`;
15
+ }
16
+ // Writes are chained, so two quick toggles in a row don't both read the
17
+ // row before either writes it.
18
+ let writes = Promise.resolve();
19
+ async function readRow(tabId) {
20
+ const row = (await chrome.storage.session.get(rowKey(tabId)))[rowKey(tabId)];
21
+ return row && typeof row === 'object' ? row : {};
22
+ }
23
+ // After the writes already asked for, so a popup opened just after a
24
+ // toggle sees it.
25
+ export async function getCommandsOpen(tabId) {
26
+ await writes;
27
+ return readRow(tabId);
28
+ }
29
+ export function setCommandsOpen(tabId, scriptId, open) {
30
+ const write = writes.then(async () => {
31
+ const row = await readRow(tabId);
32
+ await chrome.storage.session.set({ [rowKey(tabId)]: { ...row, [scriptId]: open } });
33
+ });
34
+ writes = write.catch(() => undefined);
35
+ return write;
36
+ }
37
+ export function dropCommandsOpen(tabId) {
38
+ const write = writes.then(() => chrome.storage.session.remove(rowKey(tabId)));
39
+ writes = write.catch(() => undefined);
40
+ return write;
41
+ }
@@ -0,0 +1,387 @@
1
+ // Pure model behind the toolbar popup ("Active scripts", popup.html):
2
+ // which scripts it lists for a tab, under which collection, with what
3
+ // status, tooltip, and reload banner. Built in the service worker
4
+ // (`popupViewForTab` in background.ts), which owns the inputs, and
5
+ // rendered as-is by src/extension/popup.ts. Kept free of chrome.* so the
6
+ // rules can be unit tested. Design: docs/status-popup.md.
7
+ import { RUN_SCRIPT_LABEL } from '../userscript-header.js';
8
+ import { compareScriptNames } from '../script-display.js';
9
+ import { comparePreferred, preferredCopy } from './copy-preference.js';
10
+ import { isSettling, runsHere, skippedOnDocument } from './tab-status.js';
11
+ export const POPUP_STATUS_LABELS = {
12
+ OK: 'Running',
13
+ ERROR: 'Error',
14
+ REJECTED: 'Rejected',
15
+ RELOAD: 'Updated',
16
+ MISSING: 'Missing',
17
+ UNEXPECTED: 'Unexpected',
18
+ NOT_LOADED: 'Not loaded',
19
+ UNTRACKED: 'Reload',
20
+ PENDING: 'Reload to run',
21
+ STILL_RUNNING: 'Still running',
22
+ WARNING: 'Warning',
23
+ INVALID: 'Invalid',
24
+ NOT_RUN: 'Not run',
25
+ };
26
+ // Every pill's tooltip ends with what a click does: a Reload to run or
27
+ // Reload pill reloads the page, as the banner's Reload does; the rest open
28
+ // the Log.
29
+ export const POPUP_PILL_CLICK_HINT = 'Click to open the script log.';
30
+ export const POPUP_PILL_RELOAD_HINT = 'Click to reload the page.';
31
+ // The page menu's item, and the icon's right-click item doing the same.
32
+ export const RELOAD_NO_SCRIPTS_LABEL = 'Reload page with no userscripts';
33
+ export const COLLECTION_OFF_LABEL = 'Collection off';
34
+ export const COLLECTION_OFF_TOOLTIP = 'This collection is disabled, so none of its scripts run';
35
+ export const POPUP_NO_SCRIPTS = 'No scripts match this page.';
36
+ export const POPUP_NOT_INJECTABLE = 'SourceMonkey can\'t run scripts on this page.';
37
+ // The group for scripts removed since the page loaded that still run in
38
+ // it: no stored row or collection is left to list them under.
39
+ export const REMOVED_GROUP_ID = 'sm:removed';
40
+ export const REMOVED_GROUP_NAME = 'Removed scripts';
41
+ export { RUN_SCRIPT_LABEL };
42
+ // ---- Messages between the popup page and the service worker ----
43
+ // Popup → worker: the view for a tab. Answered with a `PopupView`.
44
+ export const POPUP_VIEW_MESSAGE = 'sm:popup-view';
45
+ // Popup → worker: reload the tab, with some of its scripts kept out.
46
+ export const POPUP_RELOAD_MESSAGE = 'sm:popup-reload';
47
+ // Popup → worker: run one of a script's menu commands.
48
+ export const POPUP_COMMAND_MESSAGE = 'sm:popup-command';
49
+ // Popup → worker: run a manual script now ("Run this script").
50
+ export const POPUP_RUN_SCRIPT_MESSAGE = 'sm:popup-run-script';
51
+ // Popup → worker: tick or untick a script's or a collection's box. The
52
+ // worker writes it, so the popup needn't load the script store.
53
+ export const POPUP_SET_ENABLED_MESSAGE = 'sm:popup-set-enabled';
54
+ // Popup → worker: a script's command list was opened or closed, which
55
+ // lasts for the tab's lifetime.
56
+ export const POPUP_SET_COMMANDS_OPEN_MESSAGE = 'sm:popup-set-commands-open';
57
+ // Worker → popup: something the view for `tabId` shows changed
58
+ // (badge state, menu commands). The popup asks for a fresh view. Sent
59
+ // over the port an open popup holds, named this prefix + its tab id.
60
+ export const POPUP_CHANGED_MESSAGE = 'sm:popup-changed';
61
+ // Worker → popup, over the same port: close. The click that opened it
62
+ // turned out to be the first of a double click.
63
+ export const POPUP_CLOSE_MESSAGE = 'sm:popup-close';
64
+ // Popup → worker, over the port: closing after an action (a link, a
65
+ // menu command). Its close is then no double click's second click.
66
+ export const POPUP_CLOSING_MESSAGE = 'sm:popup-closing';
67
+ export const POPUP_PORT_PREFIX = 'sm-popup:';
68
+ const DEFAULT_TOOLTIPS = {
69
+ OK: 'Running on this page',
70
+ ERROR: 'The script reported an error on this page',
71
+ REJECTED: 'Chrome refused to register this script, so it can\'t run',
72
+ RELOAD: 'The script changed since this page loaded. Reload to run the new version.',
73
+ MISSING: 'Expected on this page, but it hasn\'t started',
74
+ UNEXPECTED: 'Started on this page, but its rules don\'t match this page',
75
+ NOT_LOADED: 'Not run on this page load',
76
+ UNTRACKED: 'This page loaded before SourceMonkey started, so its status is unknown. Reload to restart.',
77
+ PENDING: 'Enabled since this page loaded. Reload to run it.',
78
+ STILL_RUNNING: 'Disabled since this page loaded. It keeps running until a reload.',
79
+ WARNING: 'Running on this page, with warnings',
80
+ INVALID: 'The script failed to load, so it can\'t run',
81
+ NOT_RUN: `This script runs only when you choose ${RUN_SCRIPT_LABEL}`,
82
+ };
83
+ // A manual script's OK tooltip: how many times it was run here.
84
+ export function manualRunsText(runs) {
85
+ return runs === 1 ? 'Ran once on this page' : `Ran ${runs} times on this page`;
86
+ }
87
+ // OK's tooltip for a script running only in the page's iframes.
88
+ const FRAMES_OK_TOOLTIP = 'Running in frames on this page';
89
+ // The heading of every surface that says user scripts are disabled: the
90
+ // collections page's banner, the popup's bar and the toolbar tooltip.
91
+ export const USER_SCRIPTS_OFF_HEADING = 'User scripts are not allowed yet.';
92
+ // NOT_LOADED's tooltip on such a page while they still are.
93
+ const NOT_RUN_USERSCRIPTS_DISABLED = 'Not run: user scripts are disabled';
94
+ // UNTRACKED's tooltip on a page that loaded while user scripts were disabled.
95
+ const LOADED_WITHOUT_USERSCRIPTS_TOOLTIP = 'This page loaded while user scripts were disabled, so its scripts didn\'t run. Reload to run them.';
96
+ // STILL_RUNNING's tooltip for a script removed since the page loaded.
97
+ const REMOVED_TOOLTIP = 'Removed since this page loaded. It keeps running until a reload.';
98
+ // PENDING's tooltip for a script that matches only the URL the page
99
+ // moved to in place: Chrome injects at a document load, not on pushState.
100
+ const MOVED_ONLY_TOOLTIP = 'This page changed its address since it loaded, and the script matches only the new one. Reload to run it.';
101
+ // Why a NOT_LOADED script didn't run: which kind of reload, or the
102
+ // sm-dev claim, kept it out.
103
+ const SM_DEV_KEPT_OUT = 'Not run: the sm-dev run controlling this tab kept it out';
104
+ function notLoadedReason(state, nameOf) {
105
+ if (state.loadedWithoutUserscripts)
106
+ return NOT_RUN_USERSCRIPTS_DISABLED;
107
+ if (state.scriptsSkipped)
108
+ return 'Not run: the page was reloaded with no userscripts';
109
+ if (state.skipOnly !== undefined)
110
+ return `Not run: the page was reloaded with only ${nameOf(state.skipOnly)}`;
111
+ if (state.skipWithout !== undefined)
112
+ return 'Not run: the page was reloaded without this script';
113
+ if (state.smDevAllow !== undefined)
114
+ return SM_DEV_KEPT_OUT;
115
+ return DEFAULT_TOOLTIPS.NOT_LOADED;
116
+ }
117
+ // The reload banner's text, naming the most specific cause.
118
+ export function bannerText(c) {
119
+ // First: it explains every other cause there may be.
120
+ if (c.loadedBefore)
121
+ return 'Reload required: Page loaded before SourceMonkey started';
122
+ if (c.loadedWithoutUserscripts)
123
+ return 'Reload required: Page loaded while user scripts were disabled';
124
+ if (c.enabled && (c.disabled || c.removed))
125
+ return 'Reload required: Enabled scripts changed';
126
+ if (c.enabled)
127
+ return 'Reload required: Scripts enabled';
128
+ if (c.disabled && c.removed)
129
+ return 'Reload required: Scripts disabled and removed';
130
+ if (c.disabled)
131
+ return 'Reload required: Scripts disabled';
132
+ if (c.removed)
133
+ return 'Reload required: Scripts removed';
134
+ if (c.updated)
135
+ return 'Reload required: Scripts updated';
136
+ if (c.notYetRun)
137
+ return 'Reload required: Some scripts haven\'t run on this page yet';
138
+ if (c.missing && c.unexpected)
139
+ return 'Reload required: Some scripts are missing or unexpected';
140
+ if (c.missing)
141
+ return 'Reload required: Some scripts are missing';
142
+ if (c.unexpected)
143
+ return 'Reload required: Unexpected scripts are running';
144
+ return null;
145
+ }
146
+ export function buildPopupView(input) {
147
+ const { state } = input;
148
+ const smDev = state?.smDevAllow !== undefined;
149
+ const view = {
150
+ url: input.url,
151
+ ...(input.userScriptsDisabled ? { userScriptsDisabled: true } : {}),
152
+ injectable: input.injectable,
153
+ smDev,
154
+ banner: null,
155
+ bannerRecheckAt: null,
156
+ perScriptReload: !smDev && state != null,
157
+ enabled: [],
158
+ frames: [],
159
+ manual: [],
160
+ disabled: [],
161
+ collections: [...input.collections],
162
+ };
163
+ if (!input.injectable)
164
+ return view;
165
+ const collections = new Map(input.collections.map((c) => [c.id, c]));
166
+ const scripts = new Map(input.scripts.map((s) => [s.id, s]));
167
+ const badgeById = new Map(input.badgeRows.map((r) => [r.id, r]));
168
+ const nameOf = (id) => scripts.get(id)?.name ?? badgeById.get(id)?.name ?? id;
169
+ const isActive = (s) => s.enabled && (collections.get(s.collectionId)?.enabled ?? false);
170
+ const isRunning = (id) => state != null && runsHere(state, id);
171
+ const settling = state != null && isSettling(state, input.now ?? Date.now());
172
+ // Every candidate, then one identity at a time: the copies that are
173
+ // active or running here all show; with none of those, one copy
174
+ // stands for the script in Disabled.
175
+ const keep = input.keepIds ?? new Set();
176
+ const candidates = new Set([...input.matchingIds, ...badgeById.keys(), ...keep]);
177
+ const byIdentity = new Map();
178
+ // Scripts deleted since they ran that still run here: listed in a
179
+ // group of their own, since they have no collection left.
180
+ const removed = [];
181
+ for (const id of candidates) {
182
+ const s = scripts.get(id);
183
+ if (!s || !collections.has(s.collectionId)) {
184
+ if (isRunning(id) && !input.hiddenIds?.has(id))
185
+ removed.push(id);
186
+ continue;
187
+ }
188
+ const key = s.identity ?? `id:${s.id}`;
189
+ const list = byIdentity.get(key);
190
+ if (list)
191
+ list.push(s);
192
+ else
193
+ byIdentity.set(key, [s]);
194
+ }
195
+ const shown = [];
196
+ for (const copies of byIdentity.values()) {
197
+ const otherCopies = copies.length > 1;
198
+ const live = copies.filter((s) => isActive(s) || isRunning(s.id));
199
+ const kept = copies.filter((s) => keep.has(s.id) && !live.includes(s));
200
+ if (live.length > 0 || kept.length > 0) {
201
+ const activeCount = live.filter(isActive).length;
202
+ for (const s of [...live, ...kept])
203
+ shown.push({ script: s, dup: activeCount > 1 && isActive(s), otherCopies });
204
+ continue;
205
+ }
206
+ // The representative: the copy closest to running and most likely
207
+ // to be turned back on (`comparePreferred`, docs/script-enablement.md).
208
+ const pick = preferredCopy(copies, (s) => {
209
+ const c = collections.get(s.collectionId);
210
+ return {
211
+ enabled: s.enabled,
212
+ collectionOn: c.enabled,
213
+ collectionName: c.name,
214
+ activeSince: s.activeSince,
215
+ inactiveSince: s.inactiveSince,
216
+ version: s.version,
217
+ location: s.location,
218
+ };
219
+ }, comparePreferred);
220
+ shown.push({ script: pick, dup: false, otherCopies });
221
+ }
222
+ const causes = {};
223
+ const rowsByCollection = {
224
+ enabled: new Map(),
225
+ frames: new Map(),
226
+ manual: new Map(),
227
+ disabled: new Map(),
228
+ };
229
+ for (const { script: s, dup, otherCopies } of shown) {
230
+ const active = isActive(s);
231
+ const badge = badgeById.get(s.id);
232
+ let status = null;
233
+ // PENDING only because the page changed its URL in place.
234
+ let movedOnly = false;
235
+ if (s.invalid !== undefined && !isRunning(s.id)) {
236
+ // Never registered, so no badge row, and a reload won't run it:
237
+ // not PENDING, and no banner. Disabled ones say so too, so it's
238
+ // clear that turning one on won't make it run.
239
+ status = 'INVALID';
240
+ }
241
+ else if (s.manual && active) {
242
+ // Nothing expects a manual script to run, so a reload has nothing
243
+ // to say about it: no PENDING, RELOAD or banner. In a tab an
244
+ // sm-dev run controls that keeps it out, it's Not loaded, as a
245
+ // script kept out of the load would be.
246
+ if (badge && (badge.status === 'OK' || badge.status === 'ERROR'))
247
+ status = badge.status;
248
+ else if (smDev && !input.manualRunnableIds?.has(s.id))
249
+ status = 'NOT_LOADED';
250
+ else
251
+ status = 'NOT_RUN';
252
+ }
253
+ else if (active) {
254
+ status = badge?.status ?? (state ? 'PENDING' : null);
255
+ // Matched now but not when the page loaded, and never reported:
256
+ // enabled (or installed) since, or matching only the URL the page
257
+ // moved to in place. Not in a tab an sm-dev run controls, where a
258
+ // reload still wouldn't run it, nor over a refusal, which a
259
+ // reload doesn't cure.
260
+ const atLoad = state?.loadMatchIds;
261
+ if (state && atLoad && !atLoad.includes(s.id) && !isRunning(s.id)
262
+ && status !== 'REJECTED' && !(smDev && skippedOnDocument(state, s.id))) {
263
+ status = 'PENDING';
264
+ movedOnly = input.loadUrlMatchingIds !== undefined && !input.loadUrlMatchingIds.has(s.id);
265
+ }
266
+ }
267
+ else if (isRunning(s.id)) {
268
+ status = 'STILL_RUNNING';
269
+ }
270
+ const warned = isRunning(s.id) && (s.headerWarnings || (state?.warnedIds?.includes(s.id) ?? false));
271
+ if (status === 'OK' && warned)
272
+ status = 'WARNING';
273
+ if (status === 'RELOAD')
274
+ causes.updated = true;
275
+ if (status === 'PENDING') {
276
+ if (movedOnly)
277
+ causes.notYetRun = true;
278
+ else
279
+ causes.enabled = true;
280
+ }
281
+ if (status === 'STILL_RUNNING')
282
+ causes.disabled = true;
283
+ // Not while the page is still settling: its scripts may be on their way.
284
+ if (status === 'MISSING') {
285
+ if (settling)
286
+ view.bannerRecheckAt = state.settleBy;
287
+ else
288
+ causes.missing = true;
289
+ }
290
+ if (status === 'UNEXPECTED')
291
+ causes.unexpected = true;
292
+ if (status === 'UNTRACKED') {
293
+ if (state?.loadedWithoutUserscripts && !state.loadedBefore)
294
+ causes.loadedWithoutUserscripts = true;
295
+ else
296
+ causes.loadedBefore = true;
297
+ }
298
+ let tooltip = null;
299
+ if (status) {
300
+ let text = DEFAULT_TOOLTIPS[status];
301
+ if (status === 'OK' && badge?.framesOnly)
302
+ text = FRAMES_OK_TOOLTIP;
303
+ if (status === 'ERROR' && badge?.message)
304
+ text = badge.message;
305
+ if (status === 'REJECTED' && input.rejected[s.id])
306
+ text = input.rejected[s.id];
307
+ if (status === 'NOT_LOADED' && state) {
308
+ // A manual script isn't kept out by a reload mode, only by the claim.
309
+ text = s.manual ? SM_DEV_KEPT_OUT : notLoadedReason(state, nameOf);
310
+ }
311
+ if (status === 'UNTRACKED' && state?.loadedWithoutUserscripts && !state.loadedBefore)
312
+ text = LOADED_WITHOUT_USERSCRIPTS_TOOLTIP;
313
+ if (status === 'INVALID' && s.invalid)
314
+ text = `${DEFAULT_TOOLTIPS.INVALID}: ${s.invalid}`;
315
+ if (movedOnly)
316
+ text = MOVED_ONLY_TOOLTIP;
317
+ if (status === 'STILL_RUNNING' && s.enabled) {
318
+ text = 'Its collection was disabled since this page loaded. It keeps running until a reload.';
319
+ }
320
+ const runs = state?.manualRuns?.[s.id];
321
+ if (s.manual && status === 'OK' && runs)
322
+ text = manualRunsText(runs);
323
+ const reloads = status === 'PENDING' || status === 'UNTRACKED';
324
+ tooltip = `${text}\n${reloads ? POPUP_PILL_RELOAD_HINT : POPUP_PILL_CLICK_HINT}`;
325
+ }
326
+ const row = {
327
+ id: s.id,
328
+ name: s.name,
329
+ checked: s.enabled,
330
+ active,
331
+ status,
332
+ warned,
333
+ tooltip,
334
+ versionLine: dup && s.version ? `Version ${s.version}` : null,
335
+ otherCopies,
336
+ commands: active || isRunning(s.id) ? [...(input.menuCommands.get(s.id) ?? [])] : [],
337
+ ...(s.manual ? { manual: true } : {}),
338
+ ...(s.manual && active && s.invalid === undefined && input.manualRunnableIds?.has(s.id) ? { runnable: true } : {}),
339
+ };
340
+ // Anything running in the page is listed with the active scripts,
341
+ // even when it's disabled now (Still running) or unexpected. Manual
342
+ // scripts have their own section, by the same rule, and so do
343
+ // scripts running only in frames.
344
+ const into = !(active || isRunning(s.id))
345
+ ? rowsByCollection.disabled
346
+ : s.manual ? rowsByCollection.manual
347
+ : badge?.framesOnly ? rowsByCollection.frames : rowsByCollection.enabled;
348
+ const list = into.get(s.collectionId);
349
+ if (list)
350
+ list.push(row);
351
+ else
352
+ into.set(s.collectionId, [row]);
353
+ }
354
+ const toGroups = (byColl) => input.collections
355
+ .filter((c) => byColl.has(c.id))
356
+ .map((c) => ({
357
+ collection: c,
358
+ rows: byColl.get(c.id).sort((a, b) => compareScriptNames(a.name, b.name)),
359
+ }));
360
+ view.enabled = toGroups(rowsByCollection.enabled);
361
+ view.frames = toGroups(rowsByCollection.frames);
362
+ view.manual = toGroups(rowsByCollection.manual);
363
+ view.disabled = toGroups(rowsByCollection.disabled);
364
+ if (removed.length > 0) {
365
+ causes.removed = true;
366
+ const collection = { id: REMOVED_GROUP_ID, name: REMOVED_GROUP_NAME, path: '', enabled: true };
367
+ view.collections.push(collection);
368
+ view.enabled.push({
369
+ collection,
370
+ rows: removed.map((id) => ({
371
+ id,
372
+ name: nameOf(id),
373
+ checked: false,
374
+ active: false,
375
+ status: 'STILL_RUNNING',
376
+ warned: false,
377
+ // No click hint: there is no script page left to open.
378
+ tooltip: REMOVED_TOOLTIP,
379
+ versionLine: null,
380
+ otherCopies: false,
381
+ commands: [...(input.menuCommands.get(id) ?? [])],
382
+ })).sort((a, b) => compareScriptNames(a.name, b.name)),
383
+ });
384
+ }
385
+ view.banner = bannerText(causes);
386
+ return view;
387
+ }