@arraypress/extension-kit 1.5.0 → 1.7.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.
package/README.md CHANGED
@@ -14,6 +14,11 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
14
14
  ("Rename folder", or several fields as "Add site search"), the toast with Undo and a settings card's site list, built on the
15
15
  stylesheet's classes; they work in a content script's shadow root too.
16
16
  - **Sites**: what people type into a per-site list, and whether a page is on one.
17
+ - **Chrome's on-device model** (Gemini Nano, the Prompt API): availability, a download from the
18
+ person's click with progress, one base session with a `clone()` per question, a time limit on
19
+ every answer, cold starts, and the settings page's “Chrome’s built-in AI” row.
20
+ - **Content-script helpers**: a big page worked through in slices that never hold it long, and a
21
+ change watcher for single-page apps and endless lists.
17
22
  - **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
18
23
  check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
19
24
  `chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
@@ -193,6 +198,99 @@ withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list
193
198
  A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
194
199
  `localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
195
200
 
201
+ ## A content script on every site
202
+
203
+ ```js
204
+ // The content script runs everywhere only while the optional all-sites grant is held, less the
205
+ // "Never on" sites; one call brings the registration in line, whatever changed.
206
+ const sync = contentScriptSync(chrome.scripting, 'prices', async () => {
207
+ const settings = await store.load(chrome.storage);
208
+ if (!settings.everySite || !(await chrome.permissions.contains({ origins: ALL_SITES }))) return null;
209
+ return { js: ['content/loader.js'], matches: ALL_SITES, excludeMatches: settings.neverSites.flatMap(hostPatterns), runAt: 'document_idle' };
210
+ });
211
+ chrome.permissions.onAdded.addListener(() => sync());
212
+ chrome.permissions.onRemoved.addListener(() => sync()); // site access taken back on chrome://extensions
213
+ ```
214
+
215
+ Registers, updates or unregisters the script `id` to match what `want()` gives at the time (null:
216
+ none). Calls run one after another, so a quick run of changes can't register twice or end out of
217
+ order; a refused change rejects that call only.
218
+
219
+ ## Chrome's on-device model
220
+
221
+ ```js
222
+ import { chromeLanguageModel, createModelSession } from '../vendor/extension-kit/index.js';
223
+
224
+ const ENGLISH = { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] };
225
+ const model = createModelSession({
226
+ model: chromeLanguageModel, // read when used: Chrome may add it after start-up
227
+ options: ENGLISH, // availability() is asked with what create() gets
228
+ initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }, ...EXAMPLE],
229
+ });
230
+ const answer = await model.ask(prompt, { timeoutMs: 2500, signal, responseConstraint: schema });
231
+ if (answer.text === null) useRules(answer.reason); // 'unavailable' | 'late' | 'aborted' | 'failed'
232
+ ```
233
+
234
+ Following https://developer.chrome.com/docs/ai/prompt-api: only an `available` model is used (a
235
+ question never downloads it); one base session is made when first needed and kept; each question
236
+ goes to a `clone()` of it, let go after; the answer's `AbortSignal` fires at the time limit. The
237
+ first session after Chrome starts can take ~18 s while the model loads: a question with a shorter
238
+ limit comes back `'late'`, and the session goes on loading for the next one. Nothing loads it ahead
239
+ of time (that would load the model on every start). `release()` lets the base session go.
240
+
241
+ `modelAvailability(api, options)` never throws (`'missing'` without a Prompt API).
242
+ `downloadModel(api, options, {onProgress})` is the only `create()` that may download: call it
243
+ first in a click's handler, with nothing awaited before it. The settings row:
244
+
245
+ ```html
246
+ <div class="row two-line">
247
+ <span class="label">
248
+ <span data-i18n="optionsModel">Chrome’s built-in AI</span>
249
+ <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
250
+ <progress id="model-progress" max="1" value="0" hidden></progress>
251
+ </span>
252
+ <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
253
+ </div>
254
+ ```
255
+
256
+ ```js
257
+ const row = createModelRow(
258
+ { status: byId('model-status'), help: byId('model-help'), download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
259
+ { options: ENGLISH, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } },
260
+ );
261
+ await row.refresh();
262
+ ```
263
+
264
+ Its words are the page's messages under `MODEL_MESSAGES`' keys (`optionsModel`, `optionsModelReady`,
265
+ `optionsModelNeedsDownload`, `optionsModelDownloadHelp`, `optionsModelDownload`,
266
+ `optionsModelDownloading` with `$PERCENT$`, `optionsModelDownloadFailed`, `optionsModelUnsupported`,
267
+ `optionsModelRequirements`, `optionsModelMissing`, `optionsModelMissingHelp`); `describeModel` maps
268
+ a state to them. Tests use `fakeLanguageModel` (`/testing`), the Prompt API's shape: availability,
269
+ download progress (or a failed download), a cold start, slow and failed answers, aborts, and a log
270
+ of what it was asked.
271
+
272
+ ## Content scripts
273
+
274
+ ```js
275
+ import { inSlices } from '../vendor/extension-kit/content/slices.js';
276
+ import { watchChanges } from '../vendor/extension-kit/content/watch.js';
277
+
278
+ await inSlices(document.body, (block) => scanner.scan(block)); // ≤4,000 characters a block, 40 ms slices
279
+ const stop = watchChanges(document.body, {
280
+ changed: (nodes) => nodes.forEach((node) => scanner.scan(node)), // one batch, each subtree once
281
+ removed: (node) => scanner.forget(node), // at once
282
+ ignore: (node) => node instanceof Element && node.localName === 'my-note', // the extension's own
283
+ settleMs: 120, maxWaitMs: 600,
284
+ });
285
+ ```
286
+
287
+ `inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
288
+ hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
289
+ freezes. `watchChanges` hands over what was added or changed a little after the page stops
290
+ changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
291
+ default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
292
+ imports them directly (list them in `web_accessible_resources`).
293
+
196
294
  ## Time limits
197
295
 
198
296
  ```js
