codex-overleaf-link 1.8.5 → 1.9.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 (74) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8895 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1126 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/compatibility.js +1 -1
  60. package/extension/styles/panel.css +4048 -0
  61. package/native-host/src/index.js +9 -2
  62. package/native-host/src/launcher.js +2 -0
  63. package/native-host/src/managedInstall.js +393 -0
  64. package/native-host/src/managedLauncherRuntime.js +71 -0
  65. package/native-host/src/nativeEnvironment.js +4 -0
  66. package/native-host/src/nativeHostPlatform.js +16 -0
  67. package/native-host/src/runtimeInstaller.js +6 -1
  68. package/native-host/src/taskRunnerRuntime.js +8 -0
  69. package/native-host/src/updateArchive.js +253 -0
  70. package/native-host/src/updateManager.js +548 -0
  71. package/native-host/src/updateTrust.js +156 -0
  72. package/package.json +11 -2
  73. package/scripts/install-managed.mjs +64 -0
  74. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,659 @@
1
+ (function initCodexOverleafDiagnosticsController() {
2
+ 'use strict';
3
+
4
+ const CodexOverleafCompatibility = window.CodexOverleafCompatibility;
5
+
6
+ // Diagnostics controller — the check runners (inspect*) and result formatters
7
+ // carved out of contentRuntime.js (v1.4.5 structural-debt phase 1). The code
8
+ // below moved verbatim (original indentation kept); runtime collaborators are
9
+ // factory-injected so behavior is unchanged. Mutable runtime state (panel
10
+ // state, OT status) is read through injected getters.
11
+ function create(deps = {}) {
12
+ const {
13
+ tr,
14
+ tx,
15
+ getExtensionCompatibilityMetadata,
16
+ showDiagnosticsLoading,
17
+ showDiagnosticsResult,
18
+ setDiagnosticsHealth,
19
+ sendBackgroundNative,
20
+ callPageBridge,
21
+ getState,
22
+ getCurrentOtStatus,
23
+ getOtWarmMirrorState,
24
+ otWarmMirrorController,
25
+ getActiveFocusFiles,
26
+ getCurrentProjectId,
27
+ isExperimentalOtEnabled,
28
+ readOtBridgeStatus,
29
+ formatOtStatusLabel,
30
+ normalizeOtStatus,
31
+ formatProbeStatusBar,
32
+ getProbeRunReadiness,
33
+ getMirrorFreshness,
34
+ formatProjectSnapshotLog,
35
+ formatProjectSnapshotWarning,
36
+ getProjectSnapshotWarnings,
37
+ isTextSnapshotFile,
38
+ INSTALL_COMMAND,
39
+ RUN_SNAPSHOT_ZIP_TIMEOUT_MS
40
+ } = deps;
41
+
42
+ // Reduce a result status to a health bucket. 'completed' is a pass; a hard
43
+ // failure is fail; everything else (warning, partial, info) is attention.
44
+ function diagnosticsHealthBucket(status) {
45
+ if (status === 'completed') return 'ok';
46
+ if (status === 'failed') return 'fail';
47
+ return 'warn';
48
+ }
49
+
50
+ // Run every diagnostic check and render one aggregated, scannable health
51
+ // report (a status row per check) instead of four separate result screens.
52
+ // The overall bucket also updates the trigger dot.
53
+ async function runAllDiagnostics() {
54
+ showDiagnosticsLoading(tr('diagnosticsHealthTitle'), tr('diagnosticsRunningAll'));
55
+ const specs = [
56
+ { label: tr('diagnosticsNativeShort'), run: inspectNativeEnvironment },
57
+ { label: tr('diagnosticsPageShort'), run: inspectPageStateDiagnostics },
58
+ { label: tr('diagnosticsSnapshotShort'), run: inspectProjectSnapshot },
59
+ { label: tr('diagnosticsOtShort'), run: inspectOtWarmMirrorDiagnostics }
60
+ ];
61
+ const checks = [];
62
+ let worst = 'ok';
63
+ for (const spec of specs) {
64
+ let result;
65
+ try {
66
+ result = await spec.run({ collectOnly: true });
67
+ } catch (error) {
68
+ result = { status: 'failed', summary: tr('diagnosticsCheckErrored'), technical: error?.message || String(error) };
69
+ }
70
+ const bucket = diagnosticsHealthBucket(result?.status);
71
+ if (bucket === 'fail') {
72
+ worst = 'fail';
73
+ } else if (bucket === 'warn' && worst !== 'fail') {
74
+ worst = 'warn';
75
+ }
76
+ checks.push({
77
+ status: result?.status || 'info',
78
+ title: spec.label,
79
+ summary: result?.summary || result?.subtitle || '',
80
+ nextStep: bucket === 'ok' ? '' : (result?.nextStep || '')
81
+ });
82
+ }
83
+ const overallSubtitleKey = worst === 'ok'
84
+ ? 'diagnosticsHealthOk'
85
+ : worst === 'fail' ? 'diagnosticsHealthFail' : 'diagnosticsHealthWarn';
86
+ showDiagnosticsResult({
87
+ title: tr('diagnosticsHealthTitle'),
88
+ subtitle: tr(overallSubtitleKey),
89
+ status: worst === 'ok' ? 'completed' : worst === 'fail' ? 'failed' : 'warning',
90
+ checks,
91
+ technical: checks.map(check => `${check.title}: ${check.status}`).join('\n')
92
+ });
93
+ setDiagnosticsHealth(worst);
94
+ }
95
+
96
+
97
+
98
+
99
+
100
+
101
+ async function inspectProjectSnapshot(options = {}) {
102
+ if (!options.collectOnly) {
103
+ showDiagnosticsLoading(tr('diagnosticsSnapshotTitle'), tr('diagnosticsSnapshotLoading'));
104
+ }
105
+ let result;
106
+ try {
107
+ const project = await callPageBridge('getProjectSnapshot', {
108
+ force: true,
109
+ preferLightweight: true,
110
+ allowZipFallback: true,
111
+ allowEditorNavigation: false,
112
+ requireFullProject: true,
113
+ zipOnly: true,
114
+ zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS
115
+ });
116
+ result = formatProjectSnapshotDiagnosticsResult(project);
117
+ } catch (error) {
118
+ result = {
119
+ title: tx('Project Read Failed', '项目读取失败'),
120
+ subtitle: tx('The extension did not receive Overleaf project content.', '插件没有拿到 Overleaf 项目内容。'),
121
+ status: 'failed',
122
+ summary: tr('diagnosticsSnapshotErrorSummary'),
123
+ nextStep: tx('Reload Overleaf, wait for the left file tree to finish loading, then retry.', '刷新 Overleaf 页面,等左侧文件树加载完成后重试。'),
124
+ technical: error?.stack || error?.message || String(error)
125
+ };
126
+ }
127
+ if (options.collectOnly) {
128
+ return result;
129
+ }
130
+ showDiagnosticsResult(result);
131
+ }
132
+
133
+ async function inspectNativeEnvironment(options = {}) {
134
+ if (!options.collectOnly) {
135
+ showDiagnosticsLoading(tr('diagnosticsNativeTitle'), tr('diagnosticsNativeLoading'));
136
+ }
137
+ const params = CodexOverleafCompatibility?.buildBridgePingParams
138
+ ? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
139
+ : {};
140
+ const response = await sendBackgroundNative({ method: 'bridge.ping', params });
141
+ const result = formatNativeEnvironmentResult(response);
142
+ if (options.collectOnly) {
143
+ return result;
144
+ }
145
+ showDiagnosticsResult(result);
146
+ }
147
+
148
+ function formatNativeEnvironmentResult(response) {
149
+ const compatibility = CodexOverleafCompatibility?.evaluateNativeCompatibility
150
+ ? CodexOverleafCompatibility.evaluateNativeCompatibility(response, getExtensionCompatibilityMetadata())
151
+ : fallbackNativeCompatibility(response);
152
+ if (compatibility.status !== 'ok' && compatibility.status !== 'native_unhealthy') {
153
+ return formatNativeCompatibilityResult(compatibility, response);
154
+ }
155
+
156
+ if (!response?.ok) {
157
+ return {
158
+ title: tx('Local Connection Problem', '本机连接异常'),
159
+ subtitle: tx('The extension could not connect to the local service.', '插件没有连上本机服务。'),
160
+ status: 'failed',
161
+ summary: tx('The extension is not connected to the local service, so it cannot run local Codex or sync the local workspace yet.', '插件没有连上本机服务,所以暂时不能调用本地 Codex 或同步本地 workspace。'),
162
+ nextStep: tx('Make sure the native host is installed, reload the Chrome extension, then try again.', '确认 native host 已安装,并重新加载 Chrome 扩展后再试。'),
163
+ installCommand: INSTALL_COMMAND,
164
+ technical: response?.error?.message || tx('native host did not respond', 'native host 没有响应')
165
+ };
166
+ }
167
+
168
+ const environment = response.result?.environment;
169
+ if (!environment) {
170
+ return {
171
+ title: tx('Local Connection Available', '本机连接可用'),
172
+ subtitle: tx('Native Host responded.', 'Native Host 已响应。'),
173
+ status: 'completed',
174
+ summary: tr('diagnosticsNativeEmptySummary'),
175
+ nextStep: tx('If tasks fail to run, reinstall the native host or check the codex command in Terminal.', '如果运行任务失败,请重新安装 native host 或检查终端里的 codex 命令。'),
176
+ technical: JSON.stringify(response.result || {}, null, 2)
177
+ };
178
+ }
179
+
180
+ const codexOk = environment.codex?.ok === true;
181
+ const latexTools = environment.latex?.available || [];
182
+ const latexOk = latexTools.length > 0;
183
+ const bullets = [
184
+ codexOk
185
+ ? tx(`Codex available: ${environment.codex.path || 'found on PATH'}`, `Codex 可用:${environment.codex.path || '已在 PATH 中找到'}`)
186
+ : tx('Codex was not found: the extension cannot start local Codex.', 'Codex 没有找到:插件不能启动本地 Codex。'),
187
+ latexOk
188
+ ? tx(`LaTeX tools available: ${latexTools.join(', ')}`, `LaTeX 工具可用:${latexTools.join(', ')}`)
189
+ : tx('LaTeX tools were not found: text editing works, but local compile checks are limited.', 'LaTeX 工具没有找到:可以编辑文本,但不能在本地编译验证。')
190
+ ];
191
+
192
+ if (codexOk && latexOk) {
193
+ return {
194
+ title: tx('Local Connection OK', '本机连接正常'),
195
+ subtitle: tx('Codex and LaTeX are available.', 'Codex 和 LaTeX 都可以使用。'),
196
+ status: 'completed',
197
+ summary: tx('Codex is connected and local LaTeX tools are available. Codex can read, edit, and locally check this Overleaf project.', 'Codex 已连接,本机 LaTeX 工具可用。你可以让 Codex 读取、修改并本地检查 Overleaf 项目。'),
198
+ bullets,
199
+ technical: JSON.stringify(environment, null, 2)
200
+ };
201
+ }
202
+
203
+ return {
204
+ title: codexOk ? tx('Local Connection Partially Available', '本机连接部分可用') : tx('Codex Unavailable', 'Codex 不可用'),
205
+ subtitle: codexOk ? tx('Codex is available, but compile tools are incomplete.', 'Codex 可用,但编译工具不完整。') : tx('Local Codex was not found.', '没有找到本机 Codex。'),
206
+ status: codexOk ? 'warning' : 'failed',
207
+ summary: codexOk
208
+ ? tx('Local Codex can run, but LaTeX compile tools were not found. File edits are unaffected, but compile verification is limited.', '本机 Codex 可以运行,但没有找到 LaTeX 编译工具;修改文件不受影响,编译验证会受限。')
209
+ : tx('The extension reached the native host, but local Codex was not found, so tasks cannot start.', '插件已经连上 native host,但没有找到本机 Codex,所以不能启动任务。'),
210
+ bullets,
211
+ nextStep: codexOk
212
+ ? tx('For compile verification, make sure latexmk or pdflatex is available on your Terminal PATH.', '如需编译验证,请确认 latexmk 或 pdflatex 在终端 PATH 中可用。')
213
+ : tx('Make sure codex works in Terminal, then reload the extension.', '请确认终端里可以运行 codex,然后重新加载扩展。'),
214
+ technical: JSON.stringify(environment, null, 2)
215
+ };
216
+ }
217
+
218
+ function fallbackNativeCompatibility(response) {
219
+ return response?.ok
220
+ ? { status: 'ok', classification: 'compatible', native: response.result, installCommand: INSTALL_COMMAND }
221
+ : { status: 'native_missing', classification: 'incompatible', native: response, installCommand: INSTALL_COMMAND };
222
+ }
223
+
224
+ function getNativeCompatibilityClassification(compatibility = {}) {
225
+ if (compatibility.classification) {
226
+ return compatibility.classification;
227
+ }
228
+ return compatibility.status === 'ok' ? 'compatible' : 'incompatible';
229
+ }
230
+
231
+ function isNativeCompatibilityCompatible(compatibility = {}) {
232
+ return getNativeCompatibilityClassification(compatibility) === 'compatible';
233
+ }
234
+
235
+ function formatNativeCompatibilityResult(compatibility = {}, response = {}) {
236
+ const statusValue = compatibility.status || 'unknown_native';
237
+ if (statusValue === 'native_missing') {
238
+ return {
239
+ title: tx('Local Connection Problem', '本机连接异常'),
240
+ subtitle: tx('The extension could not connect to the local service.', '插件没有连上本机服务。'),
241
+ status: 'failed',
242
+ summary: tx('The extension is not connected to the local service, so it cannot run local Codex or sync the local workspace yet.', '插件没有连上本机服务,所以暂时不能调用本地 Codex 或同步本地 workspace。'),
243
+ nextStep: tx('Make sure the native host is installed, reload the Chrome extension, then try again.', '确认 native host 已安装,并重新加载 Chrome 扩展后再试。'),
244
+ installCommand: INSTALL_COMMAND,
245
+ technical: response?.error?.message || tx('native host did not respond', 'native host 没有响应')
246
+ };
247
+ }
248
+
249
+ const messages = getNativeCompatibilityMessages(statusValue);
250
+ return {
251
+ title: messages.title,
252
+ subtitle: messages.subtitle,
253
+ status: 'failed',
254
+ summary: messages.summary,
255
+ bullets: formatNativeCompatibilityBullets(compatibility),
256
+ nextStep: messages.nextStep,
257
+ installCommand: compatibility.installCommand,
258
+ technical: formatNativeCompatibilityTechnicalDetails(compatibility, response)
259
+ };
260
+ }
261
+
262
+ function getNativeCompatibilityMessages(statusValue) {
263
+ switch (statusValue) {
264
+ case 'native_too_old':
265
+ return {
266
+ title: tx('Native Host Update Required', '需要更新 Native Host'),
267
+ subtitle: tx('The local native host is too old for this extension.', '本机 native host 版本太旧。'),
268
+ summary: tx('The extension reached the native host, but its version or capabilities do not match this extension. Update the native host before running Codex.', '插件已经连上 native host,但版本或能力与当前扩展不匹配。请先更新 native host 再运行 Codex。'),
269
+ nextStep: tx('Run the installer command below, reload the Chrome extension, then check again.', '运行下面的安装命令,重新加载 Chrome 扩展后再检查。')
270
+ };
271
+ case 'protocol_unsupported':
272
+ return {
273
+ title: tx('Protocol Mismatch', '协议不匹配'),
274
+ subtitle: tx('The extension and native host do not support the same bridge protocol.', '扩展和 native host 支持的桥接协议不一致。'),
275
+ summary: tx('The native host responded, but its protocol range is not compatible with this extension. Update the extension and native host together.', 'Native host 已响应,但协议范围与当前扩展不兼容。请同时更新扩展和 native host。'),
276
+ nextStep: tx('Run the installer command below and make sure the Chrome extension is the matching release.', '运行下面的安装命令,并确认 Chrome 扩展是匹配的版本。')
277
+ };
278
+ case 'extension_too_old':
279
+ return {
280
+ title: tx('Extension Update Required', '需要更新扩展'),
281
+ subtitle: tx('The native host requires a newer Chrome extension.', 'Native host 需要更新的 Chrome 扩展。'),
282
+ summary: tx('The installed native host is newer than this extension can safely use.', '已安装的 native host 比当前扩展更新,当前扩展不能安全使用。'),
283
+ nextStep: tx('Update the Chrome extension, reload Overleaf, then check the local connection again.', '更新 Chrome 扩展,重新加载 Overleaf 后再检查本机连接。')
284
+ };
285
+ default:
286
+ return {
287
+ title: tx('Native Host Not Compatible', 'Native Host 不兼容'),
288
+ subtitle: tx('The local bridge did not match this extension.', '本机桥接服务与当前扩展不匹配。'),
289
+ summary: tx('The native host response could not be accepted by this extension.', '当前扩展不能接受 native host 的响应。'),
290
+ nextStep: tx('Update the native host and Chrome extension, then check again.', '更新 native host 和 Chrome 扩展后再检查。')
291
+ };
292
+ }
293
+ }
294
+
295
+ function formatNativeCompatibilityBullets(compatibility = {}) {
296
+ const native = compatibility.native || {};
297
+ return [
298
+ tx(`Status: ${compatibility.status || 'unknown'}`, `状态:${compatibility.status || 'unknown'}`),
299
+ tx(`Classification: ${getNativeCompatibilityClassification(compatibility)}`, `兼容分类:${getNativeCompatibilityClassification(compatibility)}`),
300
+ tx(`Extension version: ${compatibility.extensionVersion || 'unknown'}`, `扩展版本:${compatibility.extensionVersion || 'unknown'}`),
301
+ tx(`Native version: ${native.version || 'unknown'}`, `Native 版本:${native.version || 'unknown'}`),
302
+ tx(`Native protocol: ${formatNativeProtocolVersion(native)}`, `Native 协议:${formatNativeProtocolVersion(native)}`)
303
+ ];
304
+ }
305
+
306
+ function formatNativeProtocolVersion(native = {}) {
307
+ const protocolVersion = native.protocolVersion ?? 'unknown';
308
+ const range = native.supportedProtocol;
309
+ if (range && Number.isInteger(range.min) && Number.isInteger(range.max)) {
310
+ return `${protocolVersion} (${range.min}-${range.max})`;
311
+ }
312
+ return String(protocolVersion);
313
+ }
314
+
315
+ function formatNativeCompatibilityTechnicalDetails(compatibility = {}, response = {}) {
316
+ const native = compatibility.native || {};
317
+ const capabilities = native.capabilities && typeof native.capabilities === 'object'
318
+ ? Object.keys(native.capabilities).sort()
319
+ : [];
320
+ return JSON.stringify({
321
+ status: compatibility.status || 'unknown',
322
+ classification: getNativeCompatibilityClassification(compatibility),
323
+ extensionVersion: compatibility.extensionVersion || '',
324
+ nativeVersion: native.version || '',
325
+ protocolVersion: native.protocolVersion ?? null,
326
+ supportedProtocol: native.supportedProtocol || null,
327
+ capabilityKeys: capabilities,
328
+ errorCode: response?.error?.code || '',
329
+ errorMessage: response?.error?.message || ''
330
+ }, null, 2);
331
+ }
332
+
333
+ async function inspectPageStateDiagnostics(options = {}) {
334
+ if (!options.collectOnly) {
335
+ showDiagnosticsLoading(tr('diagnosticsPageTitle'), tr('diagnosticsPageLoading'));
336
+ }
337
+ let result;
338
+ try {
339
+ const probe = await callPageBridge('probe', {
340
+ manualOverride: getState()?.requireReviewing === false
341
+ });
342
+ result = formatPageStateDiagnosticsResult(probe);
343
+ } catch (error) {
344
+ result = {
345
+ title: tx('Overleaf Page Check Failed', 'Overleaf 页面检查失败'),
346
+ subtitle: tx('The extension did not read the current page state.', '插件没有读到当前页面状态。'),
347
+ status: 'failed',
348
+ summary: tx('This usually means Overleaf is still loading, or the page script is temporarily unavailable.', '这通常表示 Overleaf 页面还在加载,或者页面脚本暂时不可用。'),
349
+ nextStep: tx('Reload Overleaf, open the .tex file you want to work on, then try again.', '刷新 Overleaf 页面,点开要处理的 .tex 文件后再试。'),
350
+ technical: error?.stack || error?.message || String(error)
351
+ };
352
+ }
353
+ if (options.collectOnly) {
354
+ return result;
355
+ }
356
+ showDiagnosticsResult(result);
357
+ }
358
+
359
+ async function inspectOtWarmMirrorDiagnostics(options = {}) {
360
+ if (!options.collectOnly) {
361
+ showDiagnosticsLoading(tr('diagnosticsOtTitle'), tr('diagnosticsLoading'));
362
+ }
363
+ // OT warm mirror is experimental and off by default. When it is disabled
364
+ // there is nothing live to probe, so report a clean, healthy "disabled"
365
+ // state instead of querying (then warning about) absent OT metadata — or
366
+ // degrading to the generic "could not run" error if a probe throws. A
367
+ // turned-off feature must never read as a failure.
368
+ if (!isExperimentalOtEnabled()) {
369
+ const disabledResult = {
370
+ title: tr('diagnosticsOtTitle'),
371
+ subtitle: tr('diagnosticsOtSubtitle'),
372
+ status: 'completed',
373
+ summary: tr('diagnosticsOtSummaryDisabled'),
374
+ bullets: [],
375
+ nextStep: '',
376
+ technical: ''
377
+ };
378
+ if (options.collectOnly) {
379
+ return disabledResult;
380
+ }
381
+ showDiagnosticsResult(disabledResult);
382
+ return;
383
+ }
384
+ const projectId = getCurrentProjectId();
385
+ let otStatus = null;
386
+ let mirrorStatus = null;
387
+ let metadataWarning = false;
388
+
389
+ try {
390
+ otStatus = await callPageBridge('getOtStatus', { projectId });
391
+ if (otStatus?.ok === false) {
392
+ metadataWarning = true;
393
+ otStatus = {
394
+ ...otStatus,
395
+ lastErrorCode: 'get_ot_status_failed'
396
+ };
397
+ }
398
+ } catch (error) {
399
+ metadataWarning = true;
400
+ otStatus = {
401
+ ok: false,
402
+ status: 'unavailable',
403
+ lastErrorCode: 'get_ot_status_failed'
404
+ };
405
+ }
406
+
407
+ try {
408
+ mirrorStatus = await getMirrorFreshness();
409
+ if (!mirrorStatus) {
410
+ metadataWarning = true;
411
+ mirrorStatus = { lastOtErrorCode: 'mirror_status_unavailable' };
412
+ }
413
+ } catch (error) {
414
+ metadataWarning = true;
415
+ mirrorStatus = { lastOtErrorCode: 'mirror_status_failed' };
416
+ }
417
+
418
+ const baseResult = formatOtDiagnosticsResult({ otStatus, mirrorStatus });
419
+ const result = metadataWarning
420
+ ? {
421
+ ...baseResult,
422
+ status: 'warning',
423
+ summary: `${baseResult.summary} ${tx('Some OT diagnostic metadata is unavailable.', '部分 OT 诊断元数据暂时不可用。')}`
424
+ }
425
+ : baseResult;
426
+ if (options.collectOnly) {
427
+ return result;
428
+ }
429
+ showDiagnosticsResult(result);
430
+ }
431
+
432
+ function formatOtDiagnosticsResult({ otStatus, mirrorStatus }) {
433
+ const enabled = isExperimentalOtEnabled();
434
+ const statusValue = normalizeOtStatus(otStatus ? readOtBridgeStatus(otStatus) : getCurrentOtStatus());
435
+ const queuedEventCount = normalizeOtDiagnosticsCount(otStatus?.queuedEventCount);
436
+ const lastEventAt = formatOtDiagnosticValue(otStatus?.lastEventAt, tr('noneValue'));
437
+ const warmMirrorState = getOtWarmMirrorState() || {};
438
+ const lastOtPatchAt = formatOtDiagnosticValue(mirrorStatus?.lastOtPatchAt || warmMirrorState.lastPatchAt, tr('noneValue'));
439
+ const lastOtErrorCode = formatOtDiagnosticValue(mirrorStatus?.lastOtErrorCode || warmMirrorState.lastErrorCode, tr('noneValue'));
440
+ const lastErrorCode = formatOtDiagnosticValue(otStatus?.lastErrorCode || otStatus?.reason || otStatus?.error, tr('noneValue'));
441
+ const channelCandidates = formatOtChannelCandidates(otStatus?.channelCandidates);
442
+ const otFreshFileCount = normalizeOtDiagnosticsCount(
443
+ mirrorStatus?.otFreshFileCount,
444
+ Array.isArray(mirrorStatus?.otFreshFiles) ? mirrorStatus.otFreshFiles.length : 0
445
+ );
446
+ const bridgeFailed = otStatus?.ok === false;
447
+ const focusFiles = getActiveFocusFiles();
448
+ const otWarmStart = otWarmMirrorController?.canUseOtWarmStart?.({ enabled, focusFiles, mirrorStatus }) || { ok: false };
449
+ const fallback = !enabled || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
450
+
451
+ return {
452
+ title: tr('diagnosticsOtTitle'),
453
+ subtitle: tr('diagnosticsOtSubtitle'),
454
+ status: enabled && !fallback && !bridgeFailed ? 'completed' : (enabled || bridgeFailed ? 'warning' : 'completed'),
455
+ summary: tr(enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
456
+ bullets: [
457
+ `${tr('otStatus')}: ${formatOtStatusLabel(statusValue)}`,
458
+ `${tr('otFreshFiles')}: ${otFreshFileCount}`,
459
+ `${tr('otFallback')}: ${fallback ? tr('yes') : tr('no')}`
460
+ ],
461
+ nextStep: fallback ? tr('diagnosticsOtNextStep') : '',
462
+ technical: formatOtDiagnosticsTechnicalDetails({
463
+ strategy: formatOtDiagnosticValue(otStatus?.strategy, 'unknown'),
464
+ queuedEventCount,
465
+ lastEventAt,
466
+ lastOtPatchAt,
467
+ lastOtErrorCode,
468
+ lastErrorCode,
469
+ channelCandidates
470
+ })
471
+ };
472
+ }
473
+
474
+ function formatOtDiagnosticsTechnicalDetails(metadata = {}) {
475
+ const lines = [
476
+ `strategy: ${metadata.strategy || 'unknown'}`,
477
+ `queuedEventCount: ${metadata.queuedEventCount}`,
478
+ `lastEventAt: ${metadata.lastEventAt || tr('noneValue')}`,
479
+ `lastOtPatchAt: ${metadata.lastOtPatchAt || tr('noneValue')}`,
480
+ `lastOtErrorCode: ${metadata.lastOtErrorCode || tr('noneValue')}`,
481
+ `lastErrorCode: ${metadata.lastErrorCode || tr('noneValue')}`
482
+ ];
483
+ const candidates = Array.isArray(metadata.channelCandidates) ? metadata.channelCandidates : [];
484
+ lines.push(`channelCandidates: ${candidates.length ? '' : tr('noneValue')}`);
485
+ for (const candidate of candidates) {
486
+ lines.push(`- ${candidate}`);
487
+ }
488
+ return lines.join('\n');
489
+ }
490
+
491
+ function formatOtChannelCandidates(candidates) {
492
+ return (Array.isArray(candidates) ? candidates : [])
493
+ .slice(0, 4)
494
+ .map(candidate => {
495
+ const root = formatOtDiagnosticValue(candidate?.root, 'unknown');
496
+ const keyPaths = (Array.isArray(candidate?.keyPaths) ? candidate.keyPaths : [])
497
+ .map(value => formatOtDiagnosticValue(value, ''))
498
+ .filter(Boolean)
499
+ .slice(0, 6);
500
+ return keyPaths.length ? `${root}: ${keyPaths.join(', ')}` : root;
501
+ })
502
+ .filter(Boolean);
503
+ }
504
+
505
+ function normalizeOtDiagnosticsCount(value, fallback = 0) {
506
+ const count = Number(value);
507
+ return Number.isFinite(count) && count >= 0 ? Math.floor(count) : fallback;
508
+ }
509
+
510
+ function formatOtDiagnosticValue(value, fallback = '') {
511
+ let text = '';
512
+ if (typeof value === 'string') {
513
+ text = value;
514
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
515
+ text = String(value);
516
+ } else if (value?.code || value?.message) {
517
+ text = String(value.code || value.message);
518
+ }
519
+ if (!text) {
520
+ return fallback;
521
+ }
522
+ return text.length > 160 ? `${text.slice(0, 157)}...` : text;
523
+ }
524
+
525
+ function formatPageStateDiagnosticsResult(probe) {
526
+ const readiness = getProbeRunReadiness(probe || {});
527
+ const reviewingOk = probe?.reviewing?.ok === true;
528
+ const writable = probe?.capabilities?.editor?.write !== false;
529
+ const readable = probe?.editor?.ok === true;
530
+ const bullets = [
531
+ readable ? tx('Current file was read.', '当前文件已经读到。') : tx('Current file content has not been verified yet.', '还没有确认当前文件内容。'),
532
+ writable ? tx('Editor write access will be verified again during writeback.', '当前编辑器写入能力会在写入时再次验证。') : tx('The current editor is not writable yet.', '当前编辑器暂时不可写。'),
533
+ reviewingOk ? tx('Reviewing/Track Changes status is verified.', '留痕/Reviewing 状态已确认。') : tx('Reviewing/Track Changes status is not verified yet.', '留痕/Reviewing 状态还没有确认。')
534
+ ];
535
+
536
+ if (readable && writable) {
537
+ return {
538
+ title: tx('Overleaf Page Ready', 'Overleaf 页面可用'),
539
+ subtitle: formatProbeStatusBar(probe),
540
+ status: reviewingOk || getState()?.mode === 'ask' ? 'completed' : 'warning',
541
+ summary: tx('The current Overleaf file can be read and written. The extension still verifies before writing to avoid overwriting new user or collaborator edits.', '当前 Overleaf 文件可以读取和写入。写入前插件仍会再次验证,避免覆盖用户或协作者的新改动。'),
542
+ bullets,
543
+ nextStep: reviewingOk || getState()?.mode === 'ask' ? '' : tx('If you need tracked writes, turn on Overleaf Reviewing/Track Changes first.', '如果需要留痕写入,请先在 Overleaf 开启 Reviewing/Track Changes。'),
544
+ technical: formatPageDiagnosticsTechnicalDetails(probe)
545
+ };
546
+ }
547
+
548
+ return {
549
+ title: readable ? tx('Current File Read, Write Status Incomplete', '当前文件已读取,但写入状态不完整') : tx('Current File Not Verified Yet', '还没有确认当前文件'),
550
+ subtitle: formatProbeStatusBar(probe),
551
+ status: 'warning',
552
+ summary: readable
553
+ ? tx('The extension read the current Overleaf file, but has not verified that the current editor can be written.', '插件已读到当前 Overleaf 文件,但还没有确认当前编辑器可以写入。')
554
+ : tx('The extension has not verified that the current editor can be written. Usually this means no .tex file is open yet, or Overleaf is still loading.', '插件没有确认当前编辑器可以写入。通常是还没点开 .tex 文件,或者 Overleaf 页面仍在加载。'),
555
+ bullets,
556
+ nextStep: tx('Open the target .tex file from the left Overleaf file tree, wait for the editor to load, then check again.', '在 Overleaf 左侧文件树点开要处理的 .tex 文件,等编辑器加载完成后重新检测。'),
557
+ technical: formatPageDiagnosticsTechnicalDetails(probe)
558
+ };
559
+ }
560
+
561
+ function formatProjectSnapshotDiagnosticsResult(project) {
562
+ const files = project?.files || [];
563
+ const textCount = files.filter(isTextSnapshotFile).length;
564
+ const binaryCount = files.length - textCount;
565
+ const fullProject = project?.capabilities?.fullProjectSnapshot === true;
566
+ const warnings = getProjectSnapshotWarnings(project);
567
+ const warningText = [...warnings.blocking, ...warnings.nonBlocking].map(formatProjectSnapshotWarning);
568
+ const source = project?.capabilities?.method || 'unknown';
569
+ const bullets = [
570
+ tx(`Text files: ${textCount}`, `文本文件:${textCount} 个`),
571
+ tx(`Asset files: ${binaryCount}`, `资源文件:${binaryCount} 个`),
572
+ project?.activePath ? tx(`Current file: ${project.activePath}`, `当前文件:${project.activePath}`) : tx('Current file: not detected', '当前文件:未识别')
573
+ ];
574
+
575
+ if (fullProject && files.length) {
576
+ return {
577
+ title: tx('Project Read OK', '项目读取正常'),
578
+ subtitle: tx(`Read source: ${source}`, `读取来源:${source}`),
579
+ status: 'completed',
580
+ summary: tx(
581
+ `The extension read the full Overleaf project: ${textCount} text file(s)${binaryCount ? `, ${binaryCount} asset file(s)` : ''}.`,
582
+ `插件已读到完整 Overleaf 项目:${textCount} 个文本文件${binaryCount ? `,${binaryCount} 个资源文件` : ''}。`
583
+ ),
584
+ bullets: warningText.length ? [...bullets, ...warningText] : bullets,
585
+ technical: formatProjectSnapshotLog(project)
586
+ };
587
+ }
588
+
589
+ return {
590
+ title: files.length ? tx('Full Project Not Read', '没有读到完整项目') : tx('No Project Files Read', '没有读到项目文件'),
591
+ subtitle: tx(`Read source: ${source}`, `读取来源:${source}`),
592
+ status: 'warning',
593
+ summary: files.length
594
+ ? tx(
595
+ `The full Overleaf project was not read. Currently read ${textCount} text file(s)${binaryCount ? `, ${binaryCount} asset file(s)` : ''}.`,
596
+ `没有读到完整的 Overleaf 项目。当前只读到 ${textCount} 个文本文件${binaryCount ? `,${binaryCount} 个资源文件` : ''}。`
597
+ )
598
+ : tx('The full Overleaf project was not read, and no usable file content was available.', '没有读到完整的 Overleaf 项目,也没有拿到可用文件内容。'),
599
+ bullets: warningText.length ? [...bullets, ...warningText] : bullets,
600
+ nextStep: tx('Reload Overleaf and wait for the left file tree to load, then retry. If you only want one file, use + to add @file context.', '刷新 Overleaf 页面,等左侧文件树加载完成后重试;如果只想处理一个文件,可以用 + 添加 @file 上下文。'),
601
+ technical: formatProjectSnapshotLog(project)
602
+ };
603
+ }
604
+
605
+ function formatPageDiagnosticsTechnicalDetails(probe) {
606
+ const lines = [];
607
+ lines.push(tx(`Status: ${formatProbeStatusBar(probe || {})}`, `状态:${formatProbeStatusBar(probe || {})}`));
608
+ const diagnostics = probe?.reviewingDiagnostics;
609
+ if (diagnostics) {
610
+ lines.push(`Reviewing controls: ${diagnostics.controlCount || 0}; body=${Boolean(diagnostics.bodyTextHasReviewing)}; text=${Boolean(diagnostics.textContentHasReviewing)}`);
611
+ for (const control of (diagnostics.reviewLikeControls || []).slice(0, 4)) {
612
+ const label = [
613
+ control.text,
614
+ control.ariaLabel && `aria:${control.ariaLabel}`,
615
+ control.title && `title:${control.title}`,
616
+ control.dataTestId && `test:${control.dataTestId}`,
617
+ control.id && `id:${control.id}`
618
+ ].filter(Boolean).join(' | ');
619
+ lines.push(`Review-like ${control.tag || 'node'}: ${label || control.htmlSnippet || '(no label)'}`);
620
+ }
621
+ }
622
+ const editorDiagnostics = probe?.editorDiagnostics;
623
+ if (editorDiagnostics) {
624
+ const active = editorDiagnostics.active;
625
+ lines.push(`Editor: active ${active?.tag || 'none'} ${active?.ariaLabel || active?.className || ''} len=${active?.valueLength || 0}; textareas=${editorDiagnostics.textareaCount || 0}; editables=${editorDiagnostics.editableCount || 0}; iframes=${editorDiagnostics.iframeCount || 0}`);
626
+ if (editorDiagnostics.documentStats) {
627
+ const stats = editorDiagnostics.documentStats;
628
+ lines.push(`DOM: elements=${stats.elementCount || 0}; textareas=${stats.textareaCount || 0}; cm=${stats.cmCount || 0}; role textbox=${stats.roleTextboxCount || 0}`);
629
+ }
630
+ if (editorDiagnostics.codeMirrorView) {
631
+ const view = editorDiagnostics.codeMirrorView;
632
+ lines.push(`CodeMirror: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}`);
633
+ }
634
+ }
635
+ const projectDiagnostics = probe?.projectDiagnostics;
636
+ if (projectDiagnostics) {
637
+ lines.push(`Project records: ${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}`);
638
+ for (const record of (projectDiagnostics.docRecords || []).slice(0, 4)) {
639
+ lines.push(`Doc record: ${record.path} id=${record.id} source=${record.source || 'internal'}`);
640
+ }
641
+ }
642
+ return lines.join('\n');
643
+ }
644
+
645
+
646
+ return {
647
+ runAllDiagnostics,
648
+ inspectProjectSnapshot,
649
+ inspectNativeEnvironment,
650
+ inspectPageStateDiagnostics,
651
+ inspectOtWarmMirrorDiagnostics,
652
+ fallbackNativeCompatibility,
653
+ getNativeCompatibilityClassification,
654
+ isNativeCompatibilityCompatible
655
+ };
656
+ }
657
+
658
+ window.CodexOverleafDiagnosticsController = { create };
659
+ })();