arcane-os 0.36.1 → 0.38.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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.0
4
+
5
+ - Add `User.AI_name` as an application-owned complete string with an empty-string
6
+ default, ordinary getter/setter, profile loading and `updateExplicit` support.
7
+ Preserve older saved profiles and restore the hydrated baseline after a failed
8
+ save; no existing history or provider/model selection is rewritten.
9
+ - Distinguish saved conversation speakers in file previews using the shared
10
+ action/action-text colors for user cards and surface/text colors for assistant
11
+ cards. Assistant headings use the complete saved nonblank string name or `AI`,
12
+ without a role suffix. Preserve other role labels, timestamps, status, complete
13
+ content and persistence. Capturing names on new Chat turns is separate work.
14
+
15
+ ## 0.37.0
16
+
17
+ - Add opt-in `markdown-editor` fit layout and preview following through
18
+ `configure({fit:true, followPreview:true})` or `data-fit` and
19
+ `data-follow-preview`. Both options default to false. Fit mode keeps the
20
+ title, formatting controls, input, preview and save/status actions visible
21
+ in a bounded responsive layout with independent input and preview scrolling.
22
+ - Follow body edits and formatting at their rendered Markdown block without
23
+ moving the input or containing page. Title edits and manual preview reading
24
+ preserve the reader's position. Expose named CSS parts for application styling.
25
+ - Add `new MD(raw,{sourceMap:true})` with ordered original UTF-16 source ranges
26
+ and rendered comment anchors. Preserve complete raw content, ordinary default
27
+ rendering, formatting, save payloads, cancellation and cleanup. No saved-data
28
+ migration or model/provider/default changes.
29
+
3
30
  ## 0.36.1
4
31
 
5
32
  - Infer ordinary audio MIME types from mp3, wav, ogg, oga, opus, aac, m4a,
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.36.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.38.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
 
@@ -421,7 +421,7 @@
421
421
  ],
422
422
  "availability": "Browser and supported native WebViews",
423
423
  "transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
424
- "normalization": "Selection/filter state and neutral previews normalized; storage/provider behavior mixed. Initial directoryFilter and setDirectoryFilter(fn|null) use false-only exclusion before built-in DBOPFS key/metadata reads; data-hidden-prefixes also applies to directories unless data-show-hidden is present. Existing file predicates preserve their first argument and receive optional directory/fileName/path/layout context. previewDescriptor(original File,{directory,fileName,extension,path,mimeType,signal}) may asynchronously return text/markdown/html/json content, conversation messages with role/content/real timestamp and optional name/status, collection items, or image/audio/pdf/download content as Blob/File or a lazy ({signal}) loader. Downloads require fileName; descriptors accept title and images accept alt. Lazy items load only on explicit request and retain item-local retry. Close, replacement, and destroy abort the preview signal, suppress late output, remove iframe srcdoc, stop audio, and revoke all owned object URLs. HTML preserves the exact string in iframe srcdoc; HTML/Markdown MIME fallback supports extensionless files. Nullish descriptors preserve previewTransform for JSON-family files and native previews; untransformed JSONL/NDJSON retains complete raw text, and other binary files download the original File."
424
+ "normalization": "Selection/filter state and neutral previews normalized; storage/provider behavior mixed. Initial directoryFilter and setDirectoryFilter(fn|null) use false-only exclusion before built-in DBOPFS key/metadata reads; data-hidden-prefixes also applies to directories unless data-show-hidden is present. Existing file predicates preserve their first argument and receive optional directory/fileName/path/layout context. previewDescriptor(original File,{directory,fileName,extension,path,mimeType,signal}) may asynchronously return text/markdown/html/json content, conversation messages with role/content/real timestamp and optional name/status, collection items, or image/audio/pdf/download content as Blob/File or a lazy ({signal}) loader. Assistant conversation labels use the exact nonblank string name or AI without a role suffix; other role labels retain existing name/role presentation. User and assistant cards have distinct theme backgrounds and preserve data-role. Downloads require fileName; descriptors accept title and images accept alt. Lazy items load only on explicit request and retain item-local retry. Close, replacement, and destroy abort the preview signal, suppress late output, remove iframe srcdoc, stop audio, and revoke all owned object URLs. HTML preserves the exact string in iframe srcdoc; HTML/Markdown MIME fallback supports extensionless files. Nullish descriptors preserve previewTransform for JSON-family files and native previews; untransformed JSONL/NDJSON retains complete raw text, and other binary files download the original File."
425
425
  },
