codex-overleaf-link 2.2.1 → 2.3.1

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 (79) hide show
  1. package/README.md +25 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/runtime-manifest.json +5 -71
  4. package/extension/src/background.js +67 -63
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/composerPanel.js +0 -2
  8. package/extension/src/content/contentRuntime.js +757 -1558
  9. package/extension/src/content/contextTray.js +4 -6
  10. package/extension/src/content/diagnosticsController.js +1 -2
  11. package/extension/src/content/diffReviewPanel.js +14 -13
  12. package/extension/src/content/generated/content.bundle.js +41605 -0
  13. package/extension/src/content/generated/content.bundle.meta.json +103 -0
  14. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  15. package/extension/src/content/markdownText.js +17 -17
  16. package/extension/src/content/mathText.js +155 -9
  17. package/extension/src/content/modelPicker.js +1 -1
  18. package/extension/src/content/modelPickerSupport.js +10 -5
  19. package/extension/src/content/moduleRegistryKernel.js +125 -0
  20. package/extension/src/content/nativeCompatibilityController.js +313 -0
  21. package/extension/src/content/otWarmMirror.js +8 -6
  22. package/extension/src/content/pageBridgeClient.js +224 -0
  23. package/extension/src/content/panelMaintenance.js +8 -3
  24. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  25. package/extension/src/content/projectProviderSelection.js +4 -12
  26. package/extension/src/content/projectSessionCleanup.js +3 -3
  27. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  28. package/extension/src/content/providerSettingsDialog.js +30 -18
  29. package/extension/src/content/recentProjects.js +15 -23
  30. package/extension/src/content/runGuidanceController.js +1 -1
  31. package/extension/src/content/runQueueScheduler.js +129 -29
  32. package/extension/src/content/runSettlementPersistence.js +51 -0
  33. package/extension/src/content/runTimelineView.js +17 -8
  34. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  35. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  36. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  37. package/extension/src/content/sessionManager.js +15 -12
  38. package/extension/src/content/sessionPersistence.js +67 -8
  39. package/extension/src/content/updateNotice.js +10 -4
  40. package/extension/src/content/writebackOrchestrator.js +89 -68
  41. package/extension/src/contentScript.js +5 -1
  42. package/extension/src/page/trackedChangesLifecycle.js +16 -5
  43. package/extension/src/page/writebackRouter.js +6 -2
  44. package/extension/src/pageBridge.js +47 -98
  45. package/extension/src/shared/agentTranscript.js +18 -8
  46. package/extension/src/shared/compatibility.js +1 -1
  47. package/extension/src/shared/governanceRules.js +1 -1
  48. package/extension/src/shared/i18n.js +2 -4
  49. package/extension/src/shared/managedUpdateProjection.js +337 -0
  50. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  51. package/extension/src/shared/otText.js +0 -19
  52. package/extension/src/shared/pageRpcContract.js +136 -0
  53. package/extension/src/shared/runExecutionSnapshot.js +187 -0
  54. package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
  55. package/extension/src/shared/runInputQueue.js +81 -34
  56. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  57. package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
  58. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  59. package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
  60. package/extension/src/shared/sessionState.js +110 -59
  61. package/extension/src/shared/settlementFacts.js +351 -0
  62. package/extension/src/shared/storageDb.js +25 -10
  63. package/extension/src/shared/storageMigration.js +275 -61
  64. package/extension/src/shared/storageSessionClaims.js +60 -0
  65. package/extension/src/shared/updateConsent.js +4 -0
  66. package/extension/src/shared/updateRevocationIntent.js +109 -0
  67. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  68. package/extension/src/shared/writebackOperationEvidence.js +153 -0
  69. package/extension/src/shared/writebackSettlement.js +727 -0
  70. package/extension/styles/panel.css +18 -8
  71. package/native-host/src/codexPromptAssembly.js +1 -1
  72. package/native-host/src/codexRuntimeIdentity.js +1 -1
  73. package/native-host/src/codexSessionRunner.js +7 -13
  74. package/native-host/src/index.js +3 -69
  75. package/native-host/src/managedInstall.js +50 -8
  76. package/native-host/src/nativeTransportEnvelope.js +148 -0
  77. package/native-host/src/taskRunnerRuntime.js +14 -84
  78. package/native-host/src/updateManager.js +36 -1
  79. package/package.json +13 -5
