codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -1,11 +1,8 @@
1
1
  (function initCodexOverleafDiagnosticsController() {
2
2
  'use strict';
3
3
 
4
- // Diagnostics controller — the check runners (inspect*) and result formatters
5
- // carved out of contentRuntime.js (v1.4.5 structural-debt phase 1). The code
6
- // below moved verbatim (original indentation kept); runtime collaborators are
7
- // factory-injected so behavior is unchanged. Mutable runtime state (panel
8
- // state, OT status) is read through injected getters.
4
+ // Check runners own observations; result formatters project those observations
5
+ // into the current UI language. Runtime collaborators are factory-injected.
9
6
  function create(deps = {}) {
10
7
  const {
11
8
  Compatibility: CodexOverleafCompatibility,
@@ -23,12 +20,12 @@
23
20
  otWarmMirrorController,
24
21
  getActiveFocusFiles,
25
22
  getCurrentProjectId,
23
+ getScope,
26
24
  isExperimentalOtEnabled,
27
25
  readOtBridgeStatus,
28
26
  formatOtStatusLabel,
29
27
  normalizeOtStatus,
30
28
  formatProbeStatusBar,
31
- getProbeRunReadiness,
32
29
  getMirrorFreshness,
33
30
  formatProjectSnapshotLog,
34
31
  formatProjectSnapshotWarning,
@@ -38,6 +35,48 @@
38
35
  RUN_SNAPSHOT_ZIP_TIMEOUT_MS
39
36
  } = deps;
40
37
 
38
+ // All visible checks share one request owner. Aggregate collectOnly calls
39
+ // must not supersede their own parent, and a later single check supersedes
40
+ // an older aggregate just as a later aggregate does.
41
+ let requestGeneration = 0;
42
+ let currentProjection = null;
43
+ const resultProjections = new WeakMap();
44
+ // Preserve the observed status and raw technical evidence across UI refreshes.
45
+ function projectableResult(project) {
46
+ const result = project();
47
+ resultProjections.set(result, () => ({ ...project(), status: result.status, technical: result.technical }));
48
+ return result;
49
+ }
50
+ function refreshLocale() {
51
+ return currentProjection && isCurrentDiagnosticsRequest(currentProjection.request)
52
+ ? currentProjection.project() : null;
53
+ }
54
+ function captureDiagnosticsScope() {
55
+ const scope = getScope?.() || { projectId: getCurrentProjectId?.() };
56
+ return { accountScopeId: scope.accountScopeId, projectId: scope.projectId, generation: scope.generation };
57
+ }
58
+
59
+ function beginDiagnostics(title, subtitle) {
60
+ const request = { generation: ++requestGeneration, scope: captureDiagnosticsScope() };
61
+ currentProjection = { request, project: () => ({ title: tr(title), subtitle: tr(subtitle),
62
+ status: 'running', summary: tr('diagnosticsLoadingSummary') }) };
63
+ showDiagnosticsLoading(tr(title), tr(subtitle));
64
+ return request;
65
+ }
66
+
67
+ function isCurrentDiagnosticsRequest(request) {
68
+ if (!request || request.generation !== requestGeneration) return false;
69
+ const scope = captureDiagnosticsScope();
70
+ return ['accountScopeId', 'projectId', 'generation'].every(key => scope[key] === request.scope[key]);
71
+ }
72
+
73
+ function showCurrentDiagnosticsResult(request, result) {
74
+ if (isCurrentDiagnosticsRequest(request)) {
75
+ currentProjection = { request, project: resultProjections.get(result) || (() => result) };
76
+ showDiagnosticsResult(result);
77
+ }
78
+ }
79
+
41
80
  // Reduce a result status to a health bucket. 'completed' is a pass; a hard
42
81
  // failure is fail; everything else (warning, partial, info) is attention.
43
82
  function diagnosticsHealthBucket(status) {
@@ -50,12 +89,12 @@
50
89
  // report (a status row per check) instead of four separate result screens.
51
90
  // The overall bucket also updates the trigger dot.
52
91
  async function runAllDiagnostics() {
53
- showDiagnosticsLoading(tr('diagnosticsHealthTitle'), tr('diagnosticsRunningAll'));
92
+ const request = beginDiagnostics('diagnosticsHealthTitle', 'diagnosticsRunningAll');
54
93
  const specs = [
55
- { label: tr('diagnosticsNativeShort'), run: inspectNativeEnvironment },
56
- { label: tr('diagnosticsPageShort'), run: inspectPageStateDiagnostics },
57
- { label: tr('diagnosticsSnapshotShort'), run: inspectProjectSnapshot },
58
- { label: tr('diagnosticsOtShort'), run: inspectOtWarmMirrorDiagnostics }
94
+ { label: 'diagnosticsNativeShort', run: inspectNativeEnvironment },
95
+ { label: 'diagnosticsPageShort', run: inspectPageStateDiagnostics },
96
+ { label: 'diagnosticsSnapshotShort', run: inspectProjectSnapshot },
97
+ { label: 'diagnosticsOtShort', run: inspectOtWarmMirrorDiagnostics }
59
98
  ];
60
99
  const checks = [];
61
100
  let worst = 'ok';
@@ -64,7 +103,7 @@
64
103
  try {
65
104
  result = await spec.run({ collectOnly: true });
66
105
  } catch (error) {
67
- result = { status: 'failed', summary: tr('diagnosticsCheckErrored'), technical: error?.message || String(error) };
106
+ result = projectableResult(() => ({ status: 'failed', summary: tr('diagnosticsCheckErrored'), technical: error?.message || String(error) }));
68
107
  }
69
108
  const bucket = diagnosticsHealthBucket(result?.status);
70
109
  if (bucket === 'fail') {
@@ -72,34 +111,32 @@
72
111
  } else if (bucket === 'warn' && worst !== 'fail') {
73
112
  worst = 'warn';
74
113
  }
75
- checks.push({
76
- status: result?.status || 'info',
77
- title: spec.label,
78
- summary: result?.summary || result?.subtitle || '',
79
- nextStep: bucket === 'ok' ? '' : (result?.nextStep || '')
114
+ const project = resultProjections.get(result) || (() => result);
115
+ checks.push(() => {
116
+ const translated = project();
117
+ return {
118
+ status: result?.status || 'info',
119
+ title: tr(spec.label),
120
+ summary: translated?.summary || translated?.subtitle || '',
121
+ nextStep: bucket === 'ok' ? '' : (translated?.nextStep || '')
122
+ };
80
123
  });
81
124
  }
82
125
  const overallSubtitleKey = worst === 'ok'
83
126
  ? 'diagnosticsHealthOk'
84
127
  : worst === 'fail' ? 'diagnosticsHealthFail' : 'diagnosticsHealthWarn';
85
- showDiagnosticsResult({
128
+ showCurrentDiagnosticsResult(request, projectableResult(() => ({
86
129
  title: tr('diagnosticsHealthTitle'),
87
130
  subtitle: tr(overallSubtitleKey),
88
131
  status: worst === 'ok' ? 'completed' : worst === 'fail' ? 'failed' : 'warning',
89
- checks
90
- });
91
- setDiagnosticsHealth(worst);
132
+ checks: checks.map(project => project())
133
+ })));
134
+ if (isCurrentDiagnosticsRequest(request)) setDiagnosticsHealth(worst);
92
135
  }
93
136
 
94
-
95
-
96
-
97
-
98
-
99
137
  async function inspectProjectSnapshot(options = {}) {
100
- if (!options.collectOnly) {
101
- showDiagnosticsLoading(tr('diagnosticsSnapshotTitle'), tr('diagnosticsSnapshotLoading'));
102
- }
138
+ const request = options.collectOnly ? null
139
+ : beginDiagnostics('diagnosticsSnapshotTitle', 'diagnosticsSnapshotLoading');
103
140
  let result;
104
141
  try {
105
142
  const project = await callPageBridge('getProjectSnapshot', {
@@ -111,36 +148,35 @@
111
148
  zipOnly: true,
112
149
  zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS
113
150
  });
114
- result = formatProjectSnapshotDiagnosticsResult(project);
151
+ result = projectableResult(() => formatProjectSnapshotDiagnosticsResult(project));
115
152
  } catch (error) {
116
- result = {
153
+ result = projectableResult(() => ({
117
154
  title: tx('Project Read Failed', '项目读取失败'),
118
155
  subtitle: tx('The extension did not receive Overleaf project content.', '插件没有拿到 Overleaf 项目内容。'),
119
156
  status: 'failed',
120
157
  summary: tr('diagnosticsSnapshotErrorSummary'),
121
158
  nextStep: tx('Reload Overleaf, wait for the left file tree to finish loading, then retry.', '刷新 Overleaf 页面,等左侧文件树加载完成后重试。'),
122
159
  technical: error?.stack || error?.message || String(error)
123
- };
160
+ }));
124
161
  }
125
162
  if (options.collectOnly) {
126
163
  return result;
127
164
  }
128
- showDiagnosticsResult(result);
165
+ showCurrentDiagnosticsResult(request, result);
129
166
  }
130
167
 
131
168
  async function inspectNativeEnvironment(options = {}) {
132
- if (!options.collectOnly) {
133
- showDiagnosticsLoading(tr('diagnosticsNativeTitle'), tr('diagnosticsNativeLoading'));
134
- }
169
+ const request = options.collectOnly ? null
170
+ : beginDiagnostics('diagnosticsNativeTitle', 'diagnosticsNativeLoading');
135
171
  const params = CodexOverleafCompatibility?.buildBridgePingParams
136
172
  ? CodexOverleafCompatibility.buildBridgePingParams(getExtensionCompatibilityMetadata())
137
173
  : {};
138
174
  const response = await sendBackgroundNative({ method: 'bridge.ping', params });
139
- const result = formatNativeEnvironmentResult(response);
175
+ const result = projectableResult(() => formatNativeEnvironmentResult(response));
140
176
  if (options.collectOnly) {
141
177
  return result;
142
178
  }
143
- showDiagnosticsResult(result);
179
+ showCurrentDiagnosticsResult(request, result);
144
180
  }
145
181
 
146
182
  function formatNativeEnvironmentResult(response) {
@@ -150,7 +186,6 @@
150
186
  if (compatibility.status !== 'ok' && compatibility.status !== 'native_unhealthy') {
151
187
  return formatNativeCompatibilityResult(compatibility, response);
152
188
  }
153
-
154
189
  if (!response?.ok) {
155
190
  return {
156
191
  title: tx('Local Connection Problem', '本机连接异常'),
@@ -158,11 +193,10 @@
158
193
  status: 'failed',
159
194
  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。'),
160
195
  nextStep: tx('Make sure the native host is installed, reload the Chrome extension, then try again.', '确认 native host 已安装,并重新加载 Chrome 扩展后再试。'),
161
- installCommand: INSTALL_COMMAND,
196
+ installCommand: compatibility.installCommand ?? INSTALL_COMMAND,
162
197
  technical: response?.error?.message || tx('native host did not respond', 'native host 没有响应')
163
198
  };
164
199
  }
165
-
166
200
  const environment = response.result?.environment;
167
201
  if (!environment) {
168
202
  return {
@@ -174,7 +208,6 @@
174
208
  technical: JSON.stringify(response.result || {}, null, 2)
175
209
  };
176
210
  }
177
-
178
211
  const codexOk = environment.codex?.ok === true;
179
212
  const latexTools = environment.latex?.available || [];
180
213
  const latexOk = latexTools.length > 0;
@@ -186,7 +219,6 @@
186
219
  ? tx(`LaTeX tools available: ${latexTools.join(', ')}`, `LaTeX 工具可用:${latexTools.join(', ')}`)
187
220
  : tx('LaTeX tools were not found: text editing works, but local compile checks are limited.', 'LaTeX 工具没有找到:可以编辑文本,但不能在本地编译验证。')
188
221
  ];
189
-
190
222
  if (codexOk && latexOk) {
191
223
  return {
192
224
  title: tx('Local Connection OK', '本机连接正常'),
@@ -197,7 +229,6 @@
197
229
  technical: JSON.stringify(environment, null, 2)
198
230
  };
199
231
  }
200
-
201
232
  return {
202
233
  title: codexOk ? tx('Local Connection Partially Available', '本机连接部分可用') : tx('Codex Unavailable', 'Codex 不可用'),
203
234
  subtitle: codexOk ? tx('Codex is available, but compile tools are incomplete.', 'Codex 可用,但编译工具不完整。') : tx('Local Codex was not found.', '没有找到本机 Codex。'),
@@ -212,7 +243,6 @@
212
243
  technical: JSON.stringify(environment, null, 2)
213
244
  };
214
245
  }
215
-
216
246
  function fallbackNativeCompatibility(response) {
217
247
  return response?.ok
218
248
  ? { status: 'ok', classification: 'compatible', native: response.result, installCommand: INSTALL_COMMAND }
@@ -238,12 +268,13 @@
238
268
  subtitle: tx('The extension could not connect to the local service.', '插件没有连上本机服务。'),
239
269
  status: 'failed',
240
270
  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。'),
241
- nextStep: tx('Make sure the native host is installed, reload the Chrome extension, then try again.', '确认 native host 已安装,并重新加载 Chrome 扩展后再试。'),
242
- installCommand: INSTALL_COMMAND,
271
+ nextStep: compatibility.installIdentity?.managed && !compatibility.installCommand
272
+ ? tx('Repair Managed using the original managed root, browser and extension ID with install-managed.', '请使用原管理根目录、浏览器及扩展 ID,通过 install-managed 修复托管安装。')
273
+ : tx('Make sure the native host is installed, reload the Chrome extension, then try again.', '确认 native host 已安装,并重新加载 Chrome 扩展后再试。'),
274
+ installCommand: compatibility.installCommand ?? INSTALL_COMMAND,
243
275
  technical: response?.error?.message || tx('native host did not respond', 'native host 没有响应')
244
276
  };
245
277
  }
246
-
247
278
  const messages = getNativeCompatibilityMessages(statusValue);
248
279
  return {
249
280
  title: messages.title,
@@ -256,7 +287,6 @@
256
287
  technical: formatNativeCompatibilityTechnicalDetails(compatibility, response)
257
288
  };
258
289
  }
259
-
260
290
  function getNativeCompatibilityMessages(statusValue) {
261
291
  switch (statusValue) {
262
292
  case 'native_too_old':
@@ -329,62 +359,59 @@
329
359
  }
330
360
 
331
361
  async function inspectPageStateDiagnostics(options = {}) {
332
- if (!options.collectOnly) {
333
- showDiagnosticsLoading(tr('diagnosticsPageTitle'), tr('diagnosticsPageLoading'));
334
- }
362
+ const request = options.collectOnly ? null
363
+ : beginDiagnostics('diagnosticsPageTitle', 'diagnosticsPageLoading');
335
364
  let result;
336
365
  try {
337
366
  const probe = await callPageBridge('probe', {
338
367
  manualOverride: getState()?.requireReviewing === false
339
368
  });
340
- result = formatPageStateDiagnosticsResult(probe);
369
+ const mode = getState()?.mode;
370
+ result = projectableResult(() => formatPageStateDiagnosticsResult(probe, mode));
341
371
  } catch (error) {
342
- result = {
372
+ result = projectableResult(() => ({
343
373
  title: tx('Overleaf Page Check Failed', 'Overleaf 页面检查失败'),
344
374
  subtitle: tx('The extension did not read the current page state.', '插件没有读到当前页面状态。'),
345
375
  status: 'failed',
346
376
  summary: tx('This usually means Overleaf is still loading, or the page script is temporarily unavailable.', '这通常表示 Overleaf 页面还在加载,或者页面脚本暂时不可用。'),
347
377
  nextStep: tx('Reload Overleaf, open the .tex file you want to work on, then try again.', '刷新 Overleaf 页面,点开要处理的 .tex 文件后再试。'),
348
378
  technical: error?.stack || error?.message || String(error)
349
- };
379
+ }));
350
380
  }
351
381
  if (options.collectOnly) {
352
382
  return result;
353
383
  }
354
- showDiagnosticsResult(result);
384
+ showCurrentDiagnosticsResult(request, result);
355
385
  }
356
386
 
357
387
  async function inspectOtWarmMirrorDiagnostics(options = {}) {
358
- if (!options.collectOnly) {
359
- showDiagnosticsLoading(tr('diagnosticsOtTitle'), tr('diagnosticsLoading'));
360
- }
388
+ const request = options.collectOnly ? null
389
+ : beginDiagnostics('diagnosticsOtTitle', 'diagnosticsLoading');
361
390
  // OT warm mirror is opt-in and off by default. When it is disabled
362
391
  // there is nothing live to probe, so report a clean, healthy "disabled"
363
392
  // state instead of querying (then warning about) absent OT metadata — or
364
393
  // degrading to the generic "could not run" error if a probe throws. A
365
394
  // turned-off feature must never read as a failure.
366
395
  if (!isExperimentalOtEnabled()) {
367
- const disabledResult = {
368
- title: tr('diagnosticsOtTitle'),
369
- subtitle: tr('diagnosticsOtSubtitle'),
370
- status: 'completed',
371
- summary: tr('diagnosticsOtSummaryDisabled'),
372
- bullets: [],
373
- nextStep: '',
374
- technical: ''
375
- };
376
396
  const mirrorState = getOtWarmMirrorState?.() || {};
377
- if (mirrorState.failureActive && mirrorState.lastFailure) {
378
- const failure = mirrorState.lastFailure;
379
- disabledResult.status = 'warning';
380
- disabledResult.summary = tr(otWarmMirrorController.getFailureMessageKey(failure.code));
381
- disabledResult.nextStep = tr('otFailureNextStep');
382
- disabledResult.technical = `lastErrorCode: ${failure.code}\nphase: ${failure.phase}\nfailedAt: ${new Date(failure.at).toISOString()}`;
383
- }
397
+ const failure = mirrorState.failureActive && mirrorState.lastFailure ? { ...mirrorState.lastFailure } : null;
398
+ const disabledResult = projectableResult(() => {
399
+ const result = {
400
+ title: tr('diagnosticsOtTitle'), subtitle: tr('diagnosticsOtSubtitle'), status: 'completed',
401
+ summary: tr('diagnosticsOtSummaryDisabled'), bullets: [], nextStep: '', technical: ''
402
+ };
403
+ if (failure) {
404
+ result.status = 'warning';
405
+ result.summary = tr(otWarmMirrorController.getFailureMessageKey(failure.code));
406
+ result.nextStep = tr('otFailureNextStep');
407
+ result.technical = `lastErrorCode: ${failure.code}\nphase: ${failure.phase}\nfailedAt: ${new Date(failure.at).toISOString()}`;
408
+ }
409
+ return result;
410
+ });
384
411
  if (options.collectOnly) {
385
412
  return disabledResult;
386
413
  }
387
- showDiagnosticsResult(disabledResult);
414
+ showCurrentDiagnosticsResult(request, disabledResult);
388
415
  return;
389
416
  }
390
417
  const projectId = getCurrentProjectId();
@@ -421,26 +448,30 @@
421
448
  mirrorStatus = { lastOtErrorCode: 'mirror_status_failed' };
422
449
  }
423
450
 
424
- const baseResult = formatOtDiagnosticsResult({ otStatus, mirrorStatus });
425
- const result = metadataWarning
426
- ? {
451
+ const context = { enabled: isExperimentalOtEnabled(), status: getCurrentOtStatus(),
452
+ warmMirrorState: { ...(getOtWarmMirrorState() || {}) }, focusFiles: getActiveFocusFiles() };
453
+ context.warmStart = otWarmMirrorController?.canUseOtWarmStart?.({ enabled: context.enabled, focusFiles: context.focusFiles, mirrorStatus }) || { ok: false };
454
+ const result = projectableResult(() => {
455
+ const baseResult = formatOtDiagnosticsResult({ otStatus, mirrorStatus, context });
456
+ return metadataWarning ? {
427
457
  ...baseResult,
428
458
  status: 'warning',
429
459
  summary: `${baseResult.summary} ${tx('Some OT diagnostic metadata is unavailable.', '部分 OT 诊断元数据暂时不可用。')}`
430
460
  }
431
- : baseResult;
461
+ : baseResult;
462
+ });
432
463
  if (options.collectOnly) {
433
464
  return result;
434
465
  }
435
- showDiagnosticsResult(result);
466
+ showCurrentDiagnosticsResult(request, result);
436
467
  }
437
468
 
438
- function formatOtDiagnosticsResult({ otStatus, mirrorStatus }) {
439
- const enabled = isExperimentalOtEnabled();
440
- const statusValue = normalizeOtStatus(otStatus ? readOtBridgeStatus(otStatus) : getCurrentOtStatus());
469
+ function formatOtDiagnosticsResult({ otStatus, mirrorStatus, context = {} }) {
470
+ const enabled = context.enabled ?? isExperimentalOtEnabled();
471
+ const statusValue = normalizeOtStatus(otStatus ? readOtBridgeStatus(otStatus) : context.status ?? getCurrentOtStatus());
441
472
  const queuedEventCount = normalizeOtDiagnosticsCount(otStatus?.queuedEventCount);
442
473
  const lastEventAt = formatOtDiagnosticValue(otStatus?.lastEventAt, tr('noneValue'));
443
- const warmMirrorState = getOtWarmMirrorState() || {};
474
+ const warmMirrorState = context.warmMirrorState || getOtWarmMirrorState() || {};
444
475
  const failureActive = warmMirrorState.failureActive === true;
445
476
  const lastOtPatchAt = formatOtDiagnosticValue(mirrorStatus?.lastOtPatchAt || warmMirrorState.lastPatchAt, tr('noneValue'));
446
477
  const lastOtErrorCode = formatOtDiagnosticValue(mirrorStatus?.lastOtErrorCode || warmMirrorState.lastErrorCode, tr('noneValue'));
@@ -451,10 +482,9 @@
451
482
  Array.isArray(mirrorStatus?.otFreshFiles) ? mirrorStatus.otFreshFiles.length : 0
452
483
  );
453
484
  const bridgeFailed = otStatus?.ok === false;
454
- const focusFiles = getActiveFocusFiles();
455
- const otWarmStart = otWarmMirrorController?.canUseOtWarmStart?.({ enabled, focusFiles, mirrorStatus }) || { ok: false };
485
+ const focusFiles = context.focusFiles || getActiveFocusFiles();
486
+ const otWarmStart = context.warmStart || otWarmMirrorController?.canUseOtWarmStart?.({ enabled, focusFiles, mirrorStatus }) || { ok: false };
456
487
  const fallback = !enabled || failureActive || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
457
-
458
488
  return {
459
489
  title: tr('diagnosticsOtTitle'),
460
490
  subtitle: tr('diagnosticsOtSubtitle'),
@@ -478,7 +508,6 @@
478
508
  })
479
509
  };
480
510
  }
481
-
482
511
  function formatOtDiagnosticsTechnicalDetails(metadata = {}) {
483
512
  const lines = [
484
513
  `strategy: ${metadata.strategy || 'unknown'}`,
@@ -530,8 +559,7 @@
530
559
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
531
560
  }
532
561
 
533
- function formatPageStateDiagnosticsResult(probe) {
534
- const readiness = getProbeRunReadiness(probe || {});
562
+ function formatPageStateDiagnosticsResult(probe, mode = getState()?.mode) {
535
563
  const reviewingOk = probe?.reviewing?.ok === true;
536
564
  const writable = probe?.capabilities?.editor?.write !== false;
537
565
  const readable = probe?.editor?.ok === true;
@@ -545,10 +573,10 @@
545
573
  return {
546
574
  title: tx('Overleaf Page Ready', 'Overleaf 页面可用'),
547
575
  subtitle: formatProbeStatusBar(probe),
548
- status: reviewingOk || getState()?.mode === 'ask' ? 'completed' : 'warning',
576
+ status: reviewingOk || mode === 'ask' ? 'completed' : 'warning',
549
577
  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 文件可以读取和写入。写入前插件仍会再次验证,避免覆盖用户或协作者的新改动。'),
550
578
  bullets,
551
- nextStep: reviewingOk || getState()?.mode === 'ask' ? '' : tx('If you need tracked writes, turn on Overleaf Reviewing/Track Changes first.', '如果需要留痕写入,请先在 Overleaf 开启 Reviewing/Track Changes。'),
579
+ nextStep: reviewingOk || mode === 'ask' ? '' : tx('If you need tracked writes, turn on Overleaf Reviewing/Track Changes first.', '如果需要留痕写入,请先在 Overleaf 开启 Reviewing/Track Changes。'),
552
580
  technical: formatPageDiagnosticsTechnicalDetails(probe)
553
581
  };
554
582
  }
@@ -579,7 +607,6 @@
579
607
  tx(`Asset files: ${binaryCount}`, `资源文件:${binaryCount} 个`),
580
608
  project?.activePath ? tx(`Current file: ${project.activePath}`, `当前文件:${project.activePath}`) : tx('Current file: not detected', '当前文件:未识别')
581
609
  ];
582
-
583
610
  if (fullProject && files.length) {
584
611
  return {
585
612
  title: tx('Project Read OK', '项目读取正常'),
@@ -593,7 +620,6 @@
593
620
  technical: formatProjectSnapshotLog(project)
594
621
  };
595
622
  }
596
-
597
623
  return {
598
624
  title: files.length ? tx('Full Project Not Read', '没有读到完整项目') : tx('No Project Files Read', '没有读到项目文件'),
599
625
  subtitle: tx(`Read source: ${source}`, `读取来源:${source}`),
@@ -609,7 +635,6 @@
609
635
  technical: formatProjectSnapshotLog(project)
610
636
  };
611
637
  }
612
-
613
638
  function formatPageDiagnosticsTechnicalDetails(probe) {
614
639
  const lines = [];
615
640
  lines.push(tx(`Status: ${formatProbeStatusBar(probe || {})}`, `状态:${formatProbeStatusBar(probe || {})}`));
@@ -656,6 +681,7 @@
656
681
 
657
682
 
658
683
  return {
684
+ refreshLocale,
659
685
  runAllDiagnostics,
660
686
  inspectProjectSnapshot,
661
687
  inspectNativeEnvironment,
@@ -136,16 +136,26 @@
136
136
  throw new Error('Audit diagnostics helper is unavailable');
137
137
  }
138
138
  const projectId = getCurrentProjectId();
139
+ const accountScopeId = deps.getAccountScopeId?.() || '';
140
+ if (!accountScopeId) throw new Error('Account identity is unavailable; reopen the project before exporting diagnostics.');
139
141
  const currentRunView = getCurrentRunView();
140
142
  const activeRecord = currentRunView && findRunRecord(currentRunView.recordId, currentRunView.sessionId);
141
- const recentRuns = (getState().runs || []).filter(run => run && (!run.runProjectId || run.runProjectId === projectId))
143
+ const panelState = getState();
144
+ const sessionScope = panelState.sessions?.find(session => session.id === panelState.activeSessionId)?.accountScopeId;
145
+ // Older normalized runs lack owner fields; they still rely on the view's
146
+ // hydration boundary. Explicit ownership, when present, must agree.
147
+ const matchesAccount = (record, ...owners) => [record?.runAccountScopeId, record?.accountScopeId, ...owners]
148
+ .filter(Boolean).every(owner => owner === accountScopeId);
149
+ const recentRuns = (panelState.runs || []).filter(run => run && (!run.runProjectId || run.runProjectId === projectId)
150
+ && matchesAccount(run, sessionScope, panelState.accountScopeId))
142
151
  .slice().reverse().sort((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0)).slice(0, 8);
143
152
  const [auditLogs, mirror, nativeDiagnostics] = await Promise.all([
144
- getRecentAuditLogsForCurrentProject(),
153
+ getRecentAuditLogsForCurrentProject({ projectId, accountScopeId }),
145
154
  getMirrorFreshness().catch(error => ({ status: 'unavailable', errorCode: error.message })),
146
155
  getNativeDiagnosticsSummaryForBundle()
147
156
  ]);
148
157
  if (getCurrentProjectId() !== projectId) throw new Error('Project changed during diagnostics export; export again.');
158
+ if (deps.getAccountScopeId?.() !== accountScopeId) throw new Error('Account changed during diagnostics export; export again.');
149
159
  const bundle = AuditRecords.buildDiagnosticBundle({
150
160
  excludeContent: true,
151
161
  compatibility: {
@@ -156,7 +166,8 @@
156
166
  nativeEnvironment: nativeDiagnostics.nativeEnvironment,
157
167
  mirror,
158
168
  auditLogs,
159
- run: activeRecord?.runProjectId === projectId ? activeRecord : recentRuns[0] || {},
169
+ run: activeRecord?.runProjectId === projectId && matchesAccount(activeRecord, currentRunView?.runAccountScopeId)
170
+ ? activeRecord : recentRuns[0] || {},
160
171
  recentRuns,
161
172
  governance: getGovernanceRulesForCurrentProject(),
162
173
  projectId
@@ -204,12 +215,15 @@
204
215
  }
205
216
  }
206
217
 
207
- async function getRecentAuditLogsForCurrentProject(limit = 12) {
218
+ async function getRecentAuditLogsForCurrentProject(scope, limit = 12) {
208
219
  if (!StorageDb?.getAllByIndex) {
209
220
  return [];
210
221
  }
211
- const records = await StorageDb.getAllByIndex('auditLogs', 'projectId', getCurrentProjectId());
222
+ const records = await StorageDb.getAllByIndex('auditLogs', 'projectId', scope.projectId);
212
223
  return (records || [])
224
+ // Legacy unowned audits remain stored but cannot be assigned to whichever
225
+ // account happens to export next, even when a session ID looks familiar.
226
+ .filter(record => record?.projectId === scope.projectId && record.accountScopeId === scope.accountScopeId)
213
227
  .slice()
214
228
  .sort((left, right) => String(right.createdAt || '').localeCompare(String(left.createdAt || '')))
215
229
  .slice(0, limit);
@@ -153,6 +153,35 @@
153
153
  });
154
154
  }
155
155
 
156
+ // Repaint an already-visible observation without rerunning checks or
157
+ // reopening a dismissed result. User disclosure and scroll choices survive.
158
+ function refreshLocale(target, result) {
159
+ const instance = target?._instance || target;
160
+ if (!instance?.container) return;
161
+ for (const node of instance.container.querySelectorAll('[data-i18n]')) {
162
+ node.textContent = t(instance, node.getAttribute('data-i18n'));
163
+ }
164
+ const close = instance.container.querySelector('[data-diagnostics-result-close]');
165
+ if (close) {
166
+ close.title = t(instance, 'close');
167
+ close.setAttribute('aria-label', t(instance, 'closeDiagnostics'));
168
+ }
169
+ const health = instance.container.querySelector('[data-diagnostics-health-dot]')?.dataset.health;
170
+ updateStatus(instance, { health });
171
+ const root = instance.container.querySelector('[data-diagnostics-result]');
172
+ if (!result || !root || root.hidden || instance.resultDismissed) return;
173
+ const details = root.querySelector('[data-diagnostics-result-details]');
174
+ const scroll = root.querySelector('.codex-diagnostics-result-scroll');
175
+ const open = details?.open, scrollTop = scroll?.scrollTop;
176
+ const checks = Array.from(root.querySelectorAll('.codex-diagnostics-check'), node => node.open);
177
+ showResult(instance, result);
178
+ if (details) details.open = open;
179
+ root.querySelectorAll('.codex-diagnostics-check').forEach((node, index) => {
180
+ if (index < checks.length) node.open = checks[index];
181
+ });
182
+ if (scroll) scroll.scrollTop = scrollTop;
183
+ }
184
+
156
185
  function showResult(target, result = {}) {
157
186
  const instance = target?._instance || target;
158
187
  if (instance?.resultDismissed) return;
@@ -621,6 +650,7 @@
621
650
  closeResult,
622
651
  showLoading,
623
652
  showResult,
653
+ refreshLocale,
624
654
  uniqueContentSignatures
625
655
  };
626
656
  })();
