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,504 @@
1
+ // Options page controller. Owns the actual configurable settings
2
+ // (link capture, debug-logging gate, local-file-override search roots,
3
+ // GitHub auth token), plus a read-only storage-usage table. The
4
+ // per-collection management UI lives on `collections.html` /
5
+ // `collections.ts`; a header link points users back and forth.
6
+ import { DEBUG_LOGGING_KEY, installDebugLoggingListener, loadDebugLoggingFlag, } from '../debug-log.js';
7
+ import { GITHUB_AUTH_TOKEN_KEY } from '../github-auth.js';
8
+ import { INSTALL_CAPTURE_KEY } from './install-capture.js';
9
+ import { LOCAL_ROOTS_KEY, parseLocalRootLines, readRootsSetting, rootsFromText, textFromRoots, } from '../local-file-overrides.js';
10
+ import { fetchFileUrlText } from './directories.js';
11
+ import { describeError, getCollections } from './script-store.js';
12
+ import { updateCollection, writeSetting } from './worker-client.js';
13
+ import { timeStorageRead } from './storage-scan.js';
14
+ import { formatQuota, formatStorageUsage } from '../script-display.js';
15
+ import { summarizeLocalStorage, toWholeKb, withRemainder, } from './storage-usage.js';
16
+ import { collectionSupportsLocalFiles } from '../local-file-overrides.js';
17
+ import { isFileSchemeAccessAllowed, renderFileAccessBanner } from './file-access.js';
18
+ import { DOWNLOADS_COPIES_KEY, parseDownloadsCopiesSetting, readDownloadsCopiesSetting, } from './local-copy.js';
19
+ import { installHomeLinkHandler, onNamedTabFocus, openNamedTab } from './page-links.js';
20
+ const openCollectionsBtn = document.getElementById('open-collections');
21
+ // The collections page is a named tab: focus the open one if there is
22
+ // one, and otherwise open a new tab rather than replacing this one.
23
+ openCollectionsBtn.addEventListener('click', () => {
24
+ void openNamedTab('collections');
25
+ });
26
+ // The header bar's icon and product name are the same link.
27
+ installHomeLinkHandler();
28
+ // A link elsewhere focusing this tab rather than opening a second one.
29
+ // This page has no anchors of its own to land on, so the whole of it is
30
+ // the target: back to the top. See src/extension/page-links.ts.
31
+ onNamedTabFocus('options', () => {
32
+ scrollTo({ top: 0, behavior: 'smooth' });
33
+ });
34
+ // Both boolean settings differ only in their storage key and what an
35
+ // absent value means, so they share one binder. `defaultValue` is what
36
+ // an unset key reads back as — capture is opt-out, debug logging is
37
+ // opt-in.
38
+ initBooleanSetting('capture-userjs', INSTALL_CAPTURE_KEY, true);
39
+ // This page both sets the flag AND reads storage through shared code
40
+ // that logs under it (the storage-read timings in `storage-scan`), so
41
+ // it observes the flag like any other context: each one reads it for
42
+ // itself. Refreshes it asks for run in the worker, which has its own.
43
+ installDebugLoggingListener();
44
+ void loadDebugLoggingFlag();
45
+ initBooleanSetting('debug-logging', DEBUG_LOGGING_KEY, false);
46
+ initGithubTokenControls();
47
+ initLocalRootControls();
48
+ initDownloadsDirControls();
49
+ initStorageUsage();
50
+ // Read-only table of what is stored in `chrome.storage.local`, by
51
+ // kind, against Chrome's quota. Each paint scans every row, so a burst
52
+ // of writes (a refresh, a script spamming GM_setValue) is coalesced into
53
+ // one repaint a second at most.
54
+ function initStorageUsage() {
55
+ const table = document.getElementById('storage-usage');
56
+ const limit = document.getElementById('storage-limit');
57
+ if (!table || !limit)
58
+ return;
59
+ const quota = chrome.storage.local.QUOTA_BYTES;
60
+ if (typeof quota === 'number' && quota > 0) {
61
+ limit.textContent = `Chrome limits each extension to ${formatQuota(quota)} of storage.`;
62
+ limit.hidden = false;
63
+ }
64
+ let pending = false;
65
+ // Paints can overlap (a slow full read, then a change); only the
66
+ // newest one may draw, so an older read finishing last can't put
67
+ // stale numbers back.
68
+ let latest = 0;
69
+ const paint = async () => {
70
+ pending = false;
71
+ const mine = ++latest;
72
+ const usage = await readUsage();
73
+ if (mine === latest)
74
+ table.replaceChildren(...usageRows(usage));
75
+ };
76
+ void paint();
77
+ chrome.storage.onChanged.addListener((_changes, areaName) => {
78
+ if (areaName !== 'local' || pending)
79
+ return;
80
+ pending = true;
81
+ setTimeout(() => void paint(), 1000);
82
+ });
83
+ }
84
+ async function readUsage() {
85
+ const quota = chrome.storage.local.QUOTA_BYTES;
86
+ if (typeof quota !== 'number' || quota <= 0) {
87
+ return { error: 'Chrome did not report its storage limit' };
88
+ }
89
+ try {
90
+ const [rows, used] = await Promise.all([
91
+ timeStorageRead('storage usage', () => chrome.storage.local.get(null)),
92
+ chrome.storage.local.getBytesInUse(null),
93
+ ]);
94
+ return { rows: withRemainder(summarizeLocalStorage(rows), used), used, quota };
95
+ }
96
+ catch (err) {
97
+ return { error: `Could not read storage usage: ${describeError(err)}` };
98
+ }
99
+ }
100
+ const kbFormat = new Intl.NumberFormat();
101
+ function usageRows(usage) {
102
+ const row = (cells, className) => {
103
+ const tr = document.createElement('tr');
104
+ if (className)
105
+ tr.className = className;
106
+ for (const [text, cls, span] of cells) {
107
+ const td = document.createElement('td');
108
+ td.textContent = text;
109
+ if (cls)
110
+ td.className = cls;
111
+ if (span)
112
+ td.colSpan = span;
113
+ tr.appendChild(td);
114
+ }
115
+ return tr;
116
+ };
117
+ if ('error' in usage)
118
+ return [row([[usage.error]])];
119
+ // Whole KB throughout, so the column visibly adds up to the total.
120
+ const kb = toWholeKb(usage.rows, usage.used);
121
+ return [
122
+ // A row that rounds to nothing is left out rather than shown as 0.
123
+ ...usage.rows.flatMap((r, i) => kb[i] === 0 ? [] : [
124
+ row([[r.label], [r.count, 'num'], [`${kbFormat.format(kb[i])} KB`, 'num']]),
125
+ ]),
126
+ row([
127
+ ['Total'],
128
+ // Spans Count too: in the Size column alone, this longer text
129
+ // would set that column's width and open a wide gap before every
130
+ // size.
131
+ [formatStorageUsage(usage.used, usage.quota), 'num', 2],
132
+ ], 'total'),
133
+ ];
134
+ }
135
+ // The Downloads folder the local-copy feature learned (see
136
+ // docs/local-copy.md). Editable for the one case the extension can't
137
+ // detect on its own: Chrome's download location changed after the
138
+ // first copy. Saving keeps the recorded collection id — the next copy
139
+ // re-checks whether that collection still sits at the new path.
140
+ function initDownloadsDirControls() {
141
+ const box = document.getElementById('downloads-dir');
142
+ const saveBtn = document.getElementById('downloads-dir-save');
143
+ const status = document.getElementById('downloads-dir-status');
144
+ if (!box || !saveBtn || !status)
145
+ return;
146
+ let saved = null;
147
+ const paint = (setting) => {
148
+ saved = setting;
149
+ if (!box.matches(':focus'))
150
+ box.value = setting?.downloadsDir ?? '';
151
+ saveBtn.disabled = box.value.trim() === (saved?.downloadsDir ?? '');
152
+ };
153
+ void readDownloadsCopiesSetting().then(paint);
154
+ chrome.storage.onChanged.addListener((changes, areaName) => {
155
+ if (areaName !== 'local')
156
+ return;
157
+ const change = changes[DOWNLOADS_COPIES_KEY];
158
+ if (change)
159
+ paint(parseDownloadsCopiesSetting(change.newValue));
160
+ });
161
+ box.addEventListener('input', () => {
162
+ saveBtn.disabled = box.value.trim() === (saved?.downloadsDir ?? '');
163
+ status.textContent = '';
164
+ });
165
+ saveBtn.addEventListener('click', () => {
166
+ const dir = box.value.trim().replace(/\/+$/, '');
167
+ if (!dir.startsWith('/')) {
168
+ status.textContent = 'The Downloads folder must be an absolute path';
169
+ status.className = 'token-status err';
170
+ return;
171
+ }
172
+ void writeSetting(DOWNLOADS_COPIES_KEY, { downloadsDir: dir, collectionId: saved?.collectionId ?? null })
173
+ .then(() => {
174
+ status.textContent = 'Saved';
175
+ status.className = 'token-status ok';
176
+ }, (err) => {
177
+ status.textContent = `Could not save the Downloads folder: ${describeError(err)}`;
178
+ status.className = 'token-status err';
179
+ });
180
+ });
181
+ }
182
+ function initBooleanSetting(elementId, key, defaultValue) {
183
+ const cb = document.getElementById(elementId);
184
+ if (!cb)
185
+ return;
186
+ const read = (raw) => (typeof raw === 'boolean' ? raw : defaultValue);
187
+ // Hydrate from storage so the box reflects the persisted state, then
188
+ // mirror future writes (from anywhere — DevTools, another options
189
+ // tab) so the two never drift out of sync.
190
+ void chrome.storage.local.get(key).then((got) => {
191
+ cb.checked = read(got[key]);
192
+ });
193
+ chrome.storage.onChanged.addListener((changes, areaName) => {
194
+ if (areaName !== 'local')
195
+ return;
196
+ const change = changes[key];
197
+ if (!change)
198
+ return;
199
+ cb.checked = read(change.newValue);
200
+ });
201
+ cb.addEventListener('change', () => {
202
+ const wanted = cb.checked;
203
+ writeSetting(key, wanted).catch((err) => {
204
+ console.info(`[SourceMonkey] could not save ${key}:`, describeError(err));
205
+ // Back to what is stored, unless another change has moved it since.
206
+ if (cb.checked === wanted)
207
+ cb.checked = !wanted;
208
+ });
209
+ });
210
+ }
211
+ // Local search roots: a multi-line box, one absolute path per line.
212
+ //
213
+ // The setting stores the PARSED array, so this box is where text
214
+ // becomes roots — which is why Save is disabled while any line is
215
+ // malformed. Nothing downstream re-parses, so nothing downstream has to
216
+ // decide what a bad line means.
217
+ //
218
+ // Two kinds of complaint, and only one of them blocks Save:
219
+ // - Format. Decided locally and instantly. Blocks.
220
+ // - Existence. Requires a `file://` read, so it's debounced — and
221
+ // it's a warning: a directory can appear later (a repo you haven't
222
+ // cloned yet), and saving a root that isn't there yet costs
223
+ // nothing, since resolution simply finds no candidate under it.
224
+ function initLocalRootControls() {
225
+ const box = document.getElementById('local-roots-text');
226
+ const saveBtn = document.getElementById('local-roots-save');
227
+ const status = document.getElementById('local-roots-status');
228
+ const problems = document.getElementById('local-roots-problems');
229
+ const fileAccess = document.getElementById('local-roots-file-access');
230
+ if (!box || !saveBtn || !status || !problems || !fileAccess)
231
+ return;
232
+ let savedText = '';
233
+ let checkTimer;
234
+ // Existence results, keyed by canonical root URL. Cached for the
235
+ // page's lifetime so re-validating on each keystroke doesn't re-probe
236
+ // every unchanged line.
237
+ const exists = new Map();
238
+ void chrome.storage.local.get(LOCAL_ROOTS_KEY).then((got) => {
239
+ savedText = textFromRoots(readRootsSetting(got[LOCAL_ROOTS_KEY]));
240
+ // Don't overwrite a user who started typing before the read landed
241
+ // — the same reasoning as the cross-tab guard below, for the much
242
+ // narrower window between page load and hydration.
243
+ if (!box.value)
244
+ box.value = savedText;
245
+ void validate();
246
+ });
247
+ chrome.storage.onChanged.addListener((changes, areaName) => {
248
+ if (areaName !== 'local')
249
+ return;
250
+ const change = changes[LOCAL_ROOTS_KEY];
251
+ if (!change)
252
+ return;
253
+ const next = textFromRoots(readRootsSetting(change.newValue));
254
+ if (next === savedText)
255
+ return;
256
+ savedText = next;
257
+ // Don't clobber an edit being typed in THIS box with a write from
258
+ // another options tab. Focus is the signal: a box the user is in
259
+ // keeps its text and shows "Unsaved changes" against the new saved
260
+ // value, so nothing is lost silently either way.
261
+ if (box.value !== next && !box.matches(':focus'))
262
+ box.value = next;
263
+ void validate();
264
+ });
265
+ box.addEventListener('input', () => {
266
+ renderProblems();
267
+ if (checkTimer !== undefined)
268
+ clearTimeout(checkTimer);
269
+ checkTimer = setTimeout(() => void validate(), 400);
270
+ });
271
+ saveBtn.addEventListener('click', () => void onSave());
272
+ async function onSave() {
273
+ const roots = rootsFromText(box.value);
274
+ // Save is disabled while this is null, so reaching it means a
275
+ // keystroke landed between the last render and the click.
276
+ if (!roots)
277
+ return;
278
+ // Write back the canonical form: the stored array IS the setting,
279
+ // and showing anything else would leave the box disagreeing with
280
+ // what's actually searched.
281
+ savedText = textFromRoots(roots);
282
+ box.value = savedText;
283
+ saveBtn.disabled = true;
284
+ try {
285
+ await writeSetting(LOCAL_ROOTS_KEY, roots);
286
+ // The roots only change what the NEXT refresh resolves to, so the
287
+ // refresh is part of the save — same reasoning as the collection
288
+ // dialog's local-overrides option, which refreshes its collection
289
+ // on save. Without it a newly-added root would
290
+ // appear to do nothing until something else happened to touch a
291
+ // collection.
292
+ setStatus('Saved, refreshing scripts', '');
293
+ await refreshLocalCollections();
294
+ }
295
+ finally {
296
+ saveBtn.disabled = false;
297
+ }
298
+ await validate();
299
+ }
300
+ // Refresh only the collections a root could possibly affect. http(s)
301
+ // collections and the web / GitHub containers resolve everything over
302
+ // the network, so refreshing them would be cost with no possible
303
+ // change.
304
+ async function refreshLocalCollections() {
305
+ try {
306
+ const collections = await getCollections();
307
+ for (const collection of collections) {
308
+ if (!collectionSupportsLocalFiles(collection.path))
309
+ continue;
310
+ await updateCollection(collection.id, 'manual');
311
+ }
312
+ }
313
+ catch {
314
+ // A refresh that fails is not a failed save: the setting is
315
+ // written, and every collection re-resolves on its own next
316
+ // refresh. Per-collection failures already surface on the
317
+ // collections page.
318
+ }
319
+ }
320
+ // Probe every well-formed root that hasn't been probed yet, then
321
+ // repaint. Cheap after the first pass — `exists` short-circuits.
322
+ async function validate() {
323
+ renderProblems();
324
+ const entries = parseLocalRootLines(box.value);
325
+ // A malformed line already blocks Save, so there is nothing to be
326
+ // learned from probing the others yet — and the format complaint is
327
+ // the one the user has to act on first.
328
+ if (entries.some((e) => e.error))
329
+ return;
330
+ const unknown = entries
331
+ .flatMap((e) => (e.url ? [e.url] : []))
332
+ .filter((url) => !exists.has(url));
333
+ // Before the early return, or a banner raised by an earlier pass
334
+ // outlives the condition that raised it — deleting the offending
335
+ // line would leave it on screen with nothing left to probe.
336
+ if (unknown.length === 0) {
337
+ if (await isFileSchemeAccessAllowed())
338
+ fileAccess.replaceChildren();
339
+ return;
340
+ }
341
+ // With "Allow access to file URLs" off, every read fails the same
342
+ // way a missing directory does — so say which it is, using the
343
+ // shared banner rather than a second copy of its wording. Reload is
344
+ // the right retry here: `exists` is cached for the page's life.
345
+ if (!(await isFileSchemeAccessAllowed())) {
346
+ if (!fileAccess.firstChild)
347
+ fileAccess.appendChild(renderFileAccessBanner());
348
+ return;
349
+ }
350
+ fileAccess.replaceChildren();
351
+ await Promise.all(unknown.map(async (url) => {
352
+ exists.set(url, await isReadableDir(url));
353
+ }));
354
+ renderProblems();
355
+ }
356
+ function renderProblems() {
357
+ const entries = parseLocalRootLines(box.value);
358
+ problems.replaceChildren();
359
+ for (const entry of entries) {
360
+ const message = problemFor(entry);
361
+ if (!message)
362
+ continue;
363
+ const li = document.createElement('li');
364
+ li.textContent = message;
365
+ problems.appendChild(li);
366
+ }
367
+ renderStatus(entries);
368
+ }
369
+ function problemFor(entry) {
370
+ if (entry.error)
371
+ return `Line ${entry.lineNumber}: ${entry.error}`;
372
+ if (entry.url && exists.get(entry.url) === false) {
373
+ return `Line ${entry.lineNumber}: No directory found at ${entry.text}`;
374
+ }
375
+ return null;
376
+ }
377
+ function renderStatus(entries) {
378
+ const bad = entries.filter((e) => e.error).length;
379
+ // A format error is the one thing that blocks a save. The listed
380
+ // problem already names the line and the fix, and the button is
381
+ // visibly disabled, so the status line stays out of it rather than
382
+ // restating both.
383
+ saveBtn.disabled = bad > 0;
384
+ if (bad > 0) {
385
+ setStatus('', '');
386
+ return;
387
+ }
388
+ if (textFromRoots(rootsFromText(box.value) ?? []) !== savedText) {
389
+ setStatus('Unsaved changes', '');
390
+ return;
391
+ }
392
+ if (entries.length === 0) {
393
+ setStatus('No directories configured, so only each script\'s own folder and its collection\'s are searched', '');
394
+ return;
395
+ }
396
+ setStatus(`Searching ${entries.length} ${entries.length === 1 ? 'directory' : 'directories'} after each script's own folder and its collection's`, 'ok');
397
+ }
398
+ function setStatus(text, cls) {
399
+ status.textContent = text;
400
+ status.classList.remove('ok', 'err');
401
+ if (cls)
402
+ status.classList.add(cls);
403
+ }
404
+ }
405
+ // True when the URL names a readable directory. Chrome serves a
406
+ // directory listing for a `file://` URL ending in `/`, and fails for a
407
+ // plain file or a missing path — so the trailing slash the canonical
408
+ // root form already carries is what makes this a directory test.
409
+ async function isReadableDir(rootUrl) {
410
+ try {
411
+ await fetchFileUrlText(rootUrl);
412
+ return true;
413
+ }
414
+ catch {
415
+ return false;
416
+ }
417
+ }
418
+ function initGithubTokenControls() {
419
+ const input = document.getElementById('github-token');
420
+ const revealBtn = document.getElementById('github-token-reveal');
421
+ const saveBtn = document.getElementById('github-token-save');
422
+ const clearBtn = document.getElementById('github-token-clear');
423
+ const status = document.getElementById('github-token-status');
424
+ if (!input || !revealBtn || !saveBtn || !clearBtn || !status)
425
+ return;
426
+ // Hydrate from storage. We deliberately do NOT pre-fill the input
427
+ // when a token already exists — instead show a "token is set" status
428
+ // and leave the field empty, so a shoulder-surfer (or screenshot)
429
+ // doesn't reveal the value. Saving an empty input is a no-op; Clear
430
+ // is the only way to remove a stored token.
431
+ let storedToken = '';
432
+ void chrome.storage.local.get(GITHUB_AUTH_TOKEN_KEY).then((got) => {
433
+ const raw = got[GITHUB_AUTH_TOKEN_KEY];
434
+ storedToken = typeof raw === 'string' ? raw : '';
435
+ renderStatus();
436
+ });
437
+ chrome.storage.onChanged.addListener((changes, areaName) => {
438
+ if (areaName !== 'local')
439
+ return;
440
+ const change = changes[GITHUB_AUTH_TOKEN_KEY];
441
+ if (!change)
442
+ return;
443
+ storedToken = typeof change.newValue === 'string' ? change.newValue : '';
444
+ renderStatus();
445
+ });
446
+ revealBtn.addEventListener('click', () => {
447
+ const visible = input.type === 'text';
448
+ input.type = visible ? 'password' : 'text';
449
+ revealBtn.textContent = visible ? 'Show' : 'Hide';
450
+ });
451
+ saveBtn.addEventListener('click', () => {
452
+ const next = input.value.trim();
453
+ if (!next) {
454
+ setStatus('Enter a token, or use Clear to remove the saved one.', 'err');
455
+ return;
456
+ }
457
+ void writeSetting(GITHUB_AUTH_TOKEN_KEY, next).then(() => {
458
+ input.value = '';
459
+ input.type = 'password';
460
+ revealBtn.textContent = 'Show';
461
+ // Reuse the same "A token is saved (ends in …XXXX)" message
462
+ // renderStatus produces on a hydrated page. Don't show a transient
463
+ // "Token saved." line that vanishes — the persistent message
464
+ // makes it obvious there IS a token, and which one.
465
+ // (The storage.onChanged listener will also fire renderStatus,
466
+ // but the explicit call here keeps the UI immediate.)
467
+ storedToken = next;
468
+ renderStatus();
469
+ }, (err) => setStatus(`Could not save the token: ${describeError(err)}`, 'err'));
470
+ });
471
+ clearBtn.addEventListener('click', () => {
472
+ void writeSetting(GITHUB_AUTH_TOKEN_KEY, null).then(() => {
473
+ input.value = '';
474
+ // Mirror Save's input reset so Show→Clear doesn't leave the
475
+ // (now empty) field stuck in text mode with a "Hide" button.
476
+ input.type = 'password';
477
+ revealBtn.textContent = 'Show';
478
+ storedToken = '';
479
+ renderStatus();
480
+ }, (err) => setStatus(`Could not remove the token: ${describeError(err)}`, 'err'));
481
+ });
482
+ // Allow Enter inside the input to save, so users don't have to mouse
483
+ // over to the Save button.
484
+ input.addEventListener('keydown', (ev) => {
485
+ if (ev.key === 'Enter') {
486
+ ev.preventDefault();
487
+ saveBtn.click();
488
+ }
489
+ });
490
+ function renderStatus() {
491
+ if (storedToken) {
492
+ setStatus(`A token is saved (ends in …${storedToken.slice(-4)}). Saved tokens are never re-displayed; paste a new one to replace it.`, 'ok');
493
+ }
494
+ else {
495
+ setStatus('No token saved. SourceMonkey will use unauthenticated GitHub API calls (60/hr).', '');
496
+ }
497
+ }
498
+ function setStatus(text, cls) {
499
+ status.textContent = text;
500
+ status.classList.remove('ok', 'err');
501
+ if (cls)
502
+ status.classList.add(cls);
503
+ }
504
+ }
@@ -0,0 +1,170 @@
1
+ // The viewer's heading: the shared vocabulary for it, the DOM that
2
+ // paints its leading terms, and the early paint that puts a name in the
3
+ // header bar before the rest of the page arrives.
4
+ //
5
+ // Why this is its own module, loaded from `userscript.html` ahead of
6
+ // `userscript.js`:
7
+ // - `userscript.js` is a large module graph that used to sit behind
8
+ // blocking Prism scripts. The browser's first paint landed well
9
+ // before any of it evaluated, so the header bar drew with an icon
10
+ // and nothing else and filled in a beat later — the one thing on
11
+ // the page that is supposed to say what the page IS.
12
+ // - This module has NO static imports, which is the point: a module
13
+ // does not evaluate until everything it imports has been fetched,
14
+ // so even one small import would put its first line behind another
15
+ // round trip and back after the first paint. Everything the early
16
+ // paint needs beyond the prefix is pulled in with a dynamic
17
+ // `import()`, after the prefix is already on screen.
18
+ // - `userscript.ts` imports it as well. A module runs once, so the
19
+ // early paint happens exactly once and the constants below have a
20
+ // single home rather than a copy per entry point.
21
+ //
22
+ // `import type` is fine here — it is erased, so it costs no fetch.
23
+ // What every heading's leading terms are joined by — the same `|` the
24
+ // tab titles use (architecture.md, "Page names").
25
+ export const HEADING_SEPARATOR = ' | ';
26
+ // The product run, which every heading opens with and which the header
27
+ // bar paints in the brand colour.
28
+ const BRAND_PREFIX = `SourceMonkey${HEADING_SEPARATOR}`;
29
+ export const INSTALLED_HEADING = BRAND_PREFIX;
30
+ // The verb is a page name beside the product's, not a word governing
31
+ // the script's — so it takes its own separator rather than running
32
+ // straight into the favicon and the name.
33
+ export const PREVIEW_HEADING = `${BRAND_PREFIX}Install${HEADING_SEPARATOR}`;
34
+ // The installed title takes no prefix — a tab strip has little room,
35
+ // and the script's own name is what the reader is scanning for. The
36
+ // preview spends that room: "which of my tabs is asking to install
37
+ // something" is the question a preview tab has to answer.
38
+ export const INSTALLED_TITLE = '';
39
+ export const PREVIEW_TITLE = 'Install script: ';
40
+ // Stands in for the label when there is no script to name.
41
+ export const MISSING_LABEL = 'Not available';
42
+ // Same spelling as script-store's own (which is module-private), and as
43
+ // the copies in background.ts / injector.ts / script-log-id.ts. Reading
44
+ // one row is all this module does; importing the store to get the key
45
+ // would pull its whole graph in and undo the point of being early.
46
+ const SCRIPT_PREFIX = 'script:';
47
+ // The heading's leading run of product text. Shared with `showMissing`,
48
+ // which draws a heading with no script in it — the class, the constant
49
+ // and the ordering must not be written twice and drift.
50
+ //
51
+ // The product name and every separator wear `.brand` and take the
52
+ // header bar's colour (src/styles.css); the page names between them
53
+ // read in the heading's own. So the preview's heading is a brown
54
+ // "SourceMonkey | … | " skeleton with a dark "Install" set into it,
55
+ // which is the same shape the other pages' headings have — theirs just
56
+ // have no middle term.
57
+ export function headingPrefixEl(prefix) {
58
+ const frag = document.createDocumentFragment();
59
+ // A prefix always ends in a separator, so the last part is empty and
60
+ // contributes nothing but its own `|`.
61
+ const [product, ...rest] = prefix.split(HEADING_SEPARATOR);
62
+ frag.appendChild(brandHomeLink(product));
63
+ for (const part of rest) {
64
+ frag.appendChild(prefixSpan(HEADING_SEPARATOR, true));
65
+ if (part)
66
+ frag.appendChild(prefixSpan(part, false));
67
+ }
68
+ return frag;
69
+ }
70
+ // The product name, as a link home to the collections page — the same
71
+ // element `userscript.html` ships in its markup, so repainting the
72
+ // heading doesn't change what the bar's first term is.
73
+ //
74
+ // A plain anchor with a real href, and no listener of its own: the
75
+ // click is caught by the page's delegated handler
76
+ // (`installHomeLinkHandler`, src/extension/page-links.ts), which is what makes it
77
+ // open the NAMED collections tab. Importing that here would put this
78
+ // module's first line behind another fetch and undo the early paint, so
79
+ // the href + `target` are also the behaviour before it runs.
80
+ function brandHomeLink(text) {
81
+ const el = document.createElement('a');
82
+ el.className = 'heading-prefix brand page-home';
83
+ el.href = 'collections.html';
84
+ el.target = '_blank';
85
+ el.rel = 'noopener';
86
+ el.title = 'Open the collections page';
87
+ el.textContent = text;
88
+ return el;
89
+ }
90
+ function prefixSpan(text, brand) {
91
+ const el = document.createElement('span');
92
+ el.className = brand ? 'heading-prefix brand' : 'heading-prefix';
93
+ el.textContent = text;
94
+ return el;
95
+ }
96
+ // `<label> | SourceMonkey`, with the preview's own prefix in front so a
97
+ // tab strip full of viewer tabs says which is an install question and
98
+ // which is a script already installed.
99
+ export function setPageTitle(prefix, label) {
100
+ document.title = `${prefix}${label} | SourceMonkey`;
101
+ }
102
+ // The favicon's fixed-width slot, empty. `renderHeading` fills it with
103
+ // the real thing later; drawing it now is what keeps the name from
104
+ // shifting sideways when that lands.
105
+ function emptyIconSlot() {
106
+ const el = document.createElement('span');
107
+ el.className = 'site-icon';
108
+ return el;
109
+ }
110
+ // Whether the full page has drawn this heading yet. The early paint is
111
+ // a placeholder for exactly as long as that hasn't happened, and it must
112
+ // never land on top of the real thing — `userscript.ts` calls
113
+ // `notePainted` from every path that writes the heading, and its storage
114
+ // reads can finish either side of ours.
115
+ //
116
+ // A flag rather than an inspection of the heading's own contents: the
117
+ // markup's brand run and a finished heading are both just spans and
118
+ // text, so any test on the DOM comes down to matching bytes that the
119
+ // markup is free to change without failing anything.
120
+ let painted = false;
121
+ export function notePainted() {
122
+ painted = true;
123
+ }
124
+ // ---------- The early paint ----------
125
+ // Everything above is also used by `userscript.ts`; the rest of this
126
+ // module is the side effect that makes loading it early worth doing.
127
+ //
128
+ // The prefix goes on synchronously, before any `await` or `import()`,
129
+ // because it is the half we can know without reading anything: which of
130
+ // the two pages this is. Only the script's name needs a storage read.
131
+ const headingEl = document.getElementById('filename');
132
+ const searchParams = new URLSearchParams(location.search);
133
+ const earlyScriptId = searchParams.get('script');
134
+ if (headingEl) {
135
+ // The install page's own name for itself. The installed page's prefix
136
+ // is already in the markup, but it is repainted here anyway so both
137
+ // modes leave the heading in the same shape — prefix, then the
138
+ // favicon's fixed-width slot, so the name doesn't shift sideways when
139
+ // the real favicon lands.
140
+ const prefix = earlyScriptId ? INSTALLED_HEADING : PREVIEW_HEADING;
141
+ if (earlyScriptId || searchParams.get('preview')) {
142
+ headingEl.replaceChildren(headingPrefixEl(prefix), emptyIconSlot());
143
+ }
144
+ }
145
+ void paintEarlyName();
146
+ // The script's name, which needs the one `script:<id>` row plus the
147
+ // helpers that turn it into a label. A preview has no name to paint:
148
+ // its script's `@name` is inside a file that hasn't been fetched, and
149
+ // naming it from the URL would only move the flash — the location it
150
+ // would show is usually not what the finished heading says.
151
+ async function paintEarlyName() {
152
+ if (!headingEl || !earlyScriptId)
153
+ return;
154
+ const key = `${SCRIPT_PREFIX}${earlyScriptId}`;
155
+ const [got, display] = await Promise.all([
156
+ chrome.storage.local.get(key),
157
+ import('../script-display.js'),
158
+ ]);
159
+ const script = got[key];
160
+ if (!script)
161
+ return;
162
+ // Bail if the full page beat us to it while we were reading.
163
+ if (painted)
164
+ return;
165
+ const label = display.scriptLabel(script.header, script.relativePath);
166
+ // The same slash-aware wrap `renderHeading` uses, so the finished
167
+ // heading breaks its lines exactly where this one did.
168
+ display.appendWithSlashBreaks(headingEl, label);
169
+ setPageTitle(INSTALLED_TITLE, label);
170
+ }