@arraypress/extension-kit 1.10.0 → 1.11.1

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 (69) hide show
  1. package/README.md +103 -21
  2. package/bin/extension-kit.js +2 -1
  3. package/package.json +1 -1
  4. package/src/ai/host.d.ts +183 -0
  5. package/src/ai/host.js +225 -0
  6. package/src/ai/model.d.ts +2 -1
  7. package/src/ai/model.js +10 -2
  8. package/src/build/vendor.d.ts +29 -8
  9. package/src/build/vendor.js +136 -40
  10. package/src/content/highlight.d.ts +15 -6
  11. package/src/content/highlight.js +42 -17
  12. package/src/content/overlay.d.ts +19 -4
  13. package/src/content/overlay.js +44 -6
  14. package/src/content/slices.d.ts +3 -1
  15. package/src/content/slices.js +29 -14
  16. package/src/content/watch.d.ts +4 -2
  17. package/src/content/watch.js +17 -3
  18. package/src/dom/focus.d.ts +11 -0
  19. package/src/dom/focus.js +15 -0
  20. package/src/frame/window.d.ts +1 -1
  21. package/src/frame/window.js +8 -7
  22. package/src/index.d.ts +10 -3
  23. package/src/index.js +8 -3
  24. package/src/panel/show-on-page.d.ts +82 -0
  25. package/src/panel/show-on-page.js +83 -0
  26. package/src/scripting/call.d.ts +29 -9
  27. package/src/scripting/call.js +35 -11
  28. package/src/scripting/context-menus.d.ts +45 -0
  29. package/src/scripting/context-menus.js +53 -0
  30. package/src/scripting/registration.d.ts +2 -1
  31. package/src/scripting/registration.js +2 -1
  32. package/src/scripting/tab.d.ts +17 -5
  33. package/src/scripting/tab.js +15 -7
  34. package/src/settings/saver.js +16 -6
  35. package/src/settings/storage.d.ts +33 -2
  36. package/src/settings/storage.js +73 -3
  37. package/src/settings/store.d.ts +17 -0
  38. package/src/settings/store.js +87 -20
  39. package/src/sites/sites.d.ts +6 -1
  40. package/src/sites/sites.js +20 -3
  41. package/src/styles/chrome-ui.css +59 -30
  42. package/src/testing/e2e.d.ts +35 -4
  43. package/src/testing/e2e.js +67 -5
  44. package/src/testing/fake-model.d.ts +9 -1
  45. package/src/testing/fake-model.js +44 -23
  46. package/src/testing/fake-storage.d.ts +41 -13
  47. package/src/testing/fake-storage.js +163 -51
  48. package/src/testing/index.d.ts +3 -3
  49. package/src/testing/index.js +5 -3
  50. package/src/testing/live-run.d.ts +23 -2
  51. package/src/testing/live-run.js +64 -18
  52. package/src/testing/messages.d.ts +7 -3
  53. package/src/testing/messages.js +50 -9
  54. package/src/testing/package.d.ts +56 -7
  55. package/src/testing/package.js +57 -18
  56. package/src/testing/prompt.d.ts +4 -2
  57. package/src/testing/prompt.js +7 -3
  58. package/src/time/pool.d.ts +2 -1
  59. package/src/time/pool.js +4 -2
  60. package/src/toolbar/theme.d.ts +10 -2
  61. package/src/toolbar/theme.js +12 -4
  62. package/src/ui/menu.d.ts +13 -3
  63. package/src/ui/menu.js +48 -11
  64. package/src/ui/model-row.js +5 -1
  65. package/src/ui/site-list.js +12 -3
  66. package/src/ui/text-dialog.d.ts +3 -2
  67. package/src/ui/text-dialog.js +10 -3
  68. package/src/ui/toast.d.ts +3 -0
  69. package/src/ui/toast.js +23 -1
