codex-overleaf-link 2.0.0 → 2.1.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 (119) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +12 -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 +129 -64
  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 +1006 -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/writebackRouter.js +20 -33
  23. package/extension/src/pageBridge.js +1 -1
  24. package/extension/src/shared/agentTranscript.js +130 -0
  25. package/extension/src/shared/compatibility.js +3 -2
  26. package/extension/src/shared/failureReasons.js +75 -0
  27. package/extension/src/shared/i18n.js +22 -0
  28. package/extension/src/shared/models.js +1 -0
  29. package/extension/src/shared/providerProfiles.js +283 -0
  30. package/extension/src/shared/sessionState.js +12 -2
  31. package/extension/src/shared/storageDb.js +1 -1
  32. package/extension/styles/panel.css +689 -1
  33. package/extension/vendor/katex/LICENSE +21 -0
  34. package/extension/vendor/katex/VERSION +2 -0
  35. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  36. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  95. package/extension/vendor/katex/katex.min.css +1 -0
  96. package/extension/vendor/katex/katex.min.js +1 -0
  97. package/native-host/src/anthropicBridgeRequest.js +435 -0
  98. package/native-host/src/anthropicBridgeResponse.js +431 -0
  99. package/native-host/src/anthropicMessagesBridge.js +281 -0
  100. package/native-host/src/chatBridgeRequest.js +274 -0
  101. package/native-host/src/chatBridgeResponse.js +594 -0
  102. package/native-host/src/chatCompletionsBridge.js +485 -0
  103. package/native-host/src/codexCommand.js +25 -0
  104. package/native-host/src/codexPromptAssembly.js +9 -0
  105. package/native-host/src/codexProviderLaunch.js +213 -0
  106. package/native-host/src/codexProviderTest.js +196 -0
  107. package/native-host/src/codexSessionRunner.js +29 -25
  108. package/native-host/src/managedInstall.js +1 -0
  109. package/native-host/src/providerBridgeRoutes.js +11 -0
  110. package/native-host/src/providerModelCatalog.js +107 -0
  111. package/native-host/src/providerProfile.js +454 -0
  112. package/native-host/src/providerReasoning.js +235 -0
  113. package/native-host/src/providerRedaction.js +18 -0
  114. package/native-host/src/providerRuntime.js +365 -0
  115. package/native-host/src/providerStore.js +691 -0
  116. package/native-host/src/readProgressGuard.js +360 -0
  117. package/native-host/src/taskRunnerRuntime.js +39 -3
  118. package/native-host/src/updateArchive.js +1 -0
  119. package/package.json +2 -1
