arcane-os 0.44.0 → 0.45.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 +21 -0
- package/README.md +1 -1
- package/docs/reference/runtime-components.md +42 -3
- package/docs/reference/runtime-modules.md +93 -1
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +27 -6
- package/runtime/arcane/components/file-manager.html +111 -5
- package/runtime/arcane/components/markdown-document.html +31 -4
- package/runtime/arcane/components/markdown-editor.html +113 -4
- package/runtime/arcane/entities/Chat.js +6 -4
- package/runtime/arcane/modules/MarkdownMedia.js +211 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +22 -8
- package/runtime/arcane/modules/PrintView.js +51 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.45.0
|
|
4
|
+
|
|
5
|
+
- Add public MarkdownMedia save, read, reference parsing and hydration helpers.
|
|
6
|
+
Store complete image records in application-owned DBOPFS tables outside raw
|
|
7
|
+
Markdown, with stable references and application-selected filenames. Preserve
|
|
8
|
+
JSONL/NDJSON singleton records across cold reads and backup restoration.
|
|
9
|
+
- Hydrate local images independently in shared Markdown views while preserving
|
|
10
|
+
raw Markdown, external URLs, source maps and saving. Observe cancellation and
|
|
11
|
+
individual failures without removing successful sibling images. Retain print
|
|
12
|
+
resources through afterprint; explicit PrintView preparation awaits local
|
|
13
|
+
media while native beforeprint remains synchronous.
|
|
14
|
+
- Apply no-retention semantics to entity-wide persistence choices as well as
|
|
15
|
+
request persist:false: complete input and response remain available to the
|
|
16
|
+
active operation, then stay outside retained transcript, recurring model
|
|
17
|
+
context, DBOPFS and memory extraction. Capture the accepted choice before
|
|
18
|
+
rendering waits and preserve original pending calls after temporary tool
|
|
19
|
+
continuations. Existing retained turns and saved history remain untouched;
|
|
20
|
+
structural sanitization applies to new durable writes only, without migration.
|
|
21
|
+
- Keep image model selection, credentials, storage selection and app orchestration
|
|
22
|
+
application-owned. No model/provider/default changes are introduced.
|
|
23
|
+
|
|
3
24
|
## 0.44.0
|
|
4
25
|
|
|
5
26
|
- Add stateless `generateImages` through `arcane-os/ai/twin-cloud` with
|
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.
|
|
22
|
+
This checkout defines the `0.45.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
|
|
|
@@ -426,7 +426,8 @@ history stores each tool's required user-facing message as an ordinary
|
|
|
426
426
|
call envelope or ID and therefore does not recreate a pending executable call
|
|
427
427
|
after reload. `submitToolResult()` and `submitToolResults()` accept `status` as
|
|
428
428
|
the public result term; `disposition` remains an accepted input spelling.
|
|
429
|
-
When a submitted turn uses `persist:false`,
|
|
429
|
+
When a submitted turn uses `persist:false`, or its session's Chat entity has
|
|
430
|
+
`persist=false`, Chat may show its cards while that
|
|
430
431
|
one operation is active, then removes them when the operation settles; neither
|
|
431
432
|
the input nor response remains in the retained transcript or model context.
|
|
432
433
|
This cleanup also applies to failed, cancelled, detached, or replaced-session
|
|
@@ -437,6 +438,15 @@ operation's nodes so a completion listener can start another turn safely.
|
|
|
437
438
|
Submission code passes those card references directly when it creates them;
|
|
438
439
|
reusing an operation identifier never selects earlier conversation cards for
|
|
439
440
|
cleanup or timestamp updates.
|
|
441
|
+
The session's terminal `retained` boolean reports its actual commit decision;
|
|
442
|
+
Chat uses that decision instead of retroactively interpreting a flag changed
|
|
443
|
+
for a later request. A completion listener cannot erase an earlier settled
|
|
444
|
+
turn by changing the next operation's retention setting.
|
|
445
|
+
Chat carries a submission's no-retention choice into its copied session request
|
|
446
|
+
before any rendering wait, so re-enabling the entity before dispatch cannot
|
|
447
|
+
retain that temporary input. Existing falsy entity settings retain their
|
|
448
|
+
no-retention meaning; a compatible custom session without a Chat entity keeps
|
|
449
|
+
its existing default behavior.
|
|
440
450
|
Persistent-turn recovery and existing saved history remain unchanged.
|
|
441
451
|
|
|
442
452
|
For streaming sessions, every provisional structural card and the terminal
|
|
@@ -926,6 +936,16 @@ Native PDF and inaccessible embedded viewers require their own print command.
|
|
|
926
936
|
was produced. Closed, destroyed, or unavailable previews return `false`;
|
|
927
937
|
preparation failures reject and the visible control reports a concise failure.
|
|
928
938
|
|
|
939
|
+
Markdown previews resolve saved `arcane-media:` images through
|
|
940
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs) before connecting IMG
|
|
941
|
+
nodes. Text appears while independent image reads finish; failures leave the
|
|
942
|
+
complete text and successful images visible with a concise recovery status.
|
|
943
|
+
Explicit printing waits for local reads and decoding. Native Print reports
|
|
944
|
+
pending media because it cannot await them. Closing or replacing a preview
|
|
945
|
+
cancels display readiness without waiting for storage; URLs already retained
|
|
946
|
+
by a requested print remain until `afterprint`. Original Markdown and ordinary
|
|
947
|
+
external image URLs remain unchanged.
|
|
948
|
+
|
|
929
949
|
`await close()` cancels pending presentation work and closes delete, file, and
|
|
930
950
|
directory dialogs in that order. A settled preview keeps its rendered content,
|
|
931
951
|
media, and printing available until the file dialog actually closes; refusing
|
|
@@ -1054,7 +1074,16 @@ Events: `markdown-document-ready`, `markdown-document-state`,
|
|
|
1054
1074
|
`markdown-document-empty`, `markdown-document-error`, and
|
|
1055
1075
|
`markdown-document-navigate`.
|
|
1056
1076
|
|
|
1057
|
-
|
|
1077
|
+
Saved local image references use
|
|
1078
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs). Text rendering and
|
|
1079
|
+
the existing rendered event do not wait for image reads. The optional
|
|
1080
|
+
`labels.mediaError` supplies concise visible failure text while complete
|
|
1081
|
+
diagnostics remain in the developer console; successful images and document
|
|
1082
|
+
text remain visible. Replacing, clearing, failing, or destroying the document
|
|
1083
|
+
releases its display resources without deleting stored media. Markdown source
|
|
1084
|
+
and ordinary external image URLs remain unchanged.
|
|
1085
|
+
|
|
1086
|
+
Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`MarkdownMedia.js`](runtime-modules.md#markdownmediajs).
|
|
1058
1087
|
|
|
1059
1088
|
### Availability and normalization
|
|
1060
1089
|
|
|
@@ -1104,6 +1133,16 @@ when the browser dialog was requested. Completion of physical printing or PDF
|
|
|
1104
1133
|
saving is not observable through this method. See
|
|
1105
1134
|
[`PrintView.js`](runtime-modules.md#printviewjs).
|
|
1106
1135
|
|
|
1136
|
+
Saved `arcane-media:` image references are hydrated through
|
|
1137
|
+
[`MarkdownMedia.js`](runtime-modules.md#markdownmediajs) in detached rendered
|
|
1138
|
+
nodes. Source text, edit selection, source-map comments, ordinary image URLs,
|
|
1139
|
+
and the existing save flow remain unchanged. Image status has its own
|
|
1140
|
+
idle-hidden `media-status` part, separate from save status. Explicit printing
|
|
1141
|
+
waits for the current image reads/decoding, follows a replacement preview, and
|
|
1142
|
+
rechecks readiness before capture. Native Print reports still-pending media
|
|
1143
|
+
synchronously; it cannot wait or cancel the native dialog. Print-owned URLs
|
|
1144
|
+
outlive a changed or destroyed editor until `afterprint`.
|
|
1145
|
+
|
|
1107
1146
|
`configure({fit:true,followPreview:true})` opts into the two independent
|
|
1108
1147
|
behaviors. Both options default to `false`; omitted options retain their
|
|
1109
1148
|
previous values during reconfiguration, and explicit `false` disables either.
|
|
@@ -1140,7 +1179,7 @@ lists, quotes, and tables follow their block, while invisible source positions
|
|
|
1140
1179
|
use a nearby available rendered block rather than exact glyph geometry.
|
|
1141
1180
|
|
|
1142
1181
|
Public CSS parts are `editor`, `title`, `toolbar`, `input`, `preview`,
|
|
1143
|
-
`actions`, `status`, `print-action`, and `save-action`. Applications can make the title
|
|
1182
|
+
`actions`, `status`, `media-status`, `print-action`, and `save-action`. Applications can make the title
|
|
1144
1183
|
borderless and compact through `::part(title)` while retaining its native
|
|
1145
1184
|
editing behavior and visible keyboard focus. These parts are styling hooks,
|
|
1146
1185
|
not slots or replacement controls.
|
|
@@ -96,6 +96,7 @@ own asynchronous work, cancellation, and backpressure.
|
|
|
96
96
|
| [`Mail.js`](#mailjs) | esm | Builds complete reports and prefers the native mail capability with an explicit HTTP transport fallback. | Browser/native hybrid + cloud | Mail inputs/results normalized; transport failures mixed. |
|
|
97
97
|
| [`MailOutbox.mjs`](#mailoutboxmjs) | esm | Persists complete mail reports before delivery and normalizes idempotent enqueue, retry, reconciliation, and invalid-record maintenance. | Browser/native WebView or compatible injected host | Complete records, full work, cancellation, and lifecycle states normalized; storage, lock, and delivery failures coded. |
|
|
98
98
|
| [`MailTransport.mjs`](#mailtransportmjs) | esm | Sends one complete mail report to a normalized HTTP(S) endpoint. | Browser/server with fetch + cloud | Normalized endpoint and transport errors; remote detail preserved. |
|
|
99
|
+
| [`MarkdownMedia.js`](#markdownmediajs) | esm | Saves local Markdown images separately and resolves their stable references in rendered views. | Browser / supported native WebView | Complete image records, concurrent display reads, cancellation, and print-owned URL lifetime. |
|
|
99
100
|
| [`MarkdownSpeech.js`](#markdownspeechjs) | esm | Removes repeated Markdown formatting marks from streamed narration. | Cross-host | Speech-only filtering; single marks and ordinary punctuation remain literal. |
|
|
100
101
|
| [`Marked.min.js`](#markedminjs) | esm | Vendored Marked 18.0.5 Markdown lexer, parser, renderer, extension, and walk-token API. | Cross-host vendor module | Vendor-native Marked contract. |
|
|
101
102
|
| [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors. | Browser / native WebView | Complete raw and rendered Marked values; opt-in original-source offsets; parse errors vendor-native. |
|
|
@@ -2794,6 +2795,73 @@ import * as module from '/arcane/modules/Marked.min.js';
|
|
|
2794
2795
|
console.log(Object.keys(module));
|
|
2795
2796
|
```
|
|
2796
2797
|
|
|
2798
|
+
## MarkdownMedia.js
|
|
2799
|
+
|
|
2800
|
+
### Overview
|
|
2801
|
+
|
|
2802
|
+
Stores complete images in the application's existing DBOPFS database while
|
|
2803
|
+
Markdown contains only a short, stable `arcane-media:` reference. Rendering
|
|
2804
|
+
resolves those local references into temporary display URLs. Importing the
|
|
2805
|
+
module starts no storage, network, provider, or rendering operation.
|
|
2806
|
+
|
|
2807
|
+
### Public surface
|
|
2808
|
+
|
|
2809
|
+
Public package import: `arcane-os/modules/MarkdownMedia.js`.
|
|
2810
|
+
|
|
2811
|
+
- `await saveMarkdownMedia({blob,tableName='markdown-media',fileName})` writes
|
|
2812
|
+
`{mediaType,dataUrl}` as a JSON-compatible record and returns
|
|
2813
|
+
`{reference,tableName,fileName,mediaType}` only after the write completes.
|
|
2814
|
+
The default filename is a new UUID followed by `.json`. There is no abort
|
|
2815
|
+
option: accepted storage writes settle through the storage owner.
|
|
2816
|
+
- `parseMarkdownMediaReference(reference)` returns `{tableName,fileName}` for
|
|
2817
|
+
a local reference, or `null` for an ordinary URL. Malformed local addresses
|
|
2818
|
+
report their actual parsing error.
|
|
2819
|
+
- `await readMarkdownMedia(reference)` returns the complete `Blob`, using
|
|
2820
|
+
only local storage and data-URL decoding; stored strings are never fetched
|
|
2821
|
+
as network addresses.
|
|
2822
|
+
- `hydrateMarkdownMedia(root,{signal}={})` returns `{ready,destroy,retain}`
|
|
2823
|
+
immediately. It includes an IMG root and descendant IMG elements, removes
|
|
2824
|
+
local-reference `src` values synchronously, starts independent reads
|
|
2825
|
+
together, and assigns owned object URLs. `ready` includes image decoding.
|
|
2826
|
+
Use a detached fragment before connecting those images to the document.
|
|
2827
|
+
|
|
2828
|
+
Hydration preserves Markdown source, image descriptions, ordinary URLs, and
|
|
2829
|
+
surrounding content. Failed reads or decodes reject `ready` with an
|
|
2830
|
+
`AggregateError` whose complete `failures` array contains
|
|
2831
|
+
`{image,reference,reason}`; successful sibling images remain visible.
|
|
2832
|
+
|
|
2833
|
+
`destroy()` or abort settles display readiness promptly, prevents late display,
|
|
2834
|
+
and releases owned URLs. An already-started storage read remains observed and
|
|
2835
|
+
settles independently. `retain()` returns an idempotent release callback for
|
|
2836
|
+
printing; retained URLs survive destruction of the original view until every
|
|
2837
|
+
print owner releases them. Destruction never deletes stored images.
|
|
2838
|
+
|
|
2839
|
+
Applications own record names, entry association, cleanup policy, and which
|
|
2840
|
+
records their existing JSON backup/export includes. Reusing the same filename
|
|
2841
|
+
supports an application-selected save-only retry without another generation
|
|
2842
|
+
request. The helper performs no existing-data migration.
|
|
2843
|
+
|
|
2844
|
+
### Availability and normalization
|
|
2845
|
+
|
|
2846
|
+
Browser or native WebView with the existing app-scoped DBOPFS owner, Blob,
|
|
2847
|
+
FileReader, object URLs, and IMG decoding. Encoding, storage, missing-record,
|
|
2848
|
+
parsing, and decoding errors remain observable. No Core capability is selected.
|
|
2849
|
+
|
|
2850
|
+
### Example
|
|
2851
|
+
|
|
2852
|
+
```javascript
|
|
2853
|
+
import {saveMarkdownMedia} from 'arcane-os/modules/MarkdownMedia.js';
|
|
2854
|
+
|
|
2855
|
+
const mural = new Blob([
|
|
2856
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="240" height="80"><text x="10" y="45">Octopus parking only</text></svg>'
|
|
2857
|
+
], {type:'image/svg+xml'});
|
|
2858
|
+
const {reference} = await saveMarkdownMedia({blob:mural});
|
|
2859
|
+
editor.insertMarkdown(``);
|
|
2860
|
+
```
|
|
2861
|
+
|
|
2862
|
+
Here `editor` is the ready shared Markdown Editor. Its normal change/save flow
|
|
2863
|
+
still owns saving the Markdown; this operation saves the image record.
|
|
2864
|
+
|
|
2797
2865
|
## MD.js
|
|
2798
2866
|
|
|
2799
2867
|
### Overview
|
|
@@ -3071,6 +3139,9 @@ assistant content in configured model context and ChatEntity/DBOPFS history.
|
|
|
3071
3139
|
That assistant-only opening survives reload and empty-chat maintenance without
|
|
3072
3140
|
a fabricated user turn. A second opening rejects as `AI_CHAT_OPENING_EXISTS`;
|
|
3073
3141
|
an unavailable durable ChatEntity rejects as `AI_CHAT_PERSISTENCE_UNAVAILABLE`.
|
|
3142
|
+
If retention is disabled while the opening is being prepared, that same error
|
|
3143
|
+
rolls back the opening before its append; it does not leave hidden model context.
|
|
3144
|
+
An opening write already accepted by DBOPFS still completes normally.
|
|
3074
3145
|
|
|
3075
3146
|
`send()` accepts either
|
|
3076
3147
|
`{message:{content,role:'user'|'tool',tool_call_id?,message?,name?,status?,persist},...}`
|
|
@@ -3086,6 +3157,20 @@ the retained transcript, memory extraction, or DBOPFS. A nonpersistent response
|
|
|
3086
3157
|
therefore does not open a retained structural-tool continuation. A retained
|
|
3087
3158
|
structural tool result must use the persistence choice captured by its matching
|
|
3088
3159
|
assistant tool call.
|
|
3160
|
+
An explicitly nonretained tool-result continuation uses its matching pending
|
|
3161
|
+
call only for that operation, then rolls back without settling or changing the
|
|
3162
|
+
retained call. A later retained continuation can still settle it normally.
|
|
3163
|
+
|
|
3164
|
+
The entity-wide `session.chatEntity.persist=false` applies the same no-retention
|
|
3165
|
+
rule to new turns and direct entity additions; it is not a disk-only switch.
|
|
3166
|
+
The session captures that setting when it accepts a request and observes it
|
|
3167
|
+
again before accepting a durable append. Changing it later cannot bring back
|
|
3168
|
+
discarded inputs or responses. A write already accepted by DBOPFS finishes
|
|
3169
|
+
normally; toggling the setting does not undo that write or rewrite saved data.
|
|
3170
|
+
Completed `send()` and `stream()` responses include a session-owned `retained`
|
|
3171
|
+
boolean outside `message` and the provider payload, identifying whether the
|
|
3172
|
+
turn entered recurring history. Existing retained and stored records remain
|
|
3173
|
+
untouched: this boundary applies to new additions and performs no migration.
|
|
3089
3174
|
|
|
3090
3175
|
`history()` returns provider-safe configured model context, including the
|
|
3091
3176
|
system prompt and every complete ordinary visible committed turn. Only a
|
|
@@ -3263,11 +3348,16 @@ print-only surface expands scrollable content without editing the live screen.
|
|
|
3263
3348
|
|
|
3264
3349
|
### Public surface
|
|
3265
3350
|
|
|
3266
|
-
Named `createPrintView({host,content,title,active,priority=0,signal,onError,retain})`
|
|
3351
|
+
Named `createPrintView({host,content,title,active,priority=0,signal,onError,retain,prepare})`
|
|
3267
3352
|
returns `{print,destroy}`. `host` is a connected element; synchronous callbacks
|
|
3268
3353
|
return the rendered content node, string title, and whether the view is active.
|
|
3269
3354
|
Optional `retain()` returns a resource-release callback. It keeps owned media
|
|
3270
3355
|
available until preparation is cancelled or the browser emits `afterprint`.
|
|
3356
|
+
Optional `prepare(signal)` may await view-owned readiness during an explicit
|
|
3357
|
+
print request. After it settles, current content and title are read again
|
|
3358
|
+
synchronously immediately before capture. A view whose content changes during
|
|
3359
|
+
preparation must keep its owned media retained and reject a still-pending
|
|
3360
|
+
snapshot through its synchronous `content()` callback.
|
|
3271
3361
|
|
|
3272
3362
|
`await print()` selects that view, waits for accessible HTML frame, image, and
|
|
3273
3363
|
font readiness, and requests the browser print dialog. It returns `false` when
|
|
@@ -3281,6 +3371,8 @@ on ties. File preview uses priority `1`; editor uses `0`. Native `beforeprint`
|
|
|
3281
3371
|
is synchronous: it snapshots currently rendered content, cannot await a newly
|
|
3282
3372
|
loading resource, and cannot cancel the browser's dialog. `onError(error)` or
|
|
3283
3373
|
the developer console receives complete preparation failures.
|
|
3374
|
+
Native printing never invokes the asynchronous `prepare` callback; prepared
|
|
3375
|
+
views still receive the synchronous content/title read at capture.
|
|
3284
3376
|
|
|
3285
3377
|
`destroy()` unregisters the view and cancels pending preparation. Once the
|
|
3286
3378
|
dialog has been requested, its snapshot and retained media remain until
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
87
87
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
88
88
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
89
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
89
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
90
90
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
91
91
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
92
92
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -3255,13 +3255,22 @@
|
|
|
3255
3255
|
);
|
|
3256
3256
|
}
|
|
3257
3257
|
|
|
3258
|
-
const
|
|
3258
|
+
const submittedMessages=is.array(sessionRequest)
|
|
3259
3259
|
?sessionRequest
|
|
3260
3260
|
:[sessionRequest];
|
|
3261
|
-
if(!
|
|
3261
|
+
if(!submittedMessages.length){
|
|
3262
3262
|
throw new TypeError('A chat session request must contain at least one message.');
|
|
3263
3263
|
}
|
|
3264
|
-
|
|
3264
|
+
function entityRetainsMessages(){
|
|
3265
|
+
return !session.chatEntity||Boolean(session.chatEntity.persist);
|
|
3266
|
+
}
|
|
3267
|
+
let retainTurn=submittedMessages.every(message=>message.persist!==false)
|
|
3268
|
+
&&entityRetainsMessages();
|
|
3269
|
+
// Preserve this submission's choice across the rendering await before
|
|
3270
|
+
// dispatch; a later entity setting belongs to a later submission.
|
|
3271
|
+
const sessionRequestMessages=retainTurn
|
|
3272
|
+
?submittedMessages
|
|
3273
|
+
:submittedMessages.map(message=>({...message,persist:false}));
|
|
3265
3274
|
const request={
|
|
3266
3275
|
...(sessionRequestMessages.length===1
|
|
3267
3276
|
?{message:sessionRequestMessages[0]}
|
|
@@ -3276,9 +3285,18 @@
|
|
|
3276
3285
|
?session.aiName
|
|
3277
3286
|
:host.aiName;
|
|
3278
3287
|
const sessionMessageToken=Symbol(messageId);
|
|
3279
|
-
const transientCards=
|
|
3288
|
+
const transientCards=[...requestMessages];
|
|
3289
|
+
let retentionSettled=false;
|
|
3290
|
+
function settleTurnRetention(result){
|
|
3291
|
+
if(retentionSettled) return;
|
|
3292
|
+
// The session reports the decision made at its actual commit boundary.
|
|
3293
|
+
retainTurn=retainTurn&&(is.boolean(result?.retained)
|
|
3294
|
+
?result.retained
|
|
3295
|
+
:entityRetainsMessages());
|
|
3296
|
+
retentionSettled=true;
|
|
3297
|
+
}
|
|
3280
3298
|
function removeTransientSessionCards(){
|
|
3281
|
-
if(
|
|
3299
|
+
if(retainTurn||!transientCards.length) return;
|
|
3282
3300
|
// The submitter supplies only nodes created for this submission.
|
|
3283
3301
|
for(const card of transientCards) card.remove();
|
|
3284
3302
|
transientCards.length=0;
|
|
@@ -3290,7 +3308,7 @@
|
|
|
3290
3308
|
setSessionStatus('sending','AI is responding…');
|
|
3291
3309
|
try{
|
|
3292
3310
|
const responseCard=receivedMessage(responseName, '', messageId);
|
|
3293
|
-
|
|
3311
|
+
transientCards.push(responseCard);
|
|
3294
3312
|
await streamMessage('',messageId,true);
|
|
3295
3313
|
setMessageProgress(
|
|
3296
3314
|
messageId,
|
|
@@ -3344,6 +3362,7 @@
|
|
|
3344
3362
|
}
|
|
3345
3363
|
)
|
|
3346
3364
|
:await session.send(request);
|
|
3365
|
+
settleTurnRetention(result);
|
|
3347
3366
|
if(
|
|
3348
3367
|
destroyed
|
|
3349
3368
|
||bindingGeneration!==sessionBindingGeneration
|
|
@@ -3441,6 +3460,7 @@
|
|
|
3441
3460
|
);
|
|
3442
3461
|
return result;
|
|
3443
3462
|
}catch(error){
|
|
3463
|
+
settleTurnRetention();
|
|
3444
3464
|
if(!destroyed&&bindingGeneration===sessionBindingGeneration){
|
|
3445
3465
|
setPendingStructuralToolCalls(previousPendingToolCalls);
|
|
3446
3466
|
}
|
|
@@ -3501,6 +3521,7 @@
|
|
|
3501
3521
|
}
|
|
3502
3522
|
throw error;
|
|
3503
3523
|
}finally{
|
|
3524
|
+
settleTurnRetention();
|
|
3504
3525
|
removeTransientSessionCards();
|
|
3505
3526
|
if(activeSessionMessageToken===sessionMessageToken){
|
|
3506
3527
|
activeSessionMessageToken=null;
|
|
@@ -1555,6 +1555,8 @@
|
|
|
1555
1555
|
generation,
|
|
1556
1556
|
urls:new Set(),
|
|
1557
1557
|
media:new Set(),
|
|
1558
|
+
markdownMedia:new Set(),
|
|
1559
|
+
markdownStatus:false,
|
|
1558
1560
|
title:`${directory}/${fileName}`,
|
|
1559
1561
|
rendered:null,
|
|
1560
1562
|
printPreparation:null,
|
|
@@ -1660,6 +1662,7 @@
|
|
|
1660
1662
|
status.remove();
|
|
1661
1663
|
content.append(rendered);
|
|
1662
1664
|
preview.rendered=rendered;
|
|
1665
|
+
updateMarkdownPreviewStatus(preview);
|
|
1663
1666
|
void preparePreviewPrint(preview).catch(function reportPrintPreparation(error){
|
|
1664
1667
|
reportPreviewPrintFailure(error,preview);
|
|
1665
1668
|
});
|
|
@@ -1683,11 +1686,12 @@
|
|
|
1683
1686
|
}
|
|
1684
1687
|
const view=createPrintView({
|
|
1685
1688
|
host,
|
|
1686
|
-
content:()=>preview
|
|
1689
|
+
content:()=>previewPrintContent(preview),
|
|
1687
1690
|
title:()=>preview.title,
|
|
1688
1691
|
active:()=>isCurrentPreview(preview)&&fileModal.opened===true,
|
|
1689
1692
|
priority:1,
|
|
1690
1693
|
signal:preview.controller.signal,
|
|
1694
|
+
prepare:function preparePreviewMedia(signal){return waitForPreviewMarkdown(preview,signal);},
|
|
1691
1695
|
retain:()=>retainPreviewPrintResources(preview),
|
|
1692
1696
|
onError:error=>reportPreviewPrintFailure(error,preview)
|
|
1693
1697
|
});
|
|
@@ -1703,20 +1707,93 @@
|
|
|
1703
1707
|
if(!preview?.rendered||!isCurrentPreview(preview)||fileModal.opened!==true){
|
|
1704
1708
|
return false;
|
|
1705
1709
|
}
|
|
1710
|
+
if(!await waitForPreviewMarkdown(preview)){
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1706
1713
|
const view=await preparePreviewPrint(preview);
|
|
1707
1714
|
return view&&isCurrentPreview(preview)?view.print():false;
|
|
1708
1715
|
}
|
|
1709
1716
|
|
|
1717
|
+
async function waitForPreviewMarkdown(preview,signal){
|
|
1718
|
+
signal?.throwIfAborted();
|
|
1719
|
+
const results=await Promise.allSettled(
|
|
1720
|
+
[...preview.markdownMedia].map(function markdownReadiness(media){
|
|
1721
|
+
return media.owner.ready;
|
|
1722
|
+
})
|
|
1723
|
+
);
|
|
1724
|
+
signal?.throwIfAborted();
|
|
1725
|
+
if(!isCurrentPreview(preview)||fileModal.opened!==true){
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
const failures=results.filter(function failedMarkdown(result){
|
|
1729
|
+
return result.status==='rejected';
|
|
1730
|
+
}).map(function markdownFailure(result){
|
|
1731
|
+
return result.reason;
|
|
1732
|
+
});
|
|
1733
|
+
if(failures.length){
|
|
1734
|
+
throw new AggregateError(failures,'Local images could not be prepared for printing.');
|
|
1735
|
+
}
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function previewPrintContent(preview){
|
|
1740
|
+
const failures=[];
|
|
1741
|
+
for(const media of preview.markdownMedia){
|
|
1742
|
+
if(!media.settled){
|
|
1743
|
+
throw new Error('Local images are still loading. Use Print after they finish.');
|
|
1744
|
+
}
|
|
1745
|
+
if(media.error){
|
|
1746
|
+
failures.push(media.error);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if(failures.length){
|
|
1750
|
+
throw new AggregateError(failures,'Local images could not be prepared for printing.');
|
|
1751
|
+
}
|
|
1752
|
+
return preview.rendered;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function updateMarkdownPreviewStatus(preview){
|
|
1756
|
+
if(!isCurrentPreview(preview)||!preview.rendered){
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if([...preview.markdownMedia].some(function failedMarkdown(media){return media.error;})){
|
|
1760
|
+
preview.markdownStatus=true;
|
|
1761
|
+
preview.printStatus.textContent='Some images could not be loaded. Reopen this file to try again.';
|
|
1762
|
+
preview.printStatus.setAttribute('role','alert');
|
|
1763
|
+
preview.container.append(preview.printStatus);
|
|
1764
|
+
}else if(preview.markdownStatus&&[...preview.markdownMedia].every(function settledMarkdown(media){return media.settled;})){
|
|
1765
|
+
preview.markdownStatus=false;
|
|
1766
|
+
preview.printStatus.remove();
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1710
1770
|
function reportPreviewPrintFailure(error,preview){
|
|
1711
1771
|
arcaneLogging.error('Unable to print file preview:',error);
|
|
1712
1772
|
if(isCurrentPreview(preview)){
|
|
1713
|
-
|
|
1773
|
+
const media=[...preview.markdownMedia];
|
|
1774
|
+
preview.markdownStatus=media.some(function unavailableImage(item){return !item.settled||item.error;});
|
|
1775
|
+
preview.printStatus.textContent=media.some(function pendingImage(item){return !item.settled;})
|
|
1776
|
+
?'Images are still loading. Close this print dialog and use Print after they finish.'
|
|
1777
|
+
:media.some(function failedImage(item){return item.error;})
|
|
1778
|
+
?'Some images could not be loaded. Close any print dialog and reopen this file to try again.'
|
|
1779
|
+
:'Unable to open print preview. Please try again.';
|
|
1714
1780
|
preview.printStatus.setAttribute('role','alert');
|
|
1715
1781
|
preview.container.append(preview.printStatus);
|
|
1716
1782
|
}
|
|
1717
1783
|
}
|
|
1718
1784
|
|
|
1719
1785
|
function retainPreviewPrintResources(preview){
|
|
1786
|
+
const markdownReleases=[];
|
|
1787
|
+
try{
|
|
1788
|
+
for(const media of preview.markdownMedia){
|
|
1789
|
+
markdownReleases.push(media.owner.retain());
|
|
1790
|
+
}
|
|
1791
|
+
}catch(error){
|
|
1792
|
+
for(const release of markdownReleases){
|
|
1793
|
+
release();
|
|
1794
|
+
}
|
|
1795
|
+
throw error;
|
|
1796
|
+
}
|
|
1720
1797
|
preview.printRetains+=1;
|
|
1721
1798
|
let retained=true;
|
|
1722
1799
|
return function releasePrintedPreviewResources(){
|
|
@@ -1724,6 +1801,9 @@
|
|
|
1724
1801
|
return;
|
|
1725
1802
|
}
|
|
1726
1803
|
retained=false;
|
|
1804
|
+
for(const release of markdownReleases){
|
|
1805
|
+
release();
|
|
1806
|
+
}
|
|
1727
1807
|
preview.printRetains-=1;
|
|
1728
1808
|
if(preview.released&&preview.printRetains===0){
|
|
1729
1809
|
releasePreviewURLs(preview);
|
|
@@ -1760,6 +1840,10 @@
|
|
|
1760
1840
|
}
|
|
1761
1841
|
preview.released=true;
|
|
1762
1842
|
preview.controller.abort(reason);
|
|
1843
|
+
for(const media of preview.markdownMedia){
|
|
1844
|
+
media.owner.destroy();
|
|
1845
|
+
}
|
|
1846
|
+
preview.markdownMedia.clear();
|
|
1763
1847
|
for(const element of preview.media){
|
|
1764
1848
|
if(element.localName==='audio'){
|
|
1765
1849
|
element.pause();
|
|
@@ -1817,10 +1901,32 @@
|
|
|
1817
1901
|
}
|
|
1818
1902
|
const body=document.createElement(descriptor.kind==='html'?'iframe':'div');
|
|
1819
1903
|
if(descriptor.kind==='markdown'){
|
|
1820
|
-
markdownModulePromise=markdownModulePromise||
|
|
1821
|
-
|
|
1904
|
+
markdownModulePromise=markdownModulePromise||Promise.all([
|
|
1905
|
+
import('../modules/MD.js'),
|
|
1906
|
+
import('../modules/MarkdownMedia.js')
|
|
1907
|
+
]);
|
|
1908
|
+
const [{default:MD},{hydrateMarkdownMedia}]=await markdownModulePromise;
|
|
1822
1909
|
preview.controller.signal.throwIfAborted();
|
|
1823
|
-
|
|
1910
|
+
const template=document.createElement('template');
|
|
1911
|
+
template.innerHTML=new MD(descriptor.content).safeRendered;
|
|
1912
|
+
const owner=hydrateMarkdownMedia(template.content,{signal:preview.controller.signal});
|
|
1913
|
+
const media={owner,settled:false,error:null};
|
|
1914
|
+
preview.markdownMedia.add(media);
|
|
1915
|
+
void owner.ready.then(
|
|
1916
|
+
function markdownImagesReady(){
|
|
1917
|
+
media.settled=true;
|
|
1918
|
+
updateMarkdownPreviewStatus(preview);
|
|
1919
|
+
},
|
|
1920
|
+
function markdownImagesFailed(error){
|
|
1921
|
+
media.settled=true;
|
|
1922
|
+
media.error=error;
|
|
1923
|
+
if(isCurrentPreview(preview)){
|
|
1924
|
+
arcaneLogging.error('Unable to load local Markdown images:',error);
|
|
1925
|
+
updateMarkdownPreviewStatus(preview);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
);
|
|
1929
|
+
body.append(template.content);
|
|
1824
1930
|
}else{
|
|
1825
1931
|
body.title=descriptor.title||'HTML preview';
|
|
1826
1932
|
body.srcdoc=descriptor.content;
|
|
@@ -219,7 +219,10 @@
|
|
|
219
219
|
const host=this;
|
|
220
220
|
host.ready=false;
|
|
221
221
|
|
|
222
|
-
const {default:MD}=await
|
|
222
|
+
const [{default:MD},{hydrateMarkdownMedia}]=await Promise.all([
|
|
223
|
+
import('../modules/MD.js'),
|
|
224
|
+
import('../modules/MarkdownMedia.js')
|
|
225
|
+
]);
|
|
223
226
|
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
224
227
|
const events=createArcaneEventSource(host,{source:'arcane.component.markdown-document',eventTypes:['markdown-document-state','markdown-document-empty','markdown-document-error','markdown-document-loading','markdown-document-rendered','markdown-document-navigate','markdown-document-ready']});
|
|
225
228
|
const lifecycleController=new AbortController();
|
|
@@ -331,6 +334,7 @@
|
|
|
331
334
|
content:'Document content',
|
|
332
335
|
empty:'No document content is available.',
|
|
333
336
|
error:'Unable to display this document.',
|
|
337
|
+
mediaError:'Some images could not be displayed.',
|
|
334
338
|
loading:'Loading document…',
|
|
335
339
|
tableOfContents:'On this page'
|
|
336
340
|
},
|
|
@@ -348,6 +352,8 @@
|
|
|
348
352
|
let operationSequence=0;
|
|
349
353
|
let loadController=null;
|
|
350
354
|
let destroyed=false;
|
|
355
|
+
let renderedMedia=null;
|
|
356
|
+
let mediaError=false;
|
|
351
357
|
|
|
352
358
|
host.clear=clear;
|
|
353
359
|
host.configure=configure;
|
|
@@ -393,7 +399,7 @@
|
|
|
393
399
|
}
|
|
394
400
|
|
|
395
401
|
const labels={...previous.labels};
|
|
396
|
-
for(const name of ['content','empty','error','loading','tableOfContents']){
|
|
402
|
+
for(const name of ['content','empty','error','mediaError','loading','tableOfContents']){
|
|
397
403
|
if(labelInput[name]!==undefined){
|
|
398
404
|
if(!is.string(labelInput[name])||!labelInput[name].trim()){
|
|
399
405
|
throw new TypeError(`Markdown document labels.${name} must be a nonempty string.`);
|
|
@@ -607,6 +613,7 @@
|
|
|
607
613
|
|
|
608
614
|
currentMarkdown=markdown;
|
|
609
615
|
currentSourceURL=sourceURL;
|
|
616
|
+
releaseRenderedMedia();
|
|
610
617
|
contentElement.replaceChildren();
|
|
611
618
|
tableOfContents=[];
|
|
612
619
|
tableOfContentsList.replaceChildren();
|
|
@@ -631,6 +638,16 @@
|
|
|
631
638
|
|
|
632
639
|
tableOfContents=decorateHeadings(fragment);
|
|
633
640
|
decorateTables(fragment);
|
|
641
|
+
// Resolve local references before connecting their IMG elements. The
|
|
642
|
+
// complete Markdown and ordinary external URLs remain unchanged.
|
|
643
|
+
const media=hydrateMarkdownMedia(fragment,{signal:lifecycleController.signal});
|
|
644
|
+
renderedMedia=media;
|
|
645
|
+
media.ready.catch(function reportMarkdownMediaFailure(error){
|
|
646
|
+
if(destroyed||renderedMedia!==media) return;
|
|
647
|
+
mediaError=true;
|
|
648
|
+
arcaneLogging.error('Markdown document image display failed.',error);
|
|
649
|
+
updateStateDOM();
|
|
650
|
+
});
|
|
634
651
|
contentElement.replaceChildren(fragment);
|
|
635
652
|
buildTableOfContents(tableOfContents);
|
|
636
653
|
transition(
|
|
@@ -644,6 +661,13 @@
|
|
|
644
661
|
return true;
|
|
645
662
|
}
|
|
646
663
|
|
|
664
|
+
function releaseRenderedMedia(){
|
|
665
|
+
const previous=renderedMedia;
|
|
666
|
+
renderedMedia=null;
|
|
667
|
+
mediaError=false;
|
|
668
|
+
previous?.destroy();
|
|
669
|
+
}
|
|
670
|
+
|
|
647
671
|
function renderedMarkdownFragment(html='',sourceURL=options.sourceURL){
|
|
648
672
|
const template=document.createElement('template');
|
|
649
673
|
template.innerHTML=html;
|
|
@@ -764,6 +788,7 @@
|
|
|
764
788
|
const operationId=`${events.descriptor.instanceId}:clear:${++operationSequence}`;
|
|
765
789
|
currentMarkdown='';
|
|
766
790
|
currentSourceURL=options.sourceURL;
|
|
791
|
+
releaseRenderedMedia();
|
|
767
792
|
contentElement.replaceChildren();
|
|
768
793
|
tableOfContents=[];
|
|
769
794
|
tableOfContentsList.replaceChildren();
|
|
@@ -781,6 +806,7 @@
|
|
|
781
806
|
|
|
782
807
|
function showError(error,operationId=null){
|
|
783
808
|
currentMarkdown='';
|
|
809
|
+
releaseRenderedMedia();
|
|
784
810
|
contentElement.replaceChildren();
|
|
785
811
|
tableOfContents=[];
|
|
786
812
|
tableOfContentsList.replaceChildren();
|
|
@@ -839,7 +865,7 @@
|
|
|
839
865
|
host.setAttribute('aria-busy',String(loadingState));
|
|
840
866
|
host.dataset.state=state;
|
|
841
867
|
statusElement.dataset.state=state;
|
|
842
|
-
statusElement.hidden=readyState;
|
|
868
|
+
statusElement.hidden=readyState&&!mediaError;
|
|
843
869
|
contentElement.hidden=!readyState;
|
|
844
870
|
tableOfContentsElement.hidden=(
|
|
845
871
|
!readyState
|
|
@@ -851,7 +877,7 @@
|
|
|
851
877
|
empty:options.labels.empty,
|
|
852
878
|
error:options.labels.error,
|
|
853
879
|
loading:options.labels.loading,
|
|
854
|
-
ready:''
|
|
880
|
+
ready:mediaError?options.labels.mediaError:''
|
|
855
881
|
}[state]||'';
|
|
856
882
|
}
|
|
857
883
|
|
|
@@ -948,6 +974,7 @@
|
|
|
948
974
|
lifecycleController.abort('markdown-document-destroyed');
|
|
949
975
|
currentMarkdown='';
|
|
950
976
|
tableOfContents=[];
|
|
977
|
+
releaseRenderedMedia();
|
|
951
978
|
contentElement.replaceChildren();
|
|
952
979
|
tableOfContentsList.replaceChildren();
|
|
953
980
|
events.dispose();
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
margin-right:auto;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
.media-status:empty{
|
|
67
|
+
display:none;
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
.markdown-editor.fit{
|
|
67
71
|
height:100%;
|
|
68
72
|
min-height:0;
|
|
@@ -118,6 +122,7 @@
|
|
|
118
122
|
<textarea id="entryMarkdown" class="entry-markdown" part="input"></textarea>
|
|
119
123
|
<section class="actions" part="actions">
|
|
120
124
|
<span id="status" class="status" part="status" aria-live="polite"></span>
|
|
125
|
+
<span id="mediaStatus" class="media-status" part="media-status" aria-live="polite"></span>
|
|
121
126
|
<button id="print" part="print-action" type="button">Print</button>
|
|
122
127
|
<button id="save" part="save-action" type="button" disabled></button>
|
|
123
128
|
</section>
|
|
@@ -130,7 +135,10 @@
|
|
|
130
135
|
const {arcaneLogging}=await import('arcane-os/logging');
|
|
131
136
|
|
|
132
137
|
const host=this;
|
|
133
|
-
const {default:MD}=await
|
|
138
|
+
const [{default:MD},{hydrateMarkdownMedia}]=await Promise.all([
|
|
139
|
+
import('../modules/MD.js'),
|
|
140
|
+
import('../modules/MarkdownMedia.js')
|
|
141
|
+
]);
|
|
134
142
|
const {
|
|
135
143
|
applyMarkdownFormat,
|
|
136
144
|
normalizeMarkdownOptions
|
|
@@ -146,9 +154,12 @@
|
|
|
146
154
|
const saveButton=host.shadowRoot.querySelector('#save');
|
|
147
155
|
const printButton=host.shadowRoot.querySelector('#print');
|
|
148
156
|
const status=host.shadowRoot.querySelector('#status');
|
|
157
|
+
const mediaStatus=host.shadowRoot.querySelector('#mediaStatus');
|
|
149
158
|
|
|
150
159
|
let previewFrame=0;
|
|
151
160
|
let previewFollowPending=false;
|
|
161
|
+
let previewMedia=null;
|
|
162
|
+
let printMediaRetains=null;
|
|
152
163
|
let operationSequence=0;
|
|
153
164
|
let saveGeneration=0;
|
|
154
165
|
let saveController=null;
|
|
@@ -166,6 +177,8 @@
|
|
|
166
177
|
title:()=>titleInput.value,
|
|
167
178
|
active:()=>!destroyed&&host.isConnected&&host.getClientRects().length>0,
|
|
168
179
|
signal:lifecycleController.signal,
|
|
180
|
+
retain:retainPrintMedia,
|
|
181
|
+
prepare:preparePrintMedia,
|
|
169
182
|
onError:reportPrintFailure
|
|
170
183
|
});
|
|
171
184
|
}
|
|
@@ -401,7 +414,41 @@
|
|
|
401
414
|
const scrollTop=preview.scrollTop;
|
|
402
415
|
const scrollLeft=preview.scrollLeft;
|
|
403
416
|
const markdown=new MD(textarea.value,{sourceMap:options.followPreview});
|
|
404
|
-
|
|
417
|
+
const template=document.createElement('template');
|
|
418
|
+
template.innerHTML=markdown.safeRendered;
|
|
419
|
+
const pending=[...template.content.querySelectorAll('img')].some(
|
|
420
|
+
image=>image.getAttribute('src')?.startsWith('arcane-media:')
|
|
421
|
+
);
|
|
422
|
+
// Hydration removes custom src values before these nodes join the live DOM.
|
|
423
|
+
const handle=hydrateMarkdownMedia(template.content,{signal:lifecycleController.signal});
|
|
424
|
+
const media={handle,pending,error:null};
|
|
425
|
+
const previous=previewMedia;
|
|
426
|
+
previewMedia=media;
|
|
427
|
+
if(printMediaRetains){
|
|
428
|
+
// The print owner may still be awaiting its final current snapshot.
|
|
429
|
+
printMediaRetains.add(handle.retain());
|
|
430
|
+
}
|
|
431
|
+
previous?.handle.destroy();
|
|
432
|
+
mediaStatus.innerText=pending?'Loading saved images…':'';
|
|
433
|
+
media.ready=handle.ready.then(function mediaReady(){
|
|
434
|
+
media.pending=false;
|
|
435
|
+
if(!destroyed&&previewMedia===media){
|
|
436
|
+
mediaStatus.innerText='';
|
|
437
|
+
}
|
|
438
|
+
},function mediaFailed(error){
|
|
439
|
+
media.pending=false;
|
|
440
|
+
media.error=error;
|
|
441
|
+
const cancelled=error?.name==='AbortError'||error?.failures?.every(
|
|
442
|
+
failure=>failure.reason?.name==='AbortError'
|
|
443
|
+
);
|
|
444
|
+
if(!cancelled){
|
|
445
|
+
arcaneLogging.error('Unable to display saved Markdown images:',error);
|
|
446
|
+
}
|
|
447
|
+
if(!destroyed&&previewMedia===media&&!cancelled){
|
|
448
|
+
mediaStatus.innerText='Some saved images could not be displayed.';
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
preview.replaceChildren(template.content);
|
|
405
452
|
preview.querySelectorAll('a').forEach(
|
|
406
453
|
link=>{
|
|
407
454
|
link.target='_blank';
|
|
@@ -589,9 +636,38 @@
|
|
|
589
636
|
}
|
|
590
637
|
|
|
591
638
|
function renderedPrintContent(){
|
|
639
|
+
if(previewFrame){
|
|
640
|
+
updatePreview(false);
|
|
641
|
+
}
|
|
642
|
+
if(previewMedia?.pending){
|
|
643
|
+
const error=new Error('Images are still loading. Print again when they finish.');
|
|
644
|
+
error.code='ARCANE_MARKDOWN_MEDIA_PENDING';
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
if(previewMedia?.error){
|
|
648
|
+
throw previewMedia.error;
|
|
649
|
+
}
|
|
592
650
|
return preview;
|
|
593
651
|
}
|
|
594
652
|
|
|
653
|
+
function retainPrintMedia(){
|
|
654
|
+
renderedPrintContent();
|
|
655
|
+
const releases=new Set();
|
|
656
|
+
if(previewMedia){
|
|
657
|
+
releases.add(previewMedia.handle.retain());
|
|
658
|
+
}
|
|
659
|
+
printMediaRetains=releases;
|
|
660
|
+
return function releasePrintMedia(){
|
|
661
|
+
if(printMediaRetains===releases){
|
|
662
|
+
printMediaRetains=null;
|
|
663
|
+
}
|
|
664
|
+
for(const release of releases){
|
|
665
|
+
release();
|
|
666
|
+
}
|
|
667
|
+
releases.clear();
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
595
671
|
async function print(){
|
|
596
672
|
if(destroyed){
|
|
597
673
|
return false;
|
|
@@ -600,12 +676,40 @@
|
|
|
600
676
|
if(destroyed||!printView){
|
|
601
677
|
return false;
|
|
602
678
|
}
|
|
603
|
-
if(
|
|
604
|
-
|
|
679
|
+
if(!await preparePrintMedia()){
|
|
680
|
+
return false;
|
|
605
681
|
}
|
|
606
682
|
return printView.print();
|
|
607
683
|
}
|
|
608
684
|
|
|
685
|
+
async function preparePrintMedia(signal){
|
|
686
|
+
let cancel;
|
|
687
|
+
const cancelled=new Promise(function waitForCancellation(resolve){cancel=resolve;});
|
|
688
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
689
|
+
try{
|
|
690
|
+
while(!destroyed&&!signal?.aborted){
|
|
691
|
+
if(previewFrame){
|
|
692
|
+
updatePreview(false);
|
|
693
|
+
}
|
|
694
|
+
const media=previewMedia;
|
|
695
|
+
await Promise.race([media.ready,cancelled]);
|
|
696
|
+
if(destroyed||signal?.aborted){
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if(previewFrame||previewMedia!==media){
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if(media.error){
|
|
703
|
+
throw media.error;
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
}finally{
|
|
709
|
+
signal?.removeEventListener('abort',cancel);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
609
713
|
function printFromControl(){
|
|
610
714
|
void print().catch(reportPrintFailure);
|
|
611
715
|
}
|
|
@@ -615,6 +719,10 @@
|
|
|
615
719
|
return;
|
|
616
720
|
}
|
|
617
721
|
arcaneLogging.error('Unable to print rendered Markdown:',error);
|
|
722
|
+
if(error?.code==='ARCANE_MARKDOWN_MEDIA_PENDING'){
|
|
723
|
+
mediaStatus.innerText=error.message;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
618
726
|
status.innerText='Unable to open print preview. Please try again.';
|
|
619
727
|
}
|
|
620
728
|
|
|
@@ -686,6 +794,7 @@
|
|
|
686
794
|
}
|
|
687
795
|
destroyed=true;
|
|
688
796
|
supersedeSave('markdown-editor-destroyed');
|
|
797
|
+
previewMedia?.handle.destroy();
|
|
689
798
|
lifecycleController.abort('markdown-editor-destroyed');
|
|
690
799
|
previewFollowPending=false;
|
|
691
800
|
if(previewFrame){
|
|
@@ -391,10 +391,12 @@ class ChatEntity{
|
|
|
391
391
|
fileName='';
|
|
392
392
|
|
|
393
393
|
/**
|
|
394
|
-
* Controls
|
|
394
|
+
* Controls retention of new message additions.
|
|
395
395
|
*
|
|
396
396
|
* When true, any message addition automatically writes
|
|
397
|
-
* the updated message list to OPFS.
|
|
397
|
+
* the updated message list to OPFS. When false, new additions are excluded
|
|
398
|
+
* from the transcript, recurring context, storage, and memory extraction.
|
|
399
|
+
* Existing retained and saved messages remain unchanged.
|
|
398
400
|
*
|
|
399
401
|
* @type {boolean}
|
|
400
402
|
*/
|
|
@@ -1080,7 +1082,7 @@ ${JSON.stringify(transcript)}`
|
|
|
1080
1082
|
if(!records.length){
|
|
1081
1083
|
return false;
|
|
1082
1084
|
}
|
|
1083
|
-
if(!persist) return false;
|
|
1085
|
+
if(!persist||!this.persist) return false;
|
|
1084
1086
|
|
|
1085
1087
|
this.#messages.push(...records);
|
|
1086
1088
|
const durableRecords=storedChatRecords(records);
|
|
@@ -1088,7 +1090,7 @@ ${JSON.stringify(transcript)}`
|
|
|
1088
1090
|
this.#saved=false;
|
|
1089
1091
|
}
|
|
1090
1092
|
|
|
1091
|
-
if(!
|
|
1093
|
+
if(!durableRecords.length){
|
|
1092
1094
|
return;
|
|
1093
1095
|
}
|
|
1094
1096
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
const referencePrefix = 'arcane-media:';
|
|
5
|
+
|
|
6
|
+
/** Return the local record address, or null for an ordinary image URL. */
|
|
7
|
+
export function parseMarkdownMediaReference(reference) {
|
|
8
|
+
if (!is.string(reference) || !reference.startsWith(referencePrefix)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const address = reference.substring(referencePrefix.length);
|
|
12
|
+
const separator = address.indexOf('/');
|
|
13
|
+
if (separator < 0) {
|
|
14
|
+
throw new TypeError('A Markdown media reference needs a table and filename.');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
tableName: decodeURIComponent(address.substring(0, separator)),
|
|
18
|
+
fileName: decodeURIComponent(address.substring(separator + 1))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save a complete image in the app's existing DBOPFS database. The returned
|
|
24
|
+
* reference belongs in Markdown; the JSON-compatible record belongs in its
|
|
25
|
+
* separate table so the existing database backup can include it unchanged.
|
|
26
|
+
* Applications own filename reuse, entry association, and export selection.
|
|
27
|
+
* The promise settles the durable write; this operation has no abort option.
|
|
28
|
+
*/
|
|
29
|
+
export async function saveMarkdownMedia({
|
|
30
|
+
blob,
|
|
31
|
+
tableName = 'markdown-media',
|
|
32
|
+
fileName = `${crypto.randomUUID()}.json`
|
|
33
|
+
}) {
|
|
34
|
+
const reference = `${referencePrefix}${encodeURIComponent(tableName)}/${encodeURIComponent(fileName)}`;
|
|
35
|
+
const encoding = new Promise(function encodeImage(resolve, reject) {
|
|
36
|
+
const reader = new FileReader();
|
|
37
|
+
reader.onload = function imageEncoded() {
|
|
38
|
+
resolve(reader.result);
|
|
39
|
+
};
|
|
40
|
+
reader.onerror = function imageEncodingFailed() {
|
|
41
|
+
reject(reader.error || new Error('Image encoding failed.'));
|
|
42
|
+
};
|
|
43
|
+
reader.onabort = function imageEncodingAborted() {
|
|
44
|
+
reject(new DOMException('Image encoding was aborted.', 'AbortError'));
|
|
45
|
+
};
|
|
46
|
+
reader.readAsDataURL(blob);
|
|
47
|
+
});
|
|
48
|
+
const [dataUrl, {default: DBOPFS}] = await Promise.all([
|
|
49
|
+
encoding,
|
|
50
|
+
import('./DBOPFS.js')
|
|
51
|
+
]);
|
|
52
|
+
const mediaType = blob.type || 'application/octet-stream';
|
|
53
|
+
await new DBOPFS().set(tableName, fileName, {mediaType, dataUrl});
|
|
54
|
+
return {
|
|
55
|
+
reference,
|
|
56
|
+
tableName,
|
|
57
|
+
fileName,
|
|
58
|
+
mediaType
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read the complete local image for display or application-owned export. */
|
|
63
|
+
export async function readMarkdownMedia(reference) {
|
|
64
|
+
const address = parseMarkdownMediaReference(reference);
|
|
65
|
+
if (!address) {
|
|
66
|
+
throw new TypeError('Expected an arcane-media: reference.');
|
|
67
|
+
}
|
|
68
|
+
const {default: DBOPFS} = await import('./DBOPFS.js');
|
|
69
|
+
const stored = await new DBOPFS().get(address.tableName, address.fileName);
|
|
70
|
+
if (stored === null || stored === undefined) {
|
|
71
|
+
throw new DOMException(`Markdown image was not found: ${reference}`, 'NotFoundError');
|
|
72
|
+
}
|
|
73
|
+
// DBOPFS parses JSONL/NDJSON as record arrays; restoring a parsed backup can
|
|
74
|
+
// wrap its one record again. Never choose one image from multiple records.
|
|
75
|
+
let record = is.string(stored) ? JSON.parse(stored) : stored;
|
|
76
|
+
while (is.array(record) && record.length === 1) record = record[0];
|
|
77
|
+
const encoded = /^data:[^,]*;base64,([\s\S]*)$/u.exec(record?.dataUrl);
|
|
78
|
+
if (!encoded) {
|
|
79
|
+
throw new TypeError(`Markdown image has an unreadable data URL: ${reference}`);
|
|
80
|
+
}
|
|
81
|
+
// Decode locally; a stored string is never passed to Fetch or a network API.
|
|
82
|
+
const decoded = atob(encoded[1]);
|
|
83
|
+
const content = Uint8Array.from(decoded, function imageCharacter(character) {
|
|
84
|
+
return character.charCodeAt(0);
|
|
85
|
+
});
|
|
86
|
+
return new Blob([content], {type: record.mediaType});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve only local Markdown IMG references beneath a rendered root (including
|
|
91
|
+
* an IMG root). Markdown source and ordinary URLs are never changed. Independent
|
|
92
|
+
* reads start together; ready waits for decoding or display cancellation.
|
|
93
|
+
* Failures reject with AggregateError.failures [{image, reference, reason}];
|
|
94
|
+
* successful siblings remain visible until their owner is destroyed.
|
|
95
|
+
*
|
|
96
|
+
* destroy() prevents late assignments and releases owned object URLs. A print
|
|
97
|
+
* owner calls retain() before copying the rendered DOM and releases its returned
|
|
98
|
+
* idempotent callback after printing, allowing URLs to outlive the source view.
|
|
99
|
+
* Abort promptly rejects display readiness. An already-started DBOPFS read
|
|
100
|
+
* remains observed and settles independently; its late result is not displayed.
|
|
101
|
+
*/
|
|
102
|
+
export function hydrateMarkdownMedia(root, {signal} = {}) {
|
|
103
|
+
const images = [...root.querySelectorAll('img')];
|
|
104
|
+
if (root.localName === 'img') images.unshift(root);
|
|
105
|
+
const owned = [];
|
|
106
|
+
const displayCancellations = new Set();
|
|
107
|
+
let destroyed = false;
|
|
108
|
+
let retainers = 0;
|
|
109
|
+
let destructionReason;
|
|
110
|
+
|
|
111
|
+
function releaseURLs() {
|
|
112
|
+
if (!destroyed || retainers > 0) return;
|
|
113
|
+
for (const {image, url} of owned) {
|
|
114
|
+
if (image.getAttribute('src') === url) image.removeAttribute('src');
|
|
115
|
+
URL.revokeObjectURL(url);
|
|
116
|
+
}
|
|
117
|
+
owned.length = 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function destroy() {
|
|
121
|
+
if (destroyed) return;
|
|
122
|
+
destroyed = true;
|
|
123
|
+
destructionReason = signal?.aborted
|
|
124
|
+
? signal.reason
|
|
125
|
+
: new DOMException('Markdown media display was destroyed.', 'AbortError');
|
|
126
|
+
signal?.removeEventListener('abort', destroy);
|
|
127
|
+
for (const cancel of displayCancellations) cancel();
|
|
128
|
+
displayCancellations.clear();
|
|
129
|
+
releaseURLs();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function retain() {
|
|
133
|
+
if (destroyed) throw destructionReason;
|
|
134
|
+
retainers += 1;
|
|
135
|
+
let released = false;
|
|
136
|
+
return function releaseMarkdownMedia() {
|
|
137
|
+
if (released) return;
|
|
138
|
+
released = true;
|
|
139
|
+
retainers -= 1;
|
|
140
|
+
releaseURLs();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function waitForDisplay(operation) {
|
|
145
|
+
return new Promise(function observeDisplayOperation(resolve, reject) {
|
|
146
|
+
let settled = false;
|
|
147
|
+
function settle(callback, value) {
|
|
148
|
+
if (settled) return;
|
|
149
|
+
settled = true;
|
|
150
|
+
displayCancellations.delete(cancelDisplay);
|
|
151
|
+
callback(value);
|
|
152
|
+
}
|
|
153
|
+
function cancelDisplay() {
|
|
154
|
+
settle(reject, destructionReason);
|
|
155
|
+
}
|
|
156
|
+
displayCancellations.add(cancelDisplay);
|
|
157
|
+
// Both outcomes remain observed after display cancellation; this
|
|
158
|
+
// wait does not claim to cancel the underlying storage or decode.
|
|
159
|
+
Promise.resolve(operation).then(
|
|
160
|
+
function displayOperationFinished(value) {settle(resolve, value);},
|
|
161
|
+
function displayOperationFailed(error) {settle(reject, error);}
|
|
162
|
+
);
|
|
163
|
+
if (destroyed) cancelDisplay();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadImage(image, reference) {
|
|
168
|
+
if (destroyed) throw destructionReason;
|
|
169
|
+
const blob = await waitForDisplay(readMarkdownMedia(reference));
|
|
170
|
+
if (destroyed) throw destructionReason;
|
|
171
|
+
const url = URL.createObjectURL(blob);
|
|
172
|
+
owned.push({image, url});
|
|
173
|
+
image.setAttribute('src', url);
|
|
174
|
+
await waitForDisplay(image.decode());
|
|
175
|
+
if (destroyed) throw destructionReason;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
signal?.addEventListener('abort', destroy, {once: true});
|
|
179
|
+
if (signal?.aborted) destroy();
|
|
180
|
+
const pending = [];
|
|
181
|
+
for (const image of images) {
|
|
182
|
+
const reference = image.getAttribute('src');
|
|
183
|
+
if (!is.string(reference) || !reference.startsWith(referencePrefix)) continue;
|
|
184
|
+
// Remove the custom scheme before the first asynchronous storage wait.
|
|
185
|
+
if (!destroyed) image.removeAttribute('src');
|
|
186
|
+
pending.push({image, reference, operation: loadImage(image, reference)});
|
|
187
|
+
}
|
|
188
|
+
const ready = Promise.allSettled(pending.map(function imageOperation(item) {
|
|
189
|
+
return item.operation;
|
|
190
|
+
})).then(function imagesSettled(results) {
|
|
191
|
+
const failures = [];
|
|
192
|
+
for (const [index, result] of results.entries()) {
|
|
193
|
+
if (result.status === 'rejected') {
|
|
194
|
+
failures.push({
|
|
195
|
+
image: pending[index].image,
|
|
196
|
+
reference: pending[index].reference,
|
|
197
|
+
reason: result.reason
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (failures.length > 0) {
|
|
202
|
+
const error = new AggregateError(failures.map(function imageFailure(failure) {
|
|
203
|
+
return failure.reason;
|
|
204
|
+
}), 'Some local Markdown images could not be displayed.');
|
|
205
|
+
error.failures = failures;
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
if (destroyed) throw destructionReason;
|
|
209
|
+
});
|
|
210
|
+
return {ready, destroy, retain};
|
|
211
|
+
}
|
|
@@ -556,6 +556,7 @@ class PersistentAIChatSession{
|
|
|
556
556
|
/** Persists one model-authored opening while retaining none of its bootstrap request. */
|
|
557
557
|
async open(input){
|
|
558
558
|
const settings=normalizeOpening(input);
|
|
559
|
+
const retainOpening=this.#entity.persist===true;
|
|
559
560
|
const name=this.aiName;
|
|
560
561
|
if(this.#pending){
|
|
561
562
|
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
@@ -565,7 +566,7 @@ class PersistentAIChatSession{
|
|
|
565
566
|
try{
|
|
566
567
|
await this.ready();
|
|
567
568
|
if(this.#historyError)throw this.#historyError;
|
|
568
|
-
if(this.#entity.persist!==true){
|
|
569
|
+
if(!retainOpening||this.#entity.persist!==true){
|
|
569
570
|
throw coded(
|
|
570
571
|
new Error('The model-authored chat opening requires durable chat persistence.'),
|
|
571
572
|
'AI_CHAT_PERSISTENCE_UNAVAILABLE',
|
|
@@ -576,6 +577,15 @@ class PersistentAIChatSession{
|
|
|
576
577
|
settings.message,
|
|
577
578
|
{request:settings.request,signal:settings.signal},
|
|
578
579
|
);
|
|
580
|
+
if(this.#entity.persist!==true){
|
|
581
|
+
throw coded(
|
|
582
|
+
new Error(
|
|
583
|
+
'The model-authored chat opening requires durable chat persistence.',
|
|
584
|
+
{cause:prepared.response},
|
|
585
|
+
),
|
|
586
|
+
'AI_CHAT_PERSISTENCE_UNAVAILABLE',
|
|
587
|
+
);
|
|
588
|
+
}
|
|
579
589
|
await this.#entity.addAIMessage(
|
|
580
590
|
prepared.response.message.content,
|
|
581
591
|
{extractMemory:false,persist:true,name},
|
|
@@ -597,6 +607,7 @@ class PersistentAIChatSession{
|
|
|
597
607
|
|
|
598
608
|
async #requestTurn(input,streamHandlers=null){
|
|
599
609
|
const settings=normalizeSend(input);
|
|
610
|
+
const retainTurn=settings.messagePersist&&Boolean(this.#entity.persist);
|
|
600
611
|
const name=this.aiName;
|
|
601
612
|
if(this.#pending){
|
|
602
613
|
throw coded(new Error('A chat request is already active for this session.'),'AI_CHAT_BUSY');
|
|
@@ -621,7 +632,7 @@ class PersistentAIChatSession{
|
|
|
621
632
|
if(persistence===undefined||submittedIds.has(message.tool_call_id)){
|
|
622
633
|
throw coded(new TypeError('A tool message has no unique pending structural tool call.'),'AI_CHAT_INVALID_TOOL_MESSAGE');
|
|
623
634
|
}
|
|
624
|
-
if(persistence!==settings.messagePersist){
|
|
635
|
+
if(retainTurn&&persistence!==settings.messagePersist){
|
|
625
636
|
throw coded(
|
|
626
637
|
new TypeError('A tool result must use the persistence of its assistant tool call.'),
|
|
627
638
|
'AI_CHAT_INCOHERENT_PERSISTENCE',
|
|
@@ -674,10 +685,10 @@ class PersistentAIChatSession{
|
|
|
674
685
|
);
|
|
675
686
|
}
|
|
676
687
|
}
|
|
677
|
-
if(!
|
|
688
|
+
if(!retainTurn||!this.#entity.persist){
|
|
678
689
|
prepared.rollback();
|
|
679
690
|
prepared=null;
|
|
680
|
-
return assistantDisplayResponse(result,name);
|
|
691
|
+
return {...assistantDisplayResponse(result,name),retained:false};
|
|
681
692
|
}
|
|
682
693
|
await this.#entity.addTurn({
|
|
683
694
|
assistantMessage:result.message,
|
|
@@ -706,10 +717,13 @@ class PersistentAIChatSession{
|
|
|
706
717
|
}
|
|
707
718
|
const transcript=this.#entity.transcript;
|
|
708
719
|
const assistantRecord=transcript.at(-1);
|
|
709
|
-
return
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
return {
|
|
721
|
+
...assistantDisplayResponse(
|
|
722
|
+
committed,name,
|
|
723
|
+
assistantRecord?.role==='assistant'?assistantRecord.timestamp:undefined,
|
|
724
|
+
),
|
|
725
|
+
retained:true,
|
|
726
|
+
};
|
|
713
727
|
}catch(error){
|
|
714
728
|
prepared?.rollback();
|
|
715
729
|
throw error;
|
|
@@ -5,8 +5,9 @@ const registries=new WeakMap();
|
|
|
5
5
|
let fontSequence=0;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Print current rendered DOM.
|
|
9
|
-
*
|
|
8
|
+
* Print current rendered DOM. prepare(signal) may await explicit-print readiness;
|
|
9
|
+
* other callbacks are synchronous. retain() may return a resource release function.
|
|
10
|
+
* print() reports a browser request, never an output.
|
|
10
11
|
* Native beforeprint cannot await readiness or cancel the browser's dialog.
|
|
11
12
|
*/
|
|
12
13
|
export function createPrintView({
|
|
@@ -17,7 +18,8 @@ export function createPrintView({
|
|
|
17
18
|
priority=0,
|
|
18
19
|
signal,
|
|
19
20
|
onError,
|
|
20
|
-
retain
|
|
21
|
+
retain,
|
|
22
|
+
prepare
|
|
21
23
|
}){
|
|
22
24
|
if(!host?.ownerDocument||!is.function(content)||!is.function(title)
|
|
23
25
|
||!is.function(active)||!is.finite(priority)){
|
|
@@ -27,8 +29,11 @@ export function createPrintView({
|
|
|
27
29
|
||(retain!==undefined&&!is.function(retain))){
|
|
28
30
|
throw new TypeError('Print error and resource-retention callbacks must be functions.');
|
|
29
31
|
}
|
|
32
|
+
if(prepare!==undefined&&!is.function(prepare)){
|
|
33
|
+
throw new TypeError('A print preparation callback must be a function.');
|
|
34
|
+
}
|
|
30
35
|
const registry=documentRegistry(host.ownerDocument);
|
|
31
|
-
const view={host,content,title,active,priority,onError,retain,destroyed:false,pending:null};
|
|
36
|
+
const view={host,content,title,active,priority,onError,retain,prepare,destroyed:false,pending:null};
|
|
32
37
|
registry.views.add(view);
|
|
33
38
|
signal?.addEventListener('abort',destroy,{once:true});
|
|
34
39
|
if(signal?.aborted){
|
|
@@ -98,6 +103,10 @@ function documentRegistry(document){
|
|
|
98
103
|
}
|
|
99
104
|
const session=registry.session||startSession(registry,view);
|
|
100
105
|
if(!session.stage){
|
|
106
|
+
if(view.prepare){
|
|
107
|
+
session.content=readContent(view);
|
|
108
|
+
session.title=readTitle(view);
|
|
109
|
+
}
|
|
101
110
|
capture(registry,session);
|
|
102
111
|
}
|
|
103
112
|
session.requested=true;
|
|
@@ -153,14 +162,8 @@ function startSession(registry,view){
|
|
|
153
162
|
if(session.release&&!is.function(session.release)){
|
|
154
163
|
throw new TypeError('A print resource-retention callback must return a release function.');
|
|
155
164
|
}
|
|
156
|
-
session.content=view
|
|
157
|
-
|
|
158
|
-
throw new TypeError('Print content must be a rendered DOM Node.');
|
|
159
|
-
}
|
|
160
|
-
session.title=view.title();
|
|
161
|
-
if(!is.string(session.title)){
|
|
162
|
-
throw new TypeError('A print title must be a string.');
|
|
163
|
-
}
|
|
165
|
+
session.content=readContent(view);
|
|
166
|
+
session.title=readTitle(view);
|
|
164
167
|
return session;
|
|
165
168
|
}catch(error){
|
|
166
169
|
finishSession(registry,session);
|
|
@@ -168,6 +171,22 @@ function startSession(registry,view){
|
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
function readContent(view){
|
|
175
|
+
const content=view.content();
|
|
176
|
+
if(!content||!is.function(content.cloneNode)){
|
|
177
|
+
throw new TypeError('Print content must be a rendered DOM Node.');
|
|
178
|
+
}
|
|
179
|
+
return content;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readTitle(view){
|
|
183
|
+
const title=view.title();
|
|
184
|
+
if(!is.string(title)){
|
|
185
|
+
throw new TypeError('A print title must be a string.');
|
|
186
|
+
}
|
|
187
|
+
return title;
|
|
188
|
+
}
|
|
189
|
+
|
|
171
190
|
async function printRendered(registry,view){
|
|
172
191
|
if(registry.session){
|
|
173
192
|
throw new Error('A rendered print request is already being prepared or displayed.');
|
|
@@ -186,6 +205,23 @@ async function printRendered(registry,view){
|
|
|
186
205
|
finishSession(registry,session);
|
|
187
206
|
return false;
|
|
188
207
|
}
|
|
208
|
+
if(view.prepare){
|
|
209
|
+
await abortable(view.prepare(signal),signal);
|
|
210
|
+
if(session.error){
|
|
211
|
+
throw session.error;
|
|
212
|
+
}
|
|
213
|
+
if(session.requested){
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if(signal.aborted||!available(view)){
|
|
217
|
+
finishSession(registry,session);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
// Read and capture together: a changing view can reject pending
|
|
221
|
+
// content synchronously without an unobserved readiness gap.
|
|
222
|
+
session.content=readContent(view);
|
|
223
|
+
session.title=readTitle(view);
|
|
224
|
+
}
|
|
189
225
|
capture(registry,session);
|
|
190
226
|
const images=Array.from(session.stage.shadowRoot.querySelectorAll('img'));
|
|
191
227
|
await Promise.all(images.map(function readyImage(image){
|
|
@@ -224,6 +260,9 @@ async function printRendered(registry,view){
|
|
|
224
260
|
if(cancelled){
|
|
225
261
|
return false;
|
|
226
262
|
}
|
|
263
|
+
if(view.prepare&&session.error){
|
|
264
|
+
throw session.error;
|
|
265
|
+
}
|
|
227
266
|
throw error;
|
|
228
267
|
}
|
|
229
268
|
}
|