codex-overleaf-link 1.9.13 → 2.1.0

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 (122) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +13 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +130 -65
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +981 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/trackedChangesLifecycle.js +154 -26
  23. package/extension/src/page/writebackRouter.js +31 -60
  24. package/extension/src/pageBridge.js +1 -1
  25. package/extension/src/shared/agentTranscript.js +130 -0
  26. package/extension/src/shared/compatibility.js +10 -7
  27. package/extension/src/shared/failureReasons.js +75 -0
  28. package/extension/src/shared/i18n.js +22 -0
  29. package/extension/src/shared/models.js +1 -0
  30. package/extension/src/shared/providerProfiles.js +283 -0
  31. package/extension/src/shared/sessionState.js +12 -2
  32. package/extension/src/shared/storageDb.js +27 -19
  33. package/extension/src/shared/storageRunActions.js +99 -0
  34. package/extension/styles/panel.css +587 -1
  35. package/extension/vendor/katex/LICENSE +21 -0
  36. package/extension/vendor/katex/VERSION +2 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  95. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  96. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  97. package/extension/vendor/katex/katex.min.css +1 -0
  98. package/extension/vendor/katex/katex.min.js +1 -0
  99. package/native-host/src/anthropicBridgeRequest.js +435 -0
  100. package/native-host/src/anthropicBridgeResponse.js +431 -0
  101. package/native-host/src/anthropicMessagesBridge.js +281 -0
  102. package/native-host/src/chatBridgeRequest.js +274 -0
  103. package/native-host/src/chatBridgeResponse.js +594 -0
  104. package/native-host/src/chatCompletionsBridge.js +485 -0
  105. package/native-host/src/codexCommand.js +25 -0
  106. package/native-host/src/codexPromptAssembly.js +9 -0
  107. package/native-host/src/codexProviderLaunch.js +213 -0
  108. package/native-host/src/codexProviderTest.js +196 -0
  109. package/native-host/src/codexSessionRunner.js +29 -25
  110. package/native-host/src/managedInstall.js +1 -0
  111. package/native-host/src/providerBridgeRoutes.js +11 -0
  112. package/native-host/src/providerModelCatalog.js +107 -0
  113. package/native-host/src/providerProfile.js +454 -0
  114. package/native-host/src/providerReasoning.js +235 -0
  115. package/native-host/src/providerRedaction.js +18 -0
  116. package/native-host/src/providerRuntime.js +365 -0
  117. package/native-host/src/providerStore.js +691 -0
  118. package/native-host/src/readProgressGuard.js +360 -0
  119. package/native-host/src/taskRunnerRuntime.js +40 -4
  120. package/native-host/src/updateArchive.js +1 -0
  121. package/native-host/src/updateTrust.js +2 -1
  122. package/package.json +2 -1
