@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.
Files changed (110) hide show
  1. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
  2. package/CHANGELOG.md +307 -0
  3. package/README.md +12 -11
  4. package/bin/oar.js +54 -4
  5. package/package.json +11 -7
  6. package/server/agent-sdk-loader.mjs +116 -0
  7. package/server/agent-sdks/claude/entry.mjs +4 -0
  8. package/server/agent-sdks/claude/package-lock.json +1438 -0
  9. package/server/agent-sdks/claude/package.json +13 -0
  10. package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
  11. package/server/agent-sdks/cursor/entry.mjs +3 -0
  12. package/server/agent-sdks/cursor/package-lock.json +171 -0
  13. package/server/agent-sdks/cursor/package.json +11 -0
  14. package/server/agent-sdks/koffi/entry.cjs +2 -0
  15. package/server/agent-sdks/koffi/package-lock.json +368 -0
  16. package/server/agent-sdks/koffi/package.json +10 -0
  17. package/server/claude-worker/claude-preview-tool.mjs +5 -2
  18. package/server/claude-worker/claude-remote-relay-tool.mjs +20 -2
  19. package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
  20. package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
  21. package/server/copilot-worker/copilot-sdk-session-process.mjs +13 -8
  22. package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
  23. package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
  24. package/server/db-schema.mjs +19 -0
  25. package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
  26. package/server/migrations/0039-media-index.mjs +167 -0
  27. package/server/migrations/0040-image-session-defaults.mjs +93 -0
  28. package/server/public/app/agent-sdk-ui.js +349 -0
  29. package/server/public/app/api-client.js +149 -8
  30. package/server/public/app/ask-user-view.js +2 -1
  31. package/server/public/app/attachments-view.js +66 -9
  32. package/server/public/app/bootstrap.js +289 -94
  33. package/server/public/app/chat-actions-menu-state.mjs +1 -0
  34. package/server/public/app/context-media-tab.js +143 -0
  35. package/server/public/app/conversation-view.js +90 -5
  36. package/server/public/app/image-composer-ui.js +518 -0
  37. package/server/public/app/image-turn-placeholder.mjs +233 -0
  38. package/server/public/app/journal-view.js +12 -2
  39. package/server/public/app/media-gallery-model.mjs +202 -0
  40. package/server/public/app/media-gallery-ui.js +524 -0
  41. package/server/public/app/media-view.js +79 -0
  42. package/server/public/app/model-refresh-ui.mjs +413 -0
  43. package/server/public/app/no-conversation-state.mjs +5 -2
  44. package/server/public/app/openai-image-composer.mjs +492 -0
  45. package/server/public/app/openai-image-cost-ui.js +426 -0
  46. package/server/public/app/openai-image-spend.mjs +443 -0
  47. package/server/public/app/plan-usage-view.mjs +38 -6
  48. package/server/public/app/provider-readiness.mjs +67 -4
  49. package/server/public/app/relay-error-ctas.mjs +5 -0
  50. package/server/public/app/session-history-ui.js +2 -0
  51. package/server/public/app/settings-modal.js +171 -0
  52. package/server/public/app/socket-handlers.js +47 -0
  53. package/server/public/app/tab-strip-fade.mjs +1 -1
  54. package/server/public/app/viewer-gallery.mjs +23 -1
  55. package/server/public/index.html +478 -9
  56. package/server/relay-tools.md +1 -0
  57. package/server/repositories/image-conversation-repository.mjs +69 -0
  58. package/server/repositories/media-repository.mjs +305 -0
  59. package/server/routes/agent-sdk-routes.mjs +73 -0
  60. package/server/routes/media-routes.mjs +361 -0
  61. package/server/routes/messages-routes.mjs +1467 -343
  62. package/server/routes/sessions-routes.mjs +596 -123
  63. package/server/server-runtime.mjs +414 -81
  64. package/server/services/about-service.mjs +26 -6
  65. package/server/services/agent-sdk-launch-gate.mjs +124 -0
  66. package/server/services/agent-sdk-paths.mjs +307 -0
  67. package/server/services/agent-sdk-store.mjs +1211 -0
  68. package/server/services/claude-api-settings-service.mjs +54 -17
  69. package/server/services/cli-install-service.mjs +3 -2
  70. package/server/services/copilot-model-discovery-service.mjs +7 -1
  71. package/server/services/database-file.mjs +23 -5
  72. package/server/services/deepseek-settings-service.mjs +58 -20
  73. package/server/services/earlier-database-service.mjs +4 -2
  74. package/server/services/generated-image-cleanup-service.mjs +2 -1
  75. package/server/services/generated-image-files.mjs +110 -0
  76. package/server/services/image-input-paths.mjs +389 -0
  77. package/server/services/image-operation-service.mjs +6 -2
  78. package/server/services/media-extract.mjs +653 -0
  79. package/server/services/media-index-service.mjs +221 -0
  80. package/server/services/model-refresh-scheduler.mjs +153 -0
  81. package/server/services/model-variant-catalog-service.mjs +141 -9
  82. package/server/services/oar-cli-helpers.mjs +72 -9
  83. package/server/services/oar-state-migration-service.mjs +2 -12
  84. package/server/services/oar-state-root.mjs +17 -0
  85. package/server/services/openai-image-preview-turns.mjs +330 -0
  86. package/server/services/openai-image-spend-service.mjs +516 -0
  87. package/server/services/openai-image-stream.mjs +170 -0
  88. package/server/services/plan-usage-openai-image.mjs +148 -0
  89. package/server/services/provider-model-refresh.mjs +326 -0
  90. package/server/services/provider-model-state.mjs +395 -0
  91. package/server/services/relay-singleton-guard.mjs +72 -10
  92. package/server/services/remote-relay-caller-context.mjs +6 -0
  93. package/server/services/remote-relay-dispatcher.mjs +386 -22
  94. package/server/services/remote-relay-registry-service.mjs +3 -1
  95. package/server/services/session-worker-launch-service.mjs +13 -0
  96. package/server/services/session-worker-supervisor-service.mjs +19 -1
  97. package/server/services/session-worker-websocket-service.mjs +15 -0
  98. package/server/services/token-usage-service.mjs +15 -0
  99. package/server/services/update-install-service.mjs +2 -2
  100. package/server/services/worker-launch-note-service.mjs +14 -2
  101. package/server/services/worker-recycle-service.mjs +145 -11
  102. package/shared/image-header.mjs +90 -0
  103. package/shared/model-refresh-schedule.mjs +120 -0
  104. package/shared/openai-image-options.mjs +299 -0
  105. package/shared/openai-image-presets.mjs +175 -0
  106. package/shared/openai-image-previews.mjs +40 -0
  107. package/shared/openai-image-prices.mjs +291 -0
  108. package/shared/openai-image-spend.mjs +90 -0
  109. package/shared/remote-relay-contract.mjs +126 -4
  110. 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
