@oar-sh/oar 0.9.1 → 0.9.3
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/extension.mjs +0 -3
- package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
- package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
- package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
- package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
- package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
- package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
- package/CHANGELOG.md +269 -0
- package/README.md +407 -424
- package/bin/oar.js +142 -11
- package/package.json +8 -4
- package/server/README.md +59 -22
- package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
- package/server/claude-worker/claude-session-process.mjs +1013 -45
- package/server/claude-worker/claude-session-worker.mjs +54 -9
- package/server/claude-worker/claude-turn-publisher.mjs +110 -19
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/copilot-sdk-runtime.mjs +12 -3
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-attachments.mjs +35 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-model-switch.mjs +425 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
- package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
- package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
- package/server/cursor-worker/cursor-session-worker.mjs +5 -5
- package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
- package/server/db-schema.mjs +61 -1
- package/server/features.mjs +155 -41
- package/server/grok-worker/grok-session-worker.mjs +5 -5
- package/server/grok-worker/grok-turn-runner.mjs +45 -4
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/annotate-core.mjs +181 -0
- package/server/public/app/annotate-editor.js +617 -0
- package/server/public/app/api-client.js +86 -5
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +172 -3
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +34 -19
- package/server/public/app/bootstrap.js +153 -69
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/context-tier-options.mjs +24 -4
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-preferences.mjs +47 -10
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1032 -218
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +190 -24
- package/server/public/app/model-selector-options.mjs +185 -1
- package/server/public/app/new-conversation-model-choice.mjs +29 -9
- package/server/public/app/pwa-install.js +64 -136
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/reasoning-effort-labels.mjs +13 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
- package/server/public/app/router.js +120 -11
- package/server/public/app/settings-modal.js +225 -20
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +47 -4
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +159 -16
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/app/update-ui.js +194 -0
- package/server/public/index.html +483 -24
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/relay.mjs +23 -21
- package/server/repositories/message-repository.mjs +78 -21
- package/server/repositories/question-repository.mjs +19 -1
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/ask-user-routes.mjs +10 -0
- package/server/routes/messages-routes.mjs +689 -159
- package/server/routes/sessions-routes.mjs +574 -80
- package/server/server-runtime.mjs +393 -925
- package/server/services/claude-auth-service.mjs +7 -1
- package/server/services/cli-install-service.mjs +8 -27
- package/server/services/cli-process-runner.mjs +30 -0
- package/server/services/copilot-model-discovery-service.mjs +202 -0
- package/server/services/data-dir-resolution.mjs +47 -0
- package/server/services/grok-auth-service.mjs +9 -1
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/model-variant-catalog-service.mjs +1183 -0
- package/server/services/oar-cli-helpers.mjs +6 -2
- package/server/services/relay-prompt-sanitizer.mjs +30 -3
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +144 -16
- package/server/services/sdk-session-sync-service.mjs +84 -27
- package/server/services/session-worker-registry-service.mjs +116 -3
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +119 -24
- package/server/services/status-event-service.mjs +8 -2
- package/server/services/steer-resend.mjs +45 -0
- package/server/services/update-check-service.mjs +220 -0
- package/server/services/update-install-service.mjs +279 -0
- package/server/services/windows-autostart-service.mjs +42 -6
- package/server/services/windows-boot-autostart-service.mjs +487 -0
- package/shared/ask-user-bridge.mjs +37 -5
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/claude-model-catalog.mjs +49 -0
- package/shared/context-window-fallbacks.mjs +1 -0
- package/shared/control-poller.mjs +4 -1
- package/shared/copilot-model-order.mjs +182 -0
- package/shared/media-embed-instructions.mjs +35 -0
- package/shared/model-descriptors.mjs +181 -30
- package/shared/model-id.mjs +6 -0
- package/shared/pwa-app-name.mjs +44 -0
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/update-semver.mjs +50 -0
- package/shared/worker-crash-guard.mjs +40 -14
- package/shared/worker-runtime/api-client.mjs +34 -0
- package/shared/worker-runtime/config-loader.mjs +104 -0
- package/shared/worker-runtime/heartbeat.mjs +109 -0
- package/shared/worker-runtime/prompt-context.mjs +160 -0
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
- package/server/claude-worker/claude-session-test-harness.mjs +0 -236
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.sh +0 -62
- package/server/services/fixtures/cli-install-stub.sh +0 -75
- package/server/services/fixtures/grok-stub.sh +0 -86
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { BASE, IS_SHARED_VIEW, showTransientRelayNotice } from './store.js';
|
|
2
|
+
import { loadPwaAppNameSetting, updatePwaAppNameSetting } from './api-client.js';
|
|
2
3
|
|
|
3
4
|
const THEME_COLOR_BASE = '#0d1117';
|
|
4
5
|
const THEME_COLOR_IMMERSIVE = '#161b22';
|
|
6
|
+
// Legacy per-browser storage of the app name; kept only so adoptLegacyPwaAppName
|
|
7
|
+
// can move an existing value to the server once. The name itself now lives in
|
|
8
|
+
// app_settings and is served by /manifest.webmanifest.
|
|
5
9
|
const PWA_APP_NAME_STORAGE_KEY = 'copilot_pwa_app_name';
|
|
6
|
-
const PWA_APP_NAME_DEFAULT = 'OAR';
|
|
7
|
-
const PWA_APP_NAME_MAX_LENGTH = 60;
|
|
8
10
|
const INSTALLED_DISPLAY_MODE_QUERIES = ['(display-mode: standalone)', '(display-mode: fullscreen)'];
|
|
9
11
|
|
|
10
12
|
let deferredInstallPrompt = null;
|
|
11
13
|
let pendingInstalledFullscreenGesture = false;
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
+
let pwaAppName = '';
|
|
15
|
+
// Monotonic sequence so overlapping loads/saves resolve latest-wins: a stale
|
|
16
|
+
// background GET must never repaint over a just-saved name, and a second edit
|
|
17
|
+
// must never be dropped in favor of an older in-flight save.
|
|
18
|
+
let pwaAppNameRequestSeq = 0;
|
|
14
19
|
|
|
15
20
|
function matchesDisplayMode(query) {
|
|
16
21
|
try {
|
|
@@ -298,152 +303,75 @@ export function initFullscreenButton() {
|
|
|
298
303
|
updateFullscreenButton();
|
|
299
304
|
}
|
|
300
305
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return allowEmpty
|
|
305
|
-
? { value: '', error: null }
|
|
306
|
-
: { value: '', error: 'App name cannot be empty.' };
|
|
307
|
-
}
|
|
308
|
-
if (normalized.length > PWA_APP_NAME_MAX_LENGTH) {
|
|
309
|
-
return { value: '', error: `App name must be ${PWA_APP_NAME_MAX_LENGTH} characters or fewer.` };
|
|
310
|
-
}
|
|
311
|
-
return { value: normalized, error: null };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function derivePwaShortName(name) {
|
|
315
|
-
const text = String(name || '').trim();
|
|
316
|
-
if (!text) return 'Copilot';
|
|
317
|
-
const firstWord = text.split(/\s+/)[0] || text;
|
|
318
|
-
if (firstWord.length <= 12) return firstWord;
|
|
319
|
-
return text.slice(0, 12).trim() || 'Copilot';
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function resolveManifestUrlValue(rawValue, baseHref) {
|
|
323
|
-
const value = String(rawValue || '').trim();
|
|
324
|
-
if (!value || value.startsWith('data:') || value.startsWith('blob:')) return value;
|
|
325
|
-
try {
|
|
326
|
-
return new URL(value, baseHref).href;
|
|
327
|
-
} catch {
|
|
328
|
-
return value;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function normalizeManifestForBlob(manifest, defaultHref) {
|
|
333
|
-
const baseHref = new URL(String(defaultHref || '').trim(), window.location.href).href;
|
|
334
|
-
const next = { ...(manifest || {}) };
|
|
335
|
-
next.id = resolveManifestUrlValue(next.id, baseHref);
|
|
336
|
-
next.start_url = resolveManifestUrlValue(next.start_url, baseHref);
|
|
337
|
-
next.scope = resolveManifestUrlValue(next.scope, baseHref);
|
|
338
|
-
if (Array.isArray(next.icons)) {
|
|
339
|
-
next.icons = next.icons.map((icon) => {
|
|
340
|
-
if (!icon || typeof icon !== 'object') return icon;
|
|
341
|
-
const source = resolveManifestUrlValue(icon.src, baseHref);
|
|
342
|
-
return { ...icon, src: source };
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
return next;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function readStoredPwaAppName() {
|
|
349
|
-
const { value } = normalizePwaAppName(localStorage.getItem(PWA_APP_NAME_STORAGE_KEY), { allowEmpty: true });
|
|
350
|
-
return value;
|
|
351
|
-
}
|
|
306
|
+
// The app name is stored on the relay and baked into /manifest.webmanifest by
|
|
307
|
+
// the server, so every manifest fetch — including Android's out-of-page WebAPK
|
|
308
|
+
// update checks — sees the same name. The client only edits the setting.
|
|
352
309
|
|
|
353
|
-
|
|
310
|
+
function paintPwaAppNameInput() {
|
|
354
311
|
const input = document.getElementById('pwa-app-name-input');
|
|
355
312
|
if (!input) return;
|
|
356
|
-
|
|
313
|
+
// Don't clobber typing when a background refresh lands mid-edit.
|
|
314
|
+
if (document.activeElement === input) return;
|
|
315
|
+
input.value = pwaAppName;
|
|
357
316
|
}
|
|
358
317
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
display_override: ['standalone'],
|
|
369
|
-
display: 'standalone',
|
|
370
|
-
background_color: '#161b22',
|
|
371
|
-
theme_color: '#161b22',
|
|
372
|
-
icons: [
|
|
373
|
-
{ src: 'app-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' },
|
|
374
|
-
{ src: 'app-icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
375
|
-
{ src: 'app-icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
376
|
-
],
|
|
377
|
-
};
|
|
378
|
-
try {
|
|
379
|
-
const response = await fetch(defaultHref, { cache: 'no-store' });
|
|
380
|
-
const manifest = response.ok ? await response.json() : null;
|
|
381
|
-
if (manifest && typeof manifest === 'object' && !Array.isArray(manifest)) {
|
|
382
|
-
manifestTemplateCache = manifest;
|
|
383
|
-
return { ...manifestTemplateCache };
|
|
384
|
-
}
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.warn('Failed to load manifest template; using fallback.', error);
|
|
387
|
-
}
|
|
388
|
-
manifestTemplateCache = fallback;
|
|
389
|
-
return { ...manifestTemplateCache };
|
|
318
|
+
export function syncPwaAppNameInput() {
|
|
319
|
+
paintPwaAppNameInput();
|
|
320
|
+
const seq = ++pwaAppNameRequestSeq;
|
|
321
|
+
void loadPwaAppNameSetting().then((result) => {
|
|
322
|
+
if (seq !== pwaAppNameRequestSeq) return; // superseded by a newer load/save
|
|
323
|
+
if (!result || typeof result.appName !== 'string') return;
|
|
324
|
+
pwaAppName = result.appName;
|
|
325
|
+
paintPwaAppNameInput();
|
|
326
|
+
});
|
|
390
327
|
}
|
|
391
328
|
|
|
392
|
-
export async function
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (!manifestLink.dataset.defaultHref) manifestLink.dataset.defaultHref = defaultHref;
|
|
399
|
-
|
|
400
|
-
const customName = readStoredPwaAppName();
|
|
401
|
-
if (!customName) {
|
|
402
|
-
if (customManifestUrl) {
|
|
403
|
-
URL.revokeObjectURL(customManifestUrl);
|
|
404
|
-
customManifestUrl = null;
|
|
405
|
-
}
|
|
406
|
-
manifestLink.setAttribute('href', defaultHref);
|
|
329
|
+
export async function updatePwaAppName(rawValue) {
|
|
330
|
+
const seq = ++pwaAppNameRequestSeq;
|
|
331
|
+
const result = await updatePwaAppNameSetting(rawValue);
|
|
332
|
+
if (!result || typeof result.appName !== 'string') {
|
|
333
|
+
alert('Failed to update the install app name.');
|
|
334
|
+
if (seq === pwaAppNameRequestSeq) paintPwaAppNameInput();
|
|
407
335
|
return;
|
|
408
336
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
};
|
|
416
|
-
const normalizedManifest = normalizeManifestForBlob(nextManifest, defaultHref);
|
|
417
|
-
const manifestBlob = new Blob([JSON.stringify(normalizedManifest, null, 2)], { type: 'application/manifest+json' });
|
|
418
|
-
const objectUrl = URL.createObjectURL(manifestBlob);
|
|
419
|
-
if (customManifestUrl) URL.revokeObjectURL(customManifestUrl);
|
|
420
|
-
customManifestUrl = objectUrl;
|
|
421
|
-
manifestLink.setAttribute('href', objectUrl);
|
|
337
|
+
if (seq !== pwaAppNameRequestSeq) return; // a newer edit owns the UI now
|
|
338
|
+
pwaAppName = result.appName;
|
|
339
|
+
showTransientRelayNotice(result.appName
|
|
340
|
+
? `Install app name updated to "${result.appName}".`
|
|
341
|
+
: 'Install app name reset to default.');
|
|
342
|
+
paintPwaAppNameInput();
|
|
422
343
|
}
|
|
423
344
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
345
|
+
/**
|
|
346
|
+
* One-time move of a legacy per-browser name into the relay's settings. The
|
|
347
|
+
* key is removed whether the value is adopted or the server already has a
|
|
348
|
+
* name — either way it is dead weight; only a network/auth failure keeps it
|
|
349
|
+
* so the next boot can retry.
|
|
350
|
+
*/
|
|
351
|
+
export async function adoptLegacyPwaAppName() {
|
|
352
|
+
if (IS_SHARED_VIEW) return;
|
|
353
|
+
let stored = '';
|
|
354
|
+
try {
|
|
355
|
+
stored = String(localStorage.getItem(PWA_APP_NAME_STORAGE_KEY) || '').trim();
|
|
356
|
+
} catch {
|
|
429
357
|
return;
|
|
430
358
|
}
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
359
|
+
if (!stored) return;
|
|
360
|
+
// A legacy over-long value would 400 forever; best-effort truncate instead.
|
|
361
|
+
if (stored.length > 60) stored = stored.slice(0, 60).trim();
|
|
362
|
+
const seq = ++pwaAppNameRequestSeq;
|
|
363
|
+
const current = await loadPwaAppNameSetting();
|
|
364
|
+
if (!current || typeof current.appName !== 'string') return; // offline/unauthed: retry next boot
|
|
365
|
+
let adoptedName = current.appName;
|
|
366
|
+
if (!current.appName) {
|
|
367
|
+
const adopted = await updatePwaAppNameSetting(stored);
|
|
368
|
+
if (!adopted || typeof adopted.appName !== 'string') return; // POST failed: keep the key
|
|
369
|
+
adoptedName = adopted.appName;
|
|
435
370
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
? `Install app name updated to "${normalized.value}".`
|
|
441
|
-
: 'Install app name reset to default.');
|
|
442
|
-
})
|
|
443
|
-
.catch((error) => {
|
|
444
|
-
alert(error?.message || 'Failed to apply install app name');
|
|
445
|
-
syncPwaAppNameInput();
|
|
446
|
-
});
|
|
371
|
+
try { localStorage.removeItem(PWA_APP_NAME_STORAGE_KEY); } catch {}
|
|
372
|
+
if (seq !== pwaAppNameRequestSeq) return; // the user already edited; their state wins
|
|
373
|
+
pwaAppName = adoptedName;
|
|
374
|
+
paintPwaAppNameInput();
|
|
447
375
|
}
|
|
448
376
|
|
|
449
377
|
export function registerPwaShell() {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Multi-select question cards: the pure rules the card renderer and its
|
|
2
|
+
// submit path share, kept DOM-free so they are unit-testable.
|
|
3
|
+
//
|
|
4
|
+
// Two card hints ride the question's `context` (kept by the relay's
|
|
5
|
+
// services/relay-question-context.mjs allow-list):
|
|
6
|
+
// - `multiSelect: true` — several answers are allowed. Claude states this on
|
|
7
|
+
// every AskUserQuestion question; Copilot models declare it through the
|
|
8
|
+
// relay's own `ask_user` (`multi_select`), and the worker falls back to the
|
|
9
|
+
// wording ("select all that apply").
|
|
10
|
+
// - `allowMultiSelect: true` — the provider cannot always tell (Copilot), so
|
|
11
|
+
// the card offers a "Select several" switch. Claude cards never carry it:
|
|
12
|
+
// they follow Claude's flag strictly (maintainer decision, 2026-09-25).
|
|
13
|
+
// A multi-select card offers checkmarks and one "Reply with selection"; the
|
|
14
|
+
// answer is the selected labels joined with ", " — plus whatever the user
|
|
15
|
+
// typed as an extra answer — which is Claude's own multi-select answer format.
|
|
16
|
+
|
|
17
|
+
function choiceCount(question) {
|
|
18
|
+
return Array.isArray(question?.choices) ? question.choices.length : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the card offers the "Select several" switch. */
|
|
22
|
+
export function offersMultiSelectToggle(question) {
|
|
23
|
+
if (!question || typeof question !== 'object') return false;
|
|
24
|
+
return choiceCount(question) >= 2 && question?.context?.allowMultiSelect === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the card renders checkmarks. `override` is the user's switch
|
|
29
|
+
* position for this card (true/false), or undefined while untouched — then the
|
|
30
|
+
* provider's flag decides. The switch only counts on cards that offer it.
|
|
31
|
+
*/
|
|
32
|
+
export function isMultiSelectQuestion(question, override = undefined) {
|
|
33
|
+
if (!question || typeof question !== 'object') return false;
|
|
34
|
+
if (choiceCount(question) < 2) return false;
|
|
35
|
+
if (typeof override === 'boolean' && offersMultiSelectToggle(question)) return override;
|
|
36
|
+
return question?.context?.multiSelect === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The answer string for a multi-select card: the selected labels in offer
|
|
41
|
+
* order, then the free-text answer (if any) as one more item. Empty when
|
|
42
|
+
* nothing was selected or typed.
|
|
43
|
+
*/
|
|
44
|
+
export function composeMultiSelectAnswer(selectedLabels, freeText = '') {
|
|
45
|
+
const labels = (Array.isArray(selectedLabels) ? selectedLabels : [])
|
|
46
|
+
.map((label) => String(label ?? '').trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.filter((label, index, all) => all.indexOf(label) === index);
|
|
49
|
+
const extra = String(freeText ?? '').trim();
|
|
50
|
+
if (extra && !labels.includes(extra)) labels.push(extra);
|
|
51
|
+
return labels.join(', ');
|
|
52
|
+
}
|
|
@@ -27,16 +27,26 @@ export const ULTRACODE_EFFORT_LABEL = 'Ultracode';
|
|
|
27
27
|
export const ULTRACODE_EFFORT_OPTION_TITLE =
|
|
28
28
|
'xhigh effort plus multi-agent workflow orchestration — expect much higher token use';
|
|
29
29
|
|
|
30
|
+
// Gemini and some OpenAI models offer a rung under "low". It reads like the
|
|
31
|
+
// other plain tiers (lowercase, no rename): an "Off"-style label would promise
|
|
32
|
+
// a switch the runtime does not have, and Title Case would single it out from
|
|
33
|
+
// its neighbours. The tooltip explains the rung instead.
|
|
34
|
+
export const MINIMAL_EFFORT = 'minimal';
|
|
35
|
+
export const MINIMAL_EFFORT_LABEL = 'minimal';
|
|
36
|
+
export const MINIMAL_EFFORT_OPTION_TITLE = 'Lowest reasoning tier this model offers — faster and cheaper than low';
|
|
37
|
+
|
|
30
38
|
export function reasoningEffortOptionLabel(effort, { reasoningOffUnsupported = false } = {}) {
|
|
31
39
|
const value = String(effort || '').trim().toLowerCase();
|
|
32
40
|
if (!value) return '';
|
|
33
41
|
if (value === 'none' && reasoningOffUnsupported) return PROVIDER_DEFAULT_EFFORT_LABEL;
|
|
34
42
|
if (value === CLAUDE_ULTRACODE_EFFORT) return ULTRACODE_EFFORT_LABEL;
|
|
43
|
+
if (value === MINIMAL_EFFORT) return MINIMAL_EFFORT_LABEL;
|
|
35
44
|
return value;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export function reasoningEffortOptionTitle(effort) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
const value = String(effort || '').trim().toLowerCase();
|
|
49
|
+
if (value === CLAUDE_ULTRACODE_EFFORT) return ULTRACODE_EFFORT_OPTION_TITLE;
|
|
50
|
+
if (value === MINIMAL_EFFORT) return MINIMAL_EFFORT_OPTION_TITLE;
|
|
51
|
+
return '';
|
|
42
52
|
}
|
|
@@ -102,13 +102,36 @@ function stripAttachmentPromptArtifacts(text) {
|
|
|
102
102
|
.trim();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// The once-per-worker media-embed guidance (shared/media-embed-instructions.mjs)
|
|
106
|
+
// can precede the mode marker on the Cursor/Grok prompt paths. This module is
|
|
107
|
+
// served to the browser and cannot import shared/, so the block is matched
|
|
108
|
+
// structurally — the heading plus its single paragraph — rather than verbatim.
|
|
109
|
+
const MEDIA_EMBED_BLOCK_PATTERN = /##\s*Embedding media in replies\s+[\s\S]*?(?=\n\s*\n|$)/gi;
|
|
110
|
+
|
|
111
|
+
// A steered message's hidden note (shared/steer-note.mjs) leads the prompt the
|
|
112
|
+
// runtime stored, right after any mode marker. Matched by its stable opening
|
|
113
|
+
// words, because the browser copy of this module cannot import shared/.
|
|
114
|
+
const STEER_NOTE_PATTERN = /^\s*\[Sent while you were still working on my previous message\.[^\]\n]*\]\s*/;
|
|
115
|
+
|
|
116
|
+
// The Claude worker's slash-command guard (shared/slash-command-guard.mjs): a
|
|
117
|
+
// zero-width space in front of a "/x" it delivers as text. trim() keeps it, so
|
|
118
|
+
// it is matched on its own, and only where it guards a "/".
|
|
119
|
+
const SLASH_COMMAND_GUARD_PATTERN = /^\u200B(?=\s*\/)/;
|
|
120
|
+
|
|
121
|
+
function stripLeadingRelayNotes(text) {
|
|
122
|
+
const stripped = String(text || '').replace(STEER_NOTE_PATTERN, '').replace(SLASH_COMMAND_GUARD_PATTERN, '');
|
|
123
|
+
return stripped.trim() ? stripped.trim() : String(text || '').trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
export function stripRelayPromptContext(text, relayMode = '') {
|
|
106
|
-
const value = stripAttachmentPromptArtifacts(text)
|
|
127
|
+
const value = stripAttachmentPromptArtifacts(text)
|
|
128
|
+
.replace(MEDIA_EMBED_BLOCK_PATTERN, '')
|
|
129
|
+
.trim();
|
|
107
130
|
if (!value) return '';
|
|
108
131
|
const patterns = buildPromptPrefixPatterns(relayMode);
|
|
109
132
|
for (const pattern of patterns) {
|
|
110
133
|
const stripped = value.replace(pattern, '').trim();
|
|
111
|
-
if (stripped && stripped !== value) return stripped;
|
|
134
|
+
if (stripped && stripped !== value) return stripLeadingRelayNotes(stripped);
|
|
112
135
|
}
|
|
113
|
-
return value;
|
|
136
|
+
return stripLeadingRelayNotes(value);
|
|
114
137
|
}
|
|
@@ -121,6 +121,45 @@ export function driveFileHrefFromPath(rawPath) {
|
|
|
121
121
|
return `${BASE}/api/drives/file?path=${encodeURIComponent(normalized)}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Recognize this app's own file-serving hrefs (the shapes
|
|
126
|
+
* driveFileHrefFromPath / workspaceFileHrefFromPath emit, which embedded
|
|
127
|
+
* message media points at after rewriting) and give back the original file
|
|
128
|
+
* path, so a click on an inline image can reopen it in the file viewer.
|
|
129
|
+
*/
|
|
130
|
+
export function parseAppFileHref(rawSrc) {
|
|
131
|
+
const value = String(rawSrc || '').trim();
|
|
132
|
+
if (!value) return null;
|
|
133
|
+
let parsed = null;
|
|
134
|
+
try {
|
|
135
|
+
parsed = new URL(value, window.location.origin);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (parsed.origin !== window.location.origin) return null;
|
|
140
|
+
const base = String(BASE || '');
|
|
141
|
+
const pathname = parsed.pathname || '';
|
|
142
|
+
const appPath = base && pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
143
|
+
if (appPath === '/api/drives/file') {
|
|
144
|
+
const path = String(parsed.searchParams.get('path') || '').trim();
|
|
145
|
+
return path ? { kind: 'drive', path } : null;
|
|
146
|
+
}
|
|
147
|
+
if (appPath.startsWith('/api/files/')) {
|
|
148
|
+
const path = appPath.slice('/api/files/'.length)
|
|
149
|
+
.split('/')
|
|
150
|
+
.map((segment) => {
|
|
151
|
+
try {
|
|
152
|
+
return decodeURIComponent(segment);
|
|
153
|
+
} catch {
|
|
154
|
+
return segment;
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.join('/');
|
|
158
|
+
return path ? { kind: 'workspace', path } : null;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
export function drivePreviewApiPath(rawPath) {
|
|
125
164
|
const normalized = normalizeDriveBrowserPath(rawPath);
|
|
126
165
|
if (!normalized) return '';
|
|
@@ -196,10 +235,10 @@ export function isSafePreviewUrl(url, allowDataImage = false) {
|
|
|
196
235
|
}
|
|
197
236
|
|
|
198
237
|
const PREVIEW_ALLOWED_TAGS = new Set([
|
|
199
|
-
'a', 'article', 'b', 'blockquote', 'br', 'code', 'del', 'details', 'div', 'em',
|
|
238
|
+
'a', 'article', 'audio', 'b', 'blockquote', 'br', 'code', 'del', 'details', 'div', 'em',
|
|
200
239
|
'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
|
|
201
240
|
'kbd', 'li', 'mark', 'ol', 'p', 'pre', 's', 'small', 'span', 'strong', 'sub',
|
|
202
|
-
'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul',
|
|
241
|
+
'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul', 'video',
|
|
203
242
|
]);
|
|
204
243
|
const PREVIEW_GLOBAL_ATTRIBUTES = new Set(['dir', 'lang', 'title']);
|
|
205
244
|
const PREVIEW_ALIGNMENT_VALUES = new Set(['left', 'center', 'right', 'justify']);
|
|
@@ -210,15 +249,24 @@ const PREVIEW_SPAN_VALUE = /^[1-9]\d{0,2}$/;
|
|
|
210
249
|
function isAllowedPreviewAttribute(tagName, name, value) {
|
|
211
250
|
if (PREVIEW_GLOBAL_ATTRIBUTES.has(name)) return true;
|
|
212
251
|
if (name === 'href') return tagName === 'a' && isSafePreviewUrl(value, false);
|
|
213
|
-
if (name === 'src')
|
|
252
|
+
if (name === 'src') {
|
|
253
|
+
if (tagName === 'img') return isSafePreviewUrl(value, true);
|
|
254
|
+
return (tagName === 'video' || tagName === 'audio') && isSafePreviewUrl(value, false);
|
|
255
|
+
}
|
|
214
256
|
if (name === 'alt') return tagName === 'img';
|
|
257
|
+
if (name === 'controls' || name === 'muted' || name === 'loop') {
|
|
258
|
+
return (tagName === 'video' || tagName === 'audio') && value === '';
|
|
259
|
+
}
|
|
260
|
+
if (name === 'playsinline') return tagName === 'video' && value === '';
|
|
261
|
+
if (name === 'preload') return (tagName === 'video' || tagName === 'audio')
|
|
262
|
+
&& ['none', 'metadata', 'auto'].includes(value.toLowerCase());
|
|
215
263
|
if (name === 'target') return tagName === 'a' && value === '_blank';
|
|
216
264
|
if (name === 'rel') return tagName === 'a';
|
|
217
265
|
if (name === 'align') return ['div', 'p', 'table', 'td', 'th', 'tr'].includes(tagName)
|
|
218
266
|
&& PREVIEW_ALIGNMENT_VALUES.has(value.toLowerCase());
|
|
219
267
|
if (name === 'valign') return ['td', 'th', 'tr'].includes(tagName)
|
|
220
268
|
&& PREVIEW_VERTICAL_ALIGNMENT_VALUES.has(value.toLowerCase());
|
|
221
|
-
if (name === 'width' || name === 'height') return ['img', 'table', 'td', 'th'].includes(tagName)
|
|
269
|
+
if (name === 'width' || name === 'height') return ['img', 'table', 'td', 'th', 'video'].includes(tagName)
|
|
222
270
|
&& PREVIEW_DIMENSION_VALUE.test(value);
|
|
223
271
|
if (name === 'rowspan' || name === 'colspan') return ['td', 'th'].includes(tagName)
|
|
224
272
|
&& PREVIEW_SPAN_VALUE.test(value);
|
|
@@ -230,27 +278,88 @@ function isAllowedPreviewAttribute(tagName, name, value) {
|
|
|
230
278
|
return false;
|
|
231
279
|
}
|
|
232
280
|
|
|
281
|
+
const EMBED_VIDEO_EXTENSIONS = new Set(['mp4', 'webm', 'm4v', 'mov']);
|
|
282
|
+
const EMBED_AUDIO_EXTENSIONS = new Set(['mp3', 'm4a', 'ogg', 'opus', 'wav', 'flac']);
|
|
283
|
+
|
|
284
|
+
// 'video' | 'audio' | '' for an already-safe embed src; drive/workspace hrefs
|
|
285
|
+
// carry the real file path in their `path` query parameter.
|
|
286
|
+
function mediaKindForEmbedSource(src) {
|
|
287
|
+
const value = String(src || '').trim();
|
|
288
|
+
if (!value || value.toLowerCase().startsWith('data:')) return '';
|
|
289
|
+
let candidate = value;
|
|
290
|
+
try {
|
|
291
|
+
const parsed = new URL(value, window.location.origin);
|
|
292
|
+
candidate = parsed.searchParams.get('path') || parsed.pathname;
|
|
293
|
+
} catch {}
|
|
294
|
+
const match = /\.([A-Za-z0-9]{1,5})$/.exec(String(candidate || '').trim());
|
|
295
|
+
const extension = match ? match[1].toLowerCase() : '';
|
|
296
|
+
if (EMBED_VIDEO_EXTENSIONS.has(extension)) return 'video';
|
|
297
|
+
if (EMBED_AUDIO_EXTENSIONS.has(extension)) return 'audio';
|
|
298
|
+
return '';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function convertImageToMediaElement(img, mediaTag, src) {
|
|
302
|
+
const media = document.createElement(mediaTag);
|
|
303
|
+
media.setAttribute('src', src);
|
|
304
|
+
media.setAttribute('controls', '');
|
|
305
|
+
media.setAttribute('preload', 'metadata');
|
|
306
|
+
const alt = String(img.getAttribute('alt') || '').trim();
|
|
307
|
+
if (alt) media.setAttribute('title', alt);
|
|
308
|
+
if (mediaTag === 'video') {
|
|
309
|
+
for (const dimension of ['width', 'height']) {
|
|
310
|
+
const value = String(img.getAttribute(dimension) || '').trim();
|
|
311
|
+
if (value) media.setAttribute(dimension, value);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
img.replaceWith(media);
|
|
315
|
+
return media;
|
|
316
|
+
}
|
|
317
|
+
|
|
233
318
|
export function sanitizePreviewHtml(html) {
|
|
234
319
|
const template = document.createElement('template');
|
|
235
320
|
template.innerHTML = String(html || '');
|
|
236
321
|
const nodes = Array.from(template.content.querySelectorAll('*'));
|
|
237
322
|
for (const el of nodes) {
|
|
238
|
-
|
|
323
|
+
let element = el;
|
|
324
|
+
let tagName = String(element.tagName || '').toLowerCase();
|
|
325
|
+
if (tagName === 'img' || tagName === 'video' || tagName === 'audio') {
|
|
326
|
+
// Bare local absolute paths (C:\...\shot.png, /home/user/shot.png) are
|
|
327
|
+
// resolved to the drive/workspace file route HERE, before the generic
|
|
328
|
+
// attribute filter would strip them — this is what lets agents embed
|
|
329
|
+
// media by plain path instead of a hand-encoded /api/drives/file URL.
|
|
330
|
+
const rawSrc = String(element.getAttribute('src') || '').trim();
|
|
331
|
+
// Only genuine local-path shapes may be resolved; anything else that
|
|
332
|
+
// fails the safe-URL check (javascript:, vbscript:, …) must simply die,
|
|
333
|
+
// not be laundered into a workspace href by the resolver.
|
|
334
|
+
const looksLocalAbsolute = /^[A-Za-z]:[\\/]/.test(rawSrc) || rawSrc.startsWith('\\');
|
|
335
|
+
const safeSrc = rawSrc && isSafePreviewUrl(rawSrc, tagName === 'img')
|
|
336
|
+
? rawSrc
|
|
337
|
+
: (looksLocalAbsolute ? resolveLocalAssetUrl(rawSrc) : '');
|
|
338
|
+
if (safeSrc && safeSrc !== rawSrc) element.setAttribute('src', safeSrc);
|
|
339
|
+
else if (!safeSrc && rawSrc) element.removeAttribute('src');
|
|
340
|
+
if (tagName === 'img') {
|
|
341
|
+
const mediaTag = mediaKindForEmbedSource(safeSrc);
|
|
342
|
+
if (mediaTag) {
|
|
343
|
+
element = convertImageToMediaElement(element, mediaTag, safeSrc);
|
|
344
|
+
tagName = mediaTag;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
239
348
|
if (!PREVIEW_ALLOWED_TAGS.has(tagName)) {
|
|
240
|
-
|
|
349
|
+
element.replaceWith(document.createTextNode(element.textContent || ''));
|
|
241
350
|
continue;
|
|
242
351
|
}
|
|
243
|
-
const attrs = Array.from(
|
|
352
|
+
const attrs = Array.from(element.attributes || []);
|
|
244
353
|
for (const attr of attrs) {
|
|
245
354
|
const name = String(attr.name || '').toLowerCase();
|
|
246
355
|
const value = String(attr.value || '').trim();
|
|
247
356
|
if (!isAllowedPreviewAttribute(tagName, name, value)) {
|
|
248
|
-
|
|
357
|
+
element.removeAttribute(attr.name);
|
|
249
358
|
}
|
|
250
359
|
}
|
|
251
360
|
if (tagName === 'a') {
|
|
252
|
-
|
|
253
|
-
|
|
361
|
+
element.setAttribute('target', '_blank');
|
|
362
|
+
element.setAttribute('rel', 'noopener noreferrer');
|
|
254
363
|
}
|
|
255
364
|
}
|
|
256
365
|
return template.innerHTML;
|
|
@@ -616,7 +725,7 @@ export function rewriteLocalAssetUrlsInNode(root, options = {}) {
|
|
|
616
725
|
if (!(root instanceof Element)) return;
|
|
617
726
|
const preferDrive = options?.preferDrive === true;
|
|
618
727
|
const rewriteAnchors = options?.rewriteAnchors !== false;
|
|
619
|
-
const images = Array.from(root.querySelectorAll('img[src]'));
|
|
728
|
+
const images = Array.from(root.querySelectorAll('img[src], video[src], audio[src]'));
|
|
620
729
|
for (const img of images) {
|
|
621
730
|
const rawSrc = img.getAttribute('src') || '';
|
|
622
731
|
const nextSrc = resolveLocalAssetUrl(rawSrc, { preferDrive });
|