@arraypress/extension-kit 1.6.0 → 1.8.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
@@ -13,7 +13,14 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
13
13
  - **Chrome's page components**: the ⋮ action menu and its row button, the text dialog
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
+ - **Chrome's window around a capture**: the tab strip and toolbar, light or dark, as HTML for store
17
+ screenshots and as an SVG image an extension page draws on a canvas.
16
18
  - **Sites**: what people type into a per-site list, and whether a page is on one.
19
+ - **Chrome's on-device model** (Gemini Nano, the Prompt API): availability, a download from the
20
+ person's click with progress, one base session with a `clone()` per question, a time limit on
21
+ every answer, cold starts, and the settings page's “Chrome’s built-in AI” row.
22
+ - **Content-script helpers**: a big page worked through in slices that never hold it long, and a
23
+ change watcher for single-page apps and endless lists.
17
24
  - **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
18
25
  check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
19
26
  `chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
@@ -103,6 +110,8 @@ chrome.storage.onChanged.addListener((changes, area) => {
103
110
  const saver = createSettingsSaver(store, chrome.storage, { onSaved: showSaved });
104
111
  saver.save({ menuStyle: 'full' });
105
112
  addEventListener('pagehide', () => saver.flush());
113
+ // Changed in another tab or on another computer: shown, with this page's unwritten changes over it.
114
+ saver.follow((settings) => show(settings), chrome.storage.onChanged);
106
115
  ```
107
116
 
108
117
  | Store member | What it does |
@@ -178,6 +187,25 @@ takes it as a placeholder whose content is `$1`, and every lookup without substi
178
187
  <h2 class="section-title" data-i18n="optionsAbout"></h2> <!-- About Upright -->
179
188
  ```
180
189
 
190
+ ## Chrome's window around a capture
191
+
192
+ ```js
193
+ import { WINDOW_BARS, letterIcon, windowChromeSvg } from '../vendor/extension-kit/index.js';
194
+
195
+ const svg = windowChromeSvg({ tabs: [{ title: 'Example', icon: letterIcon('E', '#1a73e8'), active: true }], url: 'example.com/a', theme: 'dark', w: 1280 });
196
+ const bars = new Image();
197
+ bars.src = `data:image/svg+xml,${encodeURIComponent(svg)}`;
198
+ await bars.decode();
199
+ context.drawImage(bars, 0, 0, 1280 * scale, (WINDOW_BARS.strip + WINDOW_BARS.toolbar) * scale); // the capture goes below
200
+ ```
201
+
202
+ The same macOS window the store screenshots use (`storeFrame.windowChrome`, now `frame/window.js`
203
+ in the runtime half): `windowChrome(options)` is its HTML, `windowChromeSvg(options)` that HTML in
204
+ an SVG image `w` wide and `WINDOW_BARS.strip + WINDOW_BARS.toolbar` (88px) high, drawn sharp at any
205
+ scale. Chrome draws an SVG image with HTML inside (`foreignObject`) without tainting the canvas
206
+ (checked in Chromium, `tests/frame-chromium.test.js`). Images in it must be `data:` URLs
207
+ (`letterIcon` makes one). `THEMES` holds the light and dark colours.
208
+
181
209
  ## Sites
182
210
 
183
211
  ```js
@@ -211,6 +239,81 @@ Registers, updates or unregisters the script `id` to match what `want()` gives a
211
239
  none). Calls run one after another, so a quick run of changes can't register twice or end out of
212
240
  order; a refused change rejects that call only.
213
241
 
