arcane-os 0.42.2 → 0.44.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.44.0
4
+
5
+ - Add stateless `generateImages` through `arcane-os/ai/twin-cloud` with
6
+ caller-selected FAL Flux Schnell or Stable Diffusion 3.5 Large. Preserve
7
+ complete prompts and provider parameters, and return every image as a Blob
8
+ in provider order with declared media metadata.
9
+ - Expose semantic progress and complete diagnostic callbacks, asynchronous
10
+ job polling, and cancellation across credentials, callbacks, requests,
11
+ response bodies and media downloads. Submit a paid generation request once;
12
+ never automatically replay an ambiguous submission.
13
+ - Keep image model selection, credentials, display and storage with the
14
+ application. No global model default, saved-history migration, durable media
15
+ storage or change to existing chat and speech behavior is introduced.
16
+
17
+ ## 0.43.0
18
+
19
+ - Add reusable rendered printing through `createPrintView`, preserving the
20
+ complete rendered content, title, styles and prepared image/font resources.
21
+ Explicit print requests await preparation; native printing uses the active
22
+ view. Resources remain owned through `afterprint`.
23
+ - Add Markdown editor `print()` and `insertMarkdown(text)` and file-manager
24
+ `printPreview()`. Insertion replaces the current selection through the
25
+ ordinary full-content change lifecycle; printing does not save the document.
26
+ - Add reusable history/file-manager `close()` with child-first closure and
27
+ pending-open cancellation. Refused or failed closure preserves the settled
28
+ visible preview, media and printing until its dialog actually closes.
29
+ Preserve modal focus restoration and existing saved records without migration.
30
+
3
31
  ## 0.42.2
4
32
 