- select.title = isOpenAIImageModelId(modelId) ? 'Quality' : 'Reasoning effort';
1771
- const options = reasoningOptionsForModel(modelId);
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
- const current = String(select.value || '').trim().toLowerCase();
2040
- const imageSizes = openAIImageSizesForModel(modelId);
2041
- select.innerHTML = '';
2042
- for (const size of imageSizes) {
2043
- const option = document.createElement('option');
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
- const hasOpenAIByok = providers.includes('openai-byok');
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
- for (const [modelId, providers] of Object.entries(modelCatalogState.providersByModel || {})) {
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 enabled
3038
- // flag; only Copilot variant rows read the enabledVariantIds selection.
3039
- const sdkProviderModelId = row.claudeModelId || row.cursorModelId || row.grokModelId || row.deepseekModelId
3040
- || row.claudeApiModelId || '';
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 sdkModelAttr = row.claudeModelId
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
- <input class="model-variant-checkbox" type="checkbox" data-selectable="${selectable ? '1' : '0'}" data-variant-id="${escHtml(row.variantId)}"${sdkModelAttr} ${checked ? 'checked' : ''} ${selectable ? '' : 'disabled'}>
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
- const claudeCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-claude-model]'));
3176
- const selectedClaudeModels = claudeCheckboxes
3177
- .filter((input) => input.checked)
3178
- .map((input) => String(input.getAttribute('data-claude-model') || '').trim())
3179
- .filter(Boolean);
3180
- const cursorCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-cursor-model]'));
3181
- const selectedCursorModels = cursorCheckboxes
3182
- .filter((input) => input.checked)
3183
- .map((input) => String(input.getAttribute('data-cursor-model') || '').trim())
3184
- .filter(Boolean);
3185
- const grokCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-grok-model]'));
3186
- const selectedGrokModels = grokCheckboxes
3187
- .filter((input) => input.checked)
3188
- .map((input) => String(input.getAttribute('data-grok-model') || '').trim())
3189
- .filter(Boolean);
3190
- const deepseekCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-deepseek-model]'));
3191
- const selectedDeepSeekModels = deepseekCheckboxes
3192
- .filter((input) => input.checked)
3193
- .map((input) => String(input.getAttribute('data-deepseek-model') || '').trim())
3194
- .filter(Boolean);
3195
- const claudeApiCheckboxes = Array.from(body.querySelectorAll('.model-variant-checkbox[data-claude-api-model]'));
3196
- const selectedClaudeApiModels = claudeApiCheckboxes
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 renderedVariantIds = new Set(renderedVariantCheckboxes
3207
- .map((input) => String(input.getAttribute('data-variant-id') || '').trim())
3208
- .filter(Boolean));
3209
- const checkedVariantIds = renderedVariantCheckboxes
3210
- .filter((input) => input.checked)
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
- if (hasVariantRows) {
3227
- const saved = await saveEnabledModelVariants(selectedVariantIds);
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 (claudeCheckboxes.length) {
3232
- const savedClaude = await updateClaudeSettings({ enabledModels: selectedClaudeModels });
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 (cursorCheckboxes.length) {
3237
- const savedCursor = await updateCursorSettings({ enabledModels: selectedCursorModels });
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 (grokCheckboxes.length) {
3242
- const savedGrok = await updateGrokSettings({ enabledModels: selectedGrokModels });
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 (deepseekCheckboxes.length) {
3247
- const savedDeepSeek = await updateDeepSeekSettings({ enabledModels: selectedDeepSeekModels });
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 (claudeApiCheckboxes.length) {
3252
- const savedClaudeApi = await updateClaudeApiSettings({ enabledModels: selectedClaudeApiModels });
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 (claudeCheckboxes.length || cursorCheckboxes.length || grokCheckboxes.length || deepseekCheckboxes.length
3257
- || claudeApiCheckboxes.length) {
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 bodyHtml = usageHtml
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', () => commitAutoCompactDraft());
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 = () => toggleInboxView({ returnTo: sessionHistoryConversation() });
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;