package/README.md CHANGED
@@ -74,9 +74,12 @@ Other libraries are copied the same way, by name (a path ending in `/` copies a
74
74
  ```
75
75
 
76
76
  `npm install some-package` doesn't run the extension's own `postinstall`, so a copy can go stale
77
- without a word. Each copy is stamped with its package's version (`vendored.json`, left out of the
78
- zip), and `extension-kit vendor-check` fails, naming the copy, until `npm run postinstall` remakes
79
- it. From a script: `vendorPackage(name, files, dest)`, `checkVendored(dir)`.
77
+ without a word. Each copy is stamped with its package's version, what was copied and a hash of it
78
+ (`vendored.json`, left out of the zip), and `extension-kit vendor-check` fails, naming the copy,
79
+ until `npm run postinstall` remakes it: a copy of another version, one edited by hand, one whose
80
+ package's files changed under the same version (a library copied into `node_modules` to try it
81
+ before it's published), or a vendor folder with no copies in it at all. A copy is never made into
82
+ a folder that holds a `manifest.json`, a `package.json` or a `.git` (the folder is emptied first). From a script: `vendorPackage(name, files, dest)`, `checkVendored(dir)`.
80
83
 
81
84
  ### An extension with a bundler
82
85
 
@@ -116,10 +119,12 @@ saver.follow((settings) => show(settings), chrome.storage.onChanged);
116
119
 
117
120
  | Store member | What it does |
118
121
  |---|---|
119
- | `load(storage)` | Every setting: sync, then held refusals, then local, normalised. |
122
+ | `load(storage)` | Every setting: sync, then held refusals (the ones still newer than what sync holds), then local, normalised. |
120
123
  | `save(storage, changes)` | Writes only the settings named, so another computer's change to a different one survives. Resolves to `{settings, synced}`. |
121
- | `writeSynced(storage, values)` | One sync write; what sync refuses (rate limit, an item over 8,192 bytes) is kept in local under `unsyncedKey` and retried with the next write. |
124
+ | `writeSynced(storage, values)` | One sync write; what sync refuses (rate limit, an item over 8,192 bytes) is kept in local under `unsyncedKey` and retried with the next write. Items refused together are tried one at a time, and a held change is dropped once another computer has changed that setting since. Holding runs under a Web Lock, so two writers at once lose nothing. |
122
125
  | `retryUnsynced(storage)` | Try the held refusals again. |
126
+ | `retryable(storage)` | Whether trying again could help (something held is still the newest and fits one sync item). |
127
+ | `syncItemBytes(key, value)` (an export) | What Chrome Sync counts for one item: the key plus the value as Chrome's JSON writer spells it (`<` is six bytes). |
123
128
  | `changed(changes, area)` | The settings a `chrome.storage.onChanged` event touches. |
124
129
  | `normalize(raw)`, `defaults`, `localKeys`, `syncedKeys`, `unsyncedKey` | The schema. |
125
130
 
@@ -248,7 +253,7 @@ chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab
248
253
  // In the panel: every frame of the tab, or why it can't be read.
249
254
  const reading = await callInTab(chrome, tabId, 'content/main.js', 'scan');
250
255
  if (reading.state === 'read') show(reading.frames); // top frame first
251
- else showMessage(reading.state); // 'browser' (Chrome's own page) or 'no-access'
256
+ else showMessage(reading.state); // 'browser' (Chrome's own page), 'no-access', 'failed' or 'timeout'
252
257
 
253
258
  // In the content script: a layer the page's styles can't touch (a closed shadow root). The host's
254
259
  // own styles go in hostStyle: its inline `all: initial !important` reset beats a `:host` rule.
@@ -259,10 +264,28 @@ const { root, remove } = pageOverlay(document, {
259
264
  });
260
265
 
261
266
  // “Show on page”: boxes in DevTools' blue, kept on the elements as the page scrolls; Esc clears.
262
- const highlight = createHighlighter(document, { root: () => root });
267
+ // onClear says why they went, so the panel hears of the ones it didn't ask for.
268
+ const highlight = createHighlighter(document, {
269
+ root: () => root,
270
+ onClear: (reason) => { if (reason === 'escape' || reason === 'pagehide') chrome.runtime.sendMessage({ type: 'boxes-cleared' }); },
271
+ });
263
272
  highlight.show(elements, { reveal: true });
273
+
274
+ // In the panel: which item is shown, whatever ends it.
275
+ const onPage = createShowOnPage({ show: (ids, frame) => callPage('highlight', [ids, frame]), onChange: press });
276
+ button.addEventListener('click', () => onPage.toggle(font.id, [font.id], font.frameId));
277
+ chrome.runtime.onMessage.addListener((message) => { if (message.type === 'boxes-cleared') onPage.cleared(); });
278
+ addEventListener('pagehide', () => onPage.reset()); // and `await onPage.reset()` before a rescan
264
279
  ```
265
280
 
281
+ A page can load a web-accessible module by the extension's address, and so tell the extension
282
+ is installed. `"use_dynamic_url": true` stops that (the page's `import()` and `fetch()` are
283
+ refused) and `callInPage`'s own import still works, but **only for a module with no static
284
+ imports**: Chrome refuses a dynamic-URL module's `import … from './part.js'` (checked in Chromium
285
+ 156, `tests/chrome-chromium.test.js`). The family's page modules import their parts and the kit
286
+ statically and have no bundler, so they are listed without it; a single-file page module (a
287
+ bundled one) can use it.
288
+
266
289
  `callInPage(scripting, target, module, name, args, {immediately})` resolves to each frame's
