@oar-sh/oar 0.9.25 → 0.9.26

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +9 -9
  3. package/package.json +3 -1
  4. package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
  5. package/server/claude-worker/claude-session-process.mjs +4 -0
  6. package/server/claude-worker/claude-turn-publisher.mjs +25 -11
  7. package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
  8. package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
  9. package/server/grok-worker/grok-turn-runner.mjs +3 -1
  10. package/server/public/app/context-usage-view.mjs +2 -2
  11. package/server/public/app/live-activity-cap.mjs +3 -3
  12. package/server/public/app/markdown-math.mjs +471 -0
  13. package/server/public/app/pwa-install.js +25 -4
  14. package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
  15. package/server/public/app/relay-restart-ui.js +2 -2
  16. package/server/public/app/remote-relays-settings.js +33 -6
  17. package/server/public/app/router.js +49 -237
  18. package/server/public/app/settings-tabs.js +10 -1
  19. package/server/public/app/store.js +2 -0
  20. package/server/public/app/stream-view-preference.mjs +1 -1
  21. package/server/public/app/thoughts-preference.mjs +1 -1
  22. package/server/public/app/tmux-inspector-view.js +29 -16
  23. package/server/public/index.html +134 -116
  24. package/server/routes/relay-board-routes.mjs +46 -1
  25. package/server/routes/remote-relay-routes.mjs +1 -1
  26. package/server/server-runtime.mjs +2 -0
  27. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  28. package/server/services/relay-restart-queue.mjs +1 -1
  29. package/server/services/remote-relay-dispatcher.mjs +23 -1
  30. package/server/services/remote-relay-registry-service.mjs +7 -1
  31. package/server/services/session-worker-stop-service.mjs +50 -18
  32. package/shared/math-instructions.mjs +30 -0
  33. package/server/public/app/math-dollars.mjs +0 -89
@@ -7,7 +7,7 @@ import {
7
7
  WORKSPACE_FILE_EXTENSIONS,
8
8
  serverPlatform,
9
9
  } from './store.js';
10
- import { scanInlineDollars, splitAtLiteralDollars } from './math-dollars.mjs';
10
+ import { mathAppliesTo, setMathEnabled, texForKatex } from './markdown-math.mjs';
11
11
 