@@ -0,0 +1,413 @@
1
+ (function initCodexOverleafProviderSettingsCoordinator() {
2
+ 'use strict';
3
+
4
+ function create(options = {}) {
5
+ const Profiles = window.CodexOverleafProviderProfiles;
6
+ const instance = {
7
+ tx: options.tx || ((english) => english),
8
+ sendBackgroundNative: options.sendBackgroundNative,
9
+ getSettingsPanelInstance: options.getSettingsPanelInstance || (() => null),
10
+ getSelectedModel: options.getSelectedModel || (() => ''),
11
+ clearSelectedModel: options.clearSelectedModel || (() => {}),
12
+ refreshModelOptions: options.refreshModelOptions || (() => {}),
13
+ persistInputs: options.persistInputs || (() => {}),
14
+ catalog: Profiles.normalizeCatalog({}),
15
+ loaded: false,
16
+ loadPromise: null,
17
+ testOperationId: '',
18
+ channel: null,
19
+ dialog: null
20
+ };
21
+ instance.onProviderChanged = options.onProviderChanged
22
+ || ((catalog, change) => reconcileProviderSelection(instance, catalog, change));
23
+ instance.dialog = window.CodexOverleafProviderSettingsDialog.create({
24
+ document: options.document || document,
25
+ tx: instance.tx,
26
+ callbacks: {
27
+ onTest: context => testProvider(instance, context),
28
+ onCancelTest: () => cancelProviderTest(instance),
29
+ onSave: (context, action) => saveProvider(instance, context, action),
30
+ onActivate: context => activateProvider(instance, context),
31
+ onClearSecret: context => clearProviderSecret(instance, context),
32
+ onDelete: context => deleteProvider(instance, context),
33
+ onActivateBuiltin: () => activateBuiltin(instance)
34
+ }
35
+ });
36
+ setupCrossTabRefresh(instance, options.window || window);
37
+ return {
38
+ open: () => open(instance),
39
+ refreshSummary: () => refresh(instance),
40
+ ensureLoaded: () => ensureLoaded(instance),
41
+ getRunSelection: () => instance.loaded ? Profiles.buildRunSelection(instance.catalog) : null,
42
+ getCatalog: () => instance.catalog,
43
+ destroy: () => destroy(instance),
44
+ _instance: instance
45
+ };
46
+ }
47
+
48
+ async function open(instance) {
49
+ instance.dialog.open(instance.catalog);
50
+ instance.dialog.setBusy('loading', instance.tx('Loading providers…', '正在加载模型服务…'));
51
+ try {
52
+ await refresh(instance, { updateDialog: true });
53
+ instance.dialog.setBusy('', '');
54
+ } catch (_error) {
55
+ // refresh() already projected the actionable failure into the dialog.
56
+ // Keep the modal open so the user can close it or retry from Settings.
57
+ }
58
+ }
59
+
60
+ async function refresh(instance, options = {}) {
61
+ try {
62
+ const result = await request(instance, 'codex.providers.list');
63
+ return applyCatalog(instance, result, options);
64
+ } catch (error) {
65
+ updateSettingsSummary(instance, {
66
+ summary: instance.tx('Provider settings unavailable', '模型服务设置不可用'),
67
+ tone: 'failed'
68
+ });
69
+ if (options.updateDialog) {
70
+ instance.dialog.setBusy('failed', formatError(instance, error));
71
+ }
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function ensureLoaded(instance) {
77
+ if (instance.loaded) {
78
+ return instance.catalog;
79
+ }
80
+ if (!instance.loadPromise) {
81
+ instance.loadPromise = refresh(instance).finally(() => {
82
+ instance.loadPromise = null;
83
+ });
84
+ }
85
+ await instance.loadPromise;
86
+ return instance.catalog;
87
+ }
88
+
89
+ async function reconcileProviderSelection(instance, catalog, change = {}) {
90
+ if (!change.activeProviderChanged && !change.activeModelCatalogChanged) {
91
+ return;
92
+ }
93
+ const previousModel = instance.getSelectedModel();
94
+ const activeProvider = window.CodexOverleafProviderProfiles.getActiveProvider(catalog);
95
+ const previousModelUnavailable = activeProvider.kind === 'custom'
96
+ && previousModel
97
+ && !activeProvider.models.some(model => model.id === previousModel);
98
+ if (change.activeProviderChanged || previousModelUnavailable) {
99
+ instance.clearSelectedModel();
100
+ }
101
+ await instance.refreshModelOptions();
102
+ await instance.persistInputs();
103
+ }
104
+
105
+ async function testProvider(instance, context) {
106
+ const operationId = crypto.randomUUID();
107
+ instance.testOperationId = operationId;
108
+ instance.dialog.setBusy('testing', instance.tx('Preparing compatibility test…', '正在准备兼容性测试…'));
109
+ try {
110
+ const result = await request(instance, 'codex.providers.test', {
111
+ operationId,
112
+ profileId: context.profileId,
113
+ expectedRevision: context.expectedRevision,
114
+ draft: context.draft,
115
+ secretMutation: context.secretMutation,
116
+ modelId: context.testModelId,
117
+ totalBudgetMs: 120000
118
+ }, event => {
119
+ if (instance.testOperationId === operationId && event?.type === 'provider.test.progress') {
120
+ instance.dialog.setTestProgress(event);
121
+ }
122
+ });
123
+ if (instance.testOperationId !== operationId) {
124
+ return;
125
+ }
126
+ instance.dialog.setBusy('', '');
127
+ instance.dialog.setVerification(result);
128
+ } catch (error) {
129
+ if (instance.testOperationId !== operationId) {
130
+ return;
131
+ }
132
+ instance.dialog.setBusy('failed', formatError(instance, error));
133
+ instance.dialog.setVerificationFailure({
134
+ modelId: context.testModelId,
135
+ errorCode: error?.code || error?.details?.code || ''
136
+ });
137
+ instance.dialog.setStatus({ tone: 'failed', title: formatError(instance, error) });
138
+ } finally {
139
+ if (instance.testOperationId === operationId) {
140
+ instance.testOperationId = '';
141
+ }
142
+ }
143
+ }
144
+
145
+ async function cancelProviderTest(instance) {
146
+ const operationId = instance.testOperationId;
147
+ instance.testOperationId = '';
148
+ instance.dialog.setBusy('', '');
149
+ if (!operationId) {
150
+ return;
151
+ }
152
+ try {
153
+ await request(instance, 'codex.providers.test.cancel', { operationId });
154
+ } catch (_error) {
155
+ // Closing the dialog remains immediate; the test process has its own timeout.
156
+ }
157
+ }
158
+
159
+ async function saveProvider(instance, context, action = {}) {
160
+ instance.dialog.setBusy('saving', instance.tx('Saving provider…', '正在保存模型服务…'));
161
+ try {
162
+ const result = await request(instance, 'codex.providers.upsert', {
163
+ profileId: context.profileId,
164
+ expectedRevision: context.expectedRevision,
165
+ draft: context.draft,
166
+ secretMutation: context.secretMutation,
167
+ activate: action.activate === true,
168
+ disclosureHost: context.disclosureHost,
169
+ disclosureBaseUrl: context.disclosureBaseUrl,
170
+ diagnostics: context.verifications
171
+ });
172
+ const previousCatalog = instance.catalog;
173
+ const applied = applyCatalog(instance, result, {
174
+ updateDialog: true,
175
+ selectedId: result.savedProviderId
176
+ });
177
+ await notifyChanged(instance, applied.change);
178
+ instance.dialog.setBusy('', '');
179
+ const fellBackToBuiltin = previousCatalog.activeProviderId === result.savedProviderId
180
+ && result.activeProviderId === 'builtin'
181
+ && action.activate !== true;
182
+ instance.dialog.setStatus({
183
+ tone: fellBackToBuiltin ? 'warning' : 'success',
184
+ title: fellBackToBuiltin
185
+ ? instance.tx(
186
+ 'Provider saved. Its endpoint changed, so Built-in Codex is active until the new endpoint is approved.',
187
+ '模型服务已保存。由于端点发生变化,确认新端点前将使用内置 Codex。'
188
+ )
189
+ : action.activate
190
+ ? instance.tx('Provider saved and activated.', '模型服务已保存并启用。')
191
+ : instance.tx('Provider saved.', '模型服务已保存。')
192
+ });
193
+ } catch (error) {
194
+ instance.dialog.setBusy('failed', formatError(instance, error));
195
+ instance.dialog.setStatus({ tone: 'failed', title: formatError(instance, error) });
196
+ }
197
+ }
198
+
199
+ async function clearProviderSecret(instance, context) {
200
+ instance.dialog.setBusy('saving', instance.tx('Removing stored API key…', '正在删除已存储的 API 密钥…'));
201
+ try {
202
+ const result = await request(instance, 'codex.providers.clear-secret', {
203
+ providerId: context.profileId,
204
+ expectedRevision: context.expectedRevision
205
+ });
206
+ const applied = applyCatalog(instance, result, {
207
+ updateDialog: true,
208
+ selectedId: context.profileId
209
+ });
210
+ await notifyChanged(instance, applied.change);
211
+ instance.dialog.setBusy('', '');
212
+ instance.dialog.setStatus({
213
+ tone: 'success',
214
+ title: result.activeProviderId === 'builtin'
215
+ ? instance.tx('API key removed. Built-in Codex is now active.', 'API 密钥已删除,当前已切换到内置 Codex。')
216
+ : instance.tx('Stored API key removed.', '已删除存储的 API 密钥。')
217
+ });
218
+ } catch (error) {
219
+ instance.dialog.setBusy('failed', formatError(instance, error));
220
+ }
221
+ }
222
+
223
+ async function deleteProvider(instance, context) {
224
+ instance.dialog.setBusy('deleting', instance.tx('Deleting provider…', '正在删除模型服务…'));
225
+ try {
226
+ const result = await request(instance, 'codex.providers.delete', {
227
+ providerId: context.profileId,
228
+ expectedRevision: context.expectedRevision
229
+ });
230
+ const applied = applyCatalog(instance, result, { updateDialog: true });
231
+ await notifyChanged(instance, applied.change);
232
+ instance.dialog.setBusy('', '');
233
+ instance.dialog.setStatus({ tone: 'success', title: instance.tx('Provider deleted.', '模型服务已删除。') });
234
+ } catch (error) {
235
+ instance.dialog.setBusy('failed', formatError(instance, error));
236
+ }
237
+ }
238
+
239
+ async function activateProvider(instance, context) {
240
+ instance.dialog.setBusy('saving', instance.tx('Activating provider…', '正在启用模型服务…'));
241
+ try {
242
+ const result = await request(instance, 'codex.providers.activate', {
243
+ providerId: context.profileId,
244
+ expectedRevision: context.expectedRevision,
245
+ disclosureHost: context.disclosureHost,
246
+ disclosureBaseUrl: context.disclosureBaseUrl
247
+ });
248
+ const applied = applyCatalog(instance, result, {
249
+ updateDialog: true,
250
+ selectedId: context.profileId
251
+ });
252
+ await notifyChanged(instance, applied.change);
253
+ instance.dialog.setBusy('', '');
254
+ instance.dialog.setStatus({
255
+ tone: 'success',
256
+ title: instance.tx('Provider activated.', '模型服务已启用。')
257
+ });
258
+ } catch (error) {
259
+ instance.dialog.setBusy('failed', formatError(instance, error));
260
+ instance.dialog.setStatus({ tone: 'failed', title: formatError(instance, error) });
261
+ }
262
+ }
263
+
264
+ async function activateBuiltin(instance) {
265
+ instance.dialog.setBusy('saving', instance.tx('Activating built-in Codex…', '正在启用内置 Codex…'));
266
+ try {
267
+ const result = await request(instance, 'codex.providers.activate', {
268
+ providerId: 'builtin',
269
+ expectedRevision: 0
270
+ });
271
+ const applied = applyCatalog(instance, result, { updateDialog: true, selectedId: 'builtin' });
272
+ await notifyChanged(instance, applied.change);
273
+ instance.dialog.setBusy('', '');
274
+ instance.dialog.setStatus({ tone: 'success', title: instance.tx('Built-in Codex is active.', '已启用内置 Codex。') });
275
+ } catch (error) {
276
+ instance.dialog.setBusy('failed', formatError(instance, error));
277
+ }
278
+ }
279
+
280
+ async function request(instance, method, params = {}, onEvent) {
281
+ if (typeof instance.sendBackgroundNative !== 'function') {
282
+ throw createClientError('native_unavailable', 'Native Host request channel is unavailable.');
283
+ }
284
+ const response = await instance.sendBackgroundNative({ method, params }, onEvent);
285
+ if (!response?.ok) {
286
+ const error = createClientError(
287
+ response?.error?.code || 'provider_request_failed',
288
+ response?.error?.message || 'Provider request failed.'
289
+ );
290
+ error.details = response?.error || {};
291
+ throw error;
292
+ }
293
+ return response.result || {};
294
+ }
295
+
296
+ function applyCatalog(instance, result, options = {}) {
297
+ const nextCatalog = window.CodexOverleafProviderProfiles.normalizeCatalog(result);
298
+ if (instance.loaded && nextCatalog.storeRevision < instance.catalog.storeRevision) {
299
+ return { applied: false, change: emptyCatalogChange() };
300
+ }
301
+ const previousCatalog = instance.catalog;
302
+ instance.catalog = nextCatalog;
303
+ instance.loaded = true;
304
+ updateSettingsSummary(instance);
305
+ if (options.updateDialog && instance.dialog.isOpen()) {
306
+ if (options.source === 'cross-tab') {
307
+ instance.dialog.offerCatalog(instance.catalog, options.selectedId);
308
+ } else {
309
+ instance.dialog.setCatalog(instance.catalog, options.selectedId);
310
+ }
311
+ }
312
+ return { applied: true, change: describeCatalogChange(previousCatalog, nextCatalog) };
313
+ }
314
+
315
+ function updateSettingsSummary(instance, override = {}) {
316
+ const active = window.CodexOverleafProviderProfiles.getActiveProvider(instance.catalog);
317
+ const summary = override.summary || (active.kind === 'builtin'
318
+ ? instance.tx('Built-in Codex is active', '当前使用内置 Codex')
319
+ : `${active.name} · ${active.defaultModelId || instance.tx('No model', '未配置模型')} · ${instance.tx('Active', '使用中')}`);
320
+ instance.getSettingsPanelInstance()?.setProviderSummary?.({
321
+ summary,
322
+ tone: override.tone || (active.kind === 'custom' && active.wireApiPreference === 'auto' && !active.resolvedWireApi ? 'warning' : 'ok')
323
+ });
324
+ }
325
+
326
+ async function notifyChanged(instance, change = emptyCatalogChange()) {
327
+ try {
328
+ instance.channel?.postMessage?.({ type: 'provider-settings-changed', change, at: Date.now() });
329
+ } catch (_error) {
330
+ // Revision checks remain authoritative when BroadcastChannel is unavailable.
331
+ }
332
+ try {
333
+ await instance.onProviderChanged(instance.catalog, change);
334
+ } catch (_error) {
335
+ // The authoritative catalog is already saved. A later refresh can retry
336
+ // the local model projection without rolling back the provider change.
337
+ }
338
+ }
339
+
340
+ function setupCrossTabRefresh(instance, targetWindow) {
341
+ try {
342
+ instance.channel = new targetWindow.BroadcastChannel('codex-overleaf-provider-settings-v1');
343
+ instance.channel.addEventListener('message', event => {
344
+ if (event.data?.type !== 'provider-settings-changed') {
345
+ return;
346
+ }
347
+ refresh(instance, { updateDialog: instance.dialog.isOpen(), source: 'cross-tab' })
348
+ .then(applied => applied.applied && instance.onProviderChanged(instance.catalog, applied.change))
349
+ .catch(() => {});
350
+ });
351
+ } catch (_error) {
352
+ instance.channel = null;
353
+ }
354
+ }
355
+
356
+ function describeCatalogChange(previous, next) {
357
+ const previousActive = window.CodexOverleafProviderProfiles.getActiveProvider(previous);
358
+ const nextActive = window.CodexOverleafProviderProfiles.getActiveProvider(next);
359
+ return {
360
+ activeProviderChanged: previousActive.id !== nextActive.id,
361
+ activeModelCatalogChanged: modelCatalogSignature(previousActive) !== modelCatalogSignature(nextActive)
362
+ };
363
+ }
364
+
365
+ function modelCatalogSignature(provider = {}) {
366
+ return JSON.stringify({
367
+ id: provider.id || '',
368
+ defaultModelId: provider.defaultModelId || '',
369
+ reasoningAdapter: provider.reasoningAdapter || '',
370
+ reasoningCapability: provider.reasoningCapability || '',
371
+ models: (provider.models || []).map(model => ({
372
+ id: model.id,
373
+ label: model.label,
374
+ reasoningEfforts: model.reasoningEfforts || []
375
+ }))
376
+ });
377
+ }
378
+
379
+ function emptyCatalogChange() {
380
+ return { activeProviderChanged: false, activeModelCatalogChanged: false };
381
+ }
382
+
383
+ function formatError(instance, error) {
384
+ const code = error?.code || error?.details?.code || '';
385
+ const messages = {
386
+ provider_auth_rejected: instance.tx('The provider rejected the API key.', '模型服务拒绝了 API 密钥。'),
387
+ provider_model_not_found: instance.tx('The configured model was not found.', '未找到所配置的模型。'),
388
+ provider_protocol_incompatible: instance.tx('The endpoint is incompatible with the selected API protocol.', '端点与所选 API 协议不兼容。'),
389
+ provider_request_rejected: instance.tx('The provider rejected the probe request. Review the model and compatibility settings.', '模型服务拒绝了探测请求,请检查模型和兼容设置。'),
390
+ provider_agent_tools_incompatible: instance.tx('The model answered, but it could not complete the Codex tool-call loop.', '模型能够回答,但无法完成 Codex 工具调用闭环。'),
391
+ provider_stream_tool_parse_failed: instance.tx('Streaming tool calls were malformed. Test again with Auto or Buffered upstream responses.', '流式工具调用解析异常,请使用“自动”或“缓冲上游响应”后重新测试。'),
392
+ provider_response_invalid: instance.tx('The provider completed without usable text or tool calls.', '模型服务结束了请求,但没有返回可用文本或工具调用。'),
393
+ provider_configuration_invalid: instance.tx('Review the endpoint, authentication, headers, and compatibility settings.', '请检查端点、鉴权、请求头和兼容设置。'),
394
+ provider_connection_timeout: instance.tx('The provider connection timed out.', '连接模型服务超时。'),
395
+ provider_revision_conflict: instance.tx('This provider changed in another tab. Reload it and retry.', '此模型服务已在其他标签页发生变化,请刷新后重试。'),
396
+ provider_protocol_negotiation_required: instance.tx('The Auto protocol could not be negotiated for this model.', '无法为此模型自动协商 API 协议。')
397
+ };
398
+ return messages[code] || error?.message || instance.tx('Provider operation failed.', '模型服务操作失败。');
399
+ }
400
+
401
+ function createClientError(code, message) {
402
+ const error = new Error(message);
403
+ error.code = code;
404
+ return error;
405
+ }
406
+
407
+ function destroy(instance) {
408
+ instance.channel?.close?.();
409
+ instance.dialog?.destroy?.();
410
+ }
411
+
412
+ window.CodexOverleafProviderSettingsCoordinator = { create };
413
+ })();