267
290
  `{frameId, result}` (frames Chrome couldn't run in left out) and rejects when it can run in none;
268
291
  `immediately` (default true) doesn't wait for the page to be idle. `openSidePanel` sets the tab's
@@ -270,14 +293,20 @@ panel to `path?tab=<id>` (`sidePanelPath`) and opens it in the same moment: Chro
270
293
  only while the click counts. `callInTab(edges, tabId, module, name, args)` calls it in every frame
271
294
  and gives `{state: 'read', frames}` (frames that answered, top first), or `{state: 'browser'}` for
272
295
  a page Chrome keeps to itself (`isBrowserPage(url)`: its own pages, the Web Store) or
273
- `{state: 'no-access'}` (the tab went to another site since the click). `pageOverlay` puts a host
296
+ `{state: 'no-access'}` (the tab went to another site since the click), `{state: 'failed', error}`
297
+ (the page module didn't load or threw in every frame; Chrome alone reports that as null) or, with
298
+ `{timeoutMs}`, `{state: 'timeout'}` (Chrome answers only once every frame has). `pageOverlay` puts a host
274
299
  element of its own name on the root element, replacing one a run before left; `root` is its closed
275
300
  shadow root, `css` first. The host is reset inline (`all: initial !important`) so the page can't
276
301
  move or hide it; that also beats a `:host { … }` rule and `host.style.x = …`, so the host's own
277
- styles go in `hostStyle` (set `!important` after the reset). `createHighlighter(doc, {root, onClear,
302
+ styles go in `hostStyle` (set `!important` after the reset). `topLayer: true` makes the host a
303
+ manual popover, drawn over the page's modal dialogs and fullscreen element and put back on top
304
+ (`raise()`) when the page opens one after it; a modal dialog still makes it inert, so it shows but
305
+ can't be clicked while the dialog is open. `createHighlighter(doc, {root, onClear,
278
306
  max})` boxes up to 500 elements (`MAX_BOXES`) in the overlay (`HIGHLIGHT_CSS` styles them), only
279
- those in the window, following the page as it scrolls; `show` replaces the set, `clear` or Esc
280
- ends it.
307
+ those in the window, following the page as it scrolls; `show` replaces the set, `clear`, Esc or
308
+ leaving the page ends it, and `onClear(reason)` hears which (`'replaced'`, `'asked'`, `'escape'`,
309
+ `'pagehide'`). `createShowOnPage({show, onChange})` is the panel's side: `toggle`, `reset`, `cleared`.
281
310
 
282
311
  ```js
283
312
  // A page's elements in a line, and copying from a click (the copy command where the API can't).
@@ -294,6 +323,16 @@ can call it from a menu click or a shortcut (Chrome lets the focused active tab
294
323
  `noticeCard({icon, tone, title, text, buttons, actions})` is a side panel's notice (`.sp-notice`:
295
324
  frames from other sites to allow); `counted(count, one, many, {t, lang})` a count in words.
296
325
 
326
+ ## The right-click menu
327
+
328
+ ```js
329
+ // Built on install and on browser start; after a Chrome update both come in one run, and two
330
+ // builds side by side would create every item twice. Through the builder, one waits for the other.
331
+ const buildMenu = contextMenuBuilder(chrome);
332
+ chrome.runtime.onInstalled.addListener(() => buildMenu(menuItems(t)));
333
+ chrome.runtime.onStartup.addListener(() => buildMenu(menuItems(t)));
334
+ ```
335
+
297
336
  ## A content script on every site
298
337
 
299
338
  ```js
@@ -334,6 +373,34 @@ first session after Chrome starts can take ~18 s while the model loads: a questi
334
373
  limit comes back `'late'`, and the session goes on loading for the next one. Nothing loads it ahead
335
374
  of time (that would load the model on every start). `release()` lets the base session go.
336
375
 
376
+ A base session Chrome has let go since (the model was updated, its process ended) is made again
377
+ when its `clone()` fails.
378
+
379
+ **In a service worker.** Chrome's documentation says the Prompt API isn't in workers; Chrome 157
380
+ and Chromium 156 do have `LanguageModel` in an extension's service worker (checked in both, with
381
+ an unpacked extension: `'function'` in the worker, an extension page and an offscreen document).
382
+ Since neither is promised, a worker uses `createHostedModelSession`: the same `ModelSession`,
383
+ asking the worker's own `LanguageModel` when there is one and otherwise the extension's offscreen
384
+ document, over `chrome.runtime` messages.
385
+
386
+ ```js
387
+ // background.js
388
+ const words = createHostedModelSession({
389
+ name: 'words', options: ENGLISH, initialPrompts: [{ role: 'system', content: 'You list words.' }],
390
+ runtime: chrome.runtime,
391
+ open: () => watchFromOffscreen(chrome, undefined, 'Reads light or dark mode for the toolbar icon, and runs Chrome’s on-device model.'),
392
+ });
393
+
394
+ // offscreen/offscreen.js: the one offscreen document an extension gets does both jobs.
395
+ watchSchemeOffscreen(chrome.runtime);
396
+ serveModelOffscreen(chrome.runtime);
397
+ ```
398
+
399
+ The document is told each session's options and instructions with the request, keeps one base
400
+ session per `name`, keeps the time limit, and stops an answer when the caller's signal aborts. It
401
+ ignores requests from content scripts and other extensions. The schema for an answer crosses as
402
+ JSON (an object, not a RegExp).
403
+
337
404
  `modelAvailability(api, options)` never throws (`'missing'` without a Prompt API).
338
405
  `downloadModel(api, options, {onProgress})` is the only `create()` that may download: call it
339
406
  first in a click's handler, with nothing awaited before it. The settings row:
@@ -385,7 +452,9 @@ hands the main thread back whenever a slice has run its time, so a 2,000-item li
385
452
  freezes. `eachInSlices(items, work, {sliceMs, pause})` does the same for any list (each element's
386
453
  style read, each text measured): `work(item, index)`. `watchChanges` hands over what was added or changed a little after the page stops
387
454
  changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
388
- default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
455
+ default 6) whose children changed is handed over whole; what changes inside the extension's own
456
+ elements (`ignore`, asked of a change's target and its ancestors) sets nothing off. `inSlices` cuts
457
+ the page as the work goes and leaves out scripts, styles and templates. Both are pure DOM, so a content script
389
458
  imports them directly (list them in `web_accessible_resources`).
390
459
 
391
460
  `renderedElements(doc, {skip, limit})` (`content/elements.js`) lists every element Chrome draws,
@@ -439,7 +508,13 @@ It opens modally over its button, as Chrome's does (its top at the button's top,
439
508
  button's end), moved to stay 8px inside the window. A click outside, Esc, Tab or the window losing
440
509
  focus closes it; ↑/↓ (wrapping, past disabled items), Home and End move; focus goes back to the
441
510
  button (or `focusTo`); an item runs once the menu has closed. `createMenu({id, placement: 'below'})`
442
- opens it below the button instead. `menuPosition` and `isInside` are the pure geometry.
511
+ opens it below the button instead. `menuPosition` and `isInside` are the pure geometry. In a
512
+ content script, give it the overlay's shadow root (`createMenu({root})`, a closed one too): the
513
+ keys it handles don't reach the page's own shortcuts, and `destroy()` takes it off the page with
514
+ its window listener. On a right-to-left page it lines up with its button's other edge. The
515
+ button's box is read before the menu opens (opening takes the focus, and a button shown only
516
+ while its row holds the focus has no box after); a button that isn't drawn opens it from its
517
+ nearest drawn ancestor, the row.
443
518
 
444
519
  `moreActionsButton(name, options)` is the 32px ⋮ icon button, labelled "More actions for “name”"
445
520
  (`label` for a translation), with `aria-haspopup="menu"`; its icon is the stylesheet's
@@ -498,7 +573,9 @@ toast.show('Copied'); // no Undo
498
573
  `#toast` (`role="status"`), bottom left, a manual `popover` so it's in the top layer, over
499
574
  anything the page stacks (`:popover-open` while it shows). A new message replaces the last and
500
575
  restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duration` /
501
- `undoDuration`. Undo hides it and runs once. `undoLabel` translates the button.
576
+ `undoDuration`. Undo hides it and runs once. `undoLabel` translates the button. While the keyboard
577
+ focus is on Undo, or the pointer is over the toast, it stays; the wait starts again in full once
578
+ they leave (as cr-toast, which stops its timer on `focusin`).
502
579
 
503
580
  ### Site list
504
581
 
@@ -578,16 +655,16 @@ except those listed, each with a reason:
578
655
 
579
656
  | Export | What it does |
580
657
  |---|---|
581
- | `fakeLanguageModel(state)` | A fake Prompt API (`LanguageModel`): `availability`, `create` (download progress, a cold start, `failDownload`), sessions that `clone`, answer (`answerMs`, `answer(prompt)`, `failPrompt`) and stop on their signal; `log` of what it was asked, `set` to change it. Browser-safe, so a test build can put it on `globalThis`. |
582
- | `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`: JSON copies, `onChanged` for real changes only, sync's 8,192-byte item quota, its 102,400 bytes in all (`SYNC_QUOTA_BYTES`) and 120 writes a minute. `settled()` waits for listeners. |
658
+ | `fakeLanguageModel(state)` | A fake Prompt API (`LanguageModel`): `availability`, `create` (download progress, a cold start, `failDownload`), sessions that `clone`, answer (`answerMs`, `answer(prompt)`, `failPrompt`) and stop on their signal, a destroyed session that answers nothing more, a download refused without a click (`userActivation`); `log` of what it was asked, `set` to change it. Browser-safe, so a test build can put it on `globalThis`. |
659
+ | `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`, each point checked against Chromium: values as Chrome stores them (`undefined`, functions and `NaN` dropped, a `Date` is `{}`), `get` with a key, keys, defaults or nothing, `clear`, `getBytesInUse`, `onChanged` (the area's own too, `removeListener`) for real changes only, whatever order an object's keys come in. Sync's quotas with Chrome's own errors: 8,192 bytes an item counted as Chrome counts them (`syncItemBytes`), 102,400 bytes (`SYNC_QUOTA_BYTES`) and 512 items (`SYNC_MAX_ITEMS`) in all, 120 calls a minute and 1,800 an hour (`SYNC_WRITES_PER_MINUTE`, `SYNC_WRITES_PER_HOUR`) of each of `set`, `remove` and `clear`; local and session hold 10 MB (`LOCAL_QUOTA_BYTES`). `settled()` waits for listeners. |
583
660
  | `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
584
661
  | `launchDownloading(chromium, dir, options)`, `filesIn(dir)` | An extension in a browser that downloads for real: `{…launchExtension's, downloads}`, the profile's own Downloads folder (Playwright otherwise saves downloads under made-up names no extension sees); the files under a folder, downloads still running left out. |
585
662
  | `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
586
663
  | `runChecks(runner, body, cleanUps)` | A script's checks: an error outside any check fails, the clean-ups always run (each within 30 s), then the tally. |
587
- | `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
664
+ | `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales`, as Chromium answers: a locale over en, names in any case, named placeholders and `$1`–`$9` filled, `$$` a dollar sign; a message naming a placeholder it doesn't define throws (Chrome won't load that extension). For unit tests. |
588
665
  | `heartbeat(page)` | A timer beating in a page: `stop()` gives the longest the main thread was held (ms), for performance checks. |
589
- | `playPermissionPrompt(answers)` | Chrome's permission prompt played in a page (`page.evaluate` or `addInitScript`): each request recorded on `window.asked` with whether a click was behind it, answered from `answers`. |
590
- | `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
666
+ | `playPermissionPrompt(answers)` | Chrome's permission prompt played in a page (`page.evaluate` or `addInitScript`): each request recorded on `window.asked` with whether a click was behind it, answered from `answers`; one with no click behind it is refused, as Chrome refuses it. |
667
+ | `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()` (exits 1 on a failure, or when no check ran at all). A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
591
668
  | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
592
669
  | `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
593
670
  | `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
@@ -599,16 +676,21 @@ except those listed, each with a reason:
599
676
  | `until(fn, what, timeout?)` | Poll until truthy. |
600
677
  | `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
601
678
  | `recordIconCalls(worker)` | Record `chrome.action.setIcon` calls Chrome accepted. |
679
+ | `stopWorker(launched, {start})` | Stop the service worker as Chrome does when it's idle (memory gone, storage kept) and start it again, or leave it for the next event: for checks that state survives a restart. Playwright's debugger otherwise keeps the worker alive for the whole run. |
602
680
  | `installWarnings(worker, manifest)` | Chrome's own install warnings for a manifest (`chrome.management.getPermissionWarningsByManifest`, no permission needed), from a loaded extension's worker. |
603
681
  | `warningsAddedBy(worker, manifest, names)` | `{warnings, without, added}`: the warnings with and without some permissions, and those only they bring (`withoutPermissions` is the pure part). |
604
682
  | `savedTabGroups(profile)` | Chrome's saved tab groups (`{title, pinned, urls}`) read from a closed profile's `Sync Data/LevelDB`: no extension API lists them. |
605
683
  | `parseLiveOptions(argv, env, {usage})`, `parseProxy(address)` | A live checker's command line (`--only`, `--category`, `--concurrency`, `--proxy` or `HTTPS_PROXY`, `--locale`, `--timezone`, `--out`, `--help`), and a proxy address for Playwright with its password hidden for the output. |
606
- | `runLiveCheck({chromium, build, prefix, root, sites, categories, pageMs, visit, failed, onResult})`, `liveUsage(categories, n)` | A live checker's whole run: its command line (`parseLiveOptions`, usage from the categories), the sites chosen, the exit point printed, each site visited on a fresh page in a browser with the extension (one a slot, at most 4, passing for a person's), within `pageMs` (a browser whose page ran out of time replaced), Ctrl-C closing them all; `{dir, started, finished, exit, browser, results}` for the checker's report. |
684
+ | `runLiveCheck({chromium, build, prefix, root, sites, categories, pageMs, visit, failed, onResult})`, `liveUsage(categories, n)` | A live checker's whole run: its command line (`parseLiveOptions`, usage from the categories), the sites chosen, the exit point printed, each site visited on a fresh page in a browser with the extension (one a slot, at most 4, passing for a person's), within `pageMs` (a browser whose page ran out of time replaced), Ctrl-C closing them all; `{dir, started, finished, exit, browser, results}` for the checker's report. A visit that throws, runs out of time or loses its browser is `failed(site, why)` and the run goes on with a new browser. The report folder is emptied only if the kit made it (`reportFolder`, `REPORT_MARKER`): `--out` on a folder with other files in it is refused. |
607
685
  | `exitPoint(request)` | Where the run's traffic leaves for the internet (`{ip, country, where, source}`), from a keyless lookup service through the context's proxy: sites decide what to show by it. |