@@ -0,0 +1,1006 @@
1
+ (function initCodexOverleafProviderSettingsDialog() {
2
+ 'use strict';
3
+
4
+ // Official OpenAI blossom mark used by the Codex VS Code extension.
5
+ const CODEX_BLOSSOM_PATH = 'M13.798 23.976a5.7 5.7 0 0 1-2.26-.456 6.1 6.1 0 0 1-1.903-1.27 5.7 5.7 0 0 1-1.88.311 5.75 5.75 0 0 1-2.95-.79 6.2 6.2 0 0 1-2.188-2.159q-.81-1.366-.809-3.045 0-.695.19-1.51a6.4 6.4 0 0 1-1.475-2.038A5.95 5.95 0 0 1 0 10.573Q0 9.278.547 8.08q.547-1.2 1.523-2.062a5.5 5.5 0 0 1 2.307-1.223A5.7 5.7 0 0 1 5.472 2.35 6.1 6.1 0 0 1 7.565.623 5.8 5.8 0 0 1 10.206 0q1.19 0 2.26.456a6.1 6.1 0 0 1 1.903 1.27 5.7 5.7 0 0 1 1.88-.311q1.594 0 2.95.79a6 6 0 0 1 2.165 2.159q.832 1.366.832 3.045 0 .695-.19 1.51a6.3 6.3 0 0 1 1.475 2.062q.523 1.15.523 2.422a5.9 5.9 0 0 1-.547 2.493q-.547 1.2-1.546 2.086a5.4 5.4 0 0 1-2.284 1.199 5.56 5.56 0 0 1-1.118 2.445 5.9 5.9 0 0 1-2.07 1.727 5.8 5.8 0 0 1-2.64.623m-5.876-2.997q1.19 0 2.07-.504l4.472-2.589a.53.53 0 0 0 .238-.455v-2.062L8.945 18.7a.96.96 0 0 1-1.047 0l-4.496-2.613a.7.7 0 0 1-.024.168v.287q0 1.224.571 2.254a4.24 4.24 0 0 0 1.642 1.583q1.047.6 2.331.599m.238-3.908a.6.6 0 0 0 .262.072q.118 0 .238-.072l1.784-1.031-5.734-3.357q-.522-.312-.523-.935V6.545a4.3 4.3 0 0 0-1.903 1.63 4.25 4.25 0 0 0-.714 2.398q0 1.176.595 2.254.594 1.08 1.546 1.63zm5.638 5.323q1.26 0 2.284-.576a4.3 4.3 0 0 0 1.618-1.582q.595-1.008.595-2.254v-5.179a.47.47 0 0 0-.238-.431l-1.808-1.055v6.689q0 .624-.524.935l-4.496 2.613a4.3 4.3 0 0 0 2.57.84m.904-8.776v-3.26l-2.688-1.535-2.712 1.535v3.26l2.712 1.535zM7.756 5.97q0-.623.523-.935l4.496-2.613a4.3 4.3 0 0 0-2.569-.84q-1.26 0-2.284.576A4.3 4.3 0 0 0 6.304 3.74q-.57 1.008-.57 2.254v5.155q0 .287.237.455l1.785 1.055zM19.84 17.43a4.16 4.16 0 0 0 1.88-1.63 4.33 4.33 0 0 0 .713-2.397q0-1.176-.595-2.254-.594-1.08-1.546-1.63l-4.449-2.59q-.143-.096-.261-.072a.46.46 0 0 0-.238.072L13.56 7.936l5.758 3.38a.9.9 0 0 1 .38.384q.143.216.143.528zM15.059 5.25q.524-.335 1.047 0l4.52 2.662V7.48q0-1.15-.57-2.181A4.14 4.14 0 0 0 18.46 3.62q-1.023-.623-2.379-.623-1.19 0-2.07.503L9.54 6.09a.53.53 0 0 0-.238.455v2.062z';
6
+
7
+ function create(options = {}) {
8
+ const instance = {
9
+ document: options.document || document,
10
+ tx: options.tx || ((english) => english),
11
+ callbacks: options.callbacks || {},
12
+ catalog: window.CodexOverleafProviderProfiles.normalizeCatalog({}),
13
+ selectedId: 'builtin',
14
+ draft: null,
15
+ dirty: false,
16
+ busy: '',
17
+ secretAction: 'unchanged',
18
+ verifications: {},
19
+ pendingCatalog: null,
20
+ root: null,
21
+ returnFocus: null
22
+ };
23
+ ensureRoot(instance);
24
+ return {
25
+ open: catalog => open(instance, catalog),
26
+ close: () => requestClose(instance),
27
+ setCatalog: (catalog, selectedId) => setCatalog(instance, catalog, selectedId),
28
+ offerCatalog: (catalog, selectedId) => offerCatalog(instance, catalog, selectedId),
29
+ setBusy: (kind, message) => setBusy(instance, kind, message),
30
+ setStatus: status => setStatus(instance, status),
31
+ setVerification: verification => setVerification(instance, verification),
32
+ setVerificationFailure: failure => setVerificationFailure(instance, failure),
33
+ setTestProgress: progress => setTestProgress(instance, progress),
34
+ hasUnsavedChanges: () => instance.dirty,
35
+ isOpen: () => Boolean(instance.root && !instance.root.hidden),
36
+ destroy: () => destroy(instance),
37
+ _instance: instance
38
+ };
39
+ }
40
+
41
+ function ensureRoot(instance) {
42
+ if (instance.root) {
43
+ return instance.root;
44
+ }
45
+ const root = instance.document.createElement('div');
46
+ root.className = 'codex-provider-dialog-root';
47
+ root.hidden = true;
48
+ root.innerHTML = `
49
+ <div class="codex-provider-dialog-backdrop" data-provider-backdrop></div>
50
+ <section class="codex-provider-dialog" role="dialog" aria-modal="true" aria-labelledby="codex-provider-dialog-title">
51
+ <header class="codex-provider-dialog-head">
52
+ <div class="codex-provider-dialog-heading">
53
+ <div class="codex-provider-dialog-titleline">
54
+ <h2 id="codex-provider-dialog-title" data-provider-dialog-title></h2>
55
+ <span class="codex-provider-experimental-badge" data-provider-dialog-experimental></span>
56
+ </div>
57
+ <p data-provider-dialog-subtitle></p>
58
+ </div>
59
+ <button type="button" class="codex-provider-dialog-close" data-provider-action="close" aria-label="Close">×</button>
60
+ </header>
61
+ <div class="codex-provider-dialog-body">
62
+ <aside class="codex-provider-list" data-provider-list></aside>
63
+ <main class="codex-provider-detail" data-provider-detail></main>
64
+ </div>
65
+ <footer class="codex-provider-dialog-foot">
66
+ <div class="codex-provider-operation-status" data-provider-status aria-live="polite"></div>
67
+ <div class="codex-provider-footer-actions" data-provider-footer-actions></div>
68
+ </footer>
69
+ </section>
70
+ `;
71
+ instance.document.documentElement.appendChild(root);
72
+ instance.root = root;
73
+ root.addEventListener('click', event => handleClick(instance, event));
74
+ root.addEventListener('input', event => handleInput(instance, event));
75
+ root.addEventListener('change', event => handleInput(instance, event));
76
+ root.addEventListener('keydown', event => handleKeydown(instance, event));
77
+ root.addEventListener('mousedown', event => event.stopPropagation());
78
+ root.addEventListener('click', event => event.stopPropagation());
79
+ return root;
80
+ }
81
+
82
+ function open(instance, catalog) {
83
+ instance.returnFocus = instance.document.activeElement;
84
+ instance.root.hidden = false;
85
+ syncTheme(instance);
86
+ setCatalog(instance, catalog || instance.catalog);
87
+ queueMicrotask(() => {
88
+ instance.root.querySelector('[data-provider-row][aria-current="true"]')?.focus?.()
89
+ || instance.root.querySelector('input, button, select, textarea')?.focus?.();
90
+ });
91
+ }
92
+
93
+ function requestClose(instance) {
94
+ if (instance.dirty && !instance.document.defaultView.confirm(instance.tx(
95
+ 'Discard unsaved provider changes?',
96
+ '放弃尚未保存的模型服务配置吗?'
97
+ ))) {
98
+ return false;
99
+ }
100
+ if (instance.busy === 'testing') {
101
+ instance.callbacks.onCancelTest?.();
102
+ instance.busy = '';
103
+ }
104
+ instance.root.hidden = true;
105
+ instance.busy = '';
106
+ instance.returnFocus?.focus?.();
107
+ return true;
108
+ }
109
+
110
+ function setCatalog(instance, catalog, selectedId) {
111
+ instance.catalog = window.CodexOverleafProviderProfiles.normalizeCatalog(catalog || {});
112
+ const nextId = selectedId || (
113
+ instance.catalog.providers.some(provider => provider.id === instance.selectedId)
114
+ ? instance.selectedId
115
+ : instance.catalog.activeProviderId
116
+ );
117
+ instance.selectedId = nextId || 'builtin';
118
+ instance.draft = null;
119
+ instance.dirty = false;
120
+ delete instance.root.dataset.dirty;
121
+ instance.secretAction = 'unchanged';
122
+ instance.verifications = {};
123
+ instance.pendingCatalog = null;
124
+ setStatus(instance, { tone: '', title: '' });
125
+ render(instance);
126
+ }
127
+
128
+ function offerCatalog(instance, catalog, selectedId) {
129
+ if (!instance.dirty) {
130
+ setCatalog(instance, catalog, selectedId);
131
+ return true;
132
+ }
133
+ instance.pendingCatalog = { catalog, selectedId };
134
+ setStatus(instance, {
135
+ tone: 'warning',
136
+ title: instance.tx(
137
+ 'Provider settings changed in another tab. The local draft was kept.',
138
+ '其他标签页更新了模型服务配置,当前本地草稿已保留。'
139
+ )
140
+ });
141
+ renderFooter(instance);
142
+ applyBusyState(instance);
143
+ return false;
144
+ }
145
+
146
+ function render(instance) {
147
+ const tx = instance.tx;
148
+ instance.root.querySelector('[data-provider-dialog-title]').textContent = tx('Model API providers', '模型 API 服务');
149
+ instance.root.querySelector('[data-provider-dialog-experimental]').textContent = tx(
150
+ 'Third-party experimental',
151
+ '第三方实验性'
152
+ );
153
+ instance.root.querySelector('[data-provider-dialog-subtitle]').textContent = tx(
154
+ 'Configure experimental third-party integrations used by future Codex runs. Compatibility varies by provider and gateway.',
155
+ '配置后续 Codex 任务使用的实验性第三方集成;兼容性取决于具体模型服务与网关。'
156
+ );
157
+ renderProviderList(instance);
158
+ renderDetail(instance);
159
+ renderFooter(instance);
160
+ applyBusyState(instance);
161
+ }
162
+
163
+ function renderProviderList(instance) {
164
+ const tx = instance.tx;
165
+ const list = instance.root.querySelector('[data-provider-list]');
166
+ const rows = instance.catalog.providers.map(provider => {
167
+ const selected = provider.id === instance.selectedId;
168
+ const active = provider.id === instance.catalog.activeProviderId;
169
+ const diagnosticCount = Object.values(provider.modelDiagnostics || {}).filter(item => item.status === 'tested').length;
170
+ const status = active
171
+ ? tx('Active', '使用中')
172
+ : diagnosticCount
173
+ ? tx(`Tested ${diagnosticCount}/${Math.max(1, provider.models.length)}`, `已测试 ${diagnosticCount}/${Math.max(1, provider.models.length)}`)
174
+ : provider.kind === 'custom'
175
+ ? tx('Untested', '未测试')
176
+ : '';
177
+ return `
178
+ <button type="button" class="codex-provider-row" data-provider-row="${escapeAttr(provider.id)}" aria-current="${selected ? 'true' : 'false'}">
179
+ <span class="codex-provider-row-main" title="${escapeAttr(provider.name)}">${escapeHtml(provider.name)}</span>
180
+ ${status ? `<span class="codex-provider-row-status" data-tone="${active ? 'active' : 'muted'}">${escapeHtml(status)}</span>` : ''}
181
+ </button>
182
+ `;
183
+ });
184
+ if (instance.selectedId === '__new__') {
185
+ rows.push(`
186
+ <button type="button" class="codex-provider-row" data-provider-row="__new__" aria-current="true">
187
+ <span class="codex-provider-row-main">${escapeHtml(tx('New provider', '新模型服务'))}</span>
188
+ <span class="codex-provider-row-status" data-tone="muted">${escapeHtml(tx('Draft', '草稿'))}</span>
189
+ </button>
190
+ `);
191
+ }
192
+ rows.push(`<button type="button" class="codex-provider-add" data-provider-action="add">+ ${escapeHtml(tx('Add provider', '添加模型服务'))}</button>`);
193
+ list.innerHTML = rows.join('');
194
+ }
195
+
196
+ function renderDetail(instance) {
197
+ const detail = instance.root.querySelector('[data-provider-detail]');
198
+ const provider = getSelectedProvider(instance);
199
+ if (!provider || provider.kind === 'builtin') {
200
+ renderBuiltinDetail(instance, detail);
201
+ return;
202
+ }
203
+ const tx = instance.tx;
204
+ const draft = instance.draft || provider;
205
+ const defaultModel = (draft.models || []).find(model => model.id === draft.defaultModelId) || {};
206
+ const additionalModels = (draft.models || [])
207
+ .map(model => model.id)
208
+ .filter(id => id !== draft.defaultModelId)
209
+ .join('\n');
210
+ const active = provider.id && provider.id === instance.catalog.activeProviderId;
211
+ const acceptedHost = getEndpointHost(draft.baseUrl);
212
+ const secretSavedAt = formatSecretSavedAt(instance, provider.secretUpdatedAt);
213
+ const disclosureSatisfied = Boolean(
214
+ provider.endpointDisclosureHost &&
215
+ provider.endpointDisclosureHost === acceptedHost &&
216
+ provider.endpointDisclosureBaseUrl === draft.baseUrl
217
+ );
218
+ detail.innerHTML = `
219
+ <div class="codex-provider-detail-titleline">
220
+ <div>
221
+ <h3>${escapeHtml(draft.name || tx('Custom provider', '自定义模型服务'))}</h3>
222
+ <p>${escapeHtml(active ? tx('Used by new Codex runs', '新 Codex 任务将使用此服务') : tx('Saved provider profile', '已保存的模型服务配置'))}</p>
223
+ </div>
224
+ ${active ? `<span class="codex-provider-active-badge">${escapeHtml(tx('Active', '使用中'))}</span>` : ''}
225
+ </div>
226
+ <div class="codex-provider-form-grid">
227
+ <label class="codex-provider-field">
228
+ <span>${escapeHtml(tx('Provider name', '服务名称'))}</span>
229
+ <input type="text" data-provider-field="name" maxlength="64" value="${escapeAttr(draft.name || '')}">
230
+ </label>
231
+ <label class="codex-provider-field codex-provider-field--wide">
232
+ <span>${escapeHtml(tx('Base URL', '基础 URL'))}</span>
233
+ <input type="url" data-provider-field="baseUrl" value="${escapeAttr(draft.baseUrl || '')}" placeholder="https://provider.example/v1" spellcheck="false">
234
+ <small>${escapeHtml(tx('HTTPS is required except for localhost.', '除 localhost 外必须使用 HTTPS。'))}</small>
235
+ </label>
236
+ <label class="codex-provider-field codex-provider-field--wide">
237
+ <span>${escapeHtml(tx('API key', 'API 密钥'))}</span>
238
+ <div class="codex-provider-secret-row">
239
+ <input type="password" data-provider-field="apiKey" autocomplete="new-password" placeholder="${escapeAttr(provider.hasSecret ? tx('Configured; enter a new key to replace it', '已配置;输入新密钥即可替换') : tx('Optional for local no-auth endpoints', '本地无鉴权端点可留空'))}">
240
+ ${provider.hasSecret ? `<button type="button" class="codex-provider-inline-button" data-provider-action="clear-secret">${escapeHtml(tx('Clear', '清除'))}</button>` : ''}
241
+ </div>
242
+ <small data-provider-secret-note>${provider.hasSecret
243
+ ? `<strong>${escapeHtml(tx('API key saved locally.', 'API 密钥已保存到本地。'))}</strong> ${escapeHtml(secretSavedAt
244
+ ? tx(`Last replaced ${secretSavedAt}. The plaintext is never returned to the Extension.`, `上次替换于 ${secretSavedAt}。扩展不会读回密钥明文。`)
245
+ : tx('Stored by the Native Host. The plaintext is never returned to the Extension.', '由 Native Host 保管。扩展不会读回密钥明文。'))}`
246
+ : escapeHtml(tx('No API key is currently stored. Local no-auth endpoints may leave this empty.', '当前未存储 API 密钥。本地无鉴权端点可以留空。'))}</small>
247
+ </label>
248
+ <label class="codex-provider-field codex-provider-field--wide">
249
+ <span>${escapeHtml(tx('Default model', '默认模型'))}</span>
250
+ <input type="text" data-provider-field="defaultModelId" value="${escapeAttr(draft.defaultModelId || '')}" placeholder="model-id" spellcheck="false">
251
+ </label>
252
+ <label class="codex-provider-field codex-provider-field--wide">
253
+ <span>${escapeHtml(tx('Additional models', '其他模型'))}</span>
254
+ <textarea data-provider-field="additionalModels" rows="3" placeholder="one-model-id-per-line" spellcheck="false">${escapeHtml(additionalModels)}</textarea>
255
+ <small>${escapeHtml(tx('Enter one model ID per line.', '每行填写一个模型 ID。'))}</small>
256
+ </label>
257
+ </div>
258
+ <details class="codex-provider-advanced">
259
+ <summary>${escapeHtml(tx('Advanced compatibility', '高级兼容设置'))}</summary>
260
+ <div class="codex-provider-form-grid">
261
+ <label class="codex-provider-field">
262
+ <span>${escapeHtml(tx('API protocol', 'API 协议'))}</span>
263
+ <select data-provider-field="wireApiPreference">
264
+ <option value="auto" ${draft.wireApiPreference === 'auto' ? 'selected' : ''}>${escapeHtml(tx('Auto (detect during test)', '自动(测试时检测)'))}</option>
265
+ <option value="responses" ${draft.wireApiPreference === 'responses' ? 'selected' : ''}>Responses API</option>
266
+ <option value="chat" ${draft.wireApiPreference === 'chat' ? 'selected' : ''}>Chat Completions</option>
267
+ <option value="anthropic" ${draft.wireApiPreference === 'anthropic' ? 'selected' : ''}>Anthropic Messages</option>
268
+ </select>
269
+ </label>
270
+ <label class="codex-provider-field">
271
+ <span>${escapeHtml(tx('Upstream response mode', '上游响应模式'))}</span>
272
+ <select data-provider-field="upstreamResponseMode">
273
+ <option value="auto" ${defaultModel.upstreamResponseMode === 'auto' || !defaultModel.upstreamResponseMode ? 'selected' : ''}>${escapeHtml(tx('Auto (test streaming, then buffered)', '自动(先测流式,再测缓冲)'))}</option>
274
+ <option value="streaming" ${defaultModel.upstreamResponseMode === 'streaming' ? 'selected' : ''}>${escapeHtml(tx('Streaming', '流式'))}</option>
275
+ <option value="buffered" ${defaultModel.upstreamResponseMode === 'buffered' ? 'selected' : ''}>${escapeHtml(tx('Buffered', '缓冲'))}</option>
276
+ </select>
277
+ <small>${escapeHtml(tx('Applied to the default model. Buffered keeps Codex streaming locally while the provider returns one complete response.', '应用于默认模型。缓冲模式仍向 Codex 本地流式输出,但等待服务商返回完整响应。'))}</small>
278
+ </label>
279
+ <label class="codex-provider-field">
280
+ <span>${escapeHtml(tx('Request timeout', '请求超时'))}</span>
281
+ <select data-provider-field="requestTimeoutMs">
282
+ ${[15000, 30000, 60000, 120000].map(value => `<option value="${value}" ${Number(draft.requestTimeoutMs) === value ? 'selected' : ''}>${value / 1000}s</option>`).join('')}
283
+ </select>
284
+ </label>
285
+ <label class="codex-provider-field">
286
+ <span>${escapeHtml(tx('API-key authentication', 'API 密钥鉴权'))}</span>
287
+ <select data-provider-field="authMode">
288
+ <option value="bearer" ${draft.authMode === 'bearer' || !draft.authMode ? 'selected' : ''}>Authorization: Bearer</option>
289
+ <option value="x-api-key" ${draft.authMode === 'x-api-key' ? 'selected' : ''}>x-api-key</option>
290
+ <option value="api-key" ${draft.authMode === 'api-key' ? 'selected' : ''}>api-key</option>
291
+ <option value="custom" ${draft.authMode === 'custom' ? 'selected' : ''}>${escapeHtml(tx('Custom header', '自定义请求头'))}</option>
292
+ <option value="none" ${draft.authMode === 'none' ? 'selected' : ''}>${escapeHtml(tx('No authentication', '无鉴权'))}</option>
293
+ </select>
294
+ </label>
295
+ <label class="codex-provider-field">
296
+ <span>${escapeHtml(tx('Custom API-key header', '自定义密钥请求头'))}</span>
297
+ <input type="text" data-provider-field="apiKeyHeaderName" value="${escapeAttr(draft.apiKeyHeaderName || '')}" placeholder="X-API-Key" spellcheck="false">
298
+ </label>
299
+ <label class="codex-provider-field">
300
+ <span>${escapeHtml(tx('Context window', '上下文窗口'))}</span>
301
+ <input type="number" data-provider-field="contextWindow" min="8192" max="4000000" step="1024" value="${Number(draft.contextWindow) || 262144}">
302
+ </label>
303
+ <label class="codex-provider-field">
304
+ <span>${escapeHtml(tx('Input modalities', '输入模态'))}</span>
305
+ <select data-provider-field="inputModalities">
306
+ <option value="text" ${!(draft.inputModalities || []).includes('image') ? 'selected' : ''}>Text</option>
307
+ <option value="text,image" ${(draft.inputModalities || []).includes('image') ? 'selected' : ''}>Text + image</option>
308
+ </select>
309
+ </label>
310
+ <label class="codex-provider-field">
311
+ <span>${escapeHtml(tx('Reasoning control', '推理控制'))}</span>
312
+ <select data-provider-field="reasoningAdapter">
313
+ <option value="auto" ${draft.reasoningAdapter === 'auto' || !draft.reasoningAdapter ? 'selected' : ''}>${escapeHtml(tx('Auto-detect', '自动检测'))}</option>
314
+ <option value="none" ${draft.reasoningAdapter === 'none' ? 'selected' : ''}>${escapeHtml(tx('Disabled', '禁用'))}</option>
315
+ <option value="deepseek" ${draft.reasoningAdapter === 'deepseek' ? 'selected' : ''}>DeepSeek thinking + reasoning_effort</option>
316
+ <option value="anthropic" ${draft.reasoningAdapter === 'anthropic' ? 'selected' : ''}>Anthropic extended thinking</option>
317
+ <option value="reasoning_effort" ${draft.reasoningAdapter === 'reasoning_effort' ? 'selected' : ''}>reasoning_effort</option>
318
+ <option value="openrouter" ${draft.reasoningAdapter === 'openrouter' ? 'selected' : ''}>OpenRouter reasoning.effort</option>
319
+ <option value="enable_thinking" ${draft.reasoningAdapter === 'enable_thinking' ? 'selected' : ''}>enable_thinking</option>
320
+ <option value="thinking" ${draft.reasoningAdapter === 'thinking' ? 'selected' : ''}>thinking.type</option>
321
+ <option value="reasoning_split" ${draft.reasoningAdapter === 'reasoning_split' ? 'selected' : ''}>reasoning_split</option>
322
+ </select>
323
+ </label>
324
+ <label class="codex-provider-field">
325
+ <span>${escapeHtml(tx('Reasoning strengths', '推理强度'))}</span>
326
+ <select data-provider-field="reasoningCapability">
327
+ <option value="auto" ${draft.reasoningCapability === 'auto' || !draft.reasoningCapability ? 'selected' : ''}>${escapeHtml(tx('Auto-detect', '自动检测'))}</option>
328
+ <option value="effort" ${draft.reasoningCapability === 'effort' ? 'selected' : ''}>${escapeHtml(tx('Low / Medium / High', '低 / 中 / 高'))}</option>
329
+ <option value="toggle" ${draft.reasoningCapability === 'toggle' ? 'selected' : ''}>${escapeHtml(tx('On / Off only', '仅开启 / 关闭'))}</option>
330
+ <option value="none" ${draft.reasoningCapability === 'none' ? 'selected' : ''}>${escapeHtml(tx('Not supported', '不支持'))}</option>
331
+ </select>
332
+ </label>
333
+ <div class="codex-provider-field codex-provider-field--wide">
334
+ <span>${escapeHtml(tx('Protocol capabilities', '协议能力'))}</span>
335
+ <div class="codex-provider-capability-grid">
336
+ <label class="codex-provider-capability-option">
337
+ <input class="codex-provider-capability-input" type="checkbox" data-provider-field="supportsParallelToolCalls" ${draft.supportsParallelToolCalls ? 'checked' : ''}>
338
+ <span class="codex-provider-capability-control" aria-hidden="true"></span>
339
+ <span>${escapeHtml(tx('Parallel tool calls', '并行工具调用'))}</span>
340
+ </label>
341
+ <label class="codex-provider-capability-option">
342
+ <input class="codex-provider-capability-input" type="checkbox" data-provider-field="supportsStreamOptions" ${draft.supportsStreamOptions ? 'checked' : ''}>
343
+ <span class="codex-provider-capability-control" aria-hidden="true"></span>
344
+ <span>stream_options</span>
345
+ </label>
346
+ <label class="codex-provider-capability-option codex-provider-capability-option--wide">
347
+ <input class="codex-provider-capability-input" type="checkbox" data-provider-field="fullEndpoint" ${draft.fullEndpoint ? 'checked' : ''}>
348
+ <span class="codex-provider-capability-control" aria-hidden="true"></span>
349
+ <span>${escapeHtml(tx('Base URL is the full protocol endpoint', '基础 URL 已是完整协议端点'))}</span>
350
+ </label>
351
+ </div>
352
+ </div>
353
+ <label class="codex-provider-field codex-provider-field--wide">
354
+ <span>${escapeHtml(tx('Static headers (JSON)', '静态请求头(JSON)'))}</span>
355
+ <textarea data-provider-field="customHeaders" rows="2" spellcheck="false">${escapeHtml(formatJsonRecord(draft.customHeaders))}</textarea>
356
+ </label>
357
+ <label class="codex-provider-field codex-provider-field--wide">
358
+ <span>${escapeHtml(tx('Query parameters (JSON)', '查询参数(JSON)'))}</span>
359
+ <textarea data-provider-field="queryParams" rows="2" spellcheck="false">${escapeHtml(formatJsonRecord(draft.queryParams))}</textarea>
360
+ </label>
361
+ <label class="codex-provider-field codex-provider-field--wide">
362
+ <span>${escapeHtml(tx('Protocol request overrides (JSON)', '协议请求覆盖项(JSON)'))}</span>
363
+ <textarea data-provider-field="bodyOverrides" rows="2" spellcheck="false">${escapeHtml(formatJsonRecord(draft.bodyOverrides))}</textarea>
364
+ <small>${escapeHtml(tx('Vendor-specific fields only; core model, messages, tools, and stream fields are protected.', '仅填写服务商特有字段;model、messages、tools 和 stream 等核心字段不可覆盖。'))}</small>
365
+ </label>
366
+ <label class="codex-provider-field">
367
+ <span>Anthropic version</span>
368
+ <input type="text" data-provider-field="anthropicVersion" value="${escapeAttr(draft.anthropicVersion || '2023-06-01')}" spellcheck="false">
369
+ </label>
370
+ <label class="codex-provider-field">
371
+ <span>Anthropic beta</span>
372
+ <input type="text" data-provider-field="anthropicBeta" value="${escapeAttr(draft.anthropicBeta || '')}" placeholder="optional" spellcheck="false">
373
+ </label>
374
+ <label class="codex-provider-field">
375
+ <span>${escapeHtml(tx('Anthropic thinking mode', 'Anthropic 思考模式'))}</span>
376
+ <select data-provider-field="anthropicThinkingMode">
377
+ <option value="budget" ${draft.anthropicThinkingMode === 'budget' || !draft.anthropicThinkingMode ? 'selected' : ''}>${escapeHtml(tx('Token budget', 'Token 预算'))}</option>
378
+ <option value="adaptive" ${draft.anthropicThinkingMode === 'adaptive' ? 'selected' : ''}>Adaptive</option>
379
+ <option value="none" ${draft.anthropicThinkingMode === 'none' ? 'selected' : ''}>${escapeHtml(tx('Disabled', '禁用'))}</option>
380
+ </select>
381
+ </label>
382
+ <label class="codex-provider-field">
383
+ <span>${escapeHtml(tx('Maximum output tokens', '最大输出 Token'))}</span>
384
+ <input type="number" data-provider-field="maxOutputTokens" min="256" max="200000" step="256" value="${Number(draft.maxOutputTokens) || 32768}">
385
+ </label>
386
+ <div class="codex-provider-field codex-provider-field--wide">
387
+ <span>${escapeHtml(tx('Anthropic compatibility', 'Anthropic 兼容能力'))}</span>
388
+ <div class="codex-provider-capability-grid">
389
+ <label class="codex-provider-capability-option">
390
+ <input class="codex-provider-capability-input" type="checkbox" data-provider-field="anthropicPromptCaching" ${draft.anthropicPromptCaching ? 'checked' : ''}>
391
+ <span class="codex-provider-capability-control" aria-hidden="true"></span>
392
+ <span>${escapeHtml(tx('Prompt caching markers', 'Prompt 缓存标记'))}</span>
393
+ </label>
394
+ <label class="codex-provider-capability-option">
395
+ <input class="codex-provider-capability-input" type="checkbox" data-provider-field="impersonateClaudeCode" ${draft.impersonateClaudeCode ? 'checked' : ''}>
396
+ <span class="codex-provider-capability-control" aria-hidden="true"></span>
397
+ <span>${escapeHtml(tx('Claude Code gateway identity', 'Claude Code 网关身份'))}</span>
398
+ </label>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ </details>
403
+ <label class="codex-provider-disclosure">
404
+ <input type="checkbox" data-provider-disclosure ${disclosureSatisfied ? 'checked' : ''}>
405
+ <span data-provider-disclosure-text>${escapeHtml(tx(
406
+ `Future Codex runs in every Overleaf project tab may send selected project content to ${draft.baseUrl || 'this endpoint'}.`,
407
+ `所有 Overleaf 项目标签页的后续 Codex 任务都可能把所选项目内容发送到 ${draft.baseUrl || '此端点'}。`
408
+ ))}</span>
409
+ </label>
410
+ <div class="codex-provider-test-row">
411
+ <div class="codex-provider-test-copy">
412
+ <strong>${escapeHtml(tx('Connection check', '连接检查'))}</strong>
413
+ <span>${escapeHtml(tx('Probe one model before using this provider.', '使用此服务商前,可选择一个模型进行探测。'))}</span>
414
+ </div>
415
+ <div class="codex-provider-test-controls">
416
+ <label class="codex-provider-test-model">
417
+ <span class="codex-provider-test-model-label">${escapeHtml(tx('Test model', '测试模型'))}</span>
418
+ <span class="codex-provider-test-select-shell">
419
+ <select data-provider-test-model aria-label="${escapeAttr(tx('Model to test', '要测试的模型'))}">
420
+ ${(draft.models || []).map(model => `<option value="${escapeAttr(model.id)}" ${model.id === draft.defaultModelId ? 'selected' : ''}>${escapeHtml(model.label || model.id)}</option>`).join('')}
421
+ </select>
422
+ <svg class="codex-provider-test-chevron" viewBox="0 0 16 16" aria-hidden="true">
423
+ <path d="m4 6 4 4 4-4"></path>
424
+ </svg>
425
+ </span>
426
+ </label>
427
+ <button type="button" class="codex-provider-test-button" data-provider-action="test">
428
+ <svg viewBox="0 0 18 18" aria-hidden="true">
429
+ <circle cx="5" cy="9" r="2.25"></circle>
430
+ <circle cx="13" cy="9" r="2.25"></circle>
431
+ <path d="M7.25 9h3.5"></path>
432
+ </svg>
433
+ <span data-provider-test-action-label>${escapeHtml(tx('Test connection', '测试连接'))}</span>
434
+ </button>
435
+ </div>
436
+ <span class="codex-provider-test-state" data-provider-test-state>${escapeHtml(formatVerification(instance, provider, draft.defaultModelId))}</span>
437
+ </div>
438
+ `;
439
+ }
440
+
441
+ function renderBuiltinDetail(instance, detail) {
442
+ const tx = instance.tx;
443
+ const active = instance.catalog.activeProviderId === 'builtin';
444
+ detail.innerHTML = `
445
+ <div class="codex-provider-builtin">
446
+ <span class="codex-provider-builtin-mark" aria-hidden="true">
447
+ <svg viewBox="0 0 24 24" focusable="false"><path d="${CODEX_BLOSSOM_PATH}"></path></svg>
448
+ </span>
449
+ <h3>${escapeHtml(tx('Built-in Codex', '内置 Codex'))}</h3>
450
+ <p>${escapeHtml(tx(
451
+ 'Uses the authentication, model catalog, and provider configuration managed by the local Codex CLI.',
452
+ '使用本地 Codex CLI 管理的身份验证、模型目录和服务配置。'
453
+ ))}</p>
454
+ ${active
455
+ ? `<span class="codex-provider-active-badge">${escapeHtml(tx('Active', '使用中'))}</span>`
456
+ : `<button type="button" class="codex-provider-primary-button" data-provider-action="activate-builtin">${escapeHtml(tx('Use built-in Codex', '使用内置 Codex'))}</button>`}
457
+ </div>
458
+ `;
459
+ }
460
+
461
+ function renderFooter(instance) {
462
+ const actions = instance.root.querySelector('[data-provider-footer-actions]');
463
+ const provider = getSelectedProvider(instance);
464
+ const tx = instance.tx;
465
+ if (!provider || provider.kind === 'builtin') {
466
+ actions.innerHTML = `<button type="button" class="codex-provider-secondary-button" data-provider-action="close">${escapeHtml(tx('Close', '关闭'))}</button>`;
467
+ return;
468
+ }
469
+ const active = provider.id && provider.id === instance.catalog.activeProviderId;
470
+ const isNew = !provider.id;
471
+ const destinationChanged = active && getCurrentBaseUrl(instance) !== provider.endpointDisclosureBaseUrl;
472
+ const actionState = getFooterActionState({
473
+ isNew,
474
+ active,
475
+ dirty: instance.dirty,
476
+ destinationChanged,
477
+ canSave: canSaveCurrentDraft(instance),
478
+ canActivate: canActivateCurrentProvider(instance)
479
+ });
480
+ const saveDisabled = actionState.saveEnabled ? '' : ' disabled aria-disabled="true"';
481
+ const useDisabled = actionState.useEnabled ? '' : ' disabled aria-disabled="true"';
482
+ actions.innerHTML = `
483
+ ${provider.id ? `<button type="button" class="codex-provider-danger-button" data-provider-action="delete">${escapeHtml(tx('Delete', '删除'))}</button>` : ''}
484
+ ${instance.pendingCatalog ? `<button type="button" class="codex-provider-secondary-button" data-provider-action="reload-external">${escapeHtml(tx('Reload latest', '加载最新配置'))}</button>` : ''}
485
+ <span class="codex-provider-footer-spacer"></span>
486
+ <button type="button" class="codex-provider-secondary-button" data-provider-action="close">${escapeHtml(tx('Cancel', '取消'))}</button>
487
+ ${actionState.showSave ? `<button type="button" class="codex-provider-secondary-button" data-provider-action="save"${saveDisabled}>${escapeHtml(tx('Save', '保存'))}</button>` : ''}
488
+ ${actionState.showSaveAndUse ? `<button type="button" class="codex-provider-primary-button" data-provider-action="save-use"${saveDisabled}>${escapeHtml(tx('Save and use', '保存并使用'))}</button>` : ''}
489
+ ${actionState.showUse ? `<button type="button" class="codex-provider-primary-button" data-provider-action="use"${useDisabled}>${escapeHtml(tx('Use', '使用'))}</button>` : ''}
490
+ `;
491
+ }
492
+
493
+ function getFooterActionState(options = {}) {
494
+ const isNew = options.isNew === true;
495
+ const active = options.active === true;
496
+ const dirty = options.dirty === true;
497
+ return {
498
+ showSave: isNew || dirty,
499
+ showSaveAndUse: (isNew || dirty) && (!active || options.destinationChanged === true),
500
+ showUse: !isNew && !active && !dirty,
501
+ saveEnabled: options.canSave === true,
502
+ useEnabled: options.canActivate === true
503
+ };
504
+ }
505
+
506
+ function handleClick(instance, event) {
507
+ const action = event.target.closest('[data-provider-action]')?.dataset.providerAction;
508
+ const providerRow = event.target.closest('[data-provider-row]');
509
+ if (providerRow) {
510
+ selectProvider(instance, providerRow.dataset.providerRow);
511
+ return;
512
+ }
513
+ if (!action) {
514
+ return;
515
+ }
516
+ event.preventDefault();
517
+ if (action === 'close') {
518
+ requestClose(instance);
519
+ return;
520
+ }
521
+ if (action === 'add') {
522
+ selectProvider(instance, '__new__');
523
+ return;
524
+ }
525
+ if (action === 'reload-external') {
526
+ if (!instance.pendingCatalog) return;
527
+ if (instance.dirty && !instance.document.defaultView.confirm(instance.tx(
528
+ 'Discard this draft and load the latest provider settings?',
529
+ '放弃当前草稿并加载最新模型服务配置吗?'
530
+ ))) {
531
+ return;
532
+ }
533
+ const pending = instance.pendingCatalog;
534
+ setCatalog(instance, pending.catalog, pending.selectedId);
535
+ return;
536
+ }
537
+ if (action === 'clear-secret') {
538
+ const provider = getSelectedProvider(instance);
539
+ if (provider?.id && provider.hasSecret) {
540
+ const warning = instance.dirty
541
+ ? instance.tx(
542
+ 'Remove the stored API key now? Unsaved form changes will be discarded when the saved profile reloads.',
543
+ '立即删除已存储的 API 密钥吗?重新加载已保存配置时,未保存的表单改动会被放弃。'
544
+ )
545
+ : instance.tx('Remove the stored API key now?', '立即删除已存储的 API 密钥吗?');
546
+ if (instance.document.defaultView.confirm(warning)) {
547
+ instance.callbacks.onClearSecret?.({
548
+ profileId: provider.id,
549
+ expectedRevision: provider.revision
550
+ });
551
+ }
552
+ return;
553
+ }
554
+ instance.secretAction = 'clear';
555
+ const input = instance.root.querySelector('[data-provider-field="apiKey"]');
556
+ if (input) input.value = '';
557
+ const note = instance.root.querySelector('[data-provider-secret-note]');
558
+ if (note) note.textContent = instance.tx('The saved key will be removed on Save.', '保存后将删除已存储的密钥。');
559
+ markDirty(instance);
560
+ return;
561
+ }
562
+ if (action === 'activate-builtin') {
563
+ instance.callbacks.onActivateBuiltin?.();
564
+ return;
565
+ }
566
+ if (action === 'delete') {
567
+ const context = readContext(instance);
568
+ if (instance.document.defaultView.confirm(instance.tx(
569
+ `Delete ${context.draft.name}?`,
570
+ `删除 ${context.draft.name} 吗?`
571
+ ))) {
572
+ instance.callbacks.onDelete?.(context);
573
+ }
574
+ return;
575
+ }
576
+ if (action === 'test') {
577
+ const modelId = getSelectedTestModelId(instance);
578
+ invalidateVerification(instance, false, modelId);
579
+ instance.callbacks.onTest?.(readContext(instance));
580
+ return;
581
+ }
582
+ if (action === 'cancel-test') {
583
+ instance.callbacks.onCancelTest?.();
584
+ setBusy(instance, '', '');
585
+ setStatus(instance, { tone: 'warning', title: instance.tx('Connection test cancelled.', '连接测试已取消。') });
586
+ return;
587
+ }
588
+ if (action === 'use') {
589
+ if (!canActivateCurrentProvider(instance)) {
590
+ setStatus(instance, {
591
+ tone: 'failed',
592
+ title: instance.tx('Save this provider before using it.', '使用前请先保存此模型服务。')
593
+ });
594
+ return;
595
+ }
596
+ const context = readContext(instance);
597
+ if (!instance.root.querySelector('[data-provider-disclosure]')?.checked) {
598
+ setStatus(instance, {
599
+ tone: 'failed',
600
+ title: instance.tx('Confirm the endpoint disclosure before activating this provider.', '启用此服务前,请先确认端点披露说明。')
601
+ });
602
+ return;
603
+ }
604
+ instance.callbacks.onActivate?.(context);
605
+ return;
606
+ }
607
+ if (action === 'save' || action === 'save-use') {
608
+ const context = readContext(instance);
609
+ if (action === 'save-use' && !instance.root.querySelector('[data-provider-disclosure]')?.checked) {
610
+ setStatus(instance, {
611
+ tone: 'failed',
612
+ title: instance.tx('Confirm the endpoint disclosure before activating this provider.', '启用此服务前,请先确认端点披露说明。')
613
+ });
614
+ return;
615
+ }
616
+ instance.callbacks.onSave?.(context, { activate: action === 'save-use' });
617
+ }
618
+ }
619
+
620
+ function handleInput(instance, event) {
621
+ if (event.target.matches('[data-provider-test-model]')) {
622
+ refreshTestState(instance);
623
+ return;
624
+ }
625
+ if (event.target.matches('[data-provider-field]')) {
626
+ const field = event.target.dataset.providerField;
627
+ if (event.target.dataset.providerField === 'apiKey') {
628
+ instance.secretAction = event.target.value ? 'replace' : (getSelectedProvider(instance)?.hasSecret ? 'unchanged' : 'clear');
629
+ }
630
+ if (field === 'baseUrl') {
631
+ const disclosure = instance.root.querySelector('[data-provider-disclosure]');
632
+ if (disclosure) disclosure.checked = false;
633
+ updateDisclosureText(instance);
634
+ }
635
+ markDirty(instance, { invalidateVerification: field !== 'name' });
636
+ if (field === 'baseUrl') {
637
+ renderFooter(instance);
638
+ applyBusyState(instance);
639
+ }
640
+ }
641
+ }
642
+
643
+ function handleKeydown(instance, event) {
644
+ if (event.key === 'Escape') {
645
+ event.preventDefault();
646
+ requestClose(instance);
647
+ return;
648
+ }
649
+ if (event.key !== 'Tab') {
650
+ return;
651
+ }
652
+ const focusable = Array.from(instance.root.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])'))
653
+ .filter(element => !element.closest('[hidden]'));
654
+ if (!focusable.length) {
655
+ return;
656
+ }
657
+ const first = focusable[0];
658
+ const last = focusable[focusable.length - 1];
659
+ if (event.shiftKey && instance.document.activeElement === first) {
660
+ event.preventDefault();
661
+ last.focus();
662
+ } else if (!event.shiftKey && instance.document.activeElement === last) {
663
+ event.preventDefault();
664
+ first.focus();
665
+ }
666
+ }
667
+
668
+ function selectProvider(instance, id) {
669
+ if (id === instance.selectedId) {
670
+ return;
671
+ }
672
+ if (instance.dirty && !instance.document.defaultView.confirm(instance.tx(
673
+ 'Discard unsaved provider changes?',
674
+ '放弃尚未保存的模型服务配置吗?'
675
+ ))) {
676
+ return;
677
+ }
678
+ instance.selectedId = id;
679
+ instance.draft = id === '__new__'
680
+ ? window.CodexOverleafProviderProfiles.buildEmptyDraft()
681
+ : null;
682
+ instance.dirty = false;
683
+ delete instance.root.dataset.dirty;
684
+ instance.secretAction = 'unchanged';
685
+ instance.verifications = {};
686
+ instance.pendingCatalog = null;
687
+ setStatus(instance, { tone: '', title: '' });
688
+ render(instance);
689
+ }
690
+
691
+ function readContext(instance) {
692
+ const provider = getSelectedProvider(instance) || window.CodexOverleafProviderProfiles.buildEmptyDraft();
693
+ const get = name => instance.root.querySelector(`[data-provider-field="${name}"]`);
694
+ const defaultModelId = String(get('defaultModelId')?.value || '').trim();
695
+ const modelIds = [defaultModelId, ...String(get('additionalModels')?.value || '').split(/\r?\n/)]
696
+ .map(value => value.trim())
697
+ .filter((value, index, values) => value && values.indexOf(value) === index);
698
+ const existingModels = new Map((provider.models || []).map(model => [model.id, model]));
699
+ const upstreamResponseMode = get('upstreamResponseMode')?.value || 'auto';
700
+ const draft = {
701
+ name: String(get('name')?.value || '').trim(),
702
+ baseUrl: String(get('baseUrl')?.value || '').trim(),
703
+ wireApiPreference: get('wireApiPreference')?.value || 'auto',
704
+ models: modelIds.map(id => {
705
+ const existing = existingModels.get(id) || {};
706
+ return {
707
+ ...existing,
708
+ id,
709
+ label: existing.label || id,
710
+ reasoningEfforts: Array.isArray(existing.reasoningEfforts) ? existing.reasoningEfforts : [],
711
+ upstreamResponseMode: id === defaultModelId
712
+ ? upstreamResponseMode
713
+ : existing.upstreamResponseMode || 'auto',
714
+ resolvedUpstreamResponseMode: existing.resolvedUpstreamResponseMode || '',
715
+ contextWindow: Number(get('contextWindow')?.value || 262144),
716
+ supportsParallelToolCalls: Boolean(get('supportsParallelToolCalls')?.checked),
717
+ inputModalities: String(get('inputModalities')?.value || 'text').split(',')
718
+ };
719
+ }),
720
+ defaultModelId,
721
+ requestTimeoutMs: Number(get('requestTimeoutMs')?.value || 30000),
722
+ reasoningAdapter: get('reasoningAdapter')?.value || 'auto',
723
+ reasoningCapability: get('reasoningCapability')?.value || 'auto',
724
+ authMode: get('authMode')?.value || 'bearer',
725
+ apiKeyHeaderName: String(get('apiKeyHeaderName')?.value || '').trim(),
726
+ fullEndpoint: Boolean(get('fullEndpoint')?.checked),
727
+ customHeaders: String(get('customHeaders')?.value || '').trim(),
728
+ queryParams: String(get('queryParams')?.value || '').trim(),
729
+ bodyOverrides: String(get('bodyOverrides')?.value || '').trim(),
730
+ contextWindow: Number(get('contextWindow')?.value || 262144),
731
+ supportsParallelToolCalls: Boolean(get('supportsParallelToolCalls')?.checked),
732
+ supportsStreamOptions: Boolean(get('supportsStreamOptions')?.checked),
733
+ inputModalities: String(get('inputModalities')?.value || 'text').split(',')
734
+ ,anthropicVersion: String(get('anthropicVersion')?.value || '2023-06-01').trim()
735
+ ,anthropicBeta: String(get('anthropicBeta')?.value || '').trim()
736
+ ,anthropicThinkingMode: get('anthropicThinkingMode')?.value || 'budget'
737
+ ,anthropicPromptCaching: Boolean(get('anthropicPromptCaching')?.checked)
738
+ ,impersonateClaudeCode: Boolean(get('impersonateClaudeCode')?.checked)
739
+ ,maxOutputTokens: Number(get('maxOutputTokens')?.value || 32768)
740
+ };
741
+ const apiKey = String(get('apiKey')?.value || '');
742
+ const secretMutation = instance.secretAction === 'replace' && apiKey
743
+ ? { kind: 'replace', value: apiKey }
744
+ : instance.secretAction === 'clear'
745
+ ? { kind: 'clear' }
746
+ : { kind: 'unchanged' };
747
+ return {
748
+ profileId: provider.id || '',
749
+ expectedRevision: provider.revision || 0,
750
+ draft,
751
+ secretMutation,
752
+ verification: instance.verifications[getSelectedTestModelId(instance)] || null,
753
+ verifications: Object.values(instance.verifications).filter(item => item?.status === 'tested'),
754
+ testModelId: getSelectedTestModelId(instance) || defaultModelId,
755
+ disclosureHost: getEndpointHost(draft.baseUrl),
756
+ disclosureBaseUrl: draft.baseUrl
757
+ };
758
+ }
759
+
760
+ function getSelectedProvider(instance) {
761
+ if (instance.selectedId === '__new__') {
762
+ return instance.draft || window.CodexOverleafProviderProfiles.buildEmptyDraft();
763
+ }
764
+ return instance.catalog.providers.find(provider => provider.id === instance.selectedId)
765
+ || instance.catalog.providers.find(provider => provider.id === 'builtin');
766
+ }
767
+
768
+ function markDirty(instance, options = {}) {
769
+ const footerNeedsRefresh = instance.dirty !== true;
770
+ instance.dirty = true;
771
+ instance.root.dataset.dirty = 'true';
772
+ if (options.invalidateVerification !== false) {
773
+ instance.verifications = {};
774
+ invalidateVerification(instance, true);
775
+ }
776
+ if (footerNeedsRefresh) {
777
+ // Saved providers initially render Use (inactive) or no primary action
778
+ // (active). The first edit changes that action contract to Save, so the
779
+ // footer must be rebuilt without re-rendering and losing form values.
780
+ renderFooter(instance);
781
+ applyBusyState(instance);
782
+ }
783
+ }
784
+
785
+ function setBusy(instance, kind, message) {
786
+ instance.busy = kind || '';
787
+ if (message) {
788
+ setStatus(instance, { tone: kind === 'failed' ? 'failed' : 'progress', title: message });
789
+ } else if (!kind) {
790
+ setStatus(instance, { tone: '', title: '' });
791
+ }
792
+ syncTestAction(instance);
793
+ applyBusyState(instance);
794
+ }
795
+
796
+ function applyBusyState(instance) {
797
+ const busy = Boolean(instance.busy && instance.busy !== 'failed');
798
+ const canSave = canSaveCurrentDraft(instance);
799
+ const canActivate = canActivateCurrentProvider(instance);
800
+ instance.root.dataset.busy = instance.busy || '';
801
+ for (const element of instance.root.querySelectorAll('input, select, textarea, button')) {
802
+ if (element.matches('[data-provider-action="close"]')) {
803
+ element.disabled = false;
804
+ } else if (element.matches('[data-provider-action="cancel-test"]')) {
805
+ element.disabled = false;
806
+ } else if (element.matches('[data-provider-action="save"], [data-provider-action="save-use"]')) {
807
+ element.disabled = busy || !canSave;
808
+ } else if (element.matches('[data-provider-action="use"]')) {
809
+ element.disabled = busy || !canActivate;
810
+ } else {
811
+ element.disabled = busy;
812
+ }
813
+ }
814
+ }
815
+
816
+ function setStatus(instance, status = {}) {
817
+ const element = instance.root.querySelector('[data-provider-status]');
818
+ element.dataset.tone = status.tone || '';
819
+ element.textContent = [status.title, status.detail].filter(Boolean).join(' ');
820
+ }
821
+
822
+ function setVerification(instance, verification = {}) {
823
+ const modelId = String(verification.modelId || verification.testedModelId || getSelectedTestModelId(instance)).trim();
824
+ instance.verifications[modelId] = { ...verification, modelId, status: 'tested' };
825
+ setStatus(instance, {
826
+ tone: 'success',
827
+ title: instance.tx('Connection verified.', '连接验证成功。'),
828
+ detail: [
829
+ verification.resolvedWireApi,
830
+ verification.resolvedUpstreamResponseMode
831
+ ? `${verification.resolvedUpstreamResponseMode} upstream`
832
+ : '',
833
+ `${verification.durationMs || 0}ms`
834
+ ].filter(Boolean).join(' · ')
835
+ });
836
+ refreshTestState(instance);
837
+ applyBusyState(instance);
838
+ }
839
+
840
+ function setVerificationFailure(instance, failure = {}) {
841
+ const modelId = String(failure.modelId || getSelectedTestModelId(instance)).trim();
842
+ instance.verifications[modelId] = {
843
+ modelId,
844
+ status: 'failed',
845
+ errorCode: failure.errorCode || ''
846
+ };
847
+ refreshTestState(instance);
848
+ }
849
+
850
+ function setTestProgress(instance, progress = {}) {
851
+ const detail = progress.detail || progress;
852
+ const modelId = detail.modelId || getSelectedTestModelId(instance);
853
+ setStatus(instance, {
854
+ tone: 'progress',
855
+ title: instance.tx(
856
+ `Testing ${modelId}: ${detail.wireApi} · ${detail.upstreamResponseMode} (${detail.attempt}/${detail.totalAttempts})`,
857
+ `正在测试 ${modelId}:${detail.wireApi} · ${detail.upstreamResponseMode}(${detail.attempt}/${detail.totalAttempts})`
858
+ )
859
+ });
860
+ }
861
+
862
+ function invalidateVerification(instance, changed, modelId = '') {
863
+ if (modelId) {
864
+ delete instance.verifications[modelId];
865
+ }
866
+ const state = instance.root.querySelector('[data-provider-test-state]');
867
+ if (state) {
868
+ const provider = getSelectedProvider(instance);
869
+ const selectedModelId = modelId || getSelectedTestModelId(instance) || provider?.defaultModelId;
870
+ state.textContent = changed
871
+ ? instance.tx('Compatibility has not been tested for these settings', '当前配置尚未测试兼容性')
872
+ : formatVerification(instance, provider, selectedModelId);
873
+ state.dataset.tone = changed ? 'warning' : '';
874
+ }
875
+ applyBusyState(instance);
876
+ }
877
+
878
+ function canSaveCurrentDraft(instance) {
879
+ const provider = getSelectedProvider(instance);
880
+ return Boolean(provider && provider.kind === 'custom');
881
+ }
882
+
883
+ function canActivateCurrentProvider(instance) {
884
+ const provider = getSelectedProvider(instance);
885
+ return Boolean(
886
+ provider?.id &&
887
+ !instance.dirty
888
+ );
889
+ }
890
+
891
+ function formatVerification(instance, provider, modelId) {
892
+ const transient = instance.verifications[modelId];
893
+ if (transient?.status === 'failed') {
894
+ return instance.tx('Failed in this dialog', '本次测试失败');
895
+ }
896
+ if (transient?.status === 'tested') {
897
+ return instance.tx('Tested for this draft', '当前草稿已测试');
898
+ }
899
+ const diagnostic = provider?.modelDiagnostics?.[modelId];
900
+ if (diagnostic?.status === 'tested') {
901
+ const mode = diagnostic.upstreamResponseMode;
902
+ return mode
903
+ ? `${instance.tx('Tested', '已测试')} · ${diagnostic.wireApi || ''} · ${mode}`
904
+ : instance.tx('Tested', '已测试');
905
+ }
906
+ return instance.tx('Untested; testing is optional', '尚未测试;测试为可选功能');
907
+ }
908
+
909
+ function refreshTestState(instance) {
910
+ const state = instance.root.querySelector('[data-provider-test-state]');
911
+ if (!state) return;
912
+ const provider = getSelectedProvider(instance);
913
+ const modelId = getSelectedTestModelId(instance) || provider?.defaultModelId;
914
+ state.textContent = formatVerification(instance, provider, modelId);
915
+ const status = instance.verifications[modelId]?.status || provider?.modelDiagnostics?.[modelId]?.status || '';
916
+ state.dataset.tone = status === 'failed' ? 'failed' : status === 'tested' ? 'success' : '';
917
+ }
918
+
919
+ function getSelectedTestModelId(instance) {
920
+ return String(instance.root.querySelector('[data-provider-test-model]')?.value || '').trim();
921
+ }
922
+
923
+ function getCurrentBaseUrl(instance) {
924
+ return String(instance.root.querySelector('[data-provider-field="baseUrl"]')?.value || '').trim();
925
+ }
926
+
927
+ function updateDisclosureText(instance) {
928
+ const text = instance.root.querySelector('[data-provider-disclosure-text]');
929
+ if (!text) return;
930
+ const baseUrl = getCurrentBaseUrl(instance) || instance.tx('this endpoint', '此端点');
931
+ text.textContent = instance.tx(
932
+ `Future Codex runs in every Overleaf project tab may send selected project content to ${baseUrl}.`,
933
+ `所有 Overleaf 项目标签页的后续 Codex 任务都可能把所选项目内容发送到 ${baseUrl}。`
934
+ );
935
+ }
936
+
937
+ function syncTestAction(instance) {
938
+ const button = instance.root.querySelector('[data-provider-action="test"], [data-provider-action="cancel-test"]');
939
+ if (!button) return;
940
+ const testing = instance.busy === 'testing';
941
+ button.dataset.providerAction = testing ? 'cancel-test' : 'test';
942
+ const actionLabel = testing
943
+ ? instance.tx('Cancel test', '取消测试')
944
+ : instance.tx('Test connection', '测试连接');
945
+ const labelEl = button.querySelector('[data-provider-test-action-label]');
946
+ if (labelEl) labelEl.textContent = actionLabel;
947
+ else button.textContent = actionLabel;
948
+ button.setAttribute('aria-label', actionLabel);
949
+ }
950
+
951
+ function formatSecretSavedAt(instance, value) {
952
+ const timestamp = Number(value);
953
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
954
+ return '';
955
+ }
956
+ return new Date(timestamp).toLocaleString(instance.tx('en-US', 'zh-CN'), {
957
+ dateStyle: 'medium',
958
+ timeStyle: 'short'
959
+ });
960
+ }
961
+
962
+ function getEndpointHost(baseUrl) {
963
+ try {
964
+ return new URL(baseUrl).hostname;
965
+ } catch (_error) {
966
+ return '';
967
+ }
968
+ }
969
+
970
+ function syncTheme(instance) {
971
+ const panel = instance.document.querySelector('#codex-overleaf-panel');
972
+ if (!panel) {
973
+ return;
974
+ }
975
+ const style = instance.document.defaultView.getComputedStyle(panel);
976
+ instance.root.style.setProperty('--provider-panel-bg', style.backgroundColor || '#151716');
977
+ instance.root.style.setProperty('--provider-panel-color', style.color || '#eceeea');
978
+ }
979
+
980
+ function escapeHtml(value) {
981
+ return String(value ?? '')
982
+ .replaceAll('&', '&amp;')
983
+ .replaceAll('<', '&lt;')
984
+ .replaceAll('>', '&gt;')
985
+ .replaceAll('"', '&quot;')
986
+ .replaceAll("'", '&#39;');
987
+ }
988
+
989
+ function formatJsonRecord(value) {
990
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !Object.keys(value).length) {
991
+ return '';
992
+ }
993
+ return JSON.stringify(value, null, 2);
994
+ }
995
+
996
+ function escapeAttr(value) {
997
+ return escapeHtml(value).replaceAll('`', '&#96;');
998
+ }
999
+
1000
+ function destroy(instance) {
1001
+ instance.root?.remove?.();
1002
+ instance.root = null;
1003
+ }
1004
+
1005
+ window.CodexOverleafProviderSettingsDialog = { create, getFooterActionState };
1006
+ })();