426
426
  {
427
427
  "file": "runtime/arcane/components/header.html",
@@ -515,7 +515,7 @@
515
515
  {
516
516
  "file": "runtime/arcane/components/markdown-editor.html",
517
517
  "name": "markdown-editor.html",
518
- "purpose": "Configurable Markdown authoring, toolbar, preview, title, and save surface.",
518
+ "purpose": "Configurable Markdown authoring, toolbar, preview, plain-text title, and save surface with optional bounded input/preview panes and edit-position following.",
519
519
  "methods": [
520
520
  "configure()",
521
521
  "focus()",
@@ -535,7 +535,7 @@
535
535
  ],
536
536
  "availability": "Browser and supported native WebViews",
537
537
  "transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
538
- "normalization": "Editor values normalized; injected save result mixed"
538
+ "normalization": "Editor values and optional presentation behavior normalized; injected save result mixed. fit and followPreview default false, preserve omitted prior configuration, and accept explicit false resets; data-fit and data-follow-preview select the initial options. Fit requires an application-bounded host and provides independent input/preview scrolling in roomy columns or narrow stacked panes without changing the application shell. Following responds to body typing/format edits, leaves manual preview scrolling independent until the next edit, and does not treat title edits or programmatic value replacement as follow requests. Public parts editor/title/toolbar/input/preview/actions/status/save-action allow application styling while the title stays native plain text. Existing controls, complete save payload, save cancellation, and destruction behavior remain."
539
539
  },
540
540
  {
541
541
  "file": "runtime/arcane/components/media-embed.html",
@@ -19,7 +19,7 @@
19
19
  {"file":"runtime/arcane/entities/Preference.js","exports":["default","preferenceSchema"],"availability":"cross-host","normalization":"normalized"},
20
20
  {"file":"runtime/arcane/entities/TerminalSession.js","exports":["default","terminalShells"],"availability":"cross-host","normalization":"normalized"},
21
21
  {"file":"runtime/arcane/entities/Theme.js","exports":["arcaneDarkThemeTokens","arcaneLightThemeTokens","default","themeColorToHex","themeTokens"],"availability":"cross-host/dom-when-applied","normalization":"normalized/mixed"},
22
- {"file":"runtime/arcane/entities/User.js","exports":["default","profileUpdateFromSearchParams"],"availability":"browser/native-webview","normalization":"updateExplicit accepts existing partial/JSON updates or an awaited fresh-profile callback under the existing serialized write owner; callback null skips saving and returns the fresh profile, while failed writes restore the fresh baseline. Timestamp setters persist partial fields through that owner. profileUpdateFromSearchParams purely maps nonempty supplied URLSearchParams values without further decoding, trimming, or persistence."},
22
+ {"file":"runtime/arcane/entities/User.js","exports":["default","profileUpdateFromSearchParams"],"availability":"browser/native-webview","normalization":"AI_name is a persisted string preference with an empty-string default that preserves complete supplied text alongside AI_personality; loading older records does not rewrite them. updateExplicit accepts existing partial/JSON updates or an awaited fresh-profile callback under the existing serialized write owner; callback null skips saving and returns the fresh profile, while failed writes restore the fresh baseline. Timestamp setters persist partial fields through that owner. profileUpdateFromSearchParams purely maps nonempty supplied URLSearchParams values without further decoding, trimming, or persistence."},
23
23
  {"file":"runtime/arcane/entities/Weather.js","exports":["WeatherDay","WeatherLocation","WeatherObservation","WeatherSnapshot"],"availability":"cross-host","normalization":"normalized"}
24
24
  ]
25
25
  }
@@ -417,7 +417,7 @@
417
417
  "availability": "Cross-host with an injected event constructor outside DOM hosts",
418
418
  "protocol": "In-process only",
419
419
  "normalization": "Fully normalized labels, rows, definitions, visibility, formats, editor and voice options, plus capability-neutral STT activation intent and presentation state. Complete finite progress measures remain visible, including fractional and over-total values.",
420
- "surface": "Constant sets plus normalization, complete finite-progress formatting, and explicit STT activation helpers. normalizeVoiceOptions preserves manual capture and standard controls by default and accepts continuous capture timing/activity options, simple controls, showComplete, and retry/cancel labels."
420
+ "surface": "Constant sets plus normalization, complete finite-progress formatting, and explicit STT activation helpers. normalizeVoiceOptions preserves manual capture and standard controls by default and accepts continuous capture timing/activity options, simple controls, showComplete, and retry/cancel labels. normalizeMarkdownOptions adds fit and followPreview strict booleans default false, retains omitted prior values, and accepts explicit false resets without changing existing editor options."
421
421
  },
422
422
  {
423
423
  "file": "runtime/arcane/modules/ConfiguredAIChatSession.js",
@@ -885,11 +885,11 @@
885
885
  "exports": [
886
886
  "default"
887
887
  ],
888
- "summary": "Renders complete Markdown with Marked and exposes the complete rendered markup.",
888
+ "summary": "Renders complete Markdown with Marked and optionally maps source blocks to rendered comment anchors.",
889
889
  "availability": "Browser / native WebView",
890
890
  "protocol": "Marked",
891
- "normalization": "Complete raw and rendered Marked values; parse errors remain vendor-native.",
892
- "surface": "default `MD`; `raw`, `rendered`, `safeRendered`, `append()`."
891
+ "normalization": "Complete raw and rendered Marked values; parse errors remain vendor-native. Opt-in source mapping preserves original UTF-16 offsets and adds only comment anchors to complete rendered output. Lists, quotes, and tables map at top-level block granularity; invisible source or unavailable parsed anchors use the consumer's nearest available block behavior. Mapping is rebuilt on raw replacement and append, without persistence or viewport movement.",
892
+ "surface": "default MD; new MD(raw,{sourceMap:true}) enables ordered sourceMap records {start,end,marker,type}, with inclusive start/exclusive end in original UTF-16 source positions and marker equal to generated comment data. Locate anchors with a comment TreeWalker, not a CSS selector. Markers are opaque and may change after edits. sourceMap exposes the current mutable array, replaced after each successful render. raw, rendered, safeRendered, and append() remain; default sourceMap is empty and ordinary output is unchanged. Rendering and getters are DOM-independent."
893
893
  },
