arcane-os 0.44.0 → 0.46.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.
@@ -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.rendered,
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
- preview.printStatus.textContent='Unable to open print preview. Please try again.';
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||import('../modules/MD.js');
1821
- const {default:MD}=await markdownModulePromise;
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
- body.innerHTML=new MD(descriptor.content).safeRendered;
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;
@@ -2036,8 +2142,9 @@
2036
2142
  const rendered=document.createElement('div');
2037
2143
  rendered.className='file-preview';
2038
2144
  let value=fileRef.parsed;
2039
- const transformed=['json','jsonl','ndjson'].includes(fileRef.ext)
2040
- &&is.function(host.previewTransform);
2145
+ const jsonFile=['json','jsonl','ndjson'].includes(fileRef.ext);
2146
+ const transformed=jsonFile&&is.function(host.previewTransform);
2147
+ let completeJSON;
2041
2148
 
2042
2149
  if(transformed){
2043
2150
  value=await host.previewTransform(
@@ -2049,6 +2156,37 @@
2049
2156
  }
2050
2157
  );
2051
2158
  preview.controller.signal.throwIfAborted();
2159
+ }else if(jsonFile){
2160
+ // Recognition needs the complete file: FileEntity's parsed JSONL
2161
+ // rows do not include every unparseable line in the original.
2162
+ const [{decodeMarkdownMediaRecord},text]=await Promise.all([
2163
+ import('../modules/MarkdownMedia.js'),
2164
+ fileRef.text()
2165
+ ]);
2166
+ preview.controller.signal.throwIfAborted();
2167
+ completeJSON=text;
2168
+ let imageRecord;
2169
+ try {
2170
+ imageRecord = JSON.parse(completeJSON);
2171
+ } catch {
2172
+ // Mixed JSONL and ordinary text keep their complete presentation.
2173
+ imageRecord = null;
2174
+ }
2175
+ while (is.array(imageRecord) && imageRecord.length === 1) {
2176
+ imageRecord = imageRecord[0];
2177
+ }
2178
+ // A dataUrl property alone does not identify an image record.
2179
+ // Recognize its declared media type before attempting any decoding.
2180
+ if (is.string(imageRecord?.mediaType)
2181
+ && imageRecord.mediaType.toLowerCase().startsWith('image/')) {
2182
+ const image = decodeMarkdownMediaRecord(imageRecord);
2183
+ if (image?.type.startsWith('image/')) {
2184
+ return renderDescriptor(
2185
+ {kind: 'image', content: image, alt: fileRef.name},
2186
+ preview
2187
+ );
2188
+ }
2189
+ }
2052
2190
  }
2053
2191
 
2054
2192
  switch(fileRef.ext){
@@ -2062,7 +2200,7 @@
2062
2200
  case 'jsonl':
2063
2201
  case 'ndjson':{
2064
2202
  if(!transformed){
2065
- rendered.append(createPreformatted(await fileRef.text()));
2203
+ rendered.append(createPreformatted(completeJSON));
2066
2204
  break;
2067
2205
  }
2068
2206
  const records=is.array(value)?value:[];
@@ -219,7 +219,10 @@
219
219
  const host=this;
220
220
  host.ready=false;
221
221
 
222
- const {default:MD}=await import('../modules/MD.js');
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 import('../modules/MD.js');
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
- preview.innerHTML=markdown.safeRendered;
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(previewFrame){
604
- updatePreview(false);
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){
@@ -327,3 +327,44 @@ body{
327
327
  scroll-behavior:auto !important;
328
328
  transition-duration:.01ms !important;
329
329
  }
330
+
331
+ /* Paper uses the existing base light palette regardless of the screen skin.
332
+ * Print-only importance also overrides custom-theme inline tokens; no saved
333
+ * preference, screen layout, media paint, or scrolling state changes here.
334
+ */
335
+ @media print {
336
+ @page {
337
+ margin: 15mm;
338
+ }
339
+
340
+ :root,
341
+ body,
342
+ [data-arcane-palette] {
343
+ color-scheme: light !important;
344
+ --background: rgb(255, 255, 255) !important;
345
+ --modal-background: rgb(255, 255, 255) !important;
346
+ --text-color: rgb(23, 34, 56) !important;
347
+ --primary-color: rgb(23, 34, 56) !important;
348
+ --secondary-color: rgb(229, 233, 243) !important;
349
+ --accent-color: var(--focus-color) !important;
350
+ --button-text-color: rgb(255, 255, 255) !important;
351
+ --border-color: rgb(203, 211, 226) !important;
352
+ --focus-color: rgb(118, 87, 213) !important;
353
+ --muted-text-color: rgb(95, 107, 130) !important;
354
+ --success-color: rgb(39, 122, 75) !important;
355
+ --warning-color: rgb(154, 101, 0) !important;
356
+ --danger-color: rgb(165, 56, 56) !important;
357
+ --review-color: rgb(133, 85, 165) !important;
358
+ --shadow-color: rgba(23, 34, 56, .16) !important;
359
+ --platinum: rgb(102, 112, 133) !important;
360
+ --icon-filter: none !important;
361
+ --file-icon-filter: hue-rotate(8deg) saturate(.62) brightness(.86) !important;
362
+ }
363
+
364
+ html,
365
+ body {
366
+ margin: 0;
367
+ background-color: var(--modal-background);
368
+ color: var(--text-color);
369
+ }
370
+ }
@@ -391,10 +391,12 @@ class ChatEntity{
391
391
  fileName='';
392
392
 
393
393
  /**
394
- * Controls automatic persistence.
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(!this.persist||!durableRecords.length){
1093
+ if(!durableRecords.length){
1092
1094
  return;
1093
1095
  }
1094
1096
 
@@ -6,6 +6,20 @@ const markdownOptions = {
6
6
  async: false,
7
7
  pedantic: false,
8
8
  gfm: true,
9
+ tokenizer: {
10
+ html(src) {
11
+ const native = marked.Tokenizer.prototype.html.call(this, src);
12
+ if (native) return native;
13
+ // Quoted image values can contain blank lines that would otherwise
14
+ // split the tag into Markdown paragraphs before inline HTML parsing.
15
+ const image = /^ {0,3}<img(?:\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>(?=[ \t]*(?:\n|$))/i;
16
+ const match = image.exec(src);
17
+ if (!match) return;
18
+ const quotedValues = match[0].match(/"[^"]*"|'[^']*'/g) || [];
19
+ if (!quotedValues.some(function containsNewline(value) {return value.includes('\n');})) return;
20
+ return {type:'html', block:true, raw:match[0], text:match[0]};
21
+ }
22
+ },
9
23
  renderer: {
10
24
  link(href, title, text) {
11
25
  const link = marked.Renderer.prototype.link.call(this, href, title, text);