@oar-sh/oar 0.9.24 → 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/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
- package/CHANGELOG.md +284 -0
- package/README.md +12 -11
- package/package.json +5 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
- 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/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
- package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-turn-runner.mjs +3 -1
- package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
- package/server/migrations/0039-media-index.mjs +167 -0
- package/server/migrations/0040-image-session-defaults.mjs +93 -0
- package/server/public/app/api-client.js +58 -0
- package/server/public/app/ask-user-view.js +2 -1
- package/server/public/app/attachments-view.js +66 -9
- package/server/public/app/bootstrap.js +85 -25
- package/server/public/app/chat-actions-menu-state.mjs +1 -0
- package/server/public/app/context-media-tab.js +143 -0
- package/server/public/app/context-usage-view.mjs +2 -2
- package/server/public/app/conversation-view.js +90 -5
- package/server/public/app/image-composer-ui.js +518 -0
- package/server/public/app/image-turn-placeholder.mjs +233 -0
- package/server/public/app/journal-view.js +12 -2
- package/server/public/app/live-activity-cap.mjs +3 -3
- package/server/public/app/markdown-math.mjs +471 -0
- package/server/public/app/media-gallery-model.mjs +202 -0
- package/server/public/app/media-gallery-ui.js +524 -0
- package/server/public/app/media-view.js +79 -0
- package/server/public/app/openai-image-composer.mjs +492 -0
- package/server/public/app/openai-image-cost-ui.js +426 -0
- package/server/public/app/openai-image-spend.mjs +443 -0
- package/server/public/app/plan-usage-view.mjs +38 -6
- package/server/public/app/pwa-install.js +25 -4
- package/server/public/app/relay-error-ctas.mjs +5 -0
- 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/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +4 -0
- package/server/public/app/settings-tabs.js +10 -1
- package/server/public/app/socket-handlers.js +26 -0
- package/server/public/app/store.js +2 -0
- package/server/public/app/stream-view-preference.mjs +1 -1
- package/server/public/app/tab-strip-fade.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/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +517 -123
- package/server/relay-tools.md +1 -0
- package/server/repositories/image-conversation-repository.mjs +69 -0
- package/server/repositories/media-repository.mjs +305 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1410 -360
- package/server/routes/relay-board-routes.mjs +46 -1
- package/server/routes/remote-relay-routes.mjs +1 -1
- package/server/routes/sessions-routes.mjs +361 -14
- package/server/server-runtime.mjs +21 -0
- package/server/services/database-file.mjs +6 -4
- package/server/services/generated-image-cleanup-service.mjs +2 -1
- package/server/services/generated-image-files.mjs +110 -0
- package/server/services/image-input-paths.mjs +389 -0
- package/server/services/image-operation-service.mjs +6 -2
- package/server/services/media-extract.mjs +653 -0
- package/server/services/media-index-service.mjs +221 -0
- package/server/services/openai-image-preview-turns.mjs +330 -0
- package/server/services/openai-image-spend-service.mjs +516 -0
- package/server/services/openai-image-stream.mjs +170 -0
- package/server/services/plan-usage-openai-image.mjs +148 -0
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-queue.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +409 -23
- package/server/services/remote-relay-registry-service.mjs +10 -2
- package/server/services/session-worker-stop-service.mjs +50 -18
- package/server/services/token-usage-service.mjs +15 -0
- package/shared/image-header.mjs +90 -0
- package/shared/math-instructions.mjs +30 -0
- package/shared/openai-image-options.mjs +299 -0
- package/shared/openai-image-presets.mjs +175 -0
- package/shared/openai-image-previews.mjs +40 -0
- package/shared/openai-image-prices.mjs +291 -0
- package/shared/openai-image-spend.mjs +90 -0
- package/shared/remote-relay-contract.mjs +126 -4
- package/shared/worker-runtime/direct-openai-image.mjs +96 -50
- package/server/public/app/math-dollars.mjs +0 -89
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
upsertRemoteRelay,
|
|
24
24
|
} from './remote-relays-store.mjs';
|
|
25
25
|
import { REMOTE_RELAY_PERMISSIONS, normalizeRemoteRelayPermission } from './remote-relay-shared.mjs';
|
|
26
|
+
import { currentPwaAppName, isPwaAppNameSaveInFlight, syncPwaAppNameInput } from './pwa-install.js';
|
|
27
|
+
import { SETTINGS_TAB_SHOWN_EVENT } from './settings-tabs.js';
|
|
26
28
|
|
|
27
29
|
export const REMOTE_RELAY_PERMISSION_LABELS = Object.freeze({
|
|
28
30
|
read: 'Read only',
|
|
@@ -54,6 +56,9 @@ let listLoadFailed = false;
|
|
|
54
56
|
const rowBusy = new Map();
|
|
55
57
|
let listRenderDeferred = false;
|
|
56
58
|
let listFocusBound = false;
|
|
59
|
+
// The relay's name as the last snapshot had it: a change made elsewhere is
|
|
60
|
+
// read into the Relay name field.
|
|
61
|
+
let lastSelfName = null;
|
|
57
62
|
|
|
58
63
|
function el(id) {
|
|
59
64
|
return document.getElementById(id);
|
|
@@ -164,12 +169,10 @@ function setStatusLine(id, state, text) {
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
function renderSelfSection() {
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
nameEl.textContent = name || '—';
|
|
172
|
-
}
|
|
172
|
+
// Empty, the field shows the name the relay goes by then (its host name).
|
|
173
|
+
const nameInput = el('pwa-app-name-input');
|
|
174
|
+
const defaultName = String(getRemoteRelaysSnapshot()?.self?.defaultName || '').trim();
|
|
175
|
+
if (nameInput && defaultName) nameInput.placeholder = defaultName;
|
|
173
176
|
const input = el('remote-relays-public-url-input');
|
|
174
177
|
if (input) {
|
|
175
178
|
input.placeholder = browserSelfUrl() || 'https://…';
|
|
@@ -590,9 +593,27 @@ export function resetRemoteRelaysSettingsForTests() {
|
|
|
590
593
|
addInFlight = false;
|
|
591
594
|
listLoadFailed = false;
|
|
592
595
|
listRenderDeferred = false;
|
|
596
|
+
lastSelfName = null;
|
|
593
597
|
rowBusy.clear();
|
|
594
598
|
}
|
|
595
599
|
|
|
600
|
+
// A rename on another device (the relay announces it with the list) or the
|
|
601
|
+
// Relays tab coming into view reads the name again; the field keeps what is
|
|
602
|
+
// being typed in it (pwa-install.js).
|
|
603
|
+
function refreshRelayNameOnChange(snapshot) {
|
|
604
|
+
const name = typeof snapshot?.self?.name === 'string' ? snapshot.self.name : null;
|
|
605
|
+
if (name === null) return;
|
|
606
|
+
const changed = lastSelfName !== null && name !== lastSelfName;
|
|
607
|
+
lastSelfName = name;
|
|
608
|
+
if (!changed) return;
|
|
609
|
+
// This page's own rename: its answer paints the field.
|
|
610
|
+
if (isPwaAppNameSaveInFlight()) return;
|
|
611
|
+
// A name this page already has (an empty one goes by the default).
|
|
612
|
+
const known = currentPwaAppName() || String(snapshot.self.defaultName || '');
|
|
613
|
+
if (name === known) return;
|
|
614
|
+
syncPwaAppNameInput();
|
|
615
|
+
}
|
|
616
|
+
|
|
596
617
|
// The relay's own settings ride along in the list payload's `self` (GET and
|
|
597
618
|
// remote_relays_updated alike), so a change made on another device shows here
|
|
598
619
|
// too — except while this page is saving the same field.
|
|
@@ -608,6 +629,7 @@ function applySelfSettingsFromSnapshot(snapshot) {
|
|
|
608
629
|
// Live: a remote_relays_updated event (or any other store change) repaints.
|
|
609
630
|
subscribeRemoteRelays((snapshot) => {
|
|
610
631
|
applySelfSettingsFromSnapshot(snapshot);
|
|
632
|
+
refreshRelayNameOnChange(snapshot);
|
|
611
633
|
renderRemoteRelaysSection();
|
|
612
634
|
});
|
|
613
635
|
|
|
@@ -615,4 +637,9 @@ if (typeof window !== 'undefined' && typeof window.addEventListener === 'functio
|
|
|
615
637
|
window.addEventListener(RELAY_NAME_CHANGED_EVENT, () => {
|
|
616
638
|
void refreshRemoteRelays();
|
|
617
639
|
});
|
|
640
|
+
window.addEventListener(SETTINGS_TAB_SHOWN_EVENT, (event) => {
|
|
641
|
+
if (event?.detail?.tab !== 'relays') return;
|
|
642
|
+
syncPwaAppNameInput();
|
|
643
|
+
void refreshRemoteRelays();
|
|
644
|
+
});
|
|
618
645
|
}
|
|
@@ -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;
|
|
@@ -77,6 +77,8 @@ function overlayDescriptors(closers) {
|
|
|
77
77
|
{ name: 'inbox', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('inbox-view-active'), close: backToSession(closers.closeInboxView) },
|
|
78
78
|
// Status the same way.
|
|
79
79
|
{ name: 'status', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('status-view-active'), close: backToSession(closers.closeStatusView) },
|
|
80
|
+
// And the global Media view.
|
|
81
|
+
{ name: 'media', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('media-view-active'), close: backToSession(closers.closeMediaView) },
|
|
80
82
|
{ name: 'composer-settings', find: byId('composer-settings'), isOpen: isShown, close: closers.closeComposerSettings },
|
|
81
83
|
{ name: 'chat-actions-menu', find: byId('chat-actions-menu'), isOpen: isShown, close: closers.closeChatActionsMenu },
|
|
82
84
|
{ name: 'summary-modal', find: byId('summary-modal'), isOpen: isVisible, close: closers.closeSummaryModal },
|
|
@@ -65,6 +65,7 @@ import { refreshClaudeAuthSection } from './claude-auth-ui.js';
|
|
|
65
65
|
import { refreshGrokAuthSection } from './grok-auth-ui.js';
|
|
66
66
|
import { syncDeepSeekSettingsInputs } from './deepseek-settings-ui.js';
|
|
67
67
|
import { syncClaudeApiSettingsInputs } from './claude-api-settings-ui.js';
|
|
68
|
+
import { applyOpenAIImageSpendSettings, syncOpenAIImageSpendSettings } from './openai-image-cost-ui.js';
|
|
68
69
|
|
|
69
70
|
export {
|
|
70
71
|
applyDeepSeekSettingsState,
|
|
@@ -642,6 +643,8 @@ function setOpenAISettingsControlsDisabled(disabled) {
|
|
|
642
643
|
|
|
643
644
|
export function applyOpenAISettingsState(settings = {}, { resetInputs = false } = {}) {
|
|
644
645
|
ensureOpenAISettingsInputTracking();
|
|
646
|
+
// The daily image cap, when the answer names it (openai-image-cost-ui.js).
|
|
647
|
+
if (typeof document !== 'undefined') applyOpenAIImageSpendSettings(settings || {});
|
|
645
648
|
openAISettingsState = {
|
|
646
649
|
configured: settings?.configured === true,
|
|
647
650
|
enabled: settings?.configured === true && settings?.enabled === true,
|
|
@@ -2120,6 +2123,7 @@ export function openSettingsModal(tab, providerTab) {
|
|
|
2120
2123
|
openAISettingsInputsDirty = false;
|
|
2121
2124
|
ensureOpenAISettingsInputTracking();
|
|
2122
2125
|
void syncOpenAISettingsInputs();
|
|
2126
|
+
void syncOpenAIImageSpendSettings();
|
|
2123
2127
|
claudeSettingsInputsDirty = false;
|
|
2124
2128
|
ensureClaudeSettingsInputTracking();
|
|
2125
2129
|
void syncClaudeSettingsInputs();
|
|
@@ -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);
|
|
@@ -72,6 +72,8 @@ import { applyAgentSdkState } from './agent-sdk-ui.js';
|
|
|
72
72
|
import { renderUpdateSection } from './update-ui.js';
|
|
73
73
|
import { applyHostSuspendState, applyRelayShutdownState } from './host-suspend-ui.js';
|
|
74
74
|
import { applyClaudeUsageLimit, applyConversationBilling, applyUsageLimitPause } from './usage-limit-ui.js';
|
|
75
|
+
import { applyOpenAIImagePresets, applyOpenAIImageSpendState } from './openai-image-cost-ui.js';
|
|
76
|
+
import { OPENAI_IMAGE_PRESETS_EVENT, OPENAI_IMAGE_USAGE_EVENT } from './openai-image-spend.mjs';
|
|
75
77
|
import {
|
|
76
78
|
showThinking,
|
|
77
79
|
removeThinking,
|
|
@@ -111,6 +113,12 @@ import { mergeRelayThoughts } from './relay-thoughts.mjs';
|
|
|
111
113
|
import { conversationPayloadCache } from './conversation-payload-cache.mjs';
|
|
112
114
|
import { planOnlyAfterBroadcast } from './plan-board-shared.mjs';
|
|
113
115
|
import { noteConversationGone } from './session-history-ui.js';
|
|
116
|
+
import { handleImagePreview, handleImagePreviewReset, handleImageStarted } from './image-turn-placeholder.mjs';
|
|
117
|
+
import {
|
|
118
|
+
OPENAI_IMAGE_PREVIEW_EVENT,
|
|
119
|
+
OPENAI_IMAGE_PREVIEW_RESET_EVENT,
|
|
120
|
+
OPENAI_IMAGE_STARTED_EVENT,
|
|
121
|
+
} from './openai-image-composer.mjs';
|
|
114
122
|
|
|
115
123
|
// A live row of the conversation on screen joins its kept window, so a
|
|
116
124
|
// switch away and back still shows it. Rows of other conversations do not:
|
|
@@ -370,6 +378,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
370
378
|
setCliOnline(true);
|
|
371
379
|
renderConvList();
|
|
372
380
|
resyncAfterConnect();
|
|
381
|
+
// For views that load from the relay on their own (the media gallery's
|
|
382
|
+
// tiles): what failed while the relay was away can be asked for again.
|
|
383
|
+
window.dispatchEvent(new CustomEvent('oar:relay-connected'));
|
|
373
384
|
// Every loaded page again, not page one only: events missed while the
|
|
374
385
|
// socket was down touched rows anywhere in the list.
|
|
375
386
|
scheduleLoadedConversationsRefresh();
|
|
@@ -442,6 +453,15 @@ export async function connectSocket(overrideDeps) {
|
|
|
442
453
|
.catch(() => {});
|
|
443
454
|
}
|
|
444
455
|
});
|
|
456
|
+
// Today's OpenAI image spend after every booking and every change of the
|
|
457
|
+
// cap or the override (the banner, Settings, Check Usage), and the image
|
|
458
|
+
// presets after every change (the composer's select, Settings).
|
|
459
|
+
socket.on(OPENAI_IMAGE_USAGE_EVENT, (payload) => {
|
|
460
|
+
applyOpenAIImageSpendState(payload);
|
|
461
|
+
});
|
|
462
|
+
socket.on(OPENAI_IMAGE_PRESETS_EVENT, (payload) => {
|
|
463
|
+
applyOpenAIImagePresets(payload);
|
|
464
|
+
});
|
|
445
465
|
// No view refresh here: the engine switch rebinds nothing — it only changes
|
|
446
466
|
// which worker the NEXT Copilot conversation spawns.
|
|
447
467
|
socket.on('copilot_settings_updated', (payload) => {
|
|
@@ -654,6 +674,12 @@ export async function connectSocket(overrideDeps) {
|
|
|
654
674
|
if (sourceMessageId) clearRelayStreamStateForMessage(sourceMessageId);
|
|
655
675
|
refreshSessionWorkerStatus().catch(() => {});
|
|
656
676
|
});
|
|
677
|
+
// A running image turn of the open conversation: its placeholder in the
|
|
678
|
+
// live bubble (timer, size, the newest partials). Other conversations'
|
|
679
|
+
// events are ignored there.
|
|
680
|
+
socket.on(OPENAI_IMAGE_STARTED_EVENT, (payload) => handleImageStarted(payload));
|
|
681
|
+
socket.on(OPENAI_IMAGE_PREVIEW_EVENT, (payload) => handleImagePreview(payload));
|
|
682
|
+
socket.on(OPENAI_IMAGE_PREVIEW_RESET_EVENT, (payload) => handleImagePreviewReset(payload));
|
|
657
683
|
// A folder's commit attribution changed (any client): an open context modal
|
|
658
684
|
// re-reads it so its row and effect text match.
|
|
659
685
|
socket.on('workspace_root_attribution_updated', () => {
|
|
@@ -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
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { FONT_SCALE_CHANGE_EVENT } from './font-scaling.js';
|
|
8
8
|
|
|
9
|
-
export const TAB_STRIP_SELECTOR = '.settings-tab-strip, .plan-usage-tab-strip';
|
|
9
|
+
export const TAB_STRIP_SELECTOR = '.settings-tab-strip, .plan-usage-tab-strip, .context-tab-strip, .media-gallery-tabs, .media-gallery-chips';
|
|
10
10
|
|
|
11
11
|
// Sub-pixel scroll ends (a zoomed page) count as the end.
|
|
12
12
|
const EDGE_SLACK_PX = 1;
|
|
@@ -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
|