@@ -203,6 +301,13 @@ const groups = await within(chrome.tabGroups.query({}), 2000, []); // a Chrome c
203
301
  rejects; its timer cleared once the work settles (the work itself isn't stopped). In the runtime
204
302
  half, so a service worker or page can use it; the test helpers use it too.
205
303
 
304
+ ```js
305
+ await eachLimited(files, 4, async (file) => save(file)); // at most four at once
306
+ ```
307
+
308
+ `eachLimited(items, limit, work)`: `work(item, slot, index)` on every item, at most `limit` at once;
309
+ in the runtime half too (`/testing` exports it as well).
310
+
206
311
  ## Page components
207
312
 
208
313
  Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
@@ -248,7 +353,11 @@ const site = await askText({
248
353
  });
249
354
  ```
250
355
 
251
- Resolves to the text, trimmed, or `null` when cancelled. The action button (default "Save") stays
356
+ Resolves to the text, trimmed, or `null` when cancelled. A field can be `multiline` (a textarea,
357
+ `rows` high: Enter starts a new line), carry a `hint` under it (or a function of its text, kept up
358
+ as it's typed: “12 words”), and take a `value` that's still being worked out (a promise, say an
359
+ on-device model's suggestion): the field waits, disabled, with `waiting` under it, then takes the
360
+ text and the focus (unless the person has typed meanwhile); a failed one leaves it empty. The action button (default "Save") stays
252
361
  disabled while the text is blank or `validate` gives a reason, which shows under the field
253
362
  (`checkText` is that rule, pure). Enter presses the action button; Esc and Cancel cancel; the
254
363
  focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
@@ -364,21 +473,27 @@ except those listed, each with a reason:
364
473
 
365
474
  | Export | What it does |
366
475
  |---|---|
367
- | `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`: JSON copies, `onChanged` for real changes only, sync's 8,192-byte item quota and 120 writes a minute. `settled()` waits for listeners. |
476
+ | `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`. |
477
+ | `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. |
368
478
  | `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
479
+ | `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. |
369
480
  | `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()}`. |
481
+ | `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. |
482
+ | `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
370
483
  | `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. |
371
- | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
484
+ | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
372
485
  | `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
373
486
  | `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
374
487
  | `testBuildDir(root, name)` | As in `/build`, for the tests that load the build. |
375
- | `serve(routes, {port})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. |
488
+ | `serve(routes, {port, tls})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. With `tls` (`testCertificate()`, a throwaway one from `openssl`), over HTTPS: for a site Chrome reaches only over HTTPS, mapped with `--host-resolver-rules`, Chromium started with `--ignore-certificate-errors`. |
376
489
  | `until(fn, what, timeout?)` | Poll until truthy. |
377
490
  | `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
378
491
  | `recordIconCalls(worker)` | Record `chrome.action.setIcon` calls Chrome accepted. |
379
492
  | `installWarnings(worker, manifest)` | Chrome's own install warnings for a manifest (`chrome.management.getPermissionWarningsByManifest`, no permission needed), from a loaded extension's worker. |
380
493
  | `warningsAddedBy(worker, manifest, names)` | `{warnings, without, added}`: the warnings with and without some permissions, and those only they bring (`withoutPermissions` is the pure part). |
381
494
  | `savedTabGroups(profile)` | Chrome's saved tab groups (`{title, pinned, urls}`) read from a closed profile's `Sync Data/LevelDB`: no extension API lists them. |
495
+ | `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. |
496
+ | `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. |
382
497
  | `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`). |