608
- | `filesUnder`, `zipEntries`, `missingReferences`, `manifestReferences` | For a packaging test: the zip is the folder, and every path the extension names resolves (a `#fragment` or `?query` ignored: `referencedPath`). |
686
+ | `filesUnder`, `zipEntries`, `missingReferences`, `manifestReferences` | For a packaging test: the zip is the folder, and every path the extension names is a file the zip holds (one left out of the zip is as missing as one that isn't there): imports (`from`, `import()`, a bare `import './x.js'`), `getURL`, `src`/`href`, a stylesheet's `url()` and `@import`, and the manifest's worker, pages, icons, content scripts, default locale's messages, rule files and web-accessible resources. A `#fragment` or `?query` is ignored (`referencedPath`). |
609
687
 
610
688
  ## Stylesheet classes
611
689
 
690
+ The stylesheet uses logical properties (and mirrors the few things that need it: a select's arrow,
691
+ the switch's knob, the link-row arrow, the drawer), so a page with `dir="rtl"` reads as Chrome's
692
+ own pages do in a right-to-left language.
693
+
612
694
  | Piece | Markup |
613
695
  |---|---|
614
696
  | Page column (680px, 96%) | `<main class="page">` |
@@ -6,7 +6,8 @@
6
6
  * extension-kit vendor <dir> <package> <path>... copy files of an installed package into <dir>
7
7
  * (a path ending in / copies its contents)
8
8
  * extension-kit vendor-check [dir] fail when a copy in [dir] (default extension/vendor)
9
- * no longer matches the installed package
9
+ * no longer matches the installed package (another
10
+ * version, other files, edited since), or there's none
10
11
  * extension-kit icons [dir] render [dir]/icon.svg (default extension/icons)
11
12
  * extension-kit toolbar-icons [dir] render the toolbar glyph (default extension/icons/toolbar)
12
13
  * extension-kit duplication fail on copied code jscpd finds, less .jscpd-allowlist.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "Shared pieces for Chrome extensions: Chrome's own page styles and components, a Chrome Sync settings store, the light/dark toolbar glyph, Chrome's on-device model, content-script helpers, About links, and build and test helpers.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Chrome's on-device model for a service worker, wherever Chrome puts the
3
+ * Prompt API. Chrome's documentation says `LanguageModel` isn't in workers
4
+ * (https://developer.chrome.com/docs/ai/prompt-api); Chrome 157 and Chromium
5
+ * 156 do have it in an extension's service worker (checked in both: an
6
+ * unpacked extension, `typeof LanguageModel` in the worker, an extension page
7
+ * and an offscreen document, `'function'` in all three). So neither is
8
+ * promised, and a worker that uses the model shouldn't depend on which holds:
9
+ * {@link createHostedModelSession} is a `ModelSession` that asks the worker's
10
+ * own `LanguageModel` when there is one, and otherwise the extension's
11
+ * offscreen document, which has a page's Prompt API and answers over
12
+ * `chrome.runtime` messages ({@link serveModelOffscreen}).
13
+ *
14
+ * The offscreen document is the one the worker already keeps for the toolbar
15
+ * glyph (an extension gets only one): its script calls both
16
+ * `watchSchemeOffscreen` and `serveModelOffscreen`. It's told the session's
17
+ * options and instructions with each request, so it needs none of the
18
+ * extension's own code.
19
+ *
20
+ * @module ai/host
21
+ */
22
+ import type { InitialPrompt, LanguageModelOptions, LanguageModelStatic, ModelSession } from './model.js';
23
+ /** @import { AskOptions, Availability, InitialPrompt, LanguageModelOptions, LanguageModelStatic, ModelAnswer, ModelSession } from './model.js' */
24
+ /** The request's message type: `{type: MODEL_HOST, method, name, …}`. */
25
+ export declare const MODEL_HOST = "model-host";
26
+ /** How long past an answer's time limit the worker waits for the offscreen document to say so itself (ms). */
27
+ export declare const HOST_GRACE_MS = 1000;
28
+ export type HostRequest = {
29
+ /**
30
+ * The message type.
31
+ */
32
+ type: typeof MODEL_HOST;
33
+ /**
34
+ * What's asked.
35
+ */
36
+ method: 'availability' | 'ask' | 'abort' | 'release';
37
+ /**
38
+ * The session's name (one base session each).
39
+ */
40
+ name: string;
41
+ /**
42
+ * What its sessions are made with.
43
+ */
44
+ options?: LanguageModelOptions;
45
+ /**
46
+ * Its instructions and examples.
47
+ */
48
+ initialPrompts?: InitialPrompt[];
49
+ /**
50
+ * A question's id, for `abort`.
51
+ */
52
+ id?: string;
53
+ /**
54
+ * The question.
55
+ */
56
+ prompt?: string;
57
+ /**
58
+ * Its time limit.
59
+ */
60
+ timeoutMs?: number;
61
+ /**
62
+ * The answer's JSON Schema.
63
+ */
64
+ responseConstraint?: object;
65
+ };
66
+ export type HostSender = {
67
+ sendMessage(message: unknown): Promise<unknown>;
68
+ };
69
+ export type HostRuntime = {
70
+ /**
71
+ * This extension's id: requests from anyone else are ignored.
72
+ */
73
+ id: string;
74
+ /**
75
+ * `chrome.runtime.onMessage`.
76
+ */
77
+ onMessage: {
78
+ addListener(listener: (message: unknown, sender: {
79
+ id?: string;
80
+ tab?: unknown;
81
+ }, respond: (answer: unknown) => void) => boolean): void;
82
+ };
83
+ };
84
+ export type HostedSessionSetup = {
85
+ /**
86
+ * The session's name, its own among the extension's sessions ('topics').
87
+ */
88
+ name: string;
89
+ /**
90
+ * What sessions are made (and availability asked) with.
91
+ */
92
+ options: LanguageModelOptions;
93
+ /**
94
+ * The instructions and examples every question starts from.
95
+ */
96
+ initialPrompts?: InitialPrompt[];
97
+ /**
98
+ * `chrome.runtime`.
99
+ */
100
+ runtime: HostSender;
101
+ /**
102
+ * Makes sure the offscreen document is open
103
+ * (`() => watchFromOffscreen(chrome)`); only called when the worker has no Prompt API.
104
+ */
105
+ open: () => Promise<unknown>;
106
+ /**
107
+ * The worker's own Prompt API, read when used.
108
+ * Default Chrome's.
109
+ */
110
+ model?: () => LanguageModelStatic | undefined;
111
+ };
112
+ /**
113
+ * A request to the offscreen document.
114
+ *
115
+ * @typedef {object} HostRequest
116
+ * @property {typeof MODEL_HOST} type The message type.
117
+ * @property {'availability' | 'ask' | 'abort' | 'release'} method What's asked.
118
+ * @property {string} name The session's name (one base session each).
119
+ * @property {LanguageModelOptions} [options] What its sessions are made with.
120
+ * @property {InitialPrompt[]} [initialPrompts] Its instructions and examples.
121
+ * @property {string} [id] A question's id, for `abort`.
122
+ * @property {string} [prompt] The question.
123
+ * @property {number} [timeoutMs] Its time limit.
124
+ * @property {object} [responseConstraint] The answer's JSON Schema.
125
+ */
126
+ /**
127
+ * The parts of `chrome.runtime` the worker's side uses.
128
+ *
129
+ * @typedef {{sendMessage(message: unknown): Promise<unknown>}} HostSender
130
+ */
131
+ /**
132
+ * The parts of `chrome.runtime` the offscreen document's side uses.
133
+ *
134
+ * @typedef {object} HostRuntime
135
+ * @property {string} id This extension's id: requests from anyone else are ignored.
136
+ * @property {{addListener(listener: (message: unknown, sender: {id?: string, tab?: unknown}, respond: (answer: unknown) => void) => boolean): void}} onMessage
137
+ * `chrome.runtime.onMessage`.
138
+ */
139
+ /**
140
+ * @typedef {object} HostedSessionSetup
141
+ * @property {string} name The session's name, its own among the extension's sessions ('topics').
142
+ * @property {LanguageModelOptions} options What sessions are made (and availability asked) with.
143
+ * @property {InitialPrompt[]} [initialPrompts] The instructions and examples every question starts from.
144
+ * @property {HostSender} runtime `chrome.runtime`.
145
+ * @property {() => Promise<unknown>} open Makes sure the offscreen document is open
146
+ * (`() => watchFromOffscreen(chrome)`); only called when the worker has no Prompt API.
147
+ * @property {() => LanguageModelStatic | undefined} [model] The worker's own Prompt API, read when used.
148
+ * Default Chrome's.
149
+ */
150
+ /**
151
+ * A model session for a service worker: the worker's own Prompt API when it
152
+ * has one, else the offscreen document's. The same `ModelSession` either way.
153
+ * An answer's JSON Schema crosses to the offscreen document as JSON, so it
154
+ * must be a plain object there (not a RegExp).
155
+ *
156
+ * @param {HostedSessionSetup} setup - The session, and how to reach the offscreen document.
157
+ * @returns {ModelSession} The session.
158
+ * @example
159
+ * // background.js
160
+ * const words = createHostedModelSession({
161
+ * name: 'words', options: ENGLISH, initialPrompts: [{ role: 'system', content: 'You list words.' }],
162
+ * runtime: chrome.runtime, open: () => watchFromOffscreen(chrome),
163
+ * });
164
+ * const { text } = await words.ask('Topic: football', { timeoutMs: 20_000 });
165
+ */
166
+ export declare function createHostedModelSession({ name, options, initialPrompts, runtime, open, model }: HostedSessionSetup): ModelSession;
167
+ /**
168
+ * Answer the worker's model requests, in the offscreen document: one base
169
+ * session per name, made with the options and instructions the request
170
+ * carries (and again if they change). Requests from another extension, or
171
+ * from a content script (a web page's side), are ignored.
172
+ *
173
+ * @param {HostRuntime} runtime - `chrome.runtime`.
174
+ * @param {{model?: () => LanguageModelStatic | undefined}} [options] - The document's Prompt API. Default Chrome's.
175
+ * @returns {void}
176
+ * @example
177
+ * // offscreen/offscreen.js
178
+ * watchSchemeOffscreen(chrome.runtime);
179
+ * serveModelOffscreen(chrome.runtime);
180
+ */
181
+ export declare function serveModelOffscreen(runtime: HostRuntime, { model }?: {
182
+ model?: () => LanguageModelStatic | undefined;
183
+ }): void;