@@ -0,0 +1,54 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory(require('../shared/directoryCleanupProof'));
3
+ else root.CodexOverleafModuleRegistry.define('DirectoryCleanupRecovery', ['DirectoryCleanupProof'], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function (Proof) {
5
+ 'use strict';
6
+ function collect(result) {
7
+ const proofs = [], seen = new Set();
8
+ const safePath = path => typeof path === 'string' && path && path.trim() === path
9
+ && !/[\\\u0000-\u001f\u007f]/.test(path) && !path.startsWith('/') && !/^[A-Za-z]:/.test(path)
10
+ && !path.split('/').some(part => !part || part === '.' || part === '..');
11
+ function visit(value) {
12
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
13
+ seen.add(value);
14
+ if (Array.isArray(value)) { value.forEach(visit); return; }
15
+ const operation = value.operation, detail = value.result;
16
+ if (operation && detail && safePath(operation.path)) {
17
+ const remaining = Array.isArray(detail.directoryCleanup?.remaining) ? detail.directoryCleanup.remaining
18
+ : detail.ok !== true && Array.isArray(detail.createdDirectories) ? detail.createdDirectories : [];
19
+ // Forward moves may create destination parents; inverse moves can
20
+ // leave source parents behind. Other operations authorize path only.
21
+ const targets = [operation.path, ...(operation.type === 'move' && safePath(operation.to) ? [operation.to] : [])];
22
+ for (const proof of remaining) if (safePath(proof?.path)
23
+ && targets.some(target => target.startsWith(proof.path + '/'))) proofs.push(proof);
24
+ }
25
+ for (const field of ['applied', 'skipped']) visit(value[field]);
26
+ }
27
+ visit(result);
28
+ return proofs;
29
+ }
30
+ function create(deps) {
31
+ const scope = () => ({ projectId: deps.getProjectId(), accountScopeId: deps.getAccountScopeId(), generation: deps.getRouteGeneration?.() });
32
+ function capture(target, result) {
33
+ const directories = collect(result);
34
+ if (!directories.length || !target?.recordId) return null;
35
+ const captured = scope(), owner = { ...captured, sessionId: target.sessionId, runId: target.recordId };
36
+ if (!captured.accountScopeId || !captured.projectId || target.runProjectId !== captured.projectId
37
+ || (target.runAccountScopeId && target.runAccountScopeId !== captured.accountScopeId)) return null;
38
+ const record = deps.findRunRecord(owner.runId, owner.sessionId);
39
+ if (!record || record.runProjectId !== owner.projectId || record.forkSnapshot) return null;
40
+ const next = Proof.merge(owner, record.pendingDirectoryCleanup, directories);
41
+ if (!next) {
42
+ // These passive observations never hold a verified file result hostage.
43
+ // Preserve older evidence and report the omission in existing run details.
44
+ deps.onOmitted?.(owner.runId, 'directory_cleanup_capacity_exceeded');
45
+ return null;
46
+ }
47
+ record.pendingDirectoryCleanup = next;
48
+ deps.refresh?.(owner.runId);
49
+ return { owner, value: next };
50
+ }
51
+ return { capture };
52
+ }
53
+ return { create, collect };
54
+ });