383
498
 
384
499
  ## Stylesheet classes
@@ -407,6 +522,7 @@ except those listed, each with a reason:
407
522
  | Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
408
523
  | Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
409
524
  | ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
525
+ | Subpage header (settings-subpage) | `<div class="subpage-head">` with a back `button.icon-button` holding `.icon.icon-arrow-back`, then `<h2>` |
410
526
  | Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
411
527
  | Toast (cr-toast) | `<div class="toast" popover="manual">text <button>Undo</button></div>` (or `createToast`); with `hidden` instead of `popover`, it shows over the page but not the top layer |
412
528
  | Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.5.0",
4
- "description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
3
+ "version": "1.7.0",
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",
7
7
  "types": "src/index.d.ts",
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Chrome's on-device model (Gemini Nano, through the Prompt API), the way
3
+ * every extension here uses it, following
4
+ * https://developer.chrome.com/docs/ai/prompt-api:
5
+ *
6
+ * - `availability()` is asked with the options `create()` gets, and only an
7
+ * 'available' model is used: nothing here downloads it, except
8
+ * {@link downloadModel}, which a page calls from the person's click (Chrome
9
+ * only downloads from a user gesture) and which reports progress;
10
+ * - one base session, with the instructions and examples, is made once and
11
+ * kept; each question goes to a `clone()` of it, so answers don't leak into
12
+ * each other;
13
+ * - each answer has a time limit: an `AbortSignal` stops the answer once
14
+ * it's late, and the caller gets a reason instead of a hang;
15
+ * - the first session after Chrome starts can take ~18 s while the model
16
+ * loads (a cold start, M3 Max). A question with a shorter limit gets
17
+ * 'late', and the session goes on loading for the next one. Nothing loads
18
+ * it ahead of time: that would load the model on every start for work that
19
+ * may never come.
20
+ *
21
+ * Nothing here touches `chrome.*` or the global itself: the Prompt API comes
22
+ * in as `model` (read when used, since Chrome may add it after start-up), so
23
+ * tests hand it the kit's fake (`fakeLanguageModel`, `/testing`) at exactly
24
+ * this edge. TypeScript's DOM library doesn't declare the Prompt API yet, so
25
+ * the parts used are typed here.
26
+ *
27
+ * @module ai/model
28
+ */
29
+ export type Availability = 'available' | 'downloadable' | 'downloading' | 'unavailable' | 'missing';
30
+ export type PromptOptions = {
31
+ /**
32
+ * Stops the answer.
33
+ */
34
+ signal?: AbortSignal;
35
+ /**
36
+ * A JSON Schema (or RegExp) the answer must follow.
37
+ */
38
+ responseConstraint?: object;
39
+ };
40
+ export type LanguageModelSession = {
41
+ /**
42
+ * The whole answer.
43
+ */
44
+ prompt: (prompt: string, options?: PromptOptions) => Promise<string>;
45
+ /**
46
+ * A copy with the same history.
47
+ */
48
+ clone: (options?: {
49
+ signal?: AbortSignal;
50
+ }) => Promise<LanguageModelSession>;
51
+ /**
52
+ * Let it go.
53
+ */
54
+ destroy: () => void;
55
+ };
56
+ export type InitialPrompt = {
57
+ role: 'system' | 'user' | 'assistant';
58
+ content: string;
59
+ };
60
+ export type LanguageModelOptions = {
61
+ /**
62
+ * What it's given.
63
+ */
64
+ expectedInputs?: {
65
+ type: string;
66
+ languages: string[];
67
+ }[];
68
+ /**
69
+ * What it answers in.
70
+ */
71
+ expectedOutputs?: {
72
+ type: string;
73
+ languages: string[];
74
+ }[];
75
+ /**
76
+ * Its instructions and examples.
77
+ */
78
+ initialPrompts?: InitialPrompt[];
79
+ /**
80
+ * Hears `downloadprogress` (`loaded`, 0–1) while the model downloads.
81
+ */
82
+ monitor?: (monitor: EventTarget) => void;
83
+ /**
84
+ * Stops creating it.
85
+ */
86
+ signal?: AbortSignal;
87
+ };
88
+ export type LanguageModelStatic = {
89
+ /**
90
+ * Whether it can run here.
91
+ */
92
+ availability: (options?: LanguageModelOptions) => Promise<Availability>;
93
+ /**
94
+ * A session (downloads the model first, from a click).
95
+ */
96
+ create: (options?: LanguageModelOptions) => Promise<LanguageModelSession>;
97
+ };
98
+ /**
99
+ * The model's answer to `availability()`: 'available', 'downloadable',
100
+ * 'downloading' or 'unavailable'; and, from {@link modelAvailability},
101
+ * 'missing' when this Chrome has no Prompt API.
102
+ *
103
+ * @typedef {'available' | 'downloadable' | 'downloading' | 'unavailable' | 'missing'} Availability
104
+ */
105
+ /**
106
+ * @typedef {object} PromptOptions
107
+ * @property {AbortSignal} [signal] Stops the answer.
108
+ * @property {object} [responseConstraint] A JSON Schema (or RegExp) the answer must follow.
109
+ */
110
+ /**
111
+ * @typedef {object} LanguageModelSession
112
+ * @property {(prompt: string, options?: PromptOptions) => Promise<string>} prompt The whole answer.
113
+ * @property {(options?: {signal?: AbortSignal}) => Promise<LanguageModelSession>} clone A copy with the same history.
114
+ * @property {() => void} destroy Let it go.
115
+ */
116
+ /**
117
+ * @typedef {{role: 'system' | 'user' | 'assistant', content: string}} InitialPrompt
118
+ */
119
+ /**
120
+ * @typedef {object} LanguageModelOptions
121
+ * @property {{type: string, languages: string[]}[]} [expectedInputs] What it's given.
122
+ * @property {{type: string, languages: string[]}[]} [expectedOutputs] What it answers in.
123
+ * @property {InitialPrompt[]} [initialPrompts] Its instructions and examples.
124
+ * @property {(monitor: EventTarget) => void} [monitor] Hears `downloadprogress` (`loaded`, 0–1) while the model downloads.
125
+ * @property {AbortSignal} [signal] Stops creating it.
126
+ */
127
+ /**
128
+ * @typedef {object} LanguageModelStatic
129
+ * @property {(options?: LanguageModelOptions) => Promise<Availability>} availability Whether it can run here.
130
+ * @property {(options?: LanguageModelOptions) => Promise<LanguageModelSession>} create A session (downloads the model first, from a click).
131
+ */
132
+ /**
133
+ * Chrome's `LanguageModel`, or undefined when this Chrome has none.
134
+ *
135
+ * @param {object} [scope] - Where to look. Default `globalThis` (a page, or the service worker).
136
+ * @returns {LanguageModelStatic | undefined} The Prompt API.
137
+ * @example
138
+ * const sessions = createModelSession({ model: chromeLanguageModel, options: ENGLISH });
139
+ */
140
+ export declare function chromeLanguageModel(scope?: object): LanguageModelStatic | undefined;
141
+ /**
142
+ * The model's state, never throwing: 'missing' without a Prompt API, and
143
+ * 'unavailable' when asking fails.
144
+ *
145
+ * @param {LanguageModelStatic | undefined} model - The Prompt API.
146
+ * @param {LanguageModelOptions} options - The options sessions are created with.
147
+ * @returns {Promise<Availability>} Its state.
148
+ * @example
149
+ * await modelAvailability(chromeLanguageModel(), ENGLISH); // 'available'
150
+ */
151
+ export declare function modelAvailability(model: LanguageModelStatic | undefined, options: LanguageModelOptions): Promise<Availability>;
152
+ /**
153
+ * Download the model, from the person's click: `create()` with a monitor,
154
+ * the session let go once made. Call it first in the click's handler, with
155
+ * nothing awaited before it, or Chrome refuses (no user activation).
156
+ *
157
+ * @param {LanguageModelStatic | undefined} model - The Prompt API.
158
+ * @param {LanguageModelOptions} options - The options sessions are created with.
159
+ * @param {{onProgress?: (loaded: number) => void, signal?: AbortSignal}} [handlers] - `onProgress`: how much is
160
+ * downloaded, 0–1, as Chrome reports it; `signal`: stops it.
161
+ * @returns {Promise<Availability>} The state afterwards ('available' once it's here).
162
+ * @throws {Error} When there's no Prompt API, or Chrome couldn't download it.
163
+ * @example
164
+ * button.addEventListener('click', () => {
165
+ * downloadModel(chromeLanguageModel(), ENGLISH, { onProgress: (loaded) => { bar.value = loaded; } })
166
+ * .then(show, () => show('downloadable', 'The download didn’t finish. Try again.'));
167
+ * });
168
+ */
169
+ export declare function downloadModel(model: LanguageModelStatic | undefined, options: LanguageModelOptions, { onProgress, signal }?: {
170
+ onProgress?: (loaded: number) => void;
171
+ signal?: AbortSignal;
172
+ }): Promise<Availability>;
173
+ /**
174
+ * The settings page's words for a state: standard message keys (the
175
+ * extension's `_locales` names them alike), as `SITE_LIST_MESSAGES` does
176
+ * for a site list.
177
+ */
178
+ export declare const MODEL_MESSAGES: Readonly<{
179
+ /** “Chrome’s built-in AI”: the row's name. */
180
+ name: "optionsModel";
181
+ /** “Checking…” before the state is known. */
182
+ checking: "optionsModelChecking";
183
+ /** “Ready”. */
184
+ ready: "optionsModelReady";
185
+ /** “Needs download”. */
186
+ needsDownload: "optionsModelNeedsDownload";
187
+ /** “Chrome downloads its model once and shares it with every site and extension that uses it”. */
188
+ downloadHelp: "optionsModelDownloadHelp";
189
+ /** “Download”: the button. */
190
+ download: "optionsModelDownload";
191
+ /** “Downloading… $PERCENT$%” (content `$1`). */
192
+ downloading: "optionsModelDownloading";
193
+ /** “The download didn’t finish. Try again.” */
194
+ downloadFailed: "optionsModelDownloadFailed";
195
+ /** “Not supported on this computer”. */
196
+ unsupported: "optionsModelUnsupported";
197
+ /** What a computer needs to run it. */
198
+ requirements: "optionsModelRequirements";
199
+ /** “This Chrome doesn’t have it”. */
200
+ missing: "optionsModelMissing";
201
+ /** What to do about that, and that the extension works without it. */
202
+ missingHelp: "optionsModelMissingHelp";
203
+ }>;
204
+ export type ModelStatus = {
205
+ /**
206
+ * Ready to use, can be downloaded, or can't run here.
207
+ */
208
+ state: 'ready' | 'pending' | 'off';
209
+ /**
210
+ * The status's message key ({@link MODEL_MESSAGES}).
211
+ */
212
+ status: string;
213
+ /**
214
+ * The help's message key ('' for none).
215
+ */
216
+ help: string;
217
+ /**
218
+ * Whether to offer Download.
219
+ */
220
+ canDownload: boolean;
221
+ };
222
+ /**
223
+ * The model's state, as a settings page shows it.
224
+ *
225
+ * @typedef {object} ModelStatus
226
+ * @property {'ready' | 'pending' | 'off'} state Ready to use, can be downloaded, or can't run here.
227
+ * @property {string} status The status's message key ({@link MODEL_MESSAGES}).
228
+ * @property {string} help The help's message key ('' for none).
229
+ * @property {boolean} canDownload Whether to offer Download.
230
+ */
231
+ /**
232
+ * Describe an availability for a settings page. Pure.
233
+ *
234
+ * @param {string} availability - 'available', 'downloadable', 'downloading', 'unavailable' or 'missing'.
235
+ * @returns {ModelStatus} How to show it.
236
+ * @example
237
+ * describeModel('downloadable'); // { state: 'pending', status: 'optionsModelNeedsDownload', help: 'optionsModelDownloadHelp', canDownload: true }
238
+ */
239
+ export declare function describeModel(availability: string): ModelStatus;
240
+ export type ModelAnswer = {
241
+ text: string;
242
+ reason: 'answered';
243
+ } | {
244
+ text: null;
245
+ reason: 'unavailable' | 'late' | 'aborted' | 'failed';
246
+ };
247
+ export type AskOptions = {
248
+ /**
249
+ * The most the answer may take, from the moment it's asked: the base
250
+ * session (if it must be made) and the answer.
251
+ */
252
+ timeoutMs: number;
253
+ /**
254
+ * Stops it sooner (the person cancelled).
255
+ */
256
+ signal?: AbortSignal;
257
+ /**
258
+ * A JSON Schema the answer must follow.
259
+ */
260
+ responseConstraint?: object;
261
+ };
262
+ export type ModelSession = {
263
+ /**
264
+ * The model's state, or 'missing'.
265
+ */
266
+ availability: () => Promise<Availability>;
267
+ /**
268
+ * Ask a copy of the base session.
269
+ */
270
+ ask: (prompt: string, options: AskOptions) => Promise<ModelAnswer>;
271
+ /**
272
+ * Let the base session go (the next question makes it again).
273
+ */
274
+ release: () => void;
275
+ };
276
+ /**
277
+ * What a question got: the answer, or why there's none: the model isn't
278
+ * on this computer ('unavailable'), the time ran out first ('late', a cold
279
+ * start among them), the caller's signal stopped it ('aborted'), or the model
280
+ * failed ('failed').
281
+ *
282
+ * @typedef {{text: string, reason: 'answered'} | {text: null, reason: 'unavailable' | 'late' | 'aborted' | 'failed'}} ModelAnswer
283
+ */
284
+ /**
285
+ * @typedef {object} AskOptions
286
+ * @property {number} timeoutMs The most the answer may take, from the moment it's asked: the base
287
+ * session (if it must be made) and the answer.
288
+ * @property {AbortSignal} [signal] Stops it sooner (the person cancelled).
289
+ * @property {object} [responseConstraint] A JSON Schema the answer must follow.
290
+ */
291
+ /**
292
+ * @typedef {object} ModelSession
293
+ * @property {() => Promise<Availability>} availability The model's state, or 'missing'.
294
+ * @property {(prompt: string, options: AskOptions) => Promise<ModelAnswer>} ask Ask a copy of the base session.
295
+ * @property {() => void} release Let the base session go (the next question makes it again).
296
+ */
297
+ /**
298
+ * A base session over a Prompt API, made when first needed and kept, each
299
+ * question asked of a `clone()` within its time limit.
300
+ *
301
+ * @param {{model: () => LanguageModelStatic | undefined, options: LanguageModelOptions, initialPrompts?: InitialPrompt[]}} setup -
302
+ * `model`: the Prompt API, read when used; `options`: what sessions are made (and availability asked)
303
+ * with; `initialPrompts`: the instructions and examples every question starts from.
304
+ * @returns {ModelSession} The session.
305
+ * @example
306
+ * const sessions = createModelSession({
307
+ * model: chromeLanguageModel,
308
+ * options: { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] },
309
+ * initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }],
310
+ * });
311
+ * const { text } = await sessions.ask('Site: acme.com', { timeoutMs: 2500 }); // null when late
312
+ */
313
+ export declare function createModelSession({ model, options, initialPrompts }: {
314
+ model: () => LanguageModelStatic | undefined;
315
+ options: LanguageModelOptions;
316
+ initialPrompts?: InitialPrompt[];
317
+ }): ModelSession;