894
894
  {
895
895
  "file": "runtime/arcane/modules/MemoryRecords.js",
@@ -77,7 +77,7 @@ appropriate.
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, toolbar, preview, title, and save surface. | `configure()`<br>`focus()`<br>`clear()`<br>`saveEntry()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values normalized; injected save result mixed |
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 |
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 |
@@ -844,7 +844,11 @@ Each descriptor accepts an optional `title`. Supported shapes are:
844
844
  | `image`, `audio`, `pdf`, `download` | `content`: a `Blob` or `File`, or a function receiving `{signal}` and resolving to one |
845
845
 
846
846
  Conversation `role` and complete Markdown `content` are strings. Optional
847
- `name` and `status` are displayed alongside the role and time. Valid supplied
847
+ assistant `name` is displayed exactly when it is a nonblank string; otherwise
848
+ the assistant label is `AI`, without a role suffix. User and other role labels
849
+ retain their supplied name alongside the role. User and assistant cards use
850
+ distinct shared-theme backgrounds, and each card retains its original
851
+ `data-role`. Optional `status` is displayed beside the time. Valid supplied
848
852
  timestamps are displayed in local time with their real ISO datetime; absent
849
853
  timestamps display `Time unavailable`, and an unrecognized supplied timestamp
850
854
  is displayed as supplied. No timestamp is invented.
@@ -1021,11 +1025,56 @@ subject to the bound app's capabilities. [Deep protocol details](protocols.md).
1021
1025
 
1022
1026
  ### Overview
1023
1027
 
1024
- Configurable Markdown authoring, toolbar, preview, title, and save surface.
1028
+ Configurable Markdown authoring, toolbar, preview, plain-text title, and save
1029
+ surface. The default editor retains its growing stacked layout and existing
1030
+ controls. Optional fit mode keeps the Markdown input and rendered preview
1031
+ inside an application-supplied bounded area.
1025
1032
 
1026
1033
  ### Public surface
1027
1034
 
1028
- Methods/properties: `configure()`, `focus()`, `clear()`, `saveEntry()`, `destroy()`.
1035
+ Methods/properties: `configure()`, `focus()`, `clear()`, `saveEntry()`,
1036
+ `destroy()`, `value`, `entryTitle`, `options`.
1037
+
1038
+ `configure({fit:true,followPreview:true})` opts into the two independent
1039
+ behaviors. Both options default to `false`; omitted options retain their
1040
+ previous values during reconfiguration, and explicit `false` disables either.
1041
+ Initial markup can use `data-fit="true"` and `data-follow-preview="true"`.
1042
+
1043
+ `fit:true` fills the host's available height. The application must supply a
1044
+ bounded host height and allow its enclosing flex/grid item to shrink, for
1045
+ example with `min-height:0`. Input and preview have independent scroll areas:
1046
+ two columns when room permits and two bounded stacked panes on narrow widths.
1047
+ Title, toolbar, status, and save remain outside those scrolling panes. Hiding
1048
+ the preview leaves the input the available pane space. Fit mode does not
1049
+ change the application's fixed shell or scroll the page.
1050
+
1051
+ `followPreview:true` follows the Markdown body's active edit position after
1052
+ typing or applying a toolbar format. It scrolls the preview only as needed
1053
+ to bring a short corresponding block into view; a block taller than the pane
1054
+ uses the caret's approximate fraction within that block. Users can scroll the
1055
+ preview independently, and rerendering retains that reading position; the next
1056
+ body edit may reposition it. Title edits,
1057
+ programmatic value replacement, and ordinary preview scrolling do not request
1058
+ edit-position following. Moving the selection alone does not trigger following,
1059
+ and following never scrolls the input pane or containing page. The title
1060
+ remains a native plain-text input.
1061
+ Following uses the shared [Markdown block mapping](runtime-modules.md#mdjs);
1062
+ lists, quotes, and tables follow their block, while invisible source positions
1063
+ use a nearby available rendered block rather than exact glyph geometry.
1064
+
1065
+ Public CSS parts are `editor`, `title`, `toolbar`, `input`, `preview`,
1066
+ `actions`, `status`, and `save-action`. Applications can make the title
1067
+ borderless and compact through `::part(title)` while retaining its native
1068
+ editing behavior and visible keyboard focus. These parts are styling hooks,
1069
+ not slots or replacement controls.
1070
+
1071
+ Existing `showTitle`, `showToolbar`, `showSave`, `showPreview`, `readOnly`,
1072
+ `clearOnSave`, labels, formats, and callbacks keep their behavior. `onSave`
1073
+ or the host's `save` receives complete `{title,markdown}` plus a second
1074
+ `{signal,operationId}` context; layout and following add no fields to the
1075
+ saved content. Edits, replacement, clear, and destruction retain the existing
1076
+ save cancellation and late-result suppression. Destruction also cancels the
1077
+ pending preview frame.
1029
1078
 
1030
1079
  Events: `markdown-editor-ready`, `markdown-editor-change`, `markdown-editor-saved`.
1031
1080
 
@@ -1033,7 +1082,10 @@ Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`ComponentContracts.js
1033
1082
 
1034
1083
  ### Availability and normalization
1035
1084
 
1036
- **Browser and supported native WebViews.** Editor values normalized; injected save result mixed. HTMLImport + DOM; injected Arcane/provider modules where listed. Native methods remain subject to the bound app's capabilities. [Deep protocol details](protocols.md).
1085
+ **Browser and supported native WebViews.** Editor values, opt-in fit layout,
1086
+ and preview following are normalized; the injected save result remains
1087
+ application-owned. HTMLImport + DOM; native methods remain subject to the
1088
+ bound app's capabilities. [Deep protocol details](protocols.md).
1037
1089
 
1038
1090
  ### Example
1039
1091
 
@@ -1044,6 +1096,28 @@ Shared dependencies: [`MD.js`](runtime-modules.md#mdjs), [`ComponentContracts.js
1044
1096
  </html-import>
1045
1097
  ```
1046
1098
 
1099
+ For a bounded editor, choose the height in the application and style only the
1100
+ public title part:
1101
+
1102
+ ```html
1103
+ <style>
1104
+ #field-notes { display:block; height:32rem; min-height:0; }
1105
+ #field-notes::part(title) {
1106
+ border:0;
1107
+ border-radius:0;
1108
+ background:transparent;
1109
+ padding:.35em 0;
1110
+ }
1111
+ </style>
1112
+ <html-import
1113
+ id="field-notes"
1114
+ href="/arcane/components/markdown-editor.html"
1115
+ data-fit="true"
1116
+ data-follow-preview="true"
1117
+ data-title-placeholder="Dragon field notes">
1118
+ </html-import>
1119
+ ```
1120
+
1047
1121
  ## media-embed.html
1048
1122
 
1049
1123
  ### Overview
@@ -274,6 +274,29 @@ Owns persisted user preference/profile fields, explicit-preference updates,
274
274
  fresh reads, saves, and JSON projection. It installs `window.user` after its
275
275
  DBLS/DBOPFS lifecycle and emits `user-entity-loaded`.
276
276
 
277
+ ### AI_name
278
+
279
+ Saved AI-name preference alongside `AI_personality`. It is a string with an
280
+ empty-string default and preserves the complete supplied text, including
281
+ Unicode, whitespace, and an explicitly empty value. The property setter follows
282
+ the same persistence setting as `AI_personality`; non-string assignments throw
283
+ `Error('AI_name must be string')`.
284
+
285
+ `explicit`, `toJSON()`, `load()`, `refresh()`, and `updateExplicit()` include
286
+ this field through the existing profile schema. Loading an older record that
287
+ omits it leaves a new entity's empty default in memory and does not rewrite the
288
+ stored record. Partial updates that omit it retain the current value.
289
+
290
+ ```javascript
291
+ await user.updateExplicit(
292
+ {AI_name: 'Captain Moonboots'}
293
+ );
294
+ ```
295
+
296
+ The application owns naming choices and any mapping to its AI display name;
297
+ this preference changes neither the human `username` nor chat history. No
298
+ automatic migration or naming policy runs when it is loaded or saved.
299
+
277
300
  ### updateExplicit(update)
278
301
 
279
302
  Accepts an ordinary partial profile object, a JSON string, or a function
@@ -98,7 +98,7 @@ own asynchronous work, cancellation, and backpressure.
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
99
  | [`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
100
  | [`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
- | [`MD.js`](#mdjs) | esm | Renders complete Markdown with Marked and exposes the complete rendered markup. | Browser / native WebView | Complete raw and rendered Marked values; parse errors vendor-native. |
101
+ | [`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. |
102
102
  | [`MemoryRecords.js`](#memoryrecordsjs) | esm | Normalizes memory content and detects meaningful stored memory. | Cross-host | Fully normalized string/boolean results. |
103
103
  | [`MessageAdvisory.js`](#messageadvisoryjs) | esm | Normalizes message content advisories and contains per-message inspection failures. | Cross-host | Normalized advisory records; inspector failures converted to unavailable results. |
104
104
  | [`ModelDefinition.js`](#modeldefinitionjs) | esm | Parses the deterministic packaged Modelfile subset and extracts the SYSTEM prompt. | Cross-host | Complete mutable definition with coded malformed-input errors. |
@@ -1647,6 +1647,14 @@ the shared voice component. Positive quiet/chunk durations and nonnegative
1647
1647
  pre-roll/activity values must be finite. Complete callback payloads remain
1648
1648
  owned by the component and application.
1649
1649
 
1650
+ `normalizeMarkdownOptions(input,previous)` adds strict opt-in `fit` and
1651
+ `followPreview` booleans, both defaulting to `false`. Omitted fields preserve
1652
+ the corresponding previous value; explicit `false` disables the option.
1653
+ Existing Markdown labels, formats, visibility, read-only state, save behavior,
1654
+ callbacks, complete initial Markdown, and plain initial title remain intact.
1655
+ The shared editor consumes these options for bounded responsive panes and
1656
+ body-edit preview following; the normalizer itself performs no DOM work.
1657
+
1650
1658
  `createSTTActivationController({host,button,progress=null,onChange,EventClass=CustomEvent})`
1651
1659
  consumes only normalized
1652
1660
  [`AIRuntimeState`](#airuntimestatejs) `stt` role records. Its mutable controller
@@ -2759,25 +2767,66 @@ console.log(Object.keys(module));
2759
2767
  ### Overview
2760
2768
 
2761
2769
  Renders complete Markdown with Marked and exposes the same complete rendered
2762
- markup through `rendered` and `safeRendered`.
2763
-
2764
- ### Public surface
2765
-
2766
- default `MD`; `raw`, `rendered`, `safeRendered`, `append()`.
2770
+ markup through `rendered` and `safeRendered`. Optional source mapping lets a
2771
+ consumer locate rendered blocks without adding visible wrappers or changing
2772
+ the authored Markdown.
2773
+
2774
+ ### Public surface
2775
+
2776
+ Default `MD`; `raw`, `rendered`, `safeRendered`, `sourceMap`, `append()`.
2777
+
2778
+ `new MD(raw,{sourceMap:true})` enables block mapping. The `sourceMap` getter
2779
+ returns ordered `{start,end,marker,type}` records. `start` is inclusive and
2780
+ `end` is exclusive, measured in UTF-16 positions in the exact original `raw`
2781
+ string, matching native textarea selection offsets. `marker` is the complete
2782
+ data of a generated HTML comment, not a CSS selector. A consumer can locate
2783
+ it after rendering with `document.createTreeWalker(root,NodeFilter.SHOW_COMMENT)`.
2784
+ The generated marker prefix is chosen so it does not collide with authored
2785
+ source comments. Marker values are opaque and may change after an edit.
2786
+ The getter exposes the current mutable array; each successful render replaces
2787
+ it with a new array, so a previously retained array describes the prior render.
2788
+
2789
+ In this opt-in mode, `rendered` and `safeRendered` contain the complete rendered
2790
+ Markdown plus comment anchors; no element wrappers or attributes are added.
2791
+ Lists, block quotes, and tables map at their top-level block rather than to
2792
+ individual characters. Blank space and definitions have no visible block;
2793
+ raw HTML can also absorb a comment during DOM parsing. A consumer follows the
2794
+ nearest available rendered block in those cases. This is block-position
2795
+ mapping, not exact rendered glyph or caret geometry.
2796
+
2797
+ Assigning `raw` or calling `append(string)` rebuilds both the complete rendered
2798
+ result and its current mapping. Without `sourceMap:true`, `sourceMap` is empty
2799
+ and the ordinary rendered output is unchanged. Existing raw content, link
2800
+ behavior, no-op `rendered` setter, and complete `append()` result remain.
2767
2801
 
2768
2802
  Exact exports: `default`.
2769
2803
 
2770
2804
  ### Availability and normalization
2771
2805
 
2772
2806
  **Browser / native WebView.** Raw Marked behavior is preserved; parse errors are
2773
- vendor-native. Transport: Marked. [Deep protocol details](protocols.md).
2807
+ vendor-native. Rendering and the getters are DOM-independent. DOM lookup and
2808
+ scrolling belong to the consuming component;
2809
+ mapping performs no persistence or viewport movement. Transport: Marked.
2810
+ [Deep protocol details](protocols.md).
2774
2811
 
2775
2812
  ### Example
2776
2813
 
2777
2814
  ```javascript
2778
- import * as module from '/arcane/modules/MD.js';
2815
+ import MD from '/arcane/modules/MD.js';
2779
2816
 
2780
- console.log(Object.keys(module));
2817
+ const notes=new MD('# Dragon field notes\n\nThe library dragon ate the index.',{
2818
+ sourceMap:true
2819
+ });
2820
+ const preview=document.querySelector('#preview');
2821
+ preview.innerHTML=notes.safeRendered;
2822
+ const comments=document.createTreeWalker(preview,NodeFilter.SHOW_COMMENT);
2823
+ const firstBlock=notes.sourceMap[0];
2824
+ while(comments.nextNode()){
2825
+ if(comments.currentNode.data===firstBlock.marker){
2826
+ console.log(firstBlock,comments.currentNode.nextSibling);
2827
+ break;
2828
+ }
2829
+ }
2781
2830
  ```
2782
2831
 
2783
2832
  ## MemoryRecords.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.36.1",
3
+ "version": "0.38.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/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-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/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.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-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.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",
@@ -387,6 +387,21 @@
387
387
  background:var(--background);
388
388
  }
389
389
 
390
+ .file-preview-message[data-role="user"]{
391
+ background:var(--arcane-action,var(--primary-color));
392
+ color:var(--arcane-action-text,var(--button-text-color,var(--text-color)));
393
+ border-color:var(--arcane-action,var(--primary-color));
394
+ }
395
+
396
+ .file-preview-message[data-role="user"] a{
397
+ color:inherit;
398
+ }
399
+
400
+ .file-preview-message[data-role="assistant"]{
401
+ background:var(--arcane-surface,var(--background));
402
+ color:var(--text-color);
403
+ }
404
+
390
405
  .file-preview-message header,
391
406
  .file-preview-message footer{
392
407
  display:flex;
@@ -1740,9 +1755,15 @@
1740
1755
  const time=document.createElement('time');
1741
1756
  article.className='file-preview-message';
1742
1757
  article.dataset.role=message.role;
1743
- speaker.textContent=message.name===undefined
1744
- ?message.role
1745
- :`${message.name} · ${message.role}`;
1758
+ if(message.role==='assistant'){
1759
+ speaker.textContent=is.string(message.name)&&message.name.trim()
1760
+ ?message.name
1761
+ :'AI';
1762
+ }else{
1763
+ speaker.textContent=message.name===undefined
1764
+ ?message.role
1765
+ :`${message.name} · ${message.role}`;
1766
+ }
1746
1767
  header.append(speaker);
1747
1768
  if(message.timestamp===undefined||message.timestamp===null){
1748
1769
  time.textContent='Time unavailable';
@@ -4,6 +4,12 @@
4
4
  display:block;
5
5
  }
6
6
 
7
+ :host([data-fit="true"]){
8
+ min-width:0;
9
+ min-height:0;
10
+ container:arcane-markdown-editor / inline-size;
11
+ }
12
+
7
13
  .markdown-editor{
8
14
  display:grid;
9
15
  gap:.75em;
@@ -57,19 +63,62 @@
57
63
  margin-right:auto;
58
64
  }
59
65
 
66
+ .markdown-editor.fit{
67
+ height:100%;
68
+ min-height:0;
69
+ min-width:0;
70
+ grid-template-columns:minmax(0,1fr) minmax(0,1fr);
71
+ grid-template-rows:auto auto minmax(0,1fr) auto;
72
+ grid-template-areas:"title title" "toolbar toolbar" "input preview" "actions actions";
73
+ }
74
+
75
+ .fit .entry-title{grid-area:title;}
76
+ .fit .toolbar{grid-area:toolbar;}
77
+ .fit .entry-markdown{grid-area:input;}
78
+ .fit .preview{grid-area:preview;}
79
+ .fit .actions{grid-area:actions;}
80
+
81
+ .fit .entry-markdown,
82
+ .fit .preview{
83
+ box-sizing:border-box;
84
+ min-width:0;
85
+ min-height:0;
86
+ height:100%;
87
+ overflow:auto;
88
+ overscroll-behavior:contain;
89
+ }
90
+
91
+ .fit .entry-markdown{resize:none;}
92
+ .fit .preview{overflow-wrap:anywhere;}
93
+ .fit .preview img{max-width:100%;height:auto;}
94
+ .fit.without-preview{grid-template-areas:"title title" "toolbar toolbar" "input input" "actions actions";}
95
+
96
+ @container arcane-markdown-editor (max-width:44rem){
97
+ .markdown-editor.fit{
98
+ grid-template-columns:minmax(0,1fr);
99
+ grid-template-rows:auto auto minmax(0,1fr) minmax(0,1fr) auto;
100
+ grid-template-areas:"title" "toolbar" "input" "preview" "actions";
101
+ }
102
+
103
+ .markdown-editor.fit.without-preview{
104
+ grid-template-rows:auto auto minmax(0,1fr) auto;
105
+ grid-template-areas:"title" "toolbar" "input" "actions";
106
+ }
107
+ }
108
+
60
109
  .hidden{
61
110
  display:none;
62
111
  }
63
112
  </style>
64
113
 
65
- <section class="markdown-editor">
66
- <input id="entryTitle" class="entry-title" type="text">
67
- <section id="preview" class="preview"></section>
68
- <section id="toolbar" class="toolbar"></section>
69
- <textarea id="entryMarkdown" class="entry-markdown"></textarea>
70
- <section class="actions">
71
- <span id="status" class="status" aria-live="polite"></span>
72
- <button id="save" type="button" disabled></button>
114
+ <section class="markdown-editor" part="editor">
115
+ <input id="entryTitle" class="entry-title" part="title" type="text">
116
+ <section id="preview" class="preview" part="preview" tabindex="0"></section>
117
+ <section id="toolbar" class="toolbar" part="toolbar"></section>
118
+ <textarea id="entryMarkdown" class="entry-markdown" part="input"></textarea>
119
+ <section class="actions" part="actions">
120
+ <span id="status" class="status" part="status" aria-live="polite"></span>
121
+ <button id="save" part="save-action" type="button" disabled></button>
73
122
  </section>
74
123
  </section>
75
124
 
@@ -88,6 +137,7 @@
88
137
  const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
89
138
  const events=createArcaneEventSource(host,{source:'arcane.component.markdown-editor',eventTypes:['markdown-editor-change','markdown-editor-saved','markdown-editor-ready']});
90
139
  const lifecycleController=new AbortController();
140
+ const editor=host.shadowRoot.querySelector('.markdown-editor');
91
141
  const titleInput=host.shadowRoot.querySelector('#entryTitle');
92
142
  const preview=host.shadowRoot.querySelector('#preview');
93
143
  const toolbar=host.shadowRoot.querySelector('#toolbar');
@@ -96,6 +146,7 @@
96
146
  const status=host.shadowRoot.querySelector('#status');
97
147
 
98
148
  let previewFrame=0;
149
+ let previewFollowPending=false;
99
150
  let operationSequence=0;
100
151
  let saveGeneration=0;
101
152
  let saveController=null;
@@ -133,6 +184,9 @@
133
184
  titleInput.addEventListener('input',contentChanged,{signal:lifecycleController.signal});
134
185
  toolbar.addEventListener('click',formatMarkdown,{signal:lifecycleController.signal});
135
186
  saveButton.addEventListener('click',saveEntry,{signal:lifecycleController.signal});
187
+ for(const type of ['wheel','pointerdown','touchstart','keydown']){
188
+ preview.addEventListener(type,retainPreviewReadingPosition,{signal:lifecycleController.signal,passive:true});
189
+ }
136
190
 
137
191
  function optionsFromDataset(){
138
192
  const dataset=host.dataset||{};
@@ -151,6 +205,8 @@
151
205
  }
152
206
  for(const field of [
153
207
  'clearOnSave',
208
+ 'fit',
209
+ 'followPreview',
154
210
  'readOnly',
155
211
  'showPreview',
156
212
  'showSave',
@@ -191,8 +247,21 @@
191
247
  }
192
248
 
193
249
  function renderOptions(){
250
+ host.dataset.fit=String(options.fit);
251
+ editor.classList.toggle('fit',options.fit);
252
+ editor.classList.toggle('without-preview',!options.showPreview);
253
+ // Keyboard order follows the displayed order in each layout.
254
+ if(options.fit){
255
+ editor.insertBefore(toolbar,preview);
256
+ editor.insertBefore(textarea,preview);
257
+ }else{
258
+ editor.insertBefore(preview,toolbar);
259
+ editor.insertBefore(textarea,toolbar.nextSibling);
260
+ }
194
261
  titleInput.placeholder=options.labels.titlePlaceholder;
262
+ titleInput.setAttribute('aria-label',options.labels.titlePlaceholder);
195
263
  textarea.placeholder=options.labels.bodyPlaceholder;
264
+ textarea.setAttribute('aria-label',options.labels.bodyPlaceholder);
196
265
  preview.setAttribute('aria-label',options.labels.preview);
197
266
  preview.dataset.emptyLabel=options.labels.preview;
198
267
  toolbar.setAttribute('aria-label',options.labels.toolbar);
@@ -221,12 +290,14 @@
221
290
  toolbar.replaceChildren(fragment);
222
291
  }
223
292
 
224
- function contentChanged(){
293
+ function contentChanged(event){
225
294
  if(destroyed){
226
295
  return;
227
296
  }
228
297
  supersedeSave('markdown-content-edited');
229
- schedulePreview();
298
+ if(event?.currentTarget!==titleInput){
299
+ schedulePreview(options.followPreview);
300
+ }
230
301
  const detail={
231
302
  markdown:textarea.value,
232
303
  title:titleInput.value
@@ -239,17 +310,28 @@
239
310
  }
240
311
  }
241
312
 
242
- function schedulePreview(){
313
+ function schedulePreview(follow=false){
243
314
  if(destroyed){
244
315
  return;
245
316
  }
246
317
  if(previewFrame){
247
318
  cancelAnimationFrame(previewFrame);
248
319
  }
249
- previewFrame=requestAnimationFrame(updatePreview);
320
+ previewFollowPending=previewFollowPending||follow;
321
+ previewFrame=requestAnimationFrame(renderPendingPreview);
250
322
  }
251
323
 
252
- function updatePreview(){
324
+ function renderPendingPreview(){
325
+ const follow=previewFollowPending;
326
+ previewFrame=0;
327
+ updatePreview(follow);
328
+ }
329
+
330
+ function retainPreviewReadingPosition(){
331
+ previewFollowPending=false;
332
+ }
333
+
334
+ function updatePreview(follow=false){
253
335
  if(destroyed){
254
336
  return;
255
337
  }
@@ -257,13 +339,87 @@
257
339
  cancelAnimationFrame(previewFrame);
258
340
  }
259
341
  previewFrame=0;
260
- preview.innerHTML=new MD(textarea.value).safeRendered;
342
+ previewFollowPending=false;
343
+ const scrollTop=preview.scrollTop;
344
+ const scrollLeft=preview.scrollLeft;
345
+ const markdown=new MD(textarea.value,{sourceMap:options.followPreview});
346
+ preview.innerHTML=markdown.safeRendered;
261
347
  preview.querySelectorAll('a').forEach(
262
348
  link=>{
263
349
  link.target='_blank';
264
350
  link.removeAttribute('rel');
265
351
  }
266
352
  );
353
+ preview.scrollTop=scrollTop;
354
+ preview.scrollLeft=scrollLeft;
355
+ if(follow&&options.followPreview&&options.showPreview){
356
+ followTypingPosition(markdown.sourceMap);
357
+ }
358
+ }
359
+
360
+ function followTypingPosition(sourceMap){
361
+ const records=new Map(sourceMap.map(record=>[record.marker,record]));
362
+ const walker=document.createTreeWalker(preview,NodeFilter.SHOW_COMMENT);
363
+ const anchors=[];
364
+ for(let node=walker.nextNode();node;node=walker.nextNode()){
365
+ const record=records.get(node.data);
366
+ if(record){
367
+ anchors.push({node,record});
368
+ }
369
+ }
370
+ if(!anchors.length||!preview.clientHeight){
371
+ return;
372
+ }
373
+ const caret=textarea.selectionDirection==='backward'
374
+ ?textarea.selectionStart:textarea.selectionEnd;
375
+ let selected=0;
376
+ for(let index=0;index<anchors.length;index+=1){
377
+ if(anchors[index].record.start>caret){
378
+ break;
379
+ }
380
+ selected=index;
381
+ }
382
+ // Raw HTML can absorb an anchor or have no visible geometry. Use the
383
+ // nearest rendered block; never move the containing page or input pane.
384
+ let left=selected;
385
+ let right=selected+1;
386
+ while(left>=0||right<anchors.length){
387
+ const useLeft=right>=anchors.length||(
388
+ left>=0&&Math.abs(caret-anchors[left].record.end)<=Math.abs(anchors[right].record.start-caret)
389
+ );
390
+ const index=useLeft?left--:right++;
391
+ const anchor=anchors[index];
392
+ const range=document.createRange();
393
+ range.setStartAfter(anchor.node);
394
+ if(anchors[index+1]){
395
+ range.setEndBefore(anchors[index+1].node);
396
+ }else{
397
+ range.setEnd(preview,preview.childNodes.length);
398
+ }
399
+ const bounds=range.getBoundingClientRect();
400
+ if(!bounds.height){
401
+ continue;
402
+ }
403
+ const viewportTop=preview.getBoundingClientRect().top+preview.clientTop;
404
+ const viewportBottom=viewportTop+preview.clientHeight;
405
+ if(bounds.height<=preview.clientHeight){
406
+ if(bounds.top<viewportTop){
407
+ preview.scrollTop+=bounds.top-viewportTop;
408
+ }else if(bounds.bottom>viewportBottom){
409
+ preview.scrollTop+=bounds.bottom-viewportBottom;
410
+ }
411
+ }else{
412
+ // A top-level block may be a long paragraph, list or code fence.
413
+ // Follow approximate position within it without rewriting text.
414
+ const span=anchor.record.end-anchor.record.start;
415
+ const fraction=span?Math.max(0,Math.min(1,(caret-anchor.record.start)/span)):0;
416
+ const position=bounds.top+fraction*bounds.height;
417
+ if(position<viewportTop||position>viewportBottom){
418
+ preview.scrollTop+=position-viewportTop-preview.clientHeight/2;
419
+ }
420
+ }
421
+ return;
422
+ }
267
423
  }
268
424
 
269
425
  function formatMarkdown(event){
@@ -414,6 +570,7 @@
414
570
  destroyed=true;
415
571
  supersedeSave('markdown-editor-destroyed');
416
572
  lifecycleController.abort('markdown-editor-destroyed');
573
+ previewFollowPending=false;
417
574
  if(previewFrame){
418
575
  cancelAnimationFrame(previewFrame);
419
576
  previewFrame=0;
@@ -110,6 +110,7 @@ function userEntityLifecycleError(code,reason,message){
110
110
  * @property {string|number} contact_4
111
111
  * @property {string|number} contact_5
112
112
  * @property {string|number} contact_6
113
+ * @property {string} AI_name
113
114
  * @property {string} AI_personality
114
115
  * @property {string} religion
115
116
  * @property {string} AI_voice
@@ -176,6 +177,7 @@ class UserEntity {
176
177
  'contact_5',
177
178
  'contact_6',
178
179
 
180
+ 'AI_name',
179
181
  'AI_personality',
180
182
  'religion',
181
183
  'AI_voice',
@@ -211,6 +213,7 @@ class UserEntity {
211
213
  #contact_5 = '';
212
214
  #contact_6 = '';
213
215
 
216
+ #aiName = '';
214
217
  #AI_personality = '';
215
218
  #religion = '';
216
219
  #AI_voice = '';
@@ -515,6 +518,22 @@ class UserEntity {
515
518
 
516
519
 
517
520
 
521
+ /** @returns {string} */
522
+ get AI_name() {
523
+ return this.#aiName;
524
+ }
525
+
526
+ /** @param {string} v */
527
+ set AI_name(v) {
528
+ if (!is.string(v)) {
529
+ throw new Error('AI_name must be string');
530
+ }
531
+
532
+ this.#aiName = v;
533
+
534
+ this.#persist();
535
+ }
536
+
518
537
  /** @returns {string} */
519
538
  get AI_personality(){
520
539
  return this.#AI_personality;
@@ -889,6 +908,7 @@ class UserEntity {
889
908
  this.persist=persist;
890
909
  }
891
910
 
911
+ baseline = this.explicit;
892
912
  const conditional=is.function(src);
893
913
  const update=conditional?await src(this.explicit):src;
894
914
  if(conditional&&update===null){
@@ -1101,6 +1101,8 @@ function normalizeMarkdownOptions(input={},previous={}){
1101
1101
  showPreview:option(input,previous,'showPreview',true)!==false,
1102
1102
  showToolbar:option(input,previous,'showToolbar',true)!==false,
1103
1103
  showSave:option(input,previous,'showSave',true)!==false,
1104
+ fit:option(input,previous,'fit',false)===true,
1105
+ followPreview:option(input,previous,'followPreview',false)===true,
1104
1106
  readOnly:option(input,previous,'readOnly',false)===true,
1105
1107
  clearOnSave:option(input,previous,'clearOnSave',true)!==false,
1106
1108
  onSave:optionalCallback(input,previous,['onSave','save'],'onSave'),
@@ -1,30 +1,36 @@
1
1
  import { arcaneLogging } from 'arcane-os/logging';
2
- import { marked } from './Marked.min.js';
2
+ import { Marked, marked } from './Marked.min.js';
3
3
  import Is from 'strong-type';
4
4
 
5
- marked.use(
6
- {
7
- async: false,
8
- pedantic: false,
9
- gfm: true,
10
- renderer: {
11
- link(href, title, text) {
12
- const link = marked.Renderer.prototype.link.call(this, href, title, text);
13
- return link.replace("<a","<a target='_blank' ");
14
- }
5
+ const markdownOptions = {
6
+ async: false,
7
+ pedantic: false,
8
+ gfm: true,
9
+ renderer: {
10
+ link(href, title, text) {
11
+ const link = marked.Renderer.prototype.link.call(this, href, title, text);
12
+ return link.replace("<a","<a target='_blank' ");
15
13
  }
16
14
  }
17
- );
15
+ };
16
+
17
+ marked.use(markdownOptions);
18
18
 
19
19
  const is = new Is(false);
20
20
 
21
21
  class MD {
22
22
  #raw='';
23
- #rendered=''
23
+ #rendered='';
24
+ #sourceMapEnabled=false;
25
+ #sourceMap=[];
24
26
 
25
- constructor(raw=''){
27
+ constructor(raw='', options={}){
28
+ this.#sourceMapEnabled=options?.sourceMap===true;
26
29
  this.raw=raw;
27
- this.rendered=marked.parse(raw);
30
+ if(!is.string(raw)){
31
+ // Preserve the constructor's existing Marked error for invalid input.
32
+ this.rendered=marked.parse(raw);
33
+ }
28
34
  return this;
29
35
  }
30
36
 
@@ -36,6 +42,10 @@ class MD {
36
42
  return this.#rendered;
37
43
  }
38
44
 
45
+ get sourceMap(){
46
+ return this.#sourceMap;
47
+ }
48
+
39
49
  set rendered(value=''){
40
50
  return this.#rendered;
41
51
  }
@@ -50,7 +60,7 @@ class MD {
50
60
  return this.#raw;
51
61
  }
52
62
  this.#raw = value;
53
- this.#rendered = marked.parse(value);
63
+ this.#render();
54
64
  return this.#raw;
55
65
  }
56
66
 
@@ -60,9 +70,130 @@ class MD {
60
70
  return this.#raw;
61
71
  }
62
72
  this.#raw += value;
63
- this.#rendered = marked.parse(this.#raw);
73
+ this.#render();
64
74
  return this.#raw;
65
75
  }
76
+
77
+ #render(){
78
+ if(!this.#sourceMapEnabled){
79
+ this.#rendered=marked.parse(this.#raw);
80
+ this.#sourceMap=[];
81
+ return;
82
+ }
83
+
84
+ const result=renderWithSourceMap(this.#raw);
85
+ this.#rendered=result.rendered;
86
+ this.#sourceMap=result.sourceMap;
87
+ }
88
+ }
89
+
90
+ function renderWithSourceMap(raw){
91
+ const tokenRanges=new WeakMap();
92
+ const sourceMap=[];
93
+ let normalizedEnd=0;
94
+ let normalizedOffset=0;
95
+ let originalOffset=0;
96
+ let observedRoot=false;
97
+ let previousOffset=0;
98
+ let previousCount=0;
99
+ let previousRaw;
100
+ const authoredGenerations=new Set();
101
+ for(const match of raw.matchAll(/arcane-md-source-(\d+):/g)){
102
+ authoredGenerations.add(match[1]);
103
+ }
104
+ let markerGeneration=0;
105
+ while(authoredGenerations.has(String(markerGeneration))){
106
+ markerGeneration++;
107
+ }
108
+ const markerPrefix=`arcane-md-source-${markerGeneration}:`;
109
+
110
+ function captureBoundary(tokens, offset){
111
+ // Follow Marked's CRLF/CR cursor without rewriting raw or retaining a per-character map.
112
+ while(normalizedOffset<offset){
113
+ if(raw[originalOffset]==='\r' && raw[originalOffset+1]==='\n'){
114
+ originalOffset++;
115
+ }
116
+ originalOffset++;
117
+ normalizedOffset++;
118
+ }
119
+ for(let index=previousCount; index<tokens.length; index++){
120
+ tokenRanges.set(
121
+ tokens[index],
122
+ {
123
+ start:previousOffset,
124
+ end:originalOffset
125
+ }
126
+ );
127
+ }
128
+
129
+ const last=tokens.at(-1);
130
+ if(tokens.length===previousCount && last && last.raw!==previousRaw){
131
+ // Marked can merge a newline or indented continuation into a prior token.
132
+ const range=tokenRanges.get(last);
133
+ if(range){
134
+ range.end=originalOffset;
135
+ }
136
+ }
137
+ previousOffset=originalOffset;
138
+ previousCount=tokens.length;
139
+ previousRaw=last?.raw;
140
+ }
141
+
142
+ const renderer={};
143
+ const blockTypes=['code','blockquote','html','heading','hr','list','paragraph','table','text'];
144
+ for(const type of blockTypes){
145
+ const render=marked.Renderer.prototype[type];
146
+ renderer[type]=function renderMappedBlock(token){
147
+ const html=render.call(this,token);
148
+ const range=tokenRanges.get(token);
149
+ if(!range || !html){
150
+ return html;
151
+ }
152
+
153
+ const marker=`${markerPrefix}${sourceMap.length}`;
154
+ sourceMap.push(
155
+ {
156
+ start:range.start,
157
+ end:range.end,
158
+ marker,
159
+ type:token.type
160
+ }
161
+ );
162
+ return `<!--${marker}-->${html}`;
163
+ };
164
+ }
165
+
166
+ const parser=new Marked(
167
+ markdownOptions,
168
+ {
169
+ extensions:[
170
+ {
171
+ name:'arcaneSourceMap',
172
+ level:'block',
173
+ tokenizer(src,tokens){
174
+ if(tokens===this.lexer.tokens){
175
+ if(!observedRoot){
176
+ normalizedEnd=src.length;
177
+ observedRoot=true;
178
+ }
179
+ // Observe consumption, including definitions that produce no token.
180
+ captureBoundary(tokens,normalizedEnd-src.length);
181
+ }
182
+ return undefined;
183
+ }
184
+ }
185
+ ],
186
+ hooks:{
187
+ processAllTokens(tokens){
188
+ captureBoundary(tokens,normalizedEnd);
189
+ return tokens;
190
+ }
191
+ },
192
+ renderer
193
+ }
194
+ );
195
+ const rendered=parser.parse(raw);
196
+ return {rendered,sourceMap};
66
197
  }
67
198
 
68
199
  export default MD;