242
+ ## Chrome's on-device model
243
+
244
+ ```js
245
+ import { chromeLanguageModel, createModelSession } from '../vendor/extension-kit/index.js';
246
+
247
+ const ENGLISH = { expectedInputs: [{ type: 'text', languages: ['en'] }], expectedOutputs: [{ type: 'text', languages: ['en'] }] };
248
+ const model = createModelSession({
249
+ model: chromeLanguageModel, // read when used: Chrome may add it after start-up
250
+ options: ENGLISH, // availability() is asked with what create() gets
251
+ initialPrompts: [{ role: 'system', content: 'You name downloaded files.' }, ...EXAMPLE],
252
+ });
253
+ const answer = await model.ask(prompt, { timeoutMs: 2500, signal, responseConstraint: schema });
254
+ if (answer.text === null) useRules(answer.reason); // 'unavailable' | 'late' | 'aborted' | 'failed'
255
+ ```
256
+
257
+ Following https://developer.chrome.com/docs/ai/prompt-api: only an `available` model is used (a
258
+ question never downloads it); one base session is made when first needed and kept; each question
259
+ goes to a `clone()` of it, let go after; the answer's `AbortSignal` fires at the time limit. The
260
+ first session after Chrome starts can take ~18 s while the model loads: a question with a shorter
261
+ limit comes back `'late'`, and the session goes on loading for the next one. Nothing loads it ahead
262
+ of time (that would load the model on every start). `release()` lets the base session go.
263
+
264
+ `modelAvailability(api, options)` never throws (`'missing'` without a Prompt API).
265
+ `downloadModel(api, options, {onProgress})` is the only `create()` that may download: call it
266
+ first in a click's handler, with nothing awaited before it. The settings row:
267
+
268
+ ```html
269
+ <div class="row two-line">
270
+ <span class="label">
271
+ <span data-i18n="optionsModel">Chrome’s built-in AI</span>
272
+ <span class="secondary"><span id="model-status"></span> <span id="model-help" hidden></span></span>
273
+ <progress id="model-progress" max="1" value="0" hidden></progress>
274
+ </span>
275
+ <button id="model-download" type="button" data-i18n="optionsModelDownload" hidden>Download</button>
276
+ </div>
277
+ ```
278
+
279
+ ```js
280
+ const row = createModelRow(
281
+ { status: byId('model-status'), help: byId('model-help'), download: byId('model-download', HTMLButtonElement), progress: byId('model-progress', HTMLProgressElement) },
282
+ { options: ENGLISH, state: () => ask(MESSAGES.modelState), t, onShown: ({ state }) => { useAi.disabled = state === 'off'; } },
283
+ );
284
+ await row.refresh();
285
+ ```
286
+
287
+ Its words are the page's messages under `MODEL_MESSAGES`' keys (`optionsModel`, `optionsModelReady`,
288
+ `optionsModelNeedsDownload`, `optionsModelDownloadHelp`, `optionsModelDownload`,
289
+ `optionsModelDownloading` with `$PERCENT$`, `optionsModelDownloadFailed`, `optionsModelUnsupported`,
290
+ `optionsModelRequirements`, `optionsModelMissing`, `optionsModelMissingHelp`); `describeModel` maps
291
+ a state to them. Tests use `fakeLanguageModel` (`/testing`), the Prompt API's shape: availability,
292
+ download progress (or a failed download), a cold start, slow and failed answers, aborts, and a log
293
+ of what it was asked.
294
+
295
+ ## Content scripts
296
+
297
+ ```js
298
+ import { inSlices } from '../vendor/extension-kit/content/slices.js';
299
+ import { watchChanges } from '../vendor/extension-kit/content/watch.js';
300
+
301
+ await inSlices(document.body, (block) => scanner.scan(block)); // ≤4,000 characters a block, 40 ms slices
302
+ const stop = watchChanges(document.body, {
303
+ changed: (nodes) => nodes.forEach((node) => scanner.scan(node)), // one batch, each subtree once
304
+ removed: (node) => scanner.forget(node), // at once
305
+ ignore: (node) => node instanceof Element && node.localName === 'my-note', // the extension's own
306
+ settleMs: 120, maxWaitMs: 600,
307
+ });
308
+ ```
309
+
310
+ `inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
311
+ hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
312
+ freezes. `watchChanges` hands over what was added or changed a little after the page stops
313
+ changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
314
+ default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
315
+ imports them directly (list them in `web_accessible_resources`).
316
+
214
317
  ## Time limits
215
318
 
216
319
  ```js
@@ -273,7 +376,11 @@ const site = await askText({
273
376
  });
274
377
  ```
275
378
 
276
- Resolves to the text, trimmed, or `null` when cancelled. The action button (default "Save") stays
379
+ Resolves to the text, trimmed, or `null` when cancelled. A field can be `multiline` (a textarea,
380
+ `rows` high: Enter starts a new line), carry a `hint` under it (or a function of its text, kept up
381
+ as it's typed: “12 words”), and take a `value` that's still being worked out (a promise, say an
382
+ on-device model's suggestion): the field waits, disabled, with `waiting` under it, then takes the
383
+ text and the focus (unless the person has typed meanwhile); a failed one leaves it empty. The action button (default "Save") stays
277
384
  disabled while the text is blank or `validate` gives a reason, which shows under the field
278
385
  (`checkText` is that rule, pure). Enter presses the action button; Esc and Cancel cancel; the
279
386
  focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
@@ -372,7 +479,7 @@ A list with an Add button needs every key but `optionsRemoveLabel`; one with an
372
479
  | `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
373
480
  | `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
374
481
  | `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
375
- | `storeFrame` | Store screenshots: `canvas`, `windowChrome`, `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
482
+ | `storeFrame` | Store screenshots: `canvas`, `windowChrome` (and `windowChromeSvg`, from `frame/window.js`), `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
376
483
  | `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
377
484
  | `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
378
485
 
@@ -389,10 +496,15 @@ except those listed, each with a reason:
389
496
 
390
497
  | Export | What it does |
391
498
  |---|---|
392
- | `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. |
499
+ | `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`. |
500
+ | `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. |
393
501
  | `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
394
502
  | `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. |
395
503
  | `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()}`. |
504
+ | `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. |
505
+ | `messageLookup(extensionDir, locale)`, `readMessages(extensionDir, locale)` | `chrome.i18n.getMessage` over the real `_locales` (a locale over en, named placeholders filled), for unit tests. |
506
+ | `heartbeat(page)` | A timer beating in a page: `stop()` gives the longest the main thread was held (ms), for performance checks. |
507
+ | `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`. |
396
508
  | `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. |
397
509
  | `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)` (also in the main entry). |
398
510
  | `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
@@ -435,6 +547,7 @@ except those listed, each with a reason:
435
547
  | Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
436
548
  | Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
437
549
  | ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
550
+ | Subpage header (settings-subpage) | `<div class="subpage-head">` with a back `button.icon-button` holding `.icon.icon-arrow-back`, then `<h2>` |
438
551
  | Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (`askText`, `askFields`) |
439
552
  | 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 |
440
553
  | Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
@@ -455,7 +568,7 @@ chrome://bookmarks in Chromium 156 / Chrome 157; the sources are listed at the t
455
568
  ## Development
456
569
 
457
570
  `npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests (the menu,
458
- the dialog, the toast, the dialog site list, icon rendering and the Chrome readers in Playwright's
571
+ the dialog, the toast, the dialog site list, the window frame, icon rendering and the Chrome readers in Playwright's
459
572
  Chromium, the rest in Node and jsdom), and the
460
573
  copied-code guard. The `.d.ts` files are generated from the JSDoc: `npm run types` writes them, and
461
574
  the type check fails while one is stale (it regenerates them into `.types/` and compares).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.6.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.8.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;