@@ -0,0 +1,224 @@
1
+ (function initCodexOverleafPageBridgeClient(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafPageBridgeClient = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function pageBridgeClientFactory() {
6
+ 'use strict';
7
+
8
+ const PAGE_WORLD_SCRIPTS = Object.freeze([
9
+ ['src/shared/reviewing.js', 'codex-overleaf-reviewing-script'],
10
+ ['src/shared/projectFiles.js', 'codex-overleaf-project-files-script'],
11
+ ['src/shared/compatibility.js', 'codex-overleaf-compatibility-page-script'],
12
+ ['src/shared/pageRpcContract.js', 'codex-overleaf-page-rpc-contract-script'],
13
+ ['src/shared/staleGuard.js', 'codex-overleaf-stale-guard-script'],
14
+ ['src/shared/compileAdapter.js', 'codex-overleaf-compile-adapter-script'],
15
+ ['src/shared/sensitiveScan.js', 'codex-overleaf-sensitive-scan-script'],
16
+ ['src/shared/auditRecords.js', 'codex-overleaf-audit-records-script'],
17
+ ['src/page/saveState.js', 'codex-overleaf-save-state-script', true],
18
+ ['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script'],
19
+ ['src/page/compileBridge.js', 'codex-overleaf-compile-bridge-script'],
20
+ ['src/page/overleafEditor.js', 'codex-overleaf-editor-script'],
21
+ ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
22
+ ['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
23
+ ['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
24
+ ['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
25
+ ['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
26
+ ['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
27
+ ['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
28
+ ['src/pageBridge.js', 'codex-overleaf-page-bridge-script', true]
29
+ ]);
30
+ const OPTIONAL_OT_SCRIPTS = Object.freeze([
31
+ ['src/shared/otText.js', 'codex-overleaf-ot-text-script'],
32
+ ['src/page/overleafRealtimeObserver.js', 'codex-overleaf-realtime-observer-script']
33
+ ]);
34
+
35
+ function create(options = {}) {
36
+ const windowRef = options.window || globalThis.window;
37
+ const documentRef = options.document || windowRef?.document;
38
+ const chromeApi = options.chromeApi || globalThis.chrome;
39
+ const cryptoImpl = options.crypto || globalThis.crypto;
40
+ const runtimeRoot = options.root || windowRef;
41
+ const Contract = options.contract;
42
+ const Compatibility = options.compatibility;
43
+ const timeoutOverrides = { ...(options.timeoutOverrides || {}) };
44
+ const revision = Contract.REVISION;
45
+ const capability = createCapability(cryptoImpl);
46
+ const activeCancellationHandlers = new Map();
47
+ let readyPromise = null;
48
+
49
+ function start() {
50
+ if (!readyPromise) readyPromise = injectPageBridge();
51
+ return readyPromise;
52
+ }
53
+
54
+ async function call(method, params) {
55
+ try {
56
+ await start();
57
+ } catch (error) {
58
+ return { ok: false, error: `Page bridge unavailable: ${error.message}` };
59
+ }
60
+ return send(method, params, Contract.resolveDispatchPolicy(method, timeoutOverrides));
61
+ }
62
+
63
+ function send(method, params, policy = {}) {
64
+ const id = cryptoImpl.randomUUID();
65
+ return new Promise((resolve, reject) => {
66
+ const timeoutMs = Number.isFinite(Number(policy.timeoutMs))
67
+ ? Number(policy.timeoutMs)
68
+ : Contract.resolveTimeoutMs(method, timeoutOverrides);
69
+ const cancellable = policy.cancellation === 'content_abort';
70
+ let settled = false;
71
+ let timeout = null;
72
+ function cleanup() {
73
+ if (timeout !== null) windowRef.clearTimeout(timeout);
74
+ windowRef.removeEventListener('message', onMessage);
75
+ if (cancellable) activeCancellationHandlers.delete(id);
76
+ }
77
+ function resolveOnce(value) {
78
+ if (settled) return;
79
+ settled = true;
80
+ cleanup();
81
+ resolve(value);
82
+ }
83
+ function rejectOnce(error) {
84
+ if (settled) return;
85
+ settled = true;
86
+ cleanup();
87
+ reject(error);
88
+ }
89
+ function cancelRequest() {
90
+ const error = new Error('Codex run was cancelled by the user');
91
+ error.code = 'codex_cancelled';
92
+ error.cancelled = true;
93
+ rejectOnce(error);
94
+ }
95
+ timeout = windowRef.setTimeout(() => {
96
+ resolveOnce({ ok: false, error: 'Page bridge timed out' });
97
+ }, timeoutMs);
98
+ function onMessage(event) {
99
+ if (event.source !== windowRef
100
+ || event.origin !== windowRef.location.origin
101
+ || event.data?.source !== 'codex-overleaf/page'
102
+ || event.data?.pageBridgeVersion !== Compatibility?.BUILD_TARGET_VERSION
103
+ || event.data?.pageBridgeRevision !== revision
104
+ || event.data.id !== id) {
105
+ return;
106
+ }
107
+ resolveOnce(event.data.result);
108
+ }
109
+ if (cancellable) {
110
+ activeCancellationHandlers.set(id, cancelRequest);
111
+ if (options.isCancellationRequested?.()) windowRef.queueMicrotask(cancelRequest);
112
+ }
113
+ windowRef.addEventListener('message', onMessage);
114
+ // Every low-level request remains one attempt. Higher layers must opt
115
+ // into replay so a document mutation is never duplicated implicitly.
116
+ windowRef.postMessage({
117
+ source: 'codex-overleaf/content',
118
+ id,
119
+ method,
120
+ params,
121
+ capability
122
+ }, windowRef.location.origin);
123
+ });
124
+ }
125
+
126
+ function cancelActiveRequests() {
127
+ const handlers = Array.from(activeCancellationHandlers.values());
128
+ activeCancellationHandlers.clear();
129
+ handlers.forEach(cancel => cancel());
130
+ }
131
+
132
+ function resolveTimeoutMs(method) {
133
+ return Contract.resolveTimeoutMs(method, timeoutOverrides);
134
+ }
135
+
136
+ async function injectPageBridge() {
137
+ const splitAt = PAGE_WORLD_SCRIPTS.findIndex(([src]) =>
138
+ src === 'src/page/pageBridgeCapability.js'
139
+ );
140
+ for (const [src, id, force] of PAGE_WORLD_SCRIPTS.slice(0, splitAt)) {
141
+ await injectScriptOnce(src, id, { force });
142
+ }
143
+ await injectOptionalOtDependencies();
144
+ for (const [src, id, force] of PAGE_WORLD_SCRIPTS.slice(splitAt)) {
145
+ await injectScriptOnce(src, id, { force });
146
+ }
147
+ await initializeCapability();
148
+ }
149
+
150
+ async function injectOptionalOtDependencies() {
151
+ try {
152
+ for (const [src, id] of OPTIONAL_OT_SCRIPTS) await injectScriptOnce(src, id);
153
+ } catch (_error) {
154
+ // The page bridge keeps its read-only unavailable fallback.
155
+ }
156
+ }
157
+
158
+ function injectScriptOnce(src, id, injectOptions = {}) {
159
+ return new Promise((resolve, reject) => {
160
+ const existing = documentRef.getElementById(id);
161
+ if (existing && injectOptions.force !== true) {
162
+ resolve();
163
+ return;
164
+ }
165
+ existing?.remove?.();
166
+ const script = documentRef.createElement('script');
167
+ const timeout = windowRef.setTimeout(() => {
168
+ cleanup();
169
+ reject(new Error(`Timed out loading ${src}`));
170
+ }, 8000);
171
+ function cleanup() {
172
+ windowRef.clearTimeout(timeout);
173
+ script.onload = null;
174
+ script.onerror = null;
175
+ script.remove();
176
+ }
177
+ script.id = id;
178
+ const runtimePrefix = String(runtimeRoot.__CODEX_OVERLEAF_RUNTIME_PREFIX__ || '');
179
+ script.src = chromeApi.runtime.getURL(runtimePrefix + src);
180
+ script.onload = () => {
181
+ cleanup();
182
+ resolve();
183
+ };
184
+ script.onerror = () => {
185
+ cleanup();
186
+ reject(new Error(`Failed to load ${src}`));
187
+ };
188
+ (documentRef.head || documentRef.documentElement).append(script);
189
+ });
190
+ }
191
+
192
+ async function initializeCapability() {
193
+ const result = await send(
194
+ 'initializeCapability',
195
+ {},
196
+ Contract.resolveDispatchPolicy('initializeCapability', timeoutOverrides)
197
+ );
198
+ if (!result?.ok) {
199
+ throw new Error(result?.error || result?.reason || 'Page bridge capability initialization failed');
200
+ }
201
+ }
202
+
203
+ return Object.freeze({
204
+ call,
205
+ cancelActiveRequests,
206
+ resolveTimeoutMs,
207
+ send,
208
+ start
209
+ });
210
+ }
211
+
212
+ function createCapability(cryptoImpl) {
213
+ if (cryptoImpl?.randomUUID) return cryptoImpl.randomUUID();
214
+ const bytes = new Uint8Array(24);
215
+ cryptoImpl.getRandomValues(bytes);
216
+ return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
217
+ }
218
+
219
+ return Object.freeze({
220
+ OPTIONAL_OT_SCRIPTS,
221
+ PAGE_WORLD_SCRIPTS,
222
+ create
223
+ });
224
+ });
@@ -29,6 +29,7 @@
29
29
  setState,
30
30
  getCurrentRunView,
31
31
  getSettingsPanelInstance,
32
+ StorageDb
32
33
  } = deps;
33
34
 
34
35
  // --- Change history (v1.7.5): the audit log finally gets a read path. ---
@@ -48,7 +49,6 @@
48
49
  list.textContent = tx('Loading\u2026', '正在加载…');
49
50
  let records = [];
50
51
  try {
51
- const StorageDb = window.CodexOverleafStorageDb;
52
52
  records = StorageDb?.getAllByIndex
53
53
  ? await StorageDb.getAllByIndex('auditLogs', 'projectId', getCurrentProjectId())
54
54
  : [];
@@ -171,7 +171,7 @@
171
171
  return;
172
172
  }
173
173
  try {
174
- await window.CodexOverleafStorageDb?.clearAllStores?.();
174
+ await StorageDb?.clearAllStores?.();
175
175
  // v1.8.1: the dashboard's project-name cache lives in
176
176
  // chrome.storage.local, outside the IndexedDB stores — clear it too so
177
177
  // no (potentially sensitive) project titles outlive the wipe.
@@ -244,7 +244,12 @@
244
244
  return;
245
245
  }
246
246
  try {
247
- await callPageBridge('jumpToPosition', { path: target, from: 0, to: 0 });
247
+ await callPageBridge('jumpToPosition', {
248
+ path: target,
249
+ from: 0,
250
+ to: 0,
251
+ runProjectId: getCurrentProjectId()
252
+ });
248
253
  } catch (error) {
249
254
  showPluginToast(tx(`Could not open ${target}: ${error.message}`, `无法打开 ${target}:${error.message}`));
250
255
  }
@@ -0,0 +1,100 @@
1
+ (function initPostNavigationSettlementPersistence(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafPostNavigationSettlementPersistence = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function postNavigationSettlementPersistenceFactory() {
6
+ 'use strict';
7
+
8
+ async function persist(options = {}) {
9
+ if (options.__insideScopedTransaction !== true) {
10
+ const accountScopeId = String(options.accountScopeId || '').trim();
11
+ const projectId = String(options.projectId || '').trim();
12
+ if (!accountScopeId) {
13
+ return { ok: false, reason: 'account_scope_unavailable' };
14
+ }
15
+ if (!projectId) {
16
+ return { ok: false, reason: 'project_scope_unavailable' };
17
+ }
18
+ if (!options.PersistenceCoordinator
19
+ || typeof options.PersistenceCoordinator.commitDetached !== 'function') {
20
+ return { ok: false, reason: 'scoped_persistence_unavailable' };
21
+ }
22
+ const committed = await options.PersistenceCoordinator.commitDetached(
23
+ projectId,
24
+ accountScopeId,
25
+ { rebase: true },
26
+ () => persist({
27
+ ...options,
28
+ __insideScopedTransaction: true
29
+ })
30
+ );
31
+ if (!committed || typeof committed !== 'object') {
32
+ return { ok: false, reason: 'scoped_persistence_invalid_result' };
33
+ }
34
+ if (committed.ok !== true) return committed;
35
+ if (!Object.prototype.hasOwnProperty.call(committed, 'value')) return committed;
36
+ return committed.value && typeof committed.value === 'object'
37
+ ? committed.value
38
+ : { ok: false, reason: 'scoped_persistence_action_result_unavailable' };
39
+ }
40
+ const {
41
+ StorageDb,
42
+ WritebackSettlement,
43
+ sessionId,
44
+ runId,
45
+ status,
46
+ statusText,
47
+ finishedAt,
48
+ settlementResult
49
+ } = options;
50
+ if (!StorageDb || !WritebackSettlement || !sessionId || !runId || !status) {
51
+ return { ok: false, reason: 'invalid_post_navigation_settlement' };
52
+ }
53
+ const record = await StorageDb.getRecord('sessions', sessionId);
54
+ if (!record) return { ok: false, reason: 'session_not_found' };
55
+ if (String(record.accountScopeId || '').trim() !== String(options.accountScopeId || '').trim()) {
56
+ return { ok: false, reason: 'account_scope_mismatch' };
57
+ }
58
+ if (String(record.projectId || '').trim() !== String(options.projectId || '').trim()) {
59
+ return { ok: false, reason: 'project_scope_mismatch' };
60
+ }
61
+ const runs = Array.isArray(record.runs) ? [...record.runs] : [];
62
+ const runIndex = runs.findIndex(run => run && run.id === runId);
63
+ if (runIndex < 0) return { ok: false, reason: 'run_not_found' };
64
+
65
+ let transitioned = settlementResult
66
+ ? WritebackSettlement.applySettlementTransition(runs[runIndex], settlementResult)
67
+ : { ...runs[runIndex] };
68
+ transitioned = WritebackSettlement.applySettlementTransition(
69
+ transitioned,
70
+ WritebackSettlement.transitionPostNavigationStatus({
71
+ status,
72
+ statusText,
73
+ finishedAt: finishedAt || transitioned.finishedAt || new Date().toISOString()
74
+ })
75
+ );
76
+ runs[runIndex] = transitioned;
77
+
78
+ const now = options.now || new Date().toISOString();
79
+ record.runs = runs;
80
+ record.lastActivityAt = now;
81
+ record.updatedAt = now;
82
+ await StorageDb.putRecord('sessions', record);
83
+ return { ok: true, run: transitioned };
84
+ }
85
+
86
+ async function persistRequired(options = {}) {
87
+ const result = await persist(options);
88
+ if (result?.ok === true) return result;
89
+ const reason = String(result?.reason || 'unknown_failure');
90
+ throw Object.assign(
91
+ new Error(`Post-navigation settlement persistence failed: ${reason}`),
92
+ {
93
+ code: 'post_navigation_settlement_persistence_failed',
94
+ reason
95
+ }
96
+ );
97
+ }
98
+
99
+ return Object.freeze({ persist, persistRequired });
100
+ });
@@ -30,24 +30,16 @@
30
30
  const nextSpeedTier = deps.readSelectedSpeed?.() || state.speedTier;
31
31
  const runSelection = deps.getRunSelection?.(nextProviderId);
32
32
  const nextProviderRevision = Number(runSelection?.providerRevision || 0);
33
+ const providerConfigurationChanged = nextProviderId === (state.providerId || 'builtin')
34
+ && nextProviderRevision !== Number(state.providerRevision || 0);
35
+ const resetProviderThread = providerChanged || providerConfigurationChanged;
33
36
  const sessions = (state.sessions || []).map(session => ({
34
37
  ...session,
35
38
  providerId: nextProviderId,
36
39
  model: nextModel,
37
40
  reasoningEffort: nextReasoningEffort,
38
41
  speedTier: nextSpeedTier,
39
- codexThreadId: providerChanged ? '' : session.codexThreadId,
40
- pendingInputs: (session.pendingInputs || []).map(item => ({
41
- ...item,
42
- payload: {
43
- ...(item.payload || {}),
44
- providerId: nextProviderId,
45
- providerRevision: nextProviderRevision,
46
- model: nextModel,
47
- reasoningEffort: nextReasoningEffort,
48
- speedTier: nextSpeedTier
49
- }
50
- }))
42
+ codexThreadId: resetProviderThread ? '' : session.codexThreadId
51
43
  }));
52
44
  deps.setState?.(deps.normalizeState({
53
45
  ...state,
@@ -23,8 +23,8 @@
23
23
  }
24
24
 
25
25
  async function clearProjectSessions(projectId, projectName) {
26
- const StorageDb = window.CodexOverleafStorageDb;
27
- const SessionState = window.CodexOverleafSessionState;
26
+ const StorageDb = deps.StorageDb;
27
+ const SessionState = deps.SessionState;
28
28
  if (!StorageDb || !SessionState) {
29
29
  return;
30
30
  }
@@ -49,7 +49,7 @@
49
49
  if (!approved) {
50
50
  return;
51
51
  }
52
- const Migration = window.CodexOverleafStorageMigration;
52
+ const Migration = deps.StorageMigration;
53
53
  if (Migration?.addSessionTombstones) {
54
54
  await Migration.addSessionTombstones(projectId, records.map(record => record.id));
55
55
  }
@@ -2,8 +2,13 @@
2
2
  'use strict';
3
3
 
4
4
  function create(options = {}) {
5
- const Profiles = window.CodexOverleafProviderProfiles;
5
+ const Profiles = options.ProviderProfiles;
6
+ const Dialog = options.ProviderSettingsDialog;
7
+ if (!Profiles || !Dialog?.create) {
8
+ throw new Error('Provider settings require explicit profile and dialog dependencies.');
9
+ }
6
10
  const instance = {
11
+ Profiles,
7
12
  tx: options.tx || ((english) => english),
8
13
  sendBackgroundNative: options.sendBackgroundNative,
9
14
  getSettingsPanelInstance: options.getSettingsPanelInstance || (() => null),
@@ -21,12 +26,24 @@
21
26
  channel: null,
22
27
  dialog: null
23
28
  };
24
- instance.onProviderChanged = options.onProviderChanged
29
+ const projectProviderChanged = options.onProviderChanged
25
30
  || ((catalog, change) => reconcileProviderSelection(instance, catalog, change));
26
- instance.dialog = window.CodexOverleafProviderSettingsDialog.create({
31
+ instance.onProviderChanged = async (catalog, change) => {
32
+ const previousProviderId = instance.getSelectedProviderId() || 'builtin';
33
+ const result = await projectProviderChanged(catalog, change);
34
+ const nextProviderId = instance.getSelectedProviderId() || 'builtin';
35
+ updateSettingsSummary(instance);
36
+ if (previousProviderId !== nextProviderId && instance.dialog?.isOpen?.()) {
37
+ instance.dialog.setCatalog(instance.catalog, nextProviderId);
38
+ }
39
+ return result;
40
+ };
41
+ instance.dialog = Dialog.create({
27
42
  document: options.document || document,
28
43
  tx: instance.tx,
44
+ ProviderProfiles: Profiles,
29
45
  callbacks: {
46
+ getCurrentProjectProviderId: () => instance.getSelectedProviderId() || 'builtin',
30
47
  onTest: context => testProvider(instance, context),
31
48
  onCancelTest: () => cancelProviderTest(instance),
32
49
  onSave: (context, action) => saveProvider(instance, context, action),
@@ -135,7 +152,7 @@
135
152
  await ensureLoadedWithRetry(instance);
136
153
  } catch (_error) {
137
154
  return instance.refreshModelOptions(
138
- window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, instance.getSelectedProviderId())
155
+ instance.Profiles.buildRunSelection(instance.catalog, instance.getSelectedProviderId())
139
156
  );
140
157
  }
141
158
  return reconcileProviderSelection(instance, instance.catalog, { forceSessionRefresh: true });
@@ -143,12 +160,18 @@
143
160
 
144
161
  async function reconcileProviderSelection(instance, catalog, change = {}) {
145
162
  const previousProviderId = instance.getSelectedProviderId() || 'builtin';
146
- const requestedProviderId = typeof change.sessionProviderId === 'string' && change.sessionProviderId
163
+ let requestedProviderId = typeof change.sessionProviderId === 'string' && change.sessionProviderId
147
164
  ? change.sessionProviderId
148
165
  : previousProviderId;
166
+ const requestedProviderAvailable = requestedProviderId === 'builtin'
167
+ || Boolean(instance.Profiles.getProviderById(catalog, requestedProviderId));
168
+ const selectedProviderWasRemoved = !requestedProviderAvailable;
169
+ if (selectedProviderWasRemoved) {
170
+ requestedProviderId = 'builtin';
171
+ }
149
172
  const projectProviderChanged = requestedProviderId !== previousProviderId;
150
- if (projectProviderChanged && change.providerSwitchApproved !== true) {
151
- const requestedProvider = window.CodexOverleafProviderProfiles.getProviderById(catalog, requestedProviderId);
173
+ if (projectProviderChanged && !selectedProviderWasRemoved && change.providerSwitchApproved !== true) {
174
+ const requestedProvider = instance.Profiles.getProviderById(catalog, requestedProviderId);
152
175
  const approved = await instance.confirmProviderSwitch({
153
176
  providerId: requestedProviderId,
154
177
  providerName: requestedProvider?.name || requestedProviderId
@@ -158,13 +181,14 @@
158
181
  }
159
182
  }
160
183
  const changedProviderIds = Array.isArray(change.changedProviderIds) ? change.changedProviderIds : [];
161
- if (!change.forceSessionRefresh && !projectProviderChanged && !changedProviderIds.includes(requestedProviderId)) {
184
+ const providerConfigurationChanged = changedProviderIds.includes(requestedProviderId);
185
+ if (!change.forceSessionRefresh && !projectProviderChanged && !providerConfigurationChanged) {
162
186
  return;
163
187
  }
164
188
  const loadResult = change.modelsPreloaded === true
165
189
  ? { stale: false, selectedModel: change.preloadedModelId || '' }
166
190
  : await instance.refreshModelOptions(
167
- window.CodexOverleafProviderProfiles.buildRunSelection(catalog, requestedProviderId),
191
+ instance.Profiles.buildRunSelection(catalog, requestedProviderId),
168
192
  { persist: false }
169
193
  );
170
194
  if (loadResult?.stale || loadResult?.error) {
@@ -175,7 +199,7 @@
175
199
  error.details = loadResult?.error || {};
176
200
  throw error;
177
201
  }
178
- if (projectProviderChanged) {
202
+ if (projectProviderChanged || providerConfigurationChanged) {
179
203
  await instance.setSelectedProviderId(requestedProviderId, loadResult?.selectedModel || '');
180
204
  }
181
205
  await instance.persistInputs();
@@ -185,7 +209,7 @@
185
209
 
186
210
  async function prepareProviderActivation(instance, providerId) {
187
211
  const previousProviderId = instance.getSelectedProviderId() || 'builtin';
188
- const provider = window.CodexOverleafProviderProfiles.getProviderById(instance.catalog, providerId);
212
+ const provider = instance.Profiles.getProviderById(instance.catalog, providerId);
189
213
  if (providerId !== previousProviderId) {
190
214
  const approved = await instance.confirmProviderSwitch({
191
215
  providerId,
@@ -196,13 +220,13 @@
196
220
  }
197
221
  }
198
222
  const result = await instance.refreshModelOptions(
199
- window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, providerId),
223
+ instance.Profiles.buildRunSelection(instance.catalog, providerId),
200
224
  { persist: false }
201
225
  );
202
226
  if (result?.stale || result?.error) {
203
227
  if (providerId !== previousProviderId) {
204
228
  await instance.refreshModelOptions(
205
- window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, previousProviderId),
229
+ instance.Profiles.buildRunSelection(instance.catalog, previousProviderId),
206
230
  { persist: false }
207
231
  ).catch(() => {});
208
232
  }
@@ -293,7 +317,11 @@
293
317
  selectedId: result.savedProviderId
294
318
  });
295
319
  await notifyChanged(instance, applied.change, action.activate === true
296
- ? { sessionProviderId: result.savedProviderId }
320
+ ? {
321
+ sessionProviderId: result.savedProviderId,
322
+ providerSwitchApproved: true,
323
+ requireProjection: true
324
+ }
297
325
  : {});
298
326
  instance.dialog.setBusy('', '');
299
327
  const fellBackToBuiltin = previousCatalog.activeProviderId === result.savedProviderId
@@ -389,7 +417,7 @@
389
417
  } catch (error) {
390
418
  if (preparation?.previousProviderId && preparation.previousProviderId !== context.profileId) {
391
419
  await instance.refreshModelOptions(
392
- window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
420
+ instance.Profiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
393
421
  { persist: false }
394
422
  ).catch(() => {});
395
423
  }
@@ -423,7 +451,7 @@
423
451
  } catch (error) {
424
452
  if (preparation?.previousProviderId && preparation.previousProviderId !== 'builtin') {
425
453
  await instance.refreshModelOptions(
426
- window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
454
+ instance.Profiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
427
455
  { persist: false }
428
456
  ).catch(() => {});
429
457
  }
@@ -448,7 +476,7 @@
448
476
  }
449
477
 
450
478
  function applyCatalog(instance, result, options = {}) {
451
- const nextCatalog = window.CodexOverleafProviderProfiles.normalizeCatalog(result);
479
+ const nextCatalog = instance.Profiles.normalizeCatalog(result);
452
480
  if (instance.loaded && nextCatalog.storeRevision < instance.catalog.storeRevision) {
453
481
  return { applied: false, change: emptyCatalogChange() };
454
482
  }
@@ -463,19 +491,19 @@
463
491
  instance.dialog.setCatalog(instance.catalog, options.selectedId);
464
492
  }
465
493
  }
466
- return { applied: true, change: describeCatalogChange(previousCatalog, nextCatalog) };
494
+ return { applied: true, change: describeCatalogChange(instance, previousCatalog, nextCatalog) };
467
495
  }
468
496
 
469
497
  function updateSettingsSummary(instance, override = {}) {
470
498
  const selectedProviderId = instance.getSelectedProviderId() || 'builtin';
471
- const catalogProvider = window.CodexOverleafProviderProfiles.getProviderById(
499
+ const catalogProvider = instance.Profiles.getProviderById(
472
500
  instance.catalog,
473
501
  selectedProviderId
474
502
  );
475
503
  const selectedProvider = catalogProvider?.id === selectedProviderId
476
504
  ? catalogProvider
477
505
  : (selectedProviderId === 'builtin'
478
- ? window.CodexOverleafProviderProfiles.getActiveProvider(instance.catalog)
506
+ ? instance.Profiles.getActiveProvider(instance.catalog)
479
507
  : {
480
508
  id: selectedProviderId,
481
509
  name: selectedProviderId,
@@ -528,16 +556,16 @@
528
556
  }
529
557
  }
530
558
 
531
- function describeCatalogChange(previous, next) {
532
- const previousActive = window.CodexOverleafProviderProfiles.getActiveProvider(previous);
533
- const nextActive = window.CodexOverleafProviderProfiles.getActiveProvider(next);
559
+ function describeCatalogChange(instance, previous, next) {
560
+ const previousActive = instance.Profiles.getActiveProvider(previous);
561
+ const nextActive = instance.Profiles.getActiveProvider(next);
534
562
  const providerIds = new Set([
535
563
  ...(previous.providers || []).map(provider => provider.id),
536
564
  ...(next.providers || []).map(provider => provider.id)
537
565
  ]);
538
566
  const changedProviderIds = Array.from(providerIds).filter(providerId => {
539
- const previousProvider = window.CodexOverleafProviderProfiles.getProviderById(previous, providerId);
540
- const nextProvider = window.CodexOverleafProviderProfiles.getProviderById(next, providerId);
567
+ const previousProvider = instance.Profiles.getProviderById(previous, providerId);
568
+ const nextProvider = instance.Profiles.getProviderById(next, providerId);
541
569
  return modelCatalogSignature(previousProvider) !== modelCatalogSignature(nextProvider);
542
570
  });
543
571
  return {
@@ -548,8 +576,13 @@
548
576
  }
549
577
 
550
578
  function modelCatalogSignature(provider = {}) {
579
+ provider = provider || {};
551
580
  return JSON.stringify({
552
581
  id: provider.id || '',
582
+ revision: Number(provider.revision || 0),
583
+ baseUrl: provider.baseUrl || '',
584
+ wireApiPreference: provider.wireApiPreference || '',
585
+ resolvedWireApi: provider.resolvedWireApi || '',
553
586
  defaultModelId: provider.defaultModelId || '',
554
587
  reasoningAdapter: provider.reasoningAdapter || '',
555
588
  reasoningCapability: provider.reasoningCapability || '',
@@ -567,7 +600,7 @@
567
600
 
568
601
  function getProviderSnapshot(instance, providerId) {
569
602
  const requestedId = providerId || instance.getSelectedProviderId() || 'builtin';
570
- const provider = window.CodexOverleafProviderProfiles.getProviderById(instance.catalog, requestedId);
603
+ const provider = instance.Profiles.getProviderById(instance.catalog, requestedId);
571
604
  if (!provider) {
572
605
  return {
573
606
  providerId: requestedId,