12
12
  export function workspaceMentionRegex() {
13
13
  return /(?:[A-Za-z]:[\\/])?(?:\.{1,2}[\\/])?(?:[A-Za-z0-9._-]+[\\/])*[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,16}/g;
@@ -261,6 +261,9 @@ function isAllowedPreviewAttribute(tagName, name, value) {
261
261
  return (tagName === 'video' || tagName === 'audio') && isSafePreviewUrl(value, false);
262
262
  }
263
263
  if (name === 'alt') return tagName === 'img';
264
+ // The formula placeholders of markdown-math.mjs, filled in by renderMathInHtml.
265
+ if (name === 'data-math') return tagName === 'span' && (value === 'inline' || value === 'display');
266
+ if (name === 'data-tex') return tagName === 'span';
264
267
  if (name === 'controls' || name === 'muted' || name === 'loop') {
265
268
  return (tagName === 'video' || tagName === 'audio') && value === '';
266
269
  }
@@ -372,262 +375,71 @@ export function sanitizePreviewHtml(html) {
372
375
  return template.innerHTML;
373
376
  }
374
377
 
375
- const COMPATIBILITY_DISPLAY_MATH_COMMAND = /\\(?:frac|sqrt|sum|prod|int|iint|iiint|lim|log|ln|sin|cos|tan|theta|alpha|beta|gamma|delta|lambda|mu|pi|sigma|phi|omega|vec|hat|bar|overline|underline|left|right|begin|end|text|mathrm|mathbf|mathbb|mathcal|ce)\b/i;
376
- const INLINE_MATH_COMMAND = /\\(?:frac|sqrt|sum|prod|int|iint|iiint|lim|log|ln|sin|cos|tan|cot|sec|csc|arcsin|arccos|theta|alpha|beta|gamma|delta|lambda|mu|pi|rho|sigma|phi|omega|Omega|Gamma|Delta|Lambda|Pi|Sigma|Phi|Psi|Omega|vec|hat|bar|overline|underline|left|right|text|mathrm|mathbf|mathbb|mathcal|operatorname|circ|degree|times|cdot|pm|mp|leq|geq|neq|approx|equiv|infty|partial|nabla|forall|exists|rightarrow|longmapsto|to|mapsto|ce)\b/;
377
- const STRICT_INLINE_SCRIPT = /(?:\^[{]|\^[-+]?\d|_[{]|_[-+]?\d)/;
378
- const URL_LIKE_TEXT = /\b(?:https?|ftp):\/\/|www\./i;
379
- const MARKDOWN_ESCAPABLE_TEX_CHARACTERS = new Set(Array.from('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'));
378
+ // Rendered formulas by mode and TeX; null for one KaTeX refused. A streaming
379
+ // reply is rendered again with every delta, and its earlier formulas come
380
+ // from here.
381
+ const MATH_HTML_CACHE_MAX = 500;
382
+ const mathHtmlCache = new Map();
380
383
 
381
- function isStrictInlineMathCandidate(value) {
382
- const text = String(value || '').trim();
383
- if (!text || text.length > 240 || URL_LIKE_TEXT.test(text)) return false;
384
- return INLINE_MATH_COMMAND.test(text) || STRICT_INLINE_SCRIPT.test(text);
385
- }
386
-
387
- function findClosingParenthesis(source, openingIndex) {
388
- let depth = 0;
389
- for (let index = openingIndex; index < source.length; index += 1) {
390
- if (source[index] === '\\') {
391
- index += 1;
392
- continue;
393
- }
394
- if (source[index] === '(') depth += 1;
395
- if (source[index] === ')') {
396
- depth -= 1;
397
- if (depth === 0) return index;
398
- }
399
- }
400
- return -1;
401
- }
402
-
403
- function normalizeInlineMathDelimiters(line) {
404
- let output = '';
405
- let index = 0;
406
- let inCode = false;
407
- // Only real dollar maths is skipped over; a price's `$` opens nothing.
408
- const dollarMathEnds = new Map(scanInlineDollars(line).spans);
409
-
410
- while (index < line.length) {
411
- const char = line[index];
412
- if (char === '`') {
413
- inCode = !inCode;
414
- output += char;
415
- index += 1;
416
- continue;
417
- }
418
- if (inCode) {
419
- output += char;
420
- index += 1;
421
- continue;
422
- }
423
- if (char === '$' && dollarMathEnds.has(index)) {
424
- const closeIndex = dollarMathEnds.get(index);
425
- output += line.slice(index, closeIndex + 1);
426
- index = closeIndex + 1;
427
- continue;
428
- }
429
- if (char === '\\' && line[index + 1] === '(') {
430
- const closeIndex = line.indexOf('\\)', index + 2);
431
- if (closeIndex >= 0) {
432
- output += `$${line.slice(index + 2, closeIndex).trim()}$`;
433
- index = closeIndex + 2;
434
- continue;
435
- }
436
- }
437
- if (char !== '(' || line[index - 1] === '\\') {
438
- output += char;
439
- index += 1;
440
- continue;
441
- }
442
- const closeIndex = findClosingParenthesis(line, index);
443
- if (closeIndex < 0) {
444
- output += char;
445
- index += 1;
446
- continue;
447
- }
448
- const candidate = line.slice(index + 1, closeIndex);
449
- if (!isStrictInlineMathCandidate(candidate)) {
450
- output += char;
451
- index += 1;
452
- continue;
453
- }
454
- output += `$${candidate.trim()}$`;
455
- index = closeIndex + 1;
456
- }
457
-
458
- return output;
459
- }
460
-
461
- function protectMarkdownEscapesInMath(source) {
462
- const lines = String(source ?? '').split('\n');
463
- let inFence = false;
464
-
465
- return lines.map((line) => {
466
- if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
467
- inFence = !inFence;
468
- return line;
469
- }
470
- if (inFence) return line;
471
-
472
- let output = '';
473
- let index = 0;
474
- let inCode = false;
475
- const dollarMathEnds = new Map(scanInlineDollars(line).spans);
476
- while (index < line.length) {
477
- if (line[index] === '`') {
478
- inCode = !inCode;
479
- output += line[index];
480
- index += 1;
481
- continue;
482
- }
483
- if (inCode || line[index] !== '$' || line[index - 1] === '\\') {
484
- output += line[index];
485
- index += 1;
486
- continue;
487
- }
488
- const delimiter = line[index + 1] === '$' ? '$$' : '$';
489
- // A single `$` that is a plain dollar sign (a price) opens no formula.
490
- const closeIndex = delimiter === '$'
491
- ? (dollarMathEnds.has(index) ? dollarMathEnds.get(index) : -1)
492
- : line.indexOf(delimiter, index + delimiter.length);
493
- if (closeIndex < 0) {
494
- output += line[index];
495
- index += 1;
496
- continue;
497
- }
498
- const formula = line.slice(index + delimiter.length, closeIndex);
499
- const protectedFormula = formula.replace(/\\(.)/g, (match, character) => (
500
- MARKDOWN_ESCAPABLE_TEX_CHARACTERS.has(character) ? `\\\\${character}` : match
501
- ));
502
- output += `${delimiter}${protectedFormula}${delimiter}`;
503
- index = closeIndex + delimiter.length;
504
- }
505
- return output;
506
- }).join('\n');
507
- }
508
-
509
- export function normalizeMathDelimiters(source) {
510
- const lines = String(source ?? '').split('\n');
511
- const normalized = [];
512
- let inFence = false;
513
-
514
- for (let index = 0; index < lines.length; index += 1) {
515
- let line = lines[index];
516
- if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
517
- inFence = !inFence;
518
- normalized.push(line);
519
- continue;
520
- }
521
- if (inFence || /^(?: {4}|\t)/.test(line)) {
522
- normalized.push(line);
523
- continue;
524
- }
525
-
526
- line = normalizeInlineMathDelimiters(line)
527
- .replace(/\\\[([^\n]+?)\\\]/g, (_match, formula) => `$$${formula.trim()}$$`);
528
- const delimiter = line.trim();
529
- const closingDelimiter = delimiter === '$$'
530
- ? '$$'
531
- : delimiter === '\\['
532
- ? '\\]'
533
- : delimiter === '['
534
- ? ']'
535
- : '';
536
- if (!closingDelimiter) {
537
- normalized.push(line);
538
- continue;
539
- }
540
-
541
- let closingIndex = index + 1;
542
- while (closingIndex < lines.length && lines[closingIndex].trim() !== closingDelimiter) closingIndex += 1;
543
- if (closingIndex >= lines.length) {
544
- normalized.push(line);
545
- continue;
546
- }
547
-
548
- const formula = lines.slice(index + 1, closingIndex).join('\n');
549
- if (delimiter === '[' && !COMPATIBILITY_DISPLAY_MATH_COMMAND.test(formula)) {
550
- normalized.push(line);
551
- continue;
552
- }
553
- normalized.push(`$$${formula.trim().replace(/[ \t]*\n[ \t]*/g, ' ')}$$`);
554
- index = closingIndex;
384
+ function katexHtml(tex, display) {
385
+ const key = `${display ? 'd' : 'i'}${tex}`;
386
+ if (mathHtmlCache.has(key)) return mathHtmlCache.get(key);
387
+ let html = null;
388
+ try {
389
+ html = globalThis.katex.renderToString(texForKatex(tex), {
390
+ displayMode: display,
391
+ throwOnError: true,
392
+ // Set, not left to KaTeX's default ('warn'): input LaTeX would not
393
+ // take, and a character KaTeX has no metrics for (an emoji), is set
394
+ // as well as it can be, without a console warning per render.
395
+ strict: 'ignore',
396
+ trust: false,
397
+ // A command KaTeX does not trust (\href, \url, \includegraphics) is
398
+ // set as its own name, in this colour: the text's, not red.
399
+ errorColor: 'inherit',
400
+ });
401
+ } catch {
402
+ // Nothing KaTeX can set (an unknown command, a syntax error): the source
403
+ // stays as it was written. Prose that got past the delimiter rules ends
404
+ // here too, so it never shows as a broken formula.
555
405
  }
556
-
557
- return protectMarkdownEscapesInMath(normalized.join('\n'));
406
+ if (mathHtmlCache.size >= MATH_HTML_CACHE_MAX) mathHtmlCache.clear();
407
+ mathHtmlCache.set(key, html);
408
+ return html;
558
409
  }
559
410
 
560
- const MATH_IGNORED_TAGS = ['pre', 'code', 'script', 'style', 'textarea', 'option', 'a'];
561
- const LITERAL_DOLLAR_CLASS = 'literal-dollar';
562
-
563
411
  /**
564
- * KaTeX's auto-render pairs any two `$` in a block, so "$5 … $9" would become
565
- * one formula. Each plain dollar sign (see math-dollars.mjs) is moved into a
566
- * span of its own that auto-render skips; with no partner in its text node,
567
- * it stays a dollar sign.
412
+ * Fills the formula placeholders of markdown-math.mjs with KaTeX's rendering.
413
+ * It runs after the sanitiser: KaTeX generates its own layout styles, which
414
+ * are necessary for fractions, scripts and matrices and which that pass would
415
+ * strip. Without KaTeX the placeholders stay, and read as the source.
568
416
  */
569
- function isolateLiteralDollars(container) {
570
- const ignored = MATH_IGNORED_TAGS.join(',');
571
- const walker = document.createTreeWalker(container, 4 /* NodeFilter.SHOW_TEXT */);
572
- const textNodes = [];
573
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
574
- if (node.nodeValue.includes('$') && !node.parentElement?.closest(ignored)) textNodes.push(node);
575
- }
576
- for (const node of textNodes) {
577
- const parts = splitAtLiteralDollars(node.nodeValue);
578
- if (!parts.some((part) => part.literal)) continue;
579
- const fragment = document.createDocumentFragment();
580
- for (const part of parts) {
581
- if (!part.literal) {
582
- fragment.appendChild(document.createTextNode(part.text));
583
- continue;
584
- }
585
- const span = document.createElement('span');
586
- span.className = LITERAL_DOLLAR_CLASS;
587
- span.textContent = part.text;
588
- fragment.appendChild(span);
589
- }
590
- node.parentNode.replaceChild(fragment, node);
591
- }
592
- }
593
-
594
417
  function renderMathInHtml(html) {
595
- const renderMathInElement = globalThis.renderMathInElement;
596
- if (typeof renderMathInElement !== 'function' || !globalThis.document?.createElement) return html;
418
+ if (!html.includes('data-math=')) return html;
419
+ if (typeof globalThis.katex?.renderToString !== 'function' || !globalThis.document?.createElement) return html;
597
420
 
598
421
  const container = document.createElement('div');
599
422
  container.innerHTML = html;
600
- try {
601
- isolateLiteralDollars(container);
602
- renderMathInElement(container, {
603
- delimiters: [
604
- { left: '$$', right: '$$', display: true },
605
- { left: '\\[', right: '\\]', display: true },
606
- { left: '\\(', right: '\\)', display: false },
607
- { left: '$', right: '$', display: false },
608
- ],
609
- ignoredTags: MATH_IGNORED_TAGS,
610
- ignoredClasses: [LITERAL_DOLLAR_CLASS],
611
- throwOnError: false,
612
- strict: 'ignore',
613
- trust: false,
614
- });
615
- } catch (error) {
616
- console.warn('[markdown] could not render mathematical notation', error);
617
- return html;
423
+ for (const placeholder of container.querySelectorAll('span[data-math]')) {
424
+ const display = placeholder.getAttribute('data-math') === 'display';
425
+ const rendered = katexHtml(placeholder.getAttribute('data-tex') || '', display);
426
+ placeholder.removeAttribute('data-tex');
427
+ if (rendered === null) placeholder.classList.add('math-source');
428
+ else placeholder.innerHTML = rendered;
618
429
  }
619
- // The original Markdown was sanitized before this pass. KaTeX generates its own
620
- // layout styles, which are necessary for fractions, scripts, and matrices.
621
430
  return container.innerHTML;
622
431
  }
623
432
 
624
433
  export function renderMarkdownPreview(source, allowEmbeddedHtml) {
625
- const sourceText = normalizeMathDelimiters(source);
434
+ const sourceText = String(source ?? '');
626
435
  const fallbackEscaped = `<p>${escHtml(sourceText).replace(/\n/g, '<br>')}</p>`;
627
436
  const markdown = globalThis.marked;
628
437
  if (!markdown || typeof markdown.parse !== 'function' || typeof markdown.Renderer !== 'function') {
629
438
  return fallbackEscaped;
630
439
  }
440
+ // A message without a maths delimiter, or a very long one, is parsed
441
+ // without the maths rules.
442
+ setMathEnabled(mathAppliesTo(sourceText));
631
443
  if (allowEmbeddedHtml) {
632
444
  const parsed = markdown.parse(sourceText);
633
445
  if (typeof parsed !== 'string') return fallbackEscaped;
@@ -1,9 +1,13 @@
1
1
  import { revealSelectedTabs, watchTabStripFades } from './tab-strip-fade.mjs';
2
2
 
3
3
  const SETTINGS_TAB_STORAGE_KEY = 'copilot_settings_tab';
4
+ export const SETTINGS_TAB_SHOWN_EVENT = 'oar:settings-tab-shown';
4
5
  const SETTINGS_PROVIDER_TAB_STORAGE_KEY = 'copilot_settings_provider_tab';
5
6
 
6
- const SETTINGS_TABS = ['general', 'providers', 'relays', 'previews', 'shares', 'notifications', 'features'];
7
+ // General leads: `SETTINGS_TABS[0]` is the tab a first-time visitor lands on.
8
+ // A remembered tab or a deep link to General still lands there after its
9
+ // Appearance block got a tab of its own.
10
+ const SETTINGS_TABS = ['general', 'appearance', 'providers', 'relays', 'previews', 'shares', 'notifications', 'features'];
7
11
  // Copilot leads: it is the default provider, and `SETTINGS_PROVIDER_TABS[0]`
8
12
  // is also the sub-tab a first-time visitor lands on.
9
13
  const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'claude-api', 'claude-cloud', 'grok', 'cursor', 'deepseek'];
@@ -122,8 +126,13 @@ export function selectSettingsTab(tab, providerTab) {
122
126
  ensureSettingsTabsBound();
123
127
  const nextTab = normalizeTab(tab, SETTINGS_TABS) || storedSettingsTab();
124
128
  const nextProviderTab = normalizeTab(providerTab, SETTINGS_PROVIDER_TABS) || storedProviderTab();
129
+ const previousTab = document.querySelector('#settings-modal [data-settings-tab][aria-selected="true"]')?.dataset.settingsTab;
125
130
  if (applyTabState('data-settings-tab', 'data-settings-panel', nextTab)) {
126
131
  writeLocalStorage(SETTINGS_TAB_STORAGE_KEY, nextTab);
132
+ // A tab switched to re-reads what it shows (Relays: the relay name).
133
+ if (nextTab !== previousTab && typeof window !== 'undefined' && window.CustomEvent) {
134
+ window.dispatchEvent(new window.CustomEvent(SETTINGS_TAB_SHOWN_EVENT, { detail: { tab: nextTab } }));
135
+ }
127
136
  }
128
137
  if (applyTabState('data-settings-provider-tab', 'data-settings-provider-panel', nextProviderTab)) {
129
138
  writeLocalStorage(SETTINGS_PROVIDER_TAB_STORAGE_KEY, nextProviderTab);
@@ -5,6 +5,7 @@ import { resolveRelayDotState } from './relay-dot-state.mjs';
5
5
  import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
6
6
  import { isChatInteractionHeld } from './selection-guard.mjs';
7
7
  import { nextSubagentStatus } from './subagent-fold.mjs';
8
+ import { useMathMarkdown } from './markdown-math.mjs';
8
9
  import { useStrictStrikethrough } from './markdown-strikethrough.mjs';
9
10
  import { parseGroupFoldChoices, pruneGroupFoldChoices } from './conversation-group-state.mjs';
10
11
  import { isPhoneLandscapeLayout, shouldCollapseListAfterPick, syncPhoneLandscapeAttribute } from './phone-landscape.mjs';
@@ -205,6 +206,7 @@ const CONVERSATION_SCROLL_STORAGE_PREFIX = 'copilot_message_scroll_';
205
206
  if (globalThis.marked && typeof globalThis.marked.setOptions === 'function') {
206
207
  globalThis.marked.setOptions({ breaks: true });
207
208
  useStrictStrikethrough(globalThis.marked);
209
+ useMathMarkdown(globalThis.marked);
208
210
  } else {
209
211
  console.warn('[store] marked unavailable; markdown rendering will use plain-text fallback.');
210
212
  }
@@ -1,4 +1,4 @@
1
- // "Stream view" (Settings › General › Appearance, and its mirror in the 🧠
1
+ // "Stream view" (Settings › Appearance, and its mirror in the 🧠
2
2
  // modal): an agent turn's thoughts, tool calls and reply as one list in the
3
3
  // order they happened, live and in the finished message. A setting of this
4
4
  // browser, kept in localStorage like the thoughts one; off unless switched
@@ -1,4 +1,4 @@
1
- // "Expand thoughts while an agent works" (Settings › General, and its mirror
1
+ // "Expand thoughts while an agent works" (Settings › Appearance, and its mirror
2
2
  // in the 🧠 modal). A setting of this browser, kept in localStorage like the
3
3
  // scroll-on-send one. On unless switched off: it decides whether a thought
4
4
  // starts open in the live turn, in a live sub-agent bubble and in the
@@ -14,17 +14,22 @@ let inspectorState = {
14
14
  listenersBound: false,
15
15
  };
16
16
 
17
- const XTERM_STYLE_CANDIDATES = [
18
- 'https://cdn.jsdelivr.net/npm/xterm@5.5.0/css/xterm.min.css',
19
- 'https://unpkg.com/xterm@5.5.0/css/xterm.min.css',
17
+ // The files index.html loads, and unpkg as the fallback: one package version
18
+ // and one hash per file, so either CDN must serve the same bytes.
19
+ const XTERM_STYLE_INTEGRITY = 'sha384-8Xk9wy/gzEDUKrXtrmCFa2bBuK3BpjpDuL/p0SeKQX19Khl/M+lHOgD/CyYf7efP';
20
+ const XTERM_SCRIPT_INTEGRITY = 'sha384-M169f14mRZOXm3hD/v2Ti0ThIT/RnAQagXA9nlE15yHAtrW19gdePJh/HaTzUOe/';
21
+ const XTERM_FIT_SCRIPT_INTEGRITY = 'sha384-iF+jqbuti4XlB64clWgFWYEscb+UnSRv3VgVikGYZu+otNFnSHr7y7NcKfBnGizn';
22
+ export const XTERM_STYLE_CANDIDATES = [
23
+ { url: 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css', integrity: XTERM_STYLE_INTEGRITY },
24
+ { url: 'https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css', integrity: XTERM_STYLE_INTEGRITY },
20
25
  ];
21
- const XTERM_SCRIPT_CANDIDATES = [
22
- 'https://cdn.jsdelivr.net/npm/xterm@5.5.0/lib/xterm.min.js',
23
- 'https://unpkg.com/xterm@5.5.0/lib/xterm.min.js',
26
+ export const XTERM_SCRIPT_CANDIDATES = [
27
+ { url: 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js', integrity: XTERM_SCRIPT_INTEGRITY },
28
+ { url: 'https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js', integrity: XTERM_SCRIPT_INTEGRITY },
24
29
  ];
25
- const XTERM_FIT_SCRIPT_CANDIDATES = [
26
- 'https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.10.0/lib/xterm-addon-fit.min.js',
27
- 'https://unpkg.com/xterm-addon-fit@0.10.0/lib/xterm-addon-fit.min.js',
30
+ export const XTERM_FIT_SCRIPT_CANDIDATES = [
31
+ { url: 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js', integrity: XTERM_FIT_SCRIPT_INTEGRITY },
32
+ { url: 'https://unpkg.com/@xterm/addon-fit@0.10.0/lib/addon-fit.js', integrity: XTERM_FIT_SCRIPT_INTEGRITY },
28
33
  ];
29
34
 
30
35
  let xtermLoadPromise = null;
@@ -64,7 +69,7 @@ function stripAnsiControlCodes(input = '') {
64
69
  .replace(/\r(?!\n)/g, '');
65
70
  }
66
71
 
67
- function loadScript(src) {
72
+ function loadScript(src, integrity = '') {
68
73
  return new Promise((resolve, reject) => {
69
74
  const timeoutMs = 3500;
70
75
  let settled = false;
@@ -101,6 +106,10 @@ function loadScript(src) {
101
106
  }
102
107
  const script = document.createElement('script');
103
108
  script.src = src;
109
+ if (integrity) {
110
+ script.integrity = integrity;
111
+ script.crossOrigin = 'anonymous';
112
+ }
104
113
  script.async = true;
105
114
  script.onload = () => {
106
115
  script.dataset.loaded = 'true';
@@ -114,9 +123,9 @@ function loadScript(src) {
114
123
  });
115
124
  }
116
125
 
117
- function ensureStylesheet(urlCandidates = []) {
118
- for (const href of urlCandidates) {
119
- const hrefText = String(href || '').trim();
126
+ function ensureStylesheet(candidates = []) {
127
+ for (const { url, integrity } of candidates) {
128
+ const hrefText = String(url || '').trim();
120
129
  if (!hrefText) continue;
121
130
  const already = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
122
131
  .some((node) => String(node.getAttribute('href') || '').trim() === hrefText);
@@ -124,6 +133,10 @@ function ensureStylesheet(urlCandidates = []) {
124
133
  const link = document.createElement('link');
125
134
  link.rel = 'stylesheet';
126
135
  link.href = hrefText;
136
+ if (integrity) {
137
+ link.integrity = integrity;
138
+ link.crossOrigin = 'anonymous';
139
+ }
127
140
  document.head.appendChild(link);
128
141
  return;
129
142
  }
@@ -131,11 +144,11 @@ function ensureStylesheet(urlCandidates = []) {
131
144
 
132
145
  async function loadFirstSuccessful(candidates = []) {
133
146
  let lastError = null;
134
- for (const candidate of candidates) {
135
- const src = String(candidate || '').trim();
147
+ for (const { url, integrity } of candidates) {
148
+ const src = String(url || '').trim();
136
149
  if (!src) continue;
137
150
  try {
138
- await loadScript(src);
151
+ await loadScript(src, integrity);
139
152
  return true;
140
153
  } catch (error) {
141
154
  lastError = error;