5
33
  - Stop generating SDK cache suffixes in import maps, materialized runtime,
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.42.2` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.44.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -105,6 +105,240 @@ export function normalizeAIRequestAbort(error){
105
105
  return normalized;
106
106
  }
107
107
 
108
+ /** Stateless image generation; model selection and provider parameters belong to the caller. */
109
+ export async function generateImages({
110
+ model,
111
+ prompt,
112
+ parameters = {},
113
+ twinKey,
114
+ getApiKey,
115
+ signal = null,
116
+ id = Date.now(),
117
+ onRequest = function observeImageRequest(){},
118
+ onResponse = function observeImageResponse(){},
119
+ onProgress = function observeImageProgress(){}
120
+ } = {}) {
121
+ if (signal?.aborted) throw normalizeAIRequestAbort(signal.reason);
122
+ const asynchronous = model === 'fal-ai/flux/schnell';
123
+ if (!asynchronous && model !== 'stable-diffusion-3.5-large') {
124
+ const error = new TypeError('TWiN generateImages requires a supported explicit image model.');
125
+ error.code = 'ARCANE_AI_IMAGE_MODEL_UNSUPPORTED';
126
+ throw error;
127
+ }
128
+ if (!is.string(prompt)) throw new TypeError('TWiN generateImages requires a prompt string.');
129
+ if (!parameters || !is.object(parameters) || is.array(parameters)) {
130
+ throw new TypeError('Image parameters must be an object of additional provider fields.');
131
+ }
132
+ if (Object.hasOwn(parameters, 'prompt') || (!asynchronous && Object.hasOwn(parameters, 'model'))) {
133
+ throw new TypeError('Supply the image prompt and model through their named arguments.');
134
+ }
135
+ const destination = asynchronous
136
+ ? 'https://inference.do-ai.run/v1/async-invoke'
137
+ : 'https://inference.do-ai.run/v1/images/generations';
138
+ const request = asynchronous
139
+ ? {model_id: model, input: {...parameters, prompt}}
140
+ : {...parameters, model, prompt};
141
+ // Observe the complete request without allowing a later callback to rewrite its payload.
142
+ const body = JSON.stringify(request);
143
+ const outputFormat = parameters.output_format;
144
+ const controller = new AbortController();
145
+ const operationSignal = controller.signal;
146
+ function cancelImageRequest() {controller.abort(signal.reason);}
147
+ signal?.addEventListener('abort', cancelImageRequest, {once: true});
148
+ if (signal?.aborted) cancelImageRequest();
149
+
150
+ function progress(stage, requestId) {
151
+ return settleImageOperation(
152
+ function publishImageProgress() {
153
+ return onProgress({stage, model, id, ...(requestId === undefined ? {} : {requestId})});
154
+ },
155
+ operationSignal
156
+ );
157
+ }
158
+
159
+ async function inference(url, options) {
160
+ const response = await settleImageOperation(
161
+ function requestImageInference() {return fetch(url, {...options, credentials: 'omit', signal: operationSignal});},
162
+ operationSignal
163
+ );
164
+ const result = await settleImageOperation(
165
+ function readImageInference() {
166
+ if (response.ok || response.headers.get('content-type')?.includes('application/json')) {
167
+ return response.json();
168
+ }
169
+ return response.text();
170
+ },
171
+ operationSignal
172
+ );
173
+ if (!response.ok) throw result;
174
+ const retryAfter = response.headers.get('retry-after');
175
+ const seconds = retryAfter?.trim() ? Number(retryAfter) : NaN;
176
+ const date = retryAfter?.trim() ? Date.parse(retryAfter) : NaN;
177
+ const delay = is.finite(seconds) && seconds >= 0 ? seconds * 1000
178
+ : is.finite(date) ? Math.max(0, date - Date.now()) : 1000;
179
+ return {result, delay};
180
+ }
181
+
182
+ try {
183
+ await progress('credentials');
184
+ const key = twinKey === undefined && is.function(getApiKey)
185
+ ? await settleImageOperation(getApiKey, operationSignal) : twinKey;
186
+ if (!is.string(key) || !key) {
187
+ const error = new Error('AI provider is not configured.');
188
+ error.code = 'AI_PROVIDER_NOT_CONFIGURED';
189
+ throw error;
190
+ }
191
+ const headers = {'Content-Type': 'application/json', Authorization: `Bearer ${key}`};
192
+ await settleImageOperation(
193
+ function observeImageSubmission() {
194
+ return onRequest(request, id, {operation: 'images', transport: 'http', destination});
195
+ },
196
+ operationSignal
197
+ );
198
+ await progress('requesting');
199
+ // A lost POST response can represent an accepted paid job. Never replay it.
200
+ let response = await inference(destination, {method: 'POST', headers, body});
201
+ let result = response.result;
202
+ if (asynchronous) {
203
+ const requestId = result?.request_id;
204
+ while (result?.status === 'QUEUED' || result?.status === 'IN_PROGRESS') {
205
+ if (!is.string(requestId) || !requestId) {
206
+ throw imageResultError('Image generation returned a pending job without a request_id.', result);
207
+ }
208
+ await progress(result.status === 'QUEUED' ? 'queued' : 'generating', requestId);
209
+ await waitForImageStatus(response.delay, operationSignal);
210
+ response = await inference(
211
+ `${destination}/${encodeURIComponent(requestId)}/status`,
212
+ {method: 'GET', headers}
213
+ );
214
+ result = response.result;
215
+ }
216
+ if (result?.status === 'FAILED') {
217
+ throw imageResultError('TWiN Cloud image generation failed.', result);
218
+ }
219
+ if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
220
+ throw imageResultError('TWiN Cloud returned an unsupported image job status.', result);
221
+ }
222
+ if (!is.array(result.output?.images)) {
223
+ if (!is.string(requestId) || !requestId) {
224
+ throw imageResultError('Image generation returned no result or request_id.', result);
225
+ }
226
+ response = await inference(`${destination}/${encodeURIComponent(requestId)}`, {method: 'GET', headers});
227
+ result = response.result;
228
+ if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
229
+ throw imageResultError('TWiN Cloud image generation did not return a completed result.', result);
230
+ }
231
+ }
232
+ }
233
+ const returnedImages = asynchronous ? result?.output?.images : result?.data;
234
+ if (!is.array(returnedImages)) {
235
+ throw imageResultError('TWiN Cloud returned no image collection.', result);
236
+ }
237
+ const images = returnedImages.map(function retainImageDescriptor(image) {return {...image};});
238
+ const format = result.output_format ?? outputFormat;
239
+ const requestId = asynchronous ? result.request_id : undefined;
240
+ await settleImageOperation(
241
+ function observeImageResult() {return onResponse(result, id, false);},
242
+ operationSignal
243
+ );
244
+ await progress('downloading', requestId);
245
+ const completed = await Promise.all(images.map(async function materializeImage(image) {
246
+ let blob;
247
+ const mediaType = image.content_type
248
+ ?? ({png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', webp: 'image/webp'}[format])
249
+ ?? 'application/octet-stream';
250
+ if (is.string(image.b64_json)) {
251
+ blob = await settleImageOperation(function decodeImageBase64() {
252
+ return new Blob([Uint8Array.from(atob(image.b64_json), function imageCode(character) {
253
+ return character.charCodeAt(0);
254
+ })], {type: mediaType});
255
+ }, operationSignal);
256
+ } else if (is.string(image.url) && image.url) {
257
+ // Media URLs have their own delivery authority. Never forward the inference key.
258
+ const download = await settleImageOperation(function downloadGeneratedImage() {
259
+ return fetch(image.url, {credentials: 'omit', signal: operationSignal});
260
+ }, operationSignal);
261
+ if (!download.ok) {
262
+ throw await settleImageOperation(function readImageDownloadError() {
263
+ return download.headers.get('content-type')?.includes('application/json')
264
+ ? download.json() : download.text();
265
+ }, operationSignal);
266
+ }
267
+ blob = await settleImageOperation(function readGeneratedImage() {return download.blob();}, operationSignal);
268
+ if (!blob.type) blob = new Blob([blob], {type: mediaType});
269
+ } else {
270
+ throw imageResultError('TWiN Cloud returned an image without data or a URL.', image);
271
+ }
272
+ return {
273
+ blob,
274
+ mediaType: blob.type || mediaType,
275
+ ...(image.width === undefined ? {} : {width: image.width}),
276
+ ...(image.height === undefined ? {} : {height: image.height})
277
+ };
278
+ }));
279
+ await progress('complete', requestId);
280
+ if (operationSignal.aborted) throw normalizeAIRequestAbort(operationSignal.reason);
281
+ return {images: completed};
282
+ } catch (error) {
283
+ if (isAIRequestAbort(error, operationSignal)) throw normalizeAIRequestAbort(error);
284
+ throw error;
285
+ } finally {
286
+ signal?.removeEventListener('abort', cancelImageRequest);
287
+ controller.abort('Image operation settled.');
288
+ }
289
+ }
290
+
291
+ function imageResultError(message, result) {
292
+ const error = new Error(message, {cause: result});
293
+ error.code = 'ARCANE_AI_INVALID_PROVIDER_RESULT';
294
+ return error;
295
+ }
296
+
297
+ /** Abort a wait even when a credential, callback, or host Fetch ignores the signal. */
298
+ function settleImageOperation(operation, signal) {
299
+ return new Promise(function settleCloudImageOperation(resolve, reject) {
300
+ let settled = false;
301
+ function finish(error, value, failed) {
302
+ if (settled) return;
303
+ settled = true;
304
+ signal.removeEventListener('abort', cancelled);
305
+ if (failed) reject(error);
306
+ else resolve(value);
307
+ }
308
+ function cancelled() {finish(normalizeAIRequestAbort(signal.reason), undefined, true);}
309
+ signal.addEventListener('abort', cancelled, {once: true});
310
+ if (signal.aborted) {
311
+ cancelled();
312
+ return;
313
+ }
314
+ try {
315
+ Promise.resolve(operation()).then(
316
+ function accepted(value) {finish(undefined, value, false);},
317
+ function rejected(error) {finish(error, undefined, true);}
318
+ );
319
+ } catch (error) {
320
+ finish(error, undefined, true);
321
+ }
322
+ });
323
+ }
324
+
325
+ async function waitForImageStatus(delay, signal) {
326
+ let remaining = delay;
327
+ do {
328
+ // Split only at the host timer range; retain the provider's entire delay.
329
+ const milliseconds = Math.min(remaining, 2147483647);
330
+ let timer;
331
+ try {
332
+ await settleImageOperation(function waitForImagePoll() {
333
+ return new Promise(function scheduleImagePoll(finish) {timer = setTimeout(finish, milliseconds);});
334
+ }, signal);
335
+ } finally {
336
+ clearTimeout(timer);
337
+ }
338
+ remaining -= milliseconds;
339
+ } while (remaining > 0);
340
+ }
341
+
108
342
  export function structuredOutputFormat(value = false){
109
343
  if(value === false || value === null || value === undefined){
110
344
  return null;
@@ -136,6 +136,130 @@ options; they do not add a key, model, browser state, or retained conversation.
136
136
  Retry warnings and observer failures use the shared console logger and preserve
137
137
  the complete original error.
138
138
 
139
+ ## Generate images with an application-selected model
140
+
141
+ `generateImages` is a separate stateless operation on the same public import.
142
+ The application owns the selected model, labels, prompt, provider parameters,
143
+ credential source, display, and any saved image. It does not instantiate the
144
+ browser AI singleton or change chat, speech, model preferences, or history.
145
+
146
+ ```javascript
147
+ import {generateImages} from 'arcane-os/ai/twin-cloud';
148
+
149
+ const controller = new AbortController();
150
+ const {images} = await generateImages({
151
+ model: 'stable-diffusion-3.5-large',
152
+ prompt: 'A moon-powered toaster launches croissants over purple craters.',
153
+ parameters: {n: 2, size: '1024x1024', output_format: 'png'},
154
+ getApiKey: applicationRuntime.getTwinKey,
155
+ signal: controller.signal,
156
+ onProgress({stage}) {
157
+ console.log(stage);
158
+ }
159
+ });
160
+
161
+ for (const image of images) {
162
+ await applicationRuntime.saveGeneratedImage(image.blob, image.mediaType);
163
+ }
164
+ ```
165
+
166
+ `applicationRuntime.getTwinKey` and `saveGeneratedImage` are caller-owned
167
+ functions in this example, not SDK APIs. Pass the credential function with any
168
+ required receiver already bound. Use `twinKey` instead when the application
169
+ already has its key. `getApiKey()` may return a key or a promise and is called
170
+ only when `twinKey` is `undefined`. An explicitly supplied empty key fails with
171
+ `AI_PROVIDER_NOT_CONFIGURED`; it does not select another credential source.
172
+ The example's model and parameters are application choices, not SDK defaults.
173
+
174
+ ```javascript
175
+ generateImages({
176
+ model,
177
+ prompt,
178
+ parameters,
179
+ twinKey,
180
+ getApiKey,
181
+ signal,
182
+ id,
183
+ onRequest,
184
+ onResponse,
185
+ onProgress
186
+ })
187
+ // Promise<{images: [{blob, mediaType, width?, height?}]}>
188
+ ```
189
+
190
+ `parameters` defaults to `{}`, `signal` to `null`, and `id` to `Date.now()`.
191
+ All diagnostic and progress callbacks are optional.
192
+
193
+ The SDK owns the route for these exact supported identifiers:
194
+
195
+ | Explicit model | Submission and result |
196
+ | --- | --- |
197
+ | `fal-ai/flux/schnell` | POST `/v1/async-invoke` with `{model_id:model,input:{...parameters,prompt}}`; pending jobs use GET `/v1/async-invoke/{request_id}/status`, then GET `/v1/async-invoke/{request_id}` when the completed status lacks `output.images`. |
198
+ | `stable-diffusion-3.5-large` | POST `/v1/images/generations` with `{...parameters,model,prompt}`; images come from the completed response's `data` array. |
199
+
200
+ Both routes use `https://inference.do-ai.run`. Missing or unsupported model
201
+ identifiers reject with `ARCANE_AI_IMAGE_MODEL_UNSUPPORTED`; there is no SDK
202
+ image model default. The prompt must be a string and is sent unchanged,
203
+ including whitespace. `parameters` is an object of additional provider fields;
204
+ the SDK neither supplies nor changes image counts, dimensions, quality, seeds,
205
+ or output limits. Supply the prompt only through `prompt`, and the synchronous
206
+ model only through `model`; duplicate reserved fields in `parameters` throw
207
+ `TypeError` instead of silently replacing one input. Provider-supported
208
+ parameter names and values remain that selected model's contract.
209
+
210
+ Every returned image is materialized in provider order. Base64 image data
211
+ becomes a `Blob`; returned URLs are fetched concurrently without an inference
212
+ Authorization header or browser credentials. The output's `mediaType` uses the
213
+ downloaded Blob's declared type, provider `content_type`, or a declared
214
+ `output_format` of `png`, `jpeg`/`jpg`, or `webp`. With no declared type or known
215
+ format, it is `application/octet-stream`; the SDK does not guess dimensions or
216
+ image metadata. `width` and `height` are included only when the provider supplies
217
+ them. The SDK saves nothing and creates no object URLs; the caller owns any
218
+ storage and the lifetime of display URLs it creates.
219
+
220
+ `onRequest(request,id,metadata)` runs once before submission with the complete
221
+ provider request. Metadata is `{operation:'images',transport:'http',destination}`.
222
+ `onResponse(response,id,false)` runs once with the complete final provider JSON
223
+ after successful generation and before image materialization. Intermediate
224
+ poll replies do not call it. These are explicit diagnostic callbacks: keep raw
225
+ protocol outside ordinary user status and durable chat history. Credentials are
226
+ transport-only and are absent from these payloads. The request and image
227
+ descriptors are captured before their diagnostic callbacks so observer edits do
228
+ not rewrite the operation's content.
229
+
230
+ `onProgress({stage,model,id,requestId?})` reports semantic stages: `credentials`,
231
+ `requesting`, `queued`, `generating`, `downloading`, and `complete`. Pending
232
+ stages can repeat; immediately completed jobs skip them. Async job stages
233
+ include `requestId` when present. Progress contains no image data or protocol
234
+ body. All three callbacks may return a promise; their settlement is awaited
235
+ and their failure rejects the operation. `onResponse` means generation returned
236
+ a final result, not that media downloads have finished. The resolved promise
237
+ delivers all materialized images; a failed download rejects instead of returning
238
+ an incomplete collection.
239
+
240
+ The image operation submits once and performs no automatic retry, including
241
+ after a network loss, HTTP `429`, body-read failure, or callback failure. A lost
242
+ POST response may correspond to an accepted paid job. This image-specific rule
243
+ does not alter chat or speech retry behavior. While a FAL job is `QUEUED` or
244
+ `IN_PROGRESS`, polling uses the provider's `Retry-After` delay when supplied,
245
+ otherwise one second. Completed jobs accept `COMPLETED` and the guide's
246
+ `COMPLETE` spelling. Failed or unusable job results reject with
247
+ `ARCANE_AI_INVALID_PROVIDER_RESULT` and the complete result in `error.cause`.
248
+ HTTP failures throw their complete parsed JSON or text body; transport,
249
+ decoding, and callback failures remain their original errors.
250
+
251
+ Calling `controller.abort()` cancels local credential/callback waits, requests,
252
+ body reads, polling timers, and downloads. It rejects with
253
+ `ARCANE_AI_REQUEST_ABORTED` and ignores later settlement from an operation that
254
+ does not honor the signal. A callback already running remains caller-owned;
255
+ the SDK invokes no subsequent callbacks after cancellation. Local cancellation
256
+ does not claim to cancel a remote paid job. Dispose the controller with the
257
+ owning page or operation; no global startup wait is required.
258
+
259
+ The route and response contracts follow DigitalOcean's official
260
+ [FAL inference guide](https://docs.digitalocean.com/products/inference/how-to/use-fal-models/)
261
+ and [multimodal inference guide](https://docs.digitalocean.com/products/inference/how-to/use-multimodal-inference/).
262
+
139
263
  ## Existing browser AI interface
140
264
 
141
265
  The following browser example uses the same managed imports as the
@@ -67,17 +67,17 @@ appropriate.
67
67
  | [`conversation-view.html`](#conversation-viewhtml) | Provider-neutral conversation display, advisory actions, composer, busy state, and status. | `setConversation()`<br>`setBusy()`<br>`setStatus()`<br>`clearComposer()`<br>`destroy()` | `conversation-view-ready`<br>`communication-send`<br>`communication-advisory-action` | DOM-normalized |
68
68
  | [`dashboard-config.html`](#dashboard-confightml) | Selects which normalized chart definitions are visible on a dashboard. | `configure()`<br>`setDefinitions()`<br>`setVisibility()`<br>`getChartOptions()`<br>`getEffectiveVisibility()`<br>`open()`<br>`close()`<br>`destroy()` | `dashboard-config-ready`<br>`dashboard-config-opened`<br>`dashboard-config-closed`<br>`dashboard-config-change` | Fully normalized definitions and visibility |
69
69
  | [`data-maintenance.html`](#data-maintenancehtml) | Runs destructive cleanup of empty chats and memories inside the current app data scope. | `open()`<br>`destroy()` | `data-maintenance-ready`<br>`data-maintenance-complete` | Normalized counts; DBOPFS failures mixed |
70
- | [`data-view.html`](#data-viewhtml) | Opens a modal file view and forwards application-owned preview and directory-filter callbacks. | `beforeOpen()`<br>`open()`<br>`destroy()` | `data-view-ready` | DOM-native result; preview/filter configuration reaches the child before loading |
70
+ | [`data-view.html`](#data-viewhtml) | Opens a modal file view and forwards application-owned preview and directory-filter callbacks. | `beforeOpen()`<br>`open()`<br>`close()`<br>`printPreview()`<br>`destroy()` | `data-view-ready` | DOM-native result; preview/filter configuration reaches the child before loading |
71
71
  | [`directory-picker.html`](#directory-pickerhtml) | Presents the provider-owned OS directory chooser with change/cancel/error states. | `configure()`<br>`focus()`<br>`select()`<br>`destroy()` | `directory-picker-ready`<br>`directory-picker-change`<br>`directory-picker-cancel`<br>`directory-picker-error` | Complete plain-text paths and native selection/error |
72
72
  | [`document-inspector.html`](#document-inspectorhtml) | Inspects PDF, text, or source documents and records review state. | `loadDocument()`<br>`selectView()`<br>`markSaved()`<br>`destroy()` | `document-inspector-ready`<br>`document-review-change` | Document state normalized; browser document APIs mixed |
73
73
  | [`file-drop.html`](#file-drophtml) | Acquires complete file selections by drag/drop or picker and presents busy, progress, error, and cleared state. | `configure()`<br>`openPicker()`<br>`clear()`<br>`setBusy()`<br>`setError()`<br>`setProgress()`<br>`destroy()` | `file-drop-ready`<br>`file-drop-selected`<br>`file-drop-progress`<br>`file-drop-state`<br>`file-drop-error` | Complete selections preserved; browser File/drop errors mixed |
74
74
  | [`file-inspector.html`](#file-inspectorhtml) | Displays file metadata, preview, busy/error state, and caller-defined actions. | `configure()`<br>`show()`<br>`clear()`<br>`setActions()`<br>`setBusy()`<br>`setError()`<br>`setPreview()`<br>`destroy()` | `file-inspector-ready`<br>`file-inspector-action`<br>`file-inspector-change`<br>`file-inspector-cleared`<br>`file-inspector-error` | State normalized; preview/provider behavior mixed |
75
- | [`file-manager.html`](#file-managerhtml) | Browses app-scoped files, filters directories before reads, and renders application-described previews. | `setProvider()`<br>`loadAll()`<br>`setFilter()`<br>`setDirectoryFilter()`<br>`select()`<br>`clearSelection()`<br>`destroy()` | `file-manager-ready`<br>`file-manager-select`<br>`file-manager-open`<br>`file-manager-action` | Selection/filter state, neutral preview descriptors, cancellation, and media cleanup normalized; storage/provider behavior mixed |
75
+ | [`file-manager.html`](#file-managerhtml) | Browses app-scoped files, filters directories before reads, and renders application-described previews. | `setProvider()`<br>`loadAll()`<br>`setFilter()`<br>`setDirectoryFilter()`<br>`select()`<br>`clearSelection()`<br>`close()`<br>`printPreview()`<br>`destroy()` | `file-manager-ready`<br>`file-manager-select`<br>`file-manager-open`<br>`file-manager-action` | Selection/filter state, neutral preview descriptors, cancellation, and media cleanup normalized; storage/provider behavior mixed |
76
76
  | [`header.html`](#headerhtml) | Shared title bar with history, reload, online marker, presentation labels, and 988 link. | None | No component-specific event | Browser/platform-native behavior; no component-ready contract |
77
77
  | [`integration-settings.html`](#integration-settingshtml) | Edits non-secret communication service configuration and service actions. | `configure()`<br>`getValues()`<br>`setStatus()`<br>`destroy()` | `integration-settings-ready`<br>`integration-settings-save`<br>`integration-settings-close`<br>`integration-action` | Normalized non-secret values |
78
78
  | [`local-ai-status.html`](#local-ai-statushtml) | Presents local-AI standby, failure, recovery, guidance, retry, and dismissal states. | `configure()`<br>`begin()`<br>`present()`<br>`destroy()`<br>`hidden` | `local-ai-status-ready`<br>`local-ai-status-dismissed`<br>`local-ai-retry` | Fully normalized LocalAIReadiness report |
79
79
  | [`markdown-document.html`](#markdown-documenthtml) | Renders and navigates a complete Markdown document with focusable fragments. | `configure()`<br>`load()`<br>`render()`<br>`clear()`<br>`fail()`<br>`focus()`<br>`focusFragment()`<br>`destroy()` | `markdown-document-ready`<br>`markdown-document-state`<br>`markdown-document-loading`<br>`markdown-document-rendered`<br>`markdown-document-empty`<br>`markdown-document-error`<br>`markdown-document-navigate` | Complete Markdown/state normalized; malformed input and Marked/DOM failures remain visible |
80
- | [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring with optional bounded input/preview panes and edit-position following. | `configure()`<br>`focus()`<br>`clear()`<br>`saveEntry()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values, opt-in fit layout, and preview following normalized; injected save result mixed |
80
+ | [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring with optional bounded input/preview panes, edit-position following, and rendered printing. | `configure()`<br>`focus()`<br>`clear()`<br>`insertMarkdown()`<br>`saveEntry()`<br>`print()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values, opt-in fit layout, and preview following normalized; injected save result mixed |
81
81
  | [`media-embed.html`](#media-embedhtml) | Loads a parsed YouTube video or playlist embed with ordinary hosting by default, optional privacy enhancement, and an external-platform action. | `configure()`<br>`load()`<br>`destroy()` | `media-embed-ready`<br>`media-load`<br>`media-error`<br>`media-open-platform` | URL/error normalized; iframe/platform behavior native |
82
82
  | [`modal.html`](#modalhtml) | Generic modal with population, configurable user dismissal, actions, and concurrent task execution. | `configure()`<br>`populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()`<br>`running`<br>`opened` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
83
83
  | [`output-panel.html`](#output-panelhtml) | Presents status, output, body, coverage, actions, pending, error, and cleared states. | `configure()`<br>`setOutput()`<br>`setBody()`<br>`setCoverage()`<br>`setActions()`<br>`setPending()`<br>`setStatus()`<br>`setError()`<br>`clear()`<br>`destroy()` | `output-panel-ready`<br>`output-panel-state`<br>`output-panel-change`<br>`output-panel-action`<br>`output-panel-error`<br>`output-panel-cleared` | DOM-normalized |
@@ -663,7 +663,7 @@ own record mappings through the same preview callbacks as the standalone
663
663
 
664
664
  ### Public surface
665
665
 
666
- Methods/properties: `beforeOpen()`, `open()`, `destroy()`.
666
+ Methods/properties: `beforeOpen()`, `open()`, `close()`, `printPreview()`, `destroy()`.
667
667
 
668
668
  Assign `previewDescriptor`, `previewTransform`, and `directoryFilter` on the
669
669
  host before opening. The component forwards those callbacks, comma-separated
@@ -673,6 +673,20 @@ before the child's first load. Reopening refreshes those values before
673
673
  `beforeOpen()` is awaited before this configuration is consumed. `data-dirs`
674
674
  selects the initial directories; `data-layout` defaults to `files`.
675
675
 
676
+ `await close()` invalidates pending opens, closes the child's dialogs before the
677
+ parent, and returns `true` only after closure. It returns `false` when destroyed
678
+ or a child/parent cannot close; complete errors reach shared diagnostics. It is
679
+ reusable: a later `open()` reloads the existing child. Repeated closes share the
680
+ pending closure. `destroy()` remains terminal.
681
+
682
+ An application that loads a history selection into an editor must synchronously
683
+ call `preventDefault()` on the existing cancelable `file-manager-open` event
684
+ before awaiting its own save/load work. That application owns record selection,
685
+ autosave, title, and editor focus; call `close()` after loading and restore focus
686
+ after it resolves `true`. No record-loading policy is added to the SDK.
687
+ `printPreview()` forwards to the currently open child preview and returns
688
+ `false` when no such preview is available.
689
+
676
690
  Events: `data-view-ready`.
677
691
 
678
692
  Shared dependencies: [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
@@ -821,7 +835,7 @@ and `destroy()` cleanup, without delaying unrelated component startup.
821
835
  ### Public surface
822
836
 
823
837
  Methods/properties: `setProvider()`, `loadAll()`, `setFilter()`,
824
- `setDirectoryFilter()`, `select()`, `clearSelection()`, `destroy()`,
838
+ `setDirectoryFilter()`, `select()`, `clearSelection()`, `close()`, `printPreview()`, `destroy()`,
825
839
  `previewDescriptor`, `previewTransform`, `directoryFilter`.
826
840
 
827
841
  Assign `directoryFilter(entry, context)` before the first load, or call
@@ -879,8 +893,9 @@ component does not automatically read every related media variant.
879
893
 
880
894
  Closing the preview, opening another file, or destroying the manager aborts
881
895
  the preview signal, ignores late descriptor/loader results, stops owned audio,
882
- removes iframe `srcdoc`, and revokes every owned object URL, including those
883
- in nested collections.
896
+ removes iframe `srcdoc`, and releases owned object URLs, including those in
897
+ nested collections. An active print snapshot retains its URLs until the
898
+ browser's `afterprint` event, even when the preview closes meanwhile.
884
899
  Applications should observe the supplied signal in their asynchronous reads.
885
900
  Descriptor and file-open failures show `Unable to open this file.` in the
886
901
  current modal and send the original error to the shared logger.
@@ -900,6 +915,26 @@ of the original `File`.
900
915
  The larger file-preview dialog reuses `--modal-width`, `--modal-max-height`,
901
916
  and `--modal-padding`; applications can override those existing modal values.
902
917
 
918
+ The preview's **Print** button and `await printPreview()` print its current
919
+ rendered content with the file title. Native browser Print selects the open
920
+ preview ahead of the editor. Printing uses the shared
921
+ [`PrintView.js`](runtime-modules.md#printviewjs) owner; accessible HTML frames
922
+ are copied from their current rendered document, never substituted with source
923
+ text. Explicit printing waits for images/fonts and HTML frame readiness.
924
+ Native PDF and inaccessible embedded viewers require their own print command.
925
+ `true` means the browser print dialog was requested, not that paper or a PDF
926
+ was produced. Closed, destroyed, or unavailable previews return `false`;
927
+ preparation failures reject and the visible control reports a concise failure.
928
+
929
+ `await close()` cancels pending presentation work and closes delete, file, and
930
+ directory dialogs in that order. A settled preview keeps its rendered content,
931
+ media, and printing available until the file dialog actually closes; refusing
932
+ or failing to close leaves the still-visible preview usable. Actual file-dialog
933
+ closure releases its preview resources. The method returns `false` on
934
+ refused/failed closure or after destruction. The manager stays
935
+ reusable; a later opening can use it again. Already accepted upload/deletion
936
+ writes complete through the storage owner without reopening closed dialogs.
937
+
903
938
  Events: `file-manager-ready`, `file-manager-select`, `file-manager-open`, `file-manager-action`.
904
939
 
905
940
  Shared dependencies: [`DBOPFS.js`](runtime-modules.md#dbopfsjs), [`File.js`](runtime-entities.md#filejs), [`MD.js`](runtime-modules.md#mdjs), [`WaitForComponent.js`](runtime-modules.md#waitforcomponentjs).
@@ -1048,8 +1083,26 @@ inside an application-supplied bounded area.
1048
1083
 
1049
1084
  ### Public surface
1050
1085
 
1051
- Methods/properties: `configure()`, `focus()`, `clear()`, `saveEntry()`,
1052
- `destroy()`, `value`, `entryTitle`, `options`.
1086
+ Methods/properties: `configure()`, `focus()`, `clear()`, `insertMarkdown(text)`,
1087
+ `saveEntry()`, `print()`, `destroy()`, `value`, `entryTitle`, `options`.
1088
+
1089
+ `insertMarkdown(text)` replaces the current source selection with the exact
1090
+ supplied string, places the caret after it, and emits the ordinary change
1091
+ event. It preserves the source viewport and normal preview/save lifecycle.
1092
+ It returns `false` when read-only or destroyed and throws `TypeError` for a
1093
+ non-string. The application owns the inserted Markdown and any related data.
1094
+
1095
+ The visible **Print** control and `await print()` print the complete rendered
1096
+ Markdown with `entryTitle`, independently of saving. Native browser Print uses
1097
+ that same registered view unless an open file preview takes precedence.
1098
+ The SDK copies current rendered DOM/styles into a temporary print-only view,
1099
+ leaving the editor's source, controls, and screen layout in place. Explicit
1100
+ printing waits for resource readiness; native `beforeprint` is synchronous
1101
+ and uses the currently rendered content. `print()` returns `false` when the
1102
+ view is unavailable, rejects preparation failures, and returns `true` only
1103
+ when the browser dialog was requested. Completion of physical printing or PDF
1104
+ saving is not observable through this method. See
1105
+ [`PrintView.js`](runtime-modules.md#printviewjs).
1053
1106
 
1054
1107
  `configure({fit:true,followPreview:true})` opts into the two independent
1055
1108
  behaviors. Both options default to `false`; omitted options retain their
@@ -1087,7 +1140,7 @@ lists, quotes, and tables follow their block, while invisible source positions
1087
1140
  use a nearby available rendered block rather than exact glyph geometry.
1088
1141
 
1089
1142
  Public CSS parts are `editor`, `title`, `toolbar`, `input`, `preview`,
1090
- `actions`, `status`, and `save-action`. Applications can make the title
1143
+ `actions`, `status`, `print-action`, and `save-action`. Applications can make the title
1091
1144
  borderless and compact through `::part(title)` while retaining its native
1092
1145
  editing behavior and visible keyboard focus. These parts are styling hooks,
1093
1146
  not slots or replacement controls.
@@ -109,6 +109,7 @@ own asynchronous work, cancellation, and backpressure.
109
109
  | [`PersistentAIChatSession.js`](#persistentaichatsessionjs) | esm | Adds explicit retained-history/memory policy to complete configured chat without changing DBOPFS or ChatEntity semantics. | Browser / native WebView with DBOPFS and configured chat | Retained context commits atomically; `persist:false` turns are one-operation-only. |
110
110
  | [`PreferenceStore.js`](#preferencestorejs) | esm | Loads and updates schema-defined app preferences through native storage with a narrow browser fallback. | Browser/native hybrid | Complete ordinary values remain mutable; setAll uses one optional atomic adapter batch for every selected value when advertised, otherwise performs complete ordered serial writes, and only exact unsupported native capability changes future operations to the browser fallback. |
111
111
  | [`PreparedSpeech.js`](#preparedspeechjs) | esm | Owns detached ordered preparation, semantic audio reuse, and per-caller cancellation behind AI.prepareTTS. | Browser / native WebView with injected synthesis and optional DBOPFS | Complete original inputs, ordered audio metadata, durable reuse, and observable preparation results. |
112
+ | [`PrintView.js`](#printviewjs) | esm | Prints current rendered content and title through one document-owned print lifecycle. | Browser / supported native WebView print implementation | Complete rendered snapshot, preparation cancellation, resource lifetime, and honest request result. |
112
113
  | [`QRCode.min.js`](#qrcodeminjs) | classic-script | Vendored QRCode generator for DOM, canvas, SVG, and image output. | Browser vendor script | Vendor-native. |
113
114
  | [`Questionnaire.js`](#questionnairejs) | esm | Evaluates whether a one-time questionnaire prompt is due without performing the prompt. | Cross-host | Normalized conservative boolean. |
114
115
  | [`RecordLinkIndex.js`](#recordlinkindexjs) | esm | Parses record links and builds their normalized index. | Cross-host | Fully normalized. |
@@ -3251,6 +3252,69 @@ import {prepareSpeech} from '/arcane/modules/PreparedSpeech.js';
3251
3252
  console.log(typeof prepareSpeech); // function
3252
3253
  ```
3253
3254
 
3255
+ ## PrintView.js
3256
+
3257
+ ### Overview
3258
+
3259
+ Shared rendered-print owner used by Markdown Editor and File Manager. It
3260
+ captures the current DOM and computed styles, including accessible rendered
3261
+ HTML frames, complete text, selected images, and the current title. A temporary
3262
+ print-only surface expands scrollable content without editing the live screen.
3263
+
3264
+ ### Public surface
3265
+
3266
+ Named `createPrintView({host,content,title,active,priority=0,signal,onError,retain})`
3267
+ returns `{print,destroy}`. `host` is a connected element; synchronous callbacks
3268
+ return the rendered content node, string title, and whether the view is active.
3269
+ Optional `retain()` returns a resource-release callback. It keeps owned media
3270
+ available until preparation is cancelled or the browser emits `afterprint`.
3271
+
3272
+ `await print()` selects that view, waits for accessible HTML frame, image, and
3273
+ font readiness, and requests the browser print dialog. It returns `false` when
3274
+ the view is unavailable or preparation is cancelled, returns `true` for a dialog
3275
+ request, and rejects preparation errors. It cannot confirm physical printing
3276
+ or PDF saving. Overlapping print sessions on the same document reject.
3277
+
3278
+ One native `beforeprint`/`afterprint` registration serves each document. Native
3279
+ Print chooses the highest-priority visible active view, most recently registered
3280
+ on ties. File preview uses priority `1`; editor uses `0`. Native `beforeprint`
3281
+ is synchronous: it snapshots currently rendered content, cannot await a newly
3282
+ loading resource, and cannot cancel the browser's dialog. `onError(error)` or
3283
+ the developer console receives complete preparation failures.
3284
+
3285
+ `destroy()` unregisters the view and cancels pending preparation. Once the
3286
+ dialog has been requested, its snapshot and retained media remain until
3287
+ `afterprint`. Import alone creates no listener, provider, storage, or print job.
3288
+
3289
+ Exact exports: `createPrintView`.
3290
+
3291
+ ### Availability and normalization
3292
+
3293
+ **Browser and native WebViews with a print implementation.** Requires DOM,
3294
+ CSSOM, FontFace/Image readiness, and browser print events. No Core capability.
3295
+ Cross-origin/inaccessible frames, native PDF frames, and object/embed viewers
3296
+ must use their owning viewer's print command; the helper does not replace their
3297
+ content with raw source or claim an unrendered document was printed. Font
3298
+ sources in embedded documents must be accessible for snapshot preparation.
3299
+
3300
+ ### Example
3301
+
3302
+ ```javascript
3303
+ import {createPrintView} from '/arcane/modules/PrintView.js';
3304
+
3305
+ const ledger = document.querySelector('#moon-library-ledger');
3306
+ const printing = createPrintView({
3307
+ host:ledger,
3308
+ content:() => ledger,
3309
+ title:() => 'Books overdue on the Moon',
3310
+ active:() => true
3311
+ });
3312
+ document.querySelector('#print-ledger').onclick = async function printLedger(){
3313
+ try { await printing.print(); }
3314
+ catch(error) { console.error('Unable to print the ledger:', error); }
3315
+ };
3316
+ ```
3317
+
3254
3318
  ## QRCode.min.js
3255
3319
 
3256
3320
  ### Overview