@oar-sh/oar 0.9.23 → 0.9.25
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 +307 -0
- package/README.md +12 -11
- package/bin/oar.js +54 -4
- package/package.json +11 -7
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +20 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +13 -8
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- 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/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +149 -8
- 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 +289 -94
- 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/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/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/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- 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/provider-readiness.mjs +67 -4
- package/server/public/app/relay-error-ctas.mjs +5 -0
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +171 -0
- package/server/public/app/socket-handlers.js +47 -0
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +478 -9
- 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/agent-sdk-routes.mjs +73 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1467 -343
- package/server/routes/sessions-routes.mjs +596 -123
- package/server/server-runtime.mjs +414 -81
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +23 -5
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +4 -2
- 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/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -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/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +386 -22
- package/server/services/remote-relay-registry-service.mjs +3 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/token-usage-service.mjs +15 -0
- package/server/services/update-install-service.mjs +2 -2
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/image-header.mjs +90 -0
- package/shared/model-refresh-schedule.mjs +120 -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 +98 -48
|
@@ -79,8 +79,10 @@ import {
|
|
|
79
79
|
loadModelVariantCatalog,
|
|
80
80
|
refreshModelVariantCatalog,
|
|
81
81
|
saveEnabledModelVariants,
|
|
82
|
+
markModelsSeen,
|
|
82
83
|
updateClaudeSettings,
|
|
83
84
|
updateCursorSettings,
|
|
85
|
+
updateOpenAISettings,
|
|
84
86
|
updateGrokSettings,
|
|
85
87
|
updateDeepSeekSettings,
|
|
86
88
|
updateClaudeApiSettings,
|
|
@@ -159,6 +161,7 @@ import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
|
159
161
|
import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
|
|
160
162
|
import { newerPageReachesEnd } from './transcript-island.mjs';
|
|
161
163
|
import { initPinnedMessagesView, openPinnedMessagesModal, setPinsConversation } from './pinned-messages-view.mjs';
|
|
164
|
+
import { afterContextRender, initContextTabs, leaveContextTab, resetContextTab, wrapContextBody } from './context-media-tab.js';
|
|
162
165
|
import { initHiddenMessagesView, openHiddenMessagesModal, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
163
166
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
164
167
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
@@ -192,6 +195,24 @@ import {
|
|
|
192
195
|
modelSelectorOptionsEqual,
|
|
193
196
|
splitModelVariantId,
|
|
194
197
|
} from './model-selector-options.mjs';
|
|
198
|
+
import {
|
|
199
|
+
applyUnsavedModelTicks,
|
|
200
|
+
buildOpenAIModelRows,
|
|
201
|
+
collectUnsavedModelTicks,
|
|
202
|
+
isUnseenModelRow,
|
|
203
|
+
mergeUnseenByTab,
|
|
204
|
+
modelCheckboxSelection,
|
|
205
|
+
modelRefreshBlockFields,
|
|
206
|
+
modelRowSdkModelId,
|
|
207
|
+
modelSelectionSignature,
|
|
208
|
+
modelTabAutoEnableNew,
|
|
209
|
+
modelTabFallbackModel,
|
|
210
|
+
normalizeCopilotModelState,
|
|
211
|
+
renderModelAutoEnableRowHtml,
|
|
212
|
+
renderModelFallbackWarningHtml,
|
|
213
|
+
renderModelVariantCheckboxHtml,
|
|
214
|
+
renderNewModelChipHtml,
|
|
215
|
+
} from './model-refresh-ui.mjs';
|
|
195
216
|
import {
|
|
196
217
|
buildContextTierOptions,
|
|
197
218
|
resolveContextTierValue,
|
|
@@ -201,6 +222,13 @@ import {
|
|
|
201
222
|
sessionLockNoteText,
|
|
202
223
|
sessionLockProviderKey,
|
|
203
224
|
} from './conversation-provider-indicator.mjs';
|
|
225
|
+
import { imageQualityOptions } from './openai-image-composer.mjs';
|
|
226
|
+
import { hideImageComposerControls, syncImageComposerControls } from './image-composer-ui.js';
|
|
227
|
+
import {
|
|
228
|
+
onOpenAIImageSpendChange,
|
|
229
|
+
syncChatMenuImageCost,
|
|
230
|
+
syncOpenAIImageSpendBanner,
|
|
231
|
+
} from './openai-image-cost-ui.js';
|
|
204
232
|
import {
|
|
205
233
|
initMessageSearchView,
|
|
206
234
|
openMessageSearchModal,
|
|
@@ -258,6 +286,7 @@ import { closeQuestionsPanel, initQuestionsPanel, openQuestionsPanel, setQuestio
|
|
|
258
286
|
import { questionsTwinLabel } from './questions-view.mjs';
|
|
259
287
|
import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
|
|
260
288
|
import { isInboxViewActive } from './inbox-ui.js';
|
|
289
|
+
import { isMediaViewActive, leaveMediaView, toggleMediaView } from './media-view.js';
|
|
261
290
|
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
262
291
|
import {
|
|
263
292
|
autoCompactWindowNotes,
|
|
@@ -277,11 +306,11 @@ import {
|
|
|
277
306
|
autoCompactWindowFromIndex,
|
|
278
307
|
formatAutoCompactWindowLabel,
|
|
279
308
|
} from './auto-compact-window-options.mjs';
|
|
280
|
-
import { renderPlanUsageHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
309
|
+
import { renderPlanUsageHtml, renderPlanUsageCardHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
281
310
|
import { revealSelectedTabs, watchTabStripFades } from './tab-strip-fade.mjs';
|
|
282
311
|
import { FONT_SCALE_CHANGE_EVENT, getUiScale, initFontScaling, updateFontScaleFromSelect } from './font-scaling.js';
|
|
283
312
|
import { initClientDiagnostics, recordStatusEvent } from './status-store.mjs';
|
|
284
|
-
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
313
|
+
import { isStatusViewActive, leaveStatusView, toggleStatusView } from './status-view.mjs';
|
|
285
314
|
import { installExternalLinkPolicy, openExternalNavigation } from './external-link-policy.mjs';
|
|
286
315
|
import {
|
|
287
316
|
initCwdPicker,
|
|
@@ -384,6 +413,8 @@ import {
|
|
|
384
413
|
updateTaskStallMinutesSetting,
|
|
385
414
|
updateRestartResumeSettingFromToggle,
|
|
386
415
|
updateSubagentRecordRetentionSetting,
|
|
416
|
+
updateModelRefreshAutoSetting,
|
|
417
|
+
updateModelRefreshIntervalSetting,
|
|
387
418
|
} from './settings-modal.js';
|
|
388
419
|
import {
|
|
389
420
|
togglePushOnThisDevice,
|
|
@@ -486,6 +517,7 @@ import {
|
|
|
486
517
|
runCliInstall,
|
|
487
518
|
cancelCliInstallRun,
|
|
488
519
|
} from './cli-install-ui.js';
|
|
520
|
+
import { refreshAgentSdkRows } from './agent-sdk-ui.js';
|
|
489
521
|
import {
|
|
490
522
|
updateAutoCheckSetting,
|
|
491
523
|
checkForUpdatesNow,
|
|
@@ -613,6 +645,15 @@ let modelVariantCatalogState = {
|
|
|
613
645
|
reasoningEfforts: [],
|
|
614
646
|
};
|
|
615
647
|
let modelVariantCatalogProviderTab = 'copilot';
|
|
648
|
+
// The unseen ids each Select Models tab tagged "New" since the modal opened:
|
|
649
|
+
// opening it marks them seen, and the re-render that follows keeps the tags.
|
|
650
|
+
let selectModelsHeldUnseen = new Map();
|
|
651
|
+
// modelSelectionSignature of the catalog the open Select Models was last
|
|
652
|
+
// rendered from: a models_updated that changes none of it skips the re-render.
|
|
653
|
+
let selectModelsRenderedSignature = '';
|
|
654
|
+
// New models no Select Models has shown yet (GET /api/models, models_updated):
|
|
655
|
+
// the dot on the menu entry.
|
|
656
|
+
let unseenNewModelCount = 0;
|
|
616
657
|
let suppressConversationPreferenceSync = false;
|
|
617
658
|
let conversationPreferenceWriteVersion = 0;
|
|
618
659
|
let latestQueueStatus = {
|
|
@@ -1627,13 +1668,6 @@ function modelProvidersForId(modelId, providersByModel = {}) {
|
|
|
1627
1668
|
return providers.map((provider) => String(provider || '').trim().toLowerCase()).filter(Boolean);
|
|
1628
1669
|
}
|
|
1629
1670
|
|
|
1630
|
-
function openAIImageSizesForModel(modelId = '') {
|
|
1631
|
-
const normalized = String(modelId || '').trim().toLowerCase().replace(/^openai\//, '');
|
|
1632
|
-
if (normalized.startsWith('dall-e-2')) return ['256x256', '512x512', '1024x1024'];
|
|
1633
|
-
if (normalized.startsWith('dall-e-3')) return ['1024x1024', '1792x1024', '1024x1792'];
|
|
1634
|
-
return ['auto', '1024x1024', '1536x1024', '1024x1536'];
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1671
|
function modelVisibleForActiveProvider(modelId, activeProviderType, providersByModel = {}) {
|
|
1638
1672
|
const normalizedModelId = String(modelId || '').trim().toLowerCase();
|
|
1639
1673
|
if (!normalizedModelId) return false;
|
|
@@ -1767,8 +1801,12 @@ function selectedReasoningEffortValue() {
|
|
|
1767
1801
|
function updateReasoningSelectorForModel(modelId, preferredEffort = '', { persist = false } = {}) {
|
|
1768
1802
|
const select = document.getElementById('reasoning-effort-select');
|
|
1769
1803
|
if (!select) return;
|
|
1770
|
-
|
|
1771
|
-
|
|
1804
|
+
const imageModel = isOpenAIImageModelId(modelId);
|
|
1805
|
+
select.title = imageModel ? 'Quality' : 'Reasoning effort';
|
|
1806
|
+
// An image model's Quality: the catalog's values its family takes.
|
|
1807
|
+
const options = imageModel
|
|
1808
|
+
? imageQualityOptions(modelId, reasoningOptionsForModel(modelId))
|
|
1809
|
+
: reasoningOptionsForModel(modelId);
|
|
1772
1810
|
const currentEffort = String(select.value || '').trim().toLowerCase();
|
|
1773
1811
|
const storedEffort = String(localStorage.getItem(REASONING_STORAGE_KEY) || '').trim().toLowerCase();
|
|
1774
1812
|
select.innerHTML = '';
|
|
@@ -1845,10 +1883,24 @@ function rebuildModelSelectorOptionsForProvider() {
|
|
|
1845
1883
|
return true;
|
|
1846
1884
|
}
|
|
1847
1885
|
|
|
1886
|
+
function syncSelectModelsMenuDot(count) {
|
|
1887
|
+
if (Number.isFinite(Number(count))) unseenNewModelCount = Math.max(0, Math.trunc(Number(count)));
|
|
1888
|
+
const button = document.getElementById('chat-menu-select-models');
|
|
1889
|
+
const dot = document.getElementById('chat-menu-select-models-dot');
|
|
1890
|
+
if (dot) dot.hidden = unseenNewModelCount === 0;
|
|
1891
|
+
// The dot is decorative; the name says it.
|
|
1892
|
+
if (unseenNewModelCount > 0) button?.setAttribute('aria-label', 'Select Models (new models)');
|
|
1893
|
+
else button?.removeAttribute('aria-label');
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1848
1896
|
function updateModelCatalogState(payload) {
|
|
1849
1897
|
// Shared readers never load the catalog, so this would only surface
|
|
1850
1898
|
// "metadata unavailable" warnings for a picker they cannot see.
|
|
1851
1899
|
if (isSharedReaderMode()) return;
|
|
1900
|
+
// Before the early returns below: a deferred catalog still moves the dot.
|
|
1901
|
+
if (payload && Object.prototype.hasOwnProperty.call(payload, 'unseenNewModelCount')) {
|
|
1902
|
+
syncSelectModelsMenuDot(payload.unseenNewModelCount);
|
|
1903
|
+
}
|
|
1852
1904
|
// The catalog's `source` is the Copilot readiness signal (provider-readiness.mjs).
|
|
1853
1905
|
updateProviderReadinessInputs({ catalog: payload && typeof payload === 'object' ? payload : null });
|
|
1854
1906
|
const select = document.getElementById('model-select');
|
|
@@ -2036,20 +2088,16 @@ function updateContextTierSelector(modelId) {
|
|
|
2036
2088
|
const select = document.getElementById('context-tier-select');
|
|
2037
2089
|
if (!select) return;
|
|
2038
2090
|
if (isOpenAIImageModelId(modelId)) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
option.value = size;
|
|
2045
|
-
option.textContent = size;
|
|
2046
|
-
select.appendChild(option);
|
|
2047
|
-
}
|
|
2048
|
-
select.value = imageSizes.includes(current) ? current : 'auto';
|
|
2049
|
-
select.title = 'Image size';
|
|
2091
|
+
// Size (with Custom…), Count, Format and Background: image-composer-ui.js,
|
|
2092
|
+
// from the conversation's stored values.
|
|
2093
|
+
syncImageComposerControls(modelId, currentConvId);
|
|
2094
|
+
// Today's image spend near or at the daily cap: the banner of an image chat.
|
|
2095
|
+
syncOpenAIImageSpendBanner({ imageChat: true });
|
|
2050
2096
|
updateModelPricingDetails(modelId, 'default');
|
|
2051
2097
|
return;
|
|
2052
2098
|
}
|
|
2099
|
+
hideImageComposerControls();
|
|
2100
|
+
syncOpenAIImageSpendBanner({ imageChat: false });
|
|
2053
2101
|
const tierOptions = buildContextTierOptions({
|
|
2054
2102
|
modelId,
|
|
2055
2103
|
providerType: activeComposerProviderType(),
|
|
@@ -2619,6 +2667,7 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2619
2667
|
enabledModels: Array.isArray(payload.claudeModels.enabledModels)
|
|
2620
2668
|
? payload.claudeModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2621
2669
|
: [],
|
|
2670
|
+
...modelRefreshBlockFields(payload.claudeModels),
|
|
2622
2671
|
}
|
|
2623
2672
|
: null,
|
|
2624
2673
|
cursorModels: payload?.cursorModels && typeof payload.cursorModels === 'object'
|
|
@@ -2630,6 +2679,7 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2630
2679
|
enabledModels: Array.isArray(payload.cursorModels.enabledModels)
|
|
2631
2680
|
? payload.cursorModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2632
2681
|
: [],
|
|
2682
|
+
...modelRefreshBlockFields(payload.cursorModels),
|
|
2633
2683
|
}
|
|
2634
2684
|
: null,
|
|
2635
2685
|
grokModels: payload?.grokModels && typeof payload.grokModels === 'object'
|
|
@@ -2641,6 +2691,7 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2641
2691
|
enabledModels: Array.isArray(payload.grokModels.enabledModels)
|
|
2642
2692
|
? payload.grokModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2643
2693
|
: [],
|
|
2694
|
+
...modelRefreshBlockFields(payload.grokModels),
|
|
2644
2695
|
}
|
|
2645
2696
|
: null,
|
|
2646
2697
|
deepseekModels: payload?.deepseekModels && typeof payload.deepseekModels === 'object'
|
|
@@ -2652,6 +2703,7 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2652
2703
|
enabledModels: Array.isArray(payload.deepseekModels.enabledModels)
|
|
2653
2704
|
? payload.deepseekModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2654
2705
|
: [],
|
|
2706
|
+
...modelRefreshBlockFields(payload.deepseekModels),
|
|
2655
2707
|
}
|
|
2656
2708
|
: null,
|
|
2657
2709
|
claudeApiModels: payload?.claudeApiModels && typeof payload.claudeApiModels === 'object'
|
|
@@ -2663,8 +2715,24 @@ function applyModelVariantCatalogState(payload) {
|
|
|
2663
2715
|
enabledModels: Array.isArray(payload.claudeApiModels.enabledModels)
|
|
2664
2716
|
? payload.claudeApiModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2665
2717
|
: [],
|
|
2718
|
+
...modelRefreshBlockFields(payload.claudeApiModels),
|
|
2719
|
+
}
|
|
2720
|
+
: null,
|
|
2721
|
+
// OpenAI BYOK's selection, like Cursor's; null from a relay without one
|
|
2722
|
+
// (the OpenAI tab then lists its models as managed in settings).
|
|
2723
|
+
openaiModels: payload?.openaiModels && typeof payload.openaiModels === 'object'
|
|
2724
|
+
? {
|
|
2725
|
+
defaultModel: String(payload.openaiModels.defaultModel || '').trim(),
|
|
2726
|
+
availableModels: Array.isArray(payload.openaiModels.availableModels)
|
|
2727
|
+
? payload.openaiModels.availableModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2728
|
+
: [],
|
|
2729
|
+
enabledModels: Array.isArray(payload.openaiModels.enabledModels)
|
|
2730
|
+
? payload.openaiModels.enabledModels.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2731
|
+
: [],
|
|
2732
|
+
...modelRefreshBlockFields(payload.openaiModels),
|
|
2666
2733
|
}
|
|
2667
2734
|
: null,
|
|
2735
|
+
copilotModelState: normalizeCopilotModelState(payload?.copilotModelState),
|
|
2668
2736
|
};
|
|
2669
2737
|
}
|
|
2670
2738
|
|
|
@@ -2711,17 +2779,37 @@ async function reconcileOpenModelVariantModal() {
|
|
|
2711
2779
|
const payload = await loadModelVariantCatalog();
|
|
2712
2780
|
if (!payload) return;
|
|
2713
2781
|
applyModelVariantCatalogState(payload);
|
|
2782
|
+
// Another device opening or saving Select Models emits models_updated too:
|
|
2783
|
+
// when nothing this modal shows changed, the ticks not saved yet stay as
|
|
2784
|
+
// they are; otherwise they are carried across the re-render.
|
|
2785
|
+
if (modelSelectionSignature(modelVariantCatalogState) === selectModelsRenderedSignature) return;
|
|
2786
|
+
const checkboxes = () => Array.from(document.querySelectorAll(
|
|
2787
|
+
'#summary-modal-body .model-variant-checkbox, #summary-modal-body .model-auto-enable-toggle',
|
|
2788
|
+
));
|
|
2789
|
+
const unsavedTicks = collectUnsavedModelTicks(checkboxes());
|
|
2714
2790
|
renderModelVariantCatalogBody();
|
|
2791
|
+
applyUnsavedModelTicks(checkboxes(), unsavedTicks);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// Shown now: the "New" tags and the menu dot are cleared on the relay (fire
|
|
2795
|
+
// and forget). The open modal keeps its tags (selectModelsHeldUnseen).
|
|
2796
|
+
function markSelectModelsSeen() {
|
|
2797
|
+
void markModelsSeen().then((result) => {
|
|
2798
|
+
if (result) syncSelectModelsMenuDot(Number(result.unseenNewModelCount) || 0);
|
|
2799
|
+
}).catch(() => {});
|
|
2715
2800
|
}
|
|
2716
2801
|
|
|
2717
2802
|
function renderModelVariantCatalogBody() {
|
|
2718
2803
|
const providersByModel = modelCatalogState.providersByModel || {};
|
|
2804
|
+
const openaiCatalog = modelVariantCatalogState.openaiModels;
|
|
2719
2805
|
const variantBelongsToTab = (entry, tab) => {
|
|
2720
2806
|
const activeTab = String(tab || 'copilot').trim().toLowerCase();
|
|
2721
2807
|
const baseModelId = String(entry?.baseModelId || '').trim().toLowerCase();
|
|
2722
2808
|
const entryProvider = String(entry?.provider || '').trim().toLowerCase();
|
|
2723
2809
|
const providers = modelProvidersForId(baseModelId, providersByModel);
|
|
2724
|
-
|
|
2810
|
+
// With its own selection, the OpenAI tab lists exactly its own rows; a
|
|
2811
|
+
// Copilot row of the same model stays on the Copilot tab.
|
|
2812
|
+
const hasOpenAIByok = !openaiCatalog && providers.includes('openai-byok');
|
|
2725
2813
|
// Each tab lists only rows sourced from that runtime: the Claude SDK tab
|
|
2726
2814
|
// shows Claude-SDK rows exclusively, the Cursor SDK tab Cursor rows, the
|
|
2727
2815
|
// Grok tab Grok rows, and Copilot never shows rows that a different runtime
|
|
@@ -2780,7 +2868,8 @@ function renderModelVariantCatalogBody() {
|
|
|
2780
2868
|
.map((entry) => String(entry.baseModelId || '').trim().toLowerCase())
|
|
2781
2869
|
.filter(Boolean),
|
|
2782
2870
|
);
|
|
2783
|
-
|
|
2871
|
+
// A relay without an OpenAI selection: its models are listed, managed in settings.
|
|
2872
|
+
for (const [modelId, providers] of Object.entries(openaiCatalog ? {} : (modelCatalogState.providersByModel || {}))) {
|
|
2784
2873
|
const baseModelId = String(modelId || '').trim().toLowerCase();
|
|
2785
2874
|
if (!baseModelId) continue;
|
|
2786
2875
|
const providerList = Array.isArray(providers) ? providers : [];
|
|
@@ -2801,6 +2890,9 @@ function renderModelVariantCatalogBody() {
|
|
|
2801
2890
|
grouped.set('openai-byok', providerRows);
|
|
2802
2891
|
openAIBaseModelsAlreadyListed.add(baseModelId);
|
|
2803
2892
|
}
|
|
2893
|
+
if (openaiCatalog?.availableModels?.length) {
|
|
2894
|
+
grouped.set('openai-byok', buildOpenAIModelRows(openaiCatalog, humanizeModelLabel));
|
|
2895
|
+
}
|
|
2804
2896
|
const claudeCatalog = modelVariantCatalogState.claudeModels;
|
|
2805
2897
|
if (claudeCatalog?.availableModels?.length) {
|
|
2806
2898
|
const claudeEnabledSet = new Set(claudeCatalog.enabledModels || []);
|
|
@@ -2973,6 +3065,16 @@ function renderModelVariantCatalogBody() {
|
|
|
2973
3065
|
else if (hasClaudeApiTab) modelVariantCatalogProviderTab = 'claude-api';
|
|
2974
3066
|
}
|
|
2975
3067
|
const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
|
|
3068
|
+
selectModelsHeldUnseen = mergeUnseenByTab(selectModelsHeldUnseen, modelVariantCatalogState);
|
|
3069
|
+
const tabUnseenIds = selectModelsHeldUnseen.get(modelVariantCatalogProviderTab) || new Set();
|
|
3070
|
+
const tabAutoEnableNew = modelTabAutoEnableNew(modelVariantCatalogState, modelVariantCatalogProviderTab);
|
|
3071
|
+
// A relay from before the switch sends none: no row then.
|
|
3072
|
+
const autoEnableRowHtml = visibleProviderOrder.length && tabAutoEnableNew !== null
|
|
3073
|
+
? renderModelAutoEnableRowHtml(modelVariantCatalogProviderTab, tabAutoEnableNew)
|
|
3074
|
+
: '';
|
|
3075
|
+
const fallbackWarningHtml = renderModelFallbackWarningHtml(
|
|
3076
|
+
modelTabFallbackModel(modelVariantCatalogState, modelVariantCatalogProviderTab),
|
|
3077
|
+
);
|
|
2976
3078
|
const providerTabButtons = `
|
|
2977
3079
|
<div class="summary-tab-strip" style="display:flex;flex-wrap:wrap;gap:calc(4px * var(--ui-scale)) calc(5px * var(--ui-scale));align-items:center;">
|
|
2978
3080
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'copilot' ? ' active' : ''}" data-model-provider-tab="copilot">Copilot</button>
|
|
@@ -3034,13 +3136,10 @@ function renderModelVariantCatalogBody() {
|
|
|
3034
3136
|
: '';
|
|
3035
3137
|
const variantRowsHtml = variantRows.map((row) => {
|
|
3036
3138
|
const selectable = row.selectable !== false;
|
|
3037
|
-
// SDK-provider rows (Claude/Cursor/Grok/DeepSeek) carry their own
|
|
3038
|
-
// flag; only Copilot variant rows read the enabledVariantIds
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const checked = sdkProviderModelId
|
|
3042
|
-
? row.enabled === true
|
|
3043
|
-
: (selectable && selected.has(row.variantId));
|
|
3139
|
+
// SDK-provider rows (Claude/Cursor/Grok/DeepSeek/OpenAI) carry their own
|
|
3140
|
+
// enabled flag; only Copilot variant rows read the enabledVariantIds
|
|
3141
|
+
// selection (renderModelVariantCheckboxHtml).
|
|
3142
|
+
const sdkProviderModelId = modelRowSdkModelId(row);
|
|
3044
3143
|
const effortChip = row.reasoningEffort
|
|
3045
3144
|
? ` <span class="model-reasoning-chip">${escHtml(row.reasoningEffort)}</span>`
|
|
3046
3145
|
: '';
|
|
@@ -3053,20 +3152,12 @@ function renderModelVariantCatalogBody() {
|
|
|
3053
3152
|
? ' <span class="model-reasoning-chip">default model</span>'
|
|
3054
3153
|
: ' <span class="model-reasoning-chip">managed in settings</span>')
|
|
3055
3154
|
: '';
|
|
3056
|
-
const
|
|
3057
|
-
? ` data-claude-model="${escHtml(row.claudeModelId)}"`
|
|
3058
|
-
: (row.cursorModelId
|
|
3059
|
-
? ` data-cursor-model="${escHtml(row.cursorModelId)}"`
|
|
3060
|
-
: (row.grokModelId
|
|
3061
|
-
? ` data-grok-model="${escHtml(row.grokModelId)}"`
|
|
3062
|
-
: (row.deepseekModelId
|
|
3063
|
-
? ` data-deepseek-model="${escHtml(row.deepseekModelId)}"`
|
|
3064
|
-
: (row.claudeApiModelId ? ` data-claude-api-model="${escHtml(row.claudeApiModelId)}"` : ''))));
|
|
3155
|
+
const newChip = isUnseenModelRow(row, tabUnseenIds) ? renderNewModelChipHtml() : '';
|
|
3065
3156
|
return `
|
|
3066
3157
|
<label class="model-variant-row">
|
|
3067
|
-
|
|
3158
|
+
${renderModelVariantCheckboxHtml(row, selected)}
|
|
3068
3159
|
<span class="model-variant-row-copy">
|
|
3069
|
-
<span class="model-variant-row-title"${row.claudeApiModelId ? ` title="${escHtml(row.claudeApiModelId)}"` : ''}>${escHtml(row.claudeApiModelId ? row.label : row.variantId)}${effortChip}${statusChip}${fixedChip}</span>
|
|
3160
|
+
<span class="model-variant-row-title"${row.claudeApiModelId ? ` title="${escHtml(row.claudeApiModelId)}"` : ''}>${escHtml(row.claudeApiModelId ? row.label : row.variantId)}${effortChip}${statusChip}${fixedChip}${newChip}</span>
|
|
3070
3161
|
</span>
|
|
3071
3162
|
</label>
|
|
3072
3163
|
`;
|
|
@@ -3095,6 +3186,8 @@ function renderModelVariantCatalogBody() {
|
|
|
3095
3186
|
const bodyHtml = `
|
|
3096
3187
|
<div style="display:flex;flex-direction:column;gap:calc(5px * var(--ui-scale))">
|
|
3097
3188
|
${providerTabButtons}
|
|
3189
|
+
${autoEnableRowHtml}
|
|
3190
|
+
${fallbackWarningHtml}
|
|
3098
3191
|
<div style="font-size:0.8rem;color:var(--muted)">
|
|
3099
3192
|
Saved globally for this relay. Refreshed: <strong>${escHtml(refreshedLabel)}</strong>
|
|
3100
3193
|
</div>
|
|
@@ -3108,6 +3201,7 @@ function renderModelVariantCatalogBody() {
|
|
|
3108
3201
|
<div style="display:grid;gap:calc(5px * var(--ui-scale))">${groupsHtml || '<div style="color:var(--muted)">No models discovered yet. Click Refresh.</div>'}</div>
|
|
3109
3202
|
</div>
|
|
3110
3203
|
`;
|
|
3204
|
+
selectModelsRenderedSignature = modelSelectionSignature(modelVariantCatalogState);
|
|
3111
3205
|
renderSummaryModalContent({
|
|
3112
3206
|
title: '🤗 Select Models',
|
|
3113
3207
|
subtitle: "Choose model variants shown under Model in the composer's ⚙ settings",
|
|
@@ -3117,7 +3211,10 @@ function renderModelVariantCatalogBody() {
|
|
|
3117
3211
|
if (!refreshed) throw new Error('Failed to refresh model variants');
|
|
3118
3212
|
reportOpenAIModelDiscoveryFailure(refreshed);
|
|
3119
3213
|
applyModelVariantCatalogState(refreshed);
|
|
3214
|
+
// The render holds the refresh's unseen ids with the tags already
|
|
3215
|
+
// shown; the models it found are seen now too, as on open.
|
|
3120
3216
|
renderModelVariantCatalogBody();
|
|
3217
|
+
markSelectModelsSeen();
|
|
3121
3218
|
await refreshModelCatalog(true);
|
|
3122
3219
|
},
|
|
3123
3220
|
kind: 'select-models',
|
|
@@ -3143,8 +3240,9 @@ function renderModelVariantCatalogBody() {
|
|
|
3143
3240
|
}
|
|
3144
3241
|
}
|
|
3145
3242
|
|
|
3146
|
-
async function openSelectModelsModal() {
|
|
3147
|
-
modelVariantCatalogProviderTab = 'copilot';
|
|
3243
|
+
async function openSelectModelsModal(tab = 'copilot') {
|
|
3244
|
+
modelVariantCatalogProviderTab = String(tab || '').trim().toLowerCase() || 'copilot';
|
|
3245
|
+
selectModelsHeldUnseen = new Map();
|
|
3148
3246
|
openSummaryModal({
|
|
3149
3247
|
title: '🤗 Select Models',
|
|
3150
3248
|
subtitle: 'Loading…',
|
|
@@ -3157,6 +3255,8 @@ async function openSelectModelsModal() {
|
|
|
3157
3255
|
if (!payload) throw new Error('Failed to load model variants');
|
|
3158
3256
|
applyModelVariantCatalogState(payload);
|
|
3159
3257
|
renderModelVariantCatalogBody();
|
|
3258
|
+
// This modal keeps its tags until it is opened again.
|
|
3259
|
+
markSelectModelsSeen();
|
|
3160
3260
|
} catch (error) {
|
|
3161
3261
|
renderSummaryModalContent({
|
|
3162
3262
|
title: '🤗 Select Models',
|
|
@@ -3172,44 +3272,39 @@ async function openSelectModelsModal() {
|
|
|
3172
3272
|
async function saveSelectedModelsFromModal() {
|
|
3173
3273
|
const body = document.getElementById('summary-modal-body');
|
|
3174
3274
|
if (!body) return;
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
const
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
.filter((input) => input.checked)
|
|
3198
|
-
.map((input) => String(input.getAttribute('data-claude-api-model') || '').trim())
|
|
3199
|
-
.filter(Boolean);
|
|
3275
|
+
// Each provider's rendered rows: `enabled` the ticked model ids, `shown`
|
|
3276
|
+
// every rendered one (the locked default model too). Read from the DOM,
|
|
3277
|
+
// never the catalog state: a models_updated may already hold a model this
|
|
3278
|
+
// render does not show, and the relay changes only the shown ids.
|
|
3279
|
+
const providerSelection = (attribute) => {
|
|
3280
|
+
const inputs = Array.from(body.querySelectorAll(`.model-variant-checkbox[${attribute}]`));
|
|
3281
|
+
return { inputs, ...modelCheckboxSelection(inputs, attribute) };
|
|
3282
|
+
};
|
|
3283
|
+
const claudeSelection = providerSelection('data-claude-model');
|
|
3284
|
+
const cursorSelection = providerSelection('data-cursor-model');
|
|
3285
|
+
const grokSelection = providerSelection('data-grok-model');
|
|
3286
|
+
const deepseekSelection = providerSelection('data-deepseek-model');
|
|
3287
|
+
const claudeApiSelection = providerSelection('data-claude-api-model');
|
|
3288
|
+
const openaiSelection = providerSelection('data-openai-model');
|
|
3289
|
+
// The rendered tab's "Turn new models on automatically"; undefined for
|
|
3290
|
+
// every other tab, whose switch the save leaves alone.
|
|
3291
|
+
const autoEnableToggle = body.querySelector('.model-auto-enable-toggle[data-auto-enable-tab]');
|
|
3292
|
+
const autoEnableNewFor = (tab) => (
|
|
3293
|
+
autoEnableToggle && autoEnableToggle.getAttribute('data-auto-enable-tab') === tab
|
|
3294
|
+
? autoEnableToggle.checked === true
|
|
3295
|
+
: undefined
|
|
3296
|
+
);
|
|
3200
3297
|
// Only the active provider tab's rows are in the DOM, but the PATCH
|
|
3201
3298
|
// replaces the WHOLE enabled set — so the save must only change rows the
|
|
3202
3299
|
// user could actually see. Off-screen variants keep their stored state,
|
|
3203
3300
|
// otherwise saving from e.g. the OpenAI tab silently disables every
|
|
3204
3301
|
// Copilot-only vendor group (Anthropic, Google, …).
|
|
3205
|
-
const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model]):not([data-deepseek-model]):not([data-claude-api-model])'));
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
.map((input) => String(input.getAttribute('data-variant-id') || '').trim())
|
|
3212
|
-
.filter(Boolean);
|
|
3302
|
+
const renderedVariantCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-selectable="1"]:not([data-claude-model]):not([data-cursor-model]):not([data-grok-model]):not([data-deepseek-model]):not([data-claude-api-model]):not([data-openai-model])'));
|
|
3303
|
+
const {
|
|
3304
|
+
enabled: checkedVariantIds,
|
|
3305
|
+
shown: shownVariantIds,
|
|
3306
|
+
} = modelCheckboxSelection(renderedVariantCheckboxes, 'data-variant-id');
|
|
3307
|
+
const renderedVariantIds = new Set(shownVariantIds);
|
|
3213
3308
|
const checkedVariantSet = new Set(checkedVariantIds);
|
|
3214
3309
|
const selectedVariantIds = (modelVariantCatalogState.enabledVariantIds || [])
|
|
3215
3310
|
.filter((variantId) => !renderedVariantIds.has(variantId) || checkedVariantSet.has(variantId));
|
|
@@ -3223,38 +3318,73 @@ async function saveSelectedModelsFromModal() {
|
|
|
3223
3318
|
}
|
|
3224
3319
|
setSummaryModalLoading(true);
|
|
3225
3320
|
try {
|
|
3226
|
-
|
|
3227
|
-
|
|
3321
|
+
const copilotAutoEnableNew = autoEnableNewFor('copilot');
|
|
3322
|
+
if (hasVariantRows || copilotAutoEnableNew !== undefined) {
|
|
3323
|
+
// Without rows to save, only the switch: a null list keeps the selection.
|
|
3324
|
+
const saved = await saveEnabledModelVariants(hasVariantRows ? selectedVariantIds : null, {
|
|
3325
|
+
autoEnableNew: copilotAutoEnableNew,
|
|
3326
|
+
shownVariantIds: hasVariantRows ? shownVariantIds : undefined,
|
|
3327
|
+
});
|
|
3228
3328
|
if (!saved) throw new Error('Failed to save model selection');
|
|
3229
3329
|
applyModelVariantCatalogState(saved);
|
|
3230
3330
|
}
|
|
3231
|
-
if (
|
|
3232
|
-
const savedClaude = await updateClaudeSettings({
|
|
3331
|
+
if (claudeSelection.inputs.length) {
|
|
3332
|
+
const savedClaude = await updateClaudeSettings({
|
|
3333
|
+
enabledModels: claudeSelection.enabled,
|
|
3334
|
+
shownModels: claudeSelection.shown,
|
|
3335
|
+
autoEnableNew: autoEnableNewFor('anthropic'),
|
|
3336
|
+
});
|
|
3233
3337
|
if (!savedClaude) throw new Error('Failed to save Claude model selection');
|
|
3234
3338
|
applyClaudeSettingsState(savedClaude);
|
|
3235
3339
|
}
|
|
3236
|
-
if (
|
|
3237
|
-
const savedCursor = await updateCursorSettings({
|
|
3340
|
+
if (cursorSelection.inputs.length) {
|
|
3341
|
+
const savedCursor = await updateCursorSettings({
|
|
3342
|
+
enabledModels: cursorSelection.enabled,
|
|
3343
|
+
shownModels: cursorSelection.shown,
|
|
3344
|
+
autoEnableNew: autoEnableNewFor('cursor'),
|
|
3345
|
+
});
|
|
3238
3346
|
if (!savedCursor) throw new Error('Failed to save Cursor model selection');
|
|
3239
3347
|
applyCursorSettingsState(savedCursor);
|
|
3240
3348
|
}
|
|
3241
|
-
if (
|
|
3242
|
-
const savedGrok = await updateGrokSettings({
|
|
3349
|
+
if (grokSelection.inputs.length) {
|
|
3350
|
+
const savedGrok = await updateGrokSettings({
|
|
3351
|
+
enabledModels: grokSelection.enabled,
|
|
3352
|
+
shownModels: grokSelection.shown,
|
|
3353
|
+
autoEnableNew: autoEnableNewFor('grok'),
|
|
3354
|
+
});
|
|
3243
3355
|
if (!savedGrok) throw new Error('Failed to save Grok model selection');
|
|
3244
3356
|
applyGrokSettingsState(savedGrok);
|
|
3245
3357
|
}
|
|
3246
|
-
if (
|
|
3247
|
-
const savedDeepSeek = await updateDeepSeekSettings({
|
|
3358
|
+
if (deepseekSelection.inputs.length) {
|
|
3359
|
+
const savedDeepSeek = await updateDeepSeekSettings({
|
|
3360
|
+
enabledModels: deepseekSelection.enabled,
|
|
3361
|
+
shownModels: deepseekSelection.shown,
|
|
3362
|
+
autoEnableNew: autoEnableNewFor('deepseek'),
|
|
3363
|
+
});
|
|
3248
3364
|
if (!savedDeepSeek) throw new Error('Failed to save DeepSeek model selection');
|
|
3249
3365
|
applyDeepSeekSettingsState(savedDeepSeek);
|
|
3250
3366
|
}
|
|
3251
|
-
if (
|
|
3252
|
-
const savedClaudeApi = await updateClaudeApiSettings({
|
|
3367
|
+
if (claudeApiSelection.inputs.length) {
|
|
3368
|
+
const savedClaudeApi = await updateClaudeApiSettings({
|
|
3369
|
+
enabledModels: claudeApiSelection.enabled,
|
|
3370
|
+
shownModels: claudeApiSelection.shown,
|
|
3371
|
+
autoEnableNew: autoEnableNewFor('claude-api'),
|
|
3372
|
+
});
|
|
3253
3373
|
if (!savedClaudeApi) throw new Error('Failed to save Claude API model selection');
|
|
3254
3374
|
applyClaudeApiSettingsState(savedClaudeApi);
|
|
3255
3375
|
}
|
|
3256
|
-
if (
|
|
3257
|
-
|
|
3376
|
+
if (openaiSelection.inputs.length) {
|
|
3377
|
+
// No model named: the selection only, the default model stays.
|
|
3378
|
+
const savedOpenAI = await updateOpenAISettings({
|
|
3379
|
+
enabledModels: openaiSelection.enabled,
|
|
3380
|
+
shownModels: openaiSelection.shown,
|
|
3381
|
+
autoEnableNew: autoEnableNewFor('openai'),
|
|
3382
|
+
});
|
|
3383
|
+
if (!savedOpenAI) throw new Error('Failed to save OpenAI model selection');
|
|
3384
|
+
applyOpenAISettingsState(savedOpenAI);
|
|
3385
|
+
}
|
|
3386
|
+
if ([claudeSelection, cursorSelection, grokSelection, deepseekSelection, claudeApiSelection, openaiSelection]
|
|
3387
|
+
.some((selection) => selection.inputs.length)) {
|
|
3258
3388
|
const refreshedCatalog = await loadModelVariantCatalog();
|
|
3259
3389
|
if (refreshedCatalog) applyModelVariantCatalogState(refreshedCatalog);
|
|
3260
3390
|
}
|
|
@@ -3326,6 +3456,27 @@ function usageCardIsFresh(provider) {
|
|
|
3326
3456
|
return Number.isFinite(at) && Date.now() - at < USAGE_LIVE_FRESH_MS;
|
|
3327
3457
|
}
|
|
3328
3458
|
|
|
3459
|
+
/**
|
|
3460
|
+
* A new OpenAI image spend state (the socket event): the kept report's
|
|
3461
|
+
* 'openai' card takes it, and the open modal redraws that card alone.
|
|
3462
|
+
*/
|
|
3463
|
+
function applyImageSpendToUsageReport(state) {
|
|
3464
|
+
if (!state || !Array.isArray(lastUsageReport?.providers)) return;
|
|
3465
|
+
const index = lastUsageReport.providers.findIndex((card) => card?.imageSpend && normalizeUsageProvider(card.provider) === 'openai');
|
|
3466
|
+
if (index < 0) return;
|
|
3467
|
+
const card = { ...lastUsageReport.providers[index], imageSpend: { ...lastUsageReport.providers[index].imageSpend, ...state } };
|
|
3468
|
+
const providers = lastUsageReport.providers.slice();
|
|
3469
|
+
providers[index] = card;
|
|
3470
|
+
storeUsageReport({ ...lastUsageReport, providers });
|
|
3471
|
+
if (summaryModalState.kind !== 'usage') return;
|
|
3472
|
+
const panel = document.getElementById('summary-modal-body')?.querySelector('[data-usage-panel="openai"]');
|
|
3473
|
+
if (!panel) return;
|
|
3474
|
+
const updating = panel.hasAttribute('data-updating');
|
|
3475
|
+
panel.outerHTML = renderPlanUsageCardHtml(card, { active: !panel.hidden });
|
|
3476
|
+
if (updating) markUsageCardUpdating('openai', true);
|
|
3477
|
+
}
|
|
3478
|
+
onOpenAIImageSpendChange(applyImageSpendToUsageReport);
|
|
3479
|
+
|
|
3329
3480
|
/** The "Updating…" marker on one card, while its live read runs. */
|
|
3330
3481
|
function markUsageCardUpdating(provider, updating) {
|
|
3331
3482
|
const body = document.getElementById('summary-modal-body');
|
|
@@ -3908,11 +4059,13 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3908
4059
|
const detailText = String(payload.text || '').trim();
|
|
3909
4060
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
3910
4061
|
// available underneath for the details it carries that categories don't.
|
|
3911
|
-
const
|
|
4062
|
+
const contextHtml = usageHtml
|
|
3912
4063
|
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3913
4064
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
3914
4065
|
: ''}`
|
|
3915
4066
|
: `${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
4067
|
+
// Two categories: Context (this body) and the session's Media gallery.
|
|
4068
|
+
const bodyHtml = wrapContextBody(contextHtml, settingsConversationId);
|
|
3916
4069
|
|
|
3917
4070
|
renderSummaryModalContent({
|
|
3918
4071
|
title: 'Context usage',
|
|
@@ -3921,7 +4074,11 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3921
4074
|
refresh: () => loadContextSummaryAndRender(refreshLookupId),
|
|
3922
4075
|
kind: 'context',
|
|
3923
4076
|
});
|
|
3924
|
-
setSummaryModalLeaveHandler('context', () =>
|
|
4077
|
+
setSummaryModalLeaveHandler('context', () => {
|
|
4078
|
+
commitAutoCompactDraft();
|
|
4079
|
+
leaveContextTab();
|
|
4080
|
+
});
|
|
4081
|
+
afterContextRender();
|
|
3925
4082
|
}
|
|
3926
4083
|
|
|
3927
4084
|
async function showUsage() {
|
|
@@ -3983,6 +4140,9 @@ async function showContext() {
|
|
|
3983
4140
|
});
|
|
3984
4141
|
return;
|
|
3985
4142
|
}
|
|
4143
|
+
// A fresh open shows Context first; Media is one tap away.
|
|
4144
|
+
initContextTabs();
|
|
4145
|
+
resetContextTab(convId);
|
|
3986
4146
|
if (btn) {
|
|
3987
4147
|
btn.textContent = '⏳';
|
|
3988
4148
|
btn.disabled = true;
|
|
@@ -5013,6 +5173,13 @@ function toggleChatActionsMenu() {
|
|
|
5013
5173
|
syncThemeMenuLabel();
|
|
5014
5174
|
syncAboutMenuLabel();
|
|
5015
5175
|
const willOpen = !!menu.hidden;
|
|
5176
|
+
// An image chat's cost, asked from the relay each time the menu opens.
|
|
5177
|
+
if (willOpen) {
|
|
5178
|
+
void syncChatMenuImageCost({
|
|
5179
|
+
conversationId: currentConvId,
|
|
5180
|
+
imageChat: isOpenAIImageModelId(document.getElementById('model-select')?.value || ''),
|
|
5181
|
+
}).then(() => { if (!menu.hidden) placeChatActionsMenu(); });
|
|
5182
|
+
}
|
|
5016
5183
|
menu.hidden = !willOpen;
|
|
5017
5184
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
5018
5185
|
if (willOpen) {
|
|
@@ -5288,6 +5455,7 @@ initSocketHandlers({
|
|
|
5288
5455
|
refreshModelCatalog,
|
|
5289
5456
|
updateModelCatalogState,
|
|
5290
5457
|
reconcileOpenModelVariantModal,
|
|
5458
|
+
openSelectModelsModal,
|
|
5291
5459
|
applyConversationWorkspaceRootUpdate,
|
|
5292
5460
|
applyConversationTitleUpdate,
|
|
5293
5461
|
syncChatTitleControls,
|
|
@@ -5588,6 +5756,14 @@ async function initApp() {
|
|
|
5588
5756
|
closeChatActionsMenu();
|
|
5589
5757
|
openCurrentCloudSession(showExternalLinkFallback);
|
|
5590
5758
|
});
|
|
5759
|
+
// The global Media view: every session's pictures, in the transcript's place.
|
|
5760
|
+
bindMenuAction(document.getElementById('chat-menu-media'), (event) => {
|
|
5761
|
+
event.preventDefault();
|
|
5762
|
+
event.stopPropagation();
|
|
5763
|
+
lockChatActionsMenuShield(350);
|
|
5764
|
+
closeChatActionsMenu();
|
|
5765
|
+
void window.toggleMediaView();
|
|
5766
|
+
});
|
|
5591
5767
|
const chatMenuSettingsBtn = document.getElementById('chat-menu-settings');
|
|
5592
5768
|
bindMenuAction(chatMenuSettingsBtn, (event) => {
|
|
5593
5769
|
event.preventDefault();
|
|
@@ -5730,6 +5906,10 @@ async function initApp() {
|
|
|
5730
5906
|
void refreshClaudeCloudSettingsState().then((state) => {
|
|
5731
5907
|
updateProviderReadinessInputs({ claudeCloud: state || null });
|
|
5732
5908
|
});
|
|
5909
|
+
// Not awaited either: a provider whose SDK failed to install is not ready,
|
|
5910
|
+
// and the read passes the snapshot to provider readiness itself. A missing
|
|
5911
|
+
// answer changes nothing.
|
|
5912
|
+
void refreshAgentSdkRows();
|
|
5733
5913
|
await refreshModelCatalog(true);
|
|
5734
5914
|
initFullscreenButton();
|
|
5735
5915
|
initInstallButton();
|
|
@@ -5857,6 +6037,7 @@ async function bootstrap() {
|
|
|
5857
6037
|
// Leaving the Inbox goes back to the conversation it was opened from.
|
|
5858
6038
|
// Opened from Status it stored none: `returnTo` is the session history stands on.
|
|
5859
6039
|
closeInboxView: ({ returnTo = '' } = {}) => { if (isInboxViewActive()) void toggleInboxView({ returnTo }); },
|
|
6040
|
+
closeMediaView: ({ returnTo = '' } = {}) => { if (isMediaViewActive()) void toggleMediaView({ returnTo }); },
|
|
5860
6041
|
closeChangeCwdMenu: () => closeChangeCwdMenu({ focusTrigger: true }),
|
|
5861
6042
|
closeGitChangesModal,
|
|
5862
6043
|
closeGitDiffViewer,
|
|
@@ -5973,6 +6154,8 @@ window.previewTaskStallMinutesSetting = previewTaskStallMinutesSetting;
|
|
|
5973
6154
|
window.updateTaskStallMinutesSetting = updateTaskStallMinutesSetting;
|
|
5974
6155
|
window.updateRestartResumeSettingFromToggle = updateRestartResumeSettingFromToggle;
|
|
5975
6156
|
window.updateSubagentRecordRetentionSetting = updateSubagentRecordRetentionSetting;
|
|
6157
|
+
window.updateModelRefreshAutoSetting = updateModelRefreshAutoSetting;
|
|
6158
|
+
window.updateModelRefreshIntervalSetting = updateModelRefreshIntervalSetting;
|
|
5976
6159
|
window.togglePushOnThisDevice = togglePushOnThisDevice;
|
|
5977
6160
|
window.updatePushPreferencesFromControls = updatePushPreferencesFromControls;
|
|
5978
6161
|
window.openSettingsModal = openSettingsModal;
|
|
@@ -6034,9 +6217,21 @@ window.openOriginConversation = openOriginConversation;
|
|
|
6034
6217
|
// history stands on.
|
|
6035
6218
|
window.toggleStatusView = () => {
|
|
6036
6219
|
leaveInboxView();
|
|
6220
|
+
leaveMediaView();
|
|
6037
6221
|
return toggleStatusView({ returnTo: sessionHistoryConversation() });
|
|
6038
6222
|
};
|
|
6039
|
-
window.toggleInboxView = () =>
|
|
6223
|
+
window.toggleInboxView = () => {
|
|
6224
|
+
leaveMediaView();
|
|
6225
|
+
return toggleInboxView({ returnTo: sessionHistoryConversation() });
|
|
6226
|
+
};
|
|
6227
|
+
// The global Media view shares that place too.
|
|
6228
|
+
window.toggleMediaView = () => toggleMediaView({
|
|
6229
|
+
returnTo: sessionHistoryConversation(),
|
|
6230
|
+
leaveOthers: () => {
|
|
6231
|
+
leaveInboxView();
|
|
6232
|
+
leaveStatusView();
|
|
6233
|
+
},
|
|
6234
|
+
});
|
|
6040
6235
|
window.refreshConversations = refreshConversations;
|
|
6041
6236
|
window.renderConvList = renderConvList;
|
|
6042
6237
|
window.handleAttachmentInput = handleAttachmentInput;
|