@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.
- package/CHANGELOG.md +142 -0
- package/README.md +9 -9
- package/package.json +3 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
- package/server/claude-worker/claude-session-process.mjs +4 -0
- package/server/claude-worker/claude-turn-publisher.mjs +25 -11
- package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
- package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
- package/server/grok-worker/grok-turn-runner.mjs +3 -1
- package/server/public/app/context-usage-view.mjs +2 -2
- package/server/public/app/live-activity-cap.mjs +3 -3
- package/server/public/app/markdown-math.mjs +471 -0
- package/server/public/app/pwa-install.js +25 -4
- package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/remote-relays-settings.js +33 -6
- package/server/public/app/router.js +49 -237
- package/server/public/app/settings-tabs.js +10 -1
- package/server/public/app/store.js +2 -0
- package/server/public/app/stream-view-preference.mjs +1 -1
- package/server/public/app/thoughts-preference.mjs +1 -1
- package/server/public/app/tmux-inspector-view.js +29 -16
- package/server/public/index.html +134 -116
- package/server/routes/relay-board-routes.mjs +46 -1
- package/server/routes/remote-relay-routes.mjs +1 -1
- package/server/server-runtime.mjs +2 -0
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-queue.mjs +1 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -1
- package/server/services/remote-relay-registry-service.mjs +7 -1
- package/server/services/session-worker-stop-service.mjs +50 -18
- package/shared/math-instructions.mjs +30 -0
- 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 {
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
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
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
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
|
-
|
|
596
|
-
if (typeof
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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 =
|
|
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
|
-
|
|
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 ›
|
|
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 ›
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
23
|
-
'https://unpkg.com/xterm@5.5.0/lib/xterm.
|
|
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/
|
|
27
|
-
'https://unpkg.com/
|
|
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(
|
|
118
|
-
for (const
|
|
119
|
-
const hrefText = String(
|
|
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
|
|
135
|
-
const src = String(
|
|
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;
|