codex-overleaf-link 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  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 +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  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 +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -1,18 +1,29 @@
1
- (function initCodexOverleafRunTimelineView() {
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory(require('./runRecoveryActions'));
3
+ else root.CodexOverleafModuleRegistry.define('RunTimelineView', ['RunRecoveryActions'], factory);
4
+ })(typeof window !== 'undefined' ? window : globalThis, function (RunRecoveryActions) {
2
5
  'use strict';
3
6
 
7
+
4
8
  // Owns scroll, cards, reports and mature run actions. The optional activity
5
9
  // presenter projects stored events without taking ownership of settlement.
6
10
  function create(deps = {}) {
7
11
  const {
8
12
  RunActivitySummary,
9
13
  RunPresence,
14
+ PanelIcons,
10
15
  RunActivityModel,
11
16
  SubagentActivityView,
12
17
  RunGuidanceView,
18
+ getPendingGuidance = () => [],
13
19
  RunResultActions,
14
20
  RunScrollLayout,
15
21
  RunFailureNotice,
22
+ I18n,
23
+ FailureReasons,
24
+ projectContextLimitEvent = value => value,
25
+ localizeVisibleReason = value => value,
26
+ localizeCompileReason = value => value,
16
27
  tr,
17
28
  tx,
18
29
  getLocale,
@@ -47,6 +58,7 @@
47
58
  trackedChangeInFlight,
48
59
  projectRunSettlement,
49
60
  getCurrentProjectId,
61
+ getAccountScopeId,
50
62
  getProjectName
51
63
  } = deps;
52
64
 
@@ -59,6 +71,7 @@
59
71
  // drives the floating "jump to latest" button's counter.
60
72
  let scrollLogRafId = 0;
61
73
  let scrollLogPendingForce = false;
74
+ let pendingGuidanceReveal = null;
62
75
  let jumpToLatestButton = null;
63
76
  let unreadSinceDetach = 0;
64
77
  // Elapsed time belongs to the run; tool rows retain their own lifecycle.
@@ -69,11 +82,25 @@
69
82
  onLayoutChange: scroller => logAutoFollow ? scrollLogToBottom() : updateJumpToLatestButton(scroller)
70
83
  });
71
84
  const failureNotice = RunFailureNotice?.create({ tr, projectRunSettlement, sanitizeText: sanitizeAssistantVisibleText });
85
+ const { buildRecoveryButton, appendRecoveryActionForFailure } = RunRecoveryActions.create({
86
+ document: typeof document === 'undefined' ? undefined : document,
87
+ window: typeof window === 'undefined' ? undefined : window,
88
+ tx, failureNotice, refillComposerForRetry, openProjectFileForFailure, showNativeSetupGuidance,
89
+ openStorageSettings, forceCancelStuckTaskForCurrentProject
90
+ });
72
91
  const presence = RunPresence?.create({ tx, getLocale, formatElapsed });
92
+ const getNotStartedRun = run => RunResultActions.projectNotStartedRun?.(run, getLocale());
93
+ const getFinishedRun = run => RunResultActions.projectFinishedRun?.(run, getLocale(), { projectRunSettlement });
73
94
  const displayRun = run => RunFailureNotice?.projectResolvedUndo?.(run) || run;
74
- const activitySummary = RunActivitySummary?.create({ RunActivityModel, SubagentActivityView, tx, findRunRecord: (...args) => displayRun(findRunRecord(...args)), getPanel, cssEscape,
95
+ const displayLocalizedRun = run => {
96
+ const displayed = displayRun(run);
97
+ return displayed ? { ...displayed, events: (displayed.events || []).map(sanitizeRunEventForRender) } : displayed;
98
+ };
99
+ const activitySummary = RunActivitySummary?.create({ RunActivityModel, RunGuidanceView, getPendingGuidance, SubagentActivityView, PanelIcons, tx, findRunRecord: (...args) => displayLocalizedRun(findRunRecord(...args)), getPanel, cssEscape,
100
+ projectReviewResultEvent: sanitizeRunEventForRender,
101
+ formatReviewResultFailure,
75
102
  formatEventTime, formatElapsed, renderMarkdownBlockText, renderRunEvent, formatEventDetail,
76
- presence, classifyStatus: classifyRunPresence, getProjectId: () => getCurrentProjectId?.() || '',
103
+ presence, getNotStartedRun, getFinishedRun, classifyStatus: classifyRunPresence, getProjectId: () => getCurrentProjectId?.() || '',
77
104
  summarizeCommand: RunPresence?.commandSummary,
78
105
  isGuidance: event => Boolean(RunGuidanceView.getGuidanceText(event)), sanitizeAssistantVisibleText });
79
106
 
@@ -82,6 +109,7 @@
82
109
  function resetAutoFollow() {
83
110
  logAutoFollow = true;
84
111
  userScrollIntentUntil = 0;
112
+ pendingGuidanceReveal = null;
85
113
  }
86
114
 
87
115
  function bindLogAutoFollow() {
@@ -156,7 +184,7 @@
156
184
  ? tx(`↓ Latest · ${unreadSinceDetach}`, `↓ 最新 · ${unreadSinceDetach}`)
157
185
  : tx('↓ Latest', '↓ 最新');
158
186
  button.setAttribute('aria-label', unreadSinceDetach > 0
159
- ? tx(`Jump to latest, ${unreadSinceDetach} new steps`, `跳到最新,${unreadSinceDetach} 个新步骤`)
187
+ ? tx(`Jump to latest, ${unreadSinceDetach} new step${unreadSinceDetach === 1 ? '' : 's'}`, `跳到最新,${unreadSinceDetach} 个新步骤`)
160
188
  : tx('Jump to latest', '跳到最新'));
161
189
  }
162
190
 
@@ -183,6 +211,7 @@
183
211
  if ((event?.type === 'wheel' && event.deltaY < 0) || ['ArrowUp', 'PageUp', 'Home'].includes(event?.key)) {
184
212
  logAutoFollow = false;
185
213
  scrollLogPendingForce = false;
214
+ pendingGuidanceReveal = null;
186
215
  }
187
216
  }
188
217
 
@@ -207,6 +236,7 @@
207
236
  userScrollIntentUntil = 0;
208
237
  unreadSinceDetach = 0;
209
238
  scrollLogPendingForce = true;
239
+ pendingGuidanceReveal = null;
210
240
  } else if (!logAutoFollow) {
211
241
  // The user has scrolled up: do not yank them. Just keep the jump button
212
242
  // state current.
@@ -241,10 +271,26 @@
241
271
  writeNow();
242
272
  });
243
273
  }
274
+ function revealGuidance(row) {
275
+ if (!logAutoFollow || activitySummary?.isSubagentViewOpen?.() || !getLogScrollContainer()?.contains?.(row)) return false;
276
+ pendingGuidanceReveal = row;
277
+ scrollLogToBottom();
278
+ return true;
279
+ }
244
280
  function setLogScrollPosition(scroller) {
281
+ const reveal = pendingGuidanceReveal;
282
+ pendingGuidanceReveal = null;
283
+ const result = reveal && scrollLayout?.revealGuidance(reveal);
284
+ if (result) {
285
+ logAutoFollow = result.following;
286
+ if (!logAutoFollow) scrollLayout.captureAnchor(true);
287
+ return;
288
+ }
245
289
  scroller.scrollTop = scroller.scrollHeight;
246
290
  }
247
291
  function collapseRunProcess(view, statusText) {
292
+ const record = view?.recordId ? displayLocalizedRun(findRunRecord(view.recordId, view.sessionId)) : null;
293
+ if (record && getFinishedRun(record)) refreshCompletionView(view.root, view.report, record);
248
294
  if (activitySummary) { activitySummary.settle(view, statusText); return; }
249
295
  const runProcess = view?.runProcess || view?.root?.querySelector('[data-run-process]');
250
296
  if (runProcess) {
@@ -257,11 +303,22 @@
257
303
  const record = view?.recordId ? findRunRecord(view.recordId, view.sessionId) : null;
258
304
  const stepCount = (record?.events || []).filter(event => (event.kind || 'activity') === 'activity').length;
259
305
  statusEl.textContent = stepCount > 0
260
- ? `${statusText} · ${tx(`${stepCount} steps`, `${stepCount} 步`)}`
306
+ ? `${statusText} · ${tx(`${stepCount} step${stepCount === 1 ? '' : 's'}`, `${stepCount} 步`)}`
261
307
  : statusText;
262
308
  }
263
309
  }
264
310
 
311
+ function refreshCompletionView(root, report, run) {
312
+ const event = (run.events || []).filter(item => item.kind === 'report').at(-1);
313
+ if (!root || !report || !event) return;
314
+ // Result actions may already live inside the previous report's facts row.
315
+ const actions = root.querySelector('.run-result-actions');
316
+ report.replaceChildren(renderCompletionReport(event, run));
317
+ if (actions) report.append(actions);
318
+ RunResultActions.configureResultActions(root, run);
319
+ configureEditButton(root, run);
320
+ }
321
+
265
322
  // Keep elapsed labels live without creating new activity records.
266
323
  function startRunElapsedTick() {
267
324
  stopRunElapsedTick();
@@ -357,7 +414,7 @@
357
414
  }
358
415
 
359
416
  function renderRunCard(run) {
360
- run = displayRun(run);
417
+ run = displayLocalizedRun(run);
361
418
  const root = document.createElement('section');
362
419
  root.className = 'transcript-turn run-card';
363
420
  root.dataset.status = run.status || 'completed';
@@ -391,7 +448,6 @@
391
448
  <div class="run-result-actions" hidden>
392
449
  <button type="button" class="run-copy-action" data-run-copy hidden></button>
393
450
  <button type="button" class="run-fork-action" data-run-fork hidden></button>
394
- <span class="run-completed-time" data-run-completed-time hidden></span>
395
451
  </div>
396
452
  <div class="run-report" data-run-report aria-live="polite" hidden></div>
397
453
  </div>
@@ -418,28 +474,15 @@
418
474
  truncated.textContent = tr('eventsTruncatedNote');
419
475
  events.append(truncated);
420
476
  }
421
- const guidanceTarget = guidance;
422
- const renderedGuidanceIds = new Set();
423
- const completedLegacyGuidance = new Set((run.events || [])
424
- .filter(event => event.kind === 'guidance' && !event.guidanceId && event.status === 'completed')
425
- .map(event => event.title));
477
+ if (!activitySummary) RunGuidanceView.reconcileGuidance?.(guidance, (run.events || []).concat(getPendingGuidance(run)), tx, { active: run.status === 'running' });
426
478
  for (const event of run.events || []) {
427
479
  if (event.kind === 'report') {
428
480
  report.hidden = false;
429
481
  report.replaceChildren(renderCompletionReport(event, run));
430
- } else if (activitySummary || event.kind === 'technical') {
482
+ } else if (getNotStartedRun(run) || activitySummary || event.kind === 'technical') {
431
483
  continue;
432
484
  } else if (RunGuidanceView.getGuidanceText(event)) {
433
- if (event.guidanceId && renderedGuidanceIds.has(event.guidanceId)) {
434
- continue;
435
- }
436
- if (!event.guidanceId && event.status === 'queued' && completedLegacyGuidance.has(event.title)) {
437
- continue;
438
- }
439
- if (event.guidanceId) {
440
- renderedGuidanceIds.add(event.guidanceId);
441
- }
442
- guidanceTarget.append(renderRunEvent(event));
485
+ continue;
443
486
  } else if (event.kind === 'stream') {
444
487
  upsertStreamEvent({ events }, event);
445
488
  } else {
@@ -457,7 +500,19 @@
457
500
  }
458
501
 
459
502
  function getRunStatusText(run = {}) {
503
+ const notStarted = getNotStartedRun(run);
504
+ if (notStarted) return notStarted.status;
505
+ const outcome = getFinishedRun(run);
506
+ if (outcome?.status) return outcome.status;
460
507
  if (run.retryingWriteback) return tx('Checking file sync', '正在检查文件同步');
508
+ if (['Stopped tracking after a page refresh', '页面刷新后已停止跟踪'].includes(run.statusText)) return tr('restoredRunStoppedStatus');
509
+ const knownStatus = /^(?:Done|Failed|Cancelled|Running|已处理|处理失败|已取消|处理中)(?:\s+[\d.]+(?:ms|s|m|h|秒|分钟)(?:\s+[\d.]+(?:ms|s|m|h|秒|分钟))*)?$/.test(run.statusText || '');
510
+ if (knownStatus) {
511
+ const elapsed = String(run.statusText).replace(/^(?:Done|Failed|Cancelled|Running|已处理|处理失败|已取消|处理中)\s*/, '');
512
+ const key = run.status === 'running' ? 'processing' : run.status === 'failed' ? 'processedFailed'
513
+ : ['rejected', 'cancelled'].includes(run.status) ? 'processedCancelled' : 'processed';
514
+ return tr(key, { elapsed }).trim();
515
+ }
461
516
  if (run.statusText) {
462
517
  return run.statusText;
463
518
  }
@@ -532,20 +587,64 @@
532
587
  }
533
588
 
534
589
  function sanitizeRunEventForRender(event = {}) {
590
+ event = projectContextLimitEvent(event);
535
591
  const streamRole = sanitizeAssistantVisibleText(event.streamRole);
536
592
  const title = sanitizeAssistantVisibleText(event.title);
593
+ const activity = !streamRole && (!event.kind || event.kind === 'activity') && !event.guidanceId;
594
+ const localized = activity ? localizeProductActivity(title, sanitizeAssistantVisibleValue(event.detail)) : null;
537
595
  return {
538
596
  ...event,
597
+ ...(activity && event.activity?.kind === 'compile' ? { activity: { ...event.activity, output: localizeCompileReason(event.activity.output) } } : {}),
539
598
  title: streamRole === 'reasoning'
540
599
  ? stripEmptyHtmlCommentPlaceholders(title)
541
- : title,
600
+ : localized?.title || title,
542
601
  status: sanitizeAssistantVisibleText(event.status),
543
- detail: sanitizeAssistantVisibleValue(event.detail),
602
+ detail: localized?.detail ?? sanitizeAssistantVisibleValue(event.detail),
544
603
  technicalDetail: sanitizeAssistantVisibleValue(event.technicalDetail),
545
604
  streamKey: sanitizeAssistantVisibleText(event.streamKey),
546
605
  streamRole
547
606
  };
548
607
  }
608
+
609
+ function localizeProductActivity(title, detail) {
610
+ let known = false, nextTitle = title;
611
+ const keys = ['undoUnsafeFullFileTitle', 'undoNoTraceTitle', 'undoFileSelectTitle', 'undoNoTraceStarted', 'undoTrackedTitle', 'undoNativeTitle', 'undoTrackedStarted', 'undoNativeStarted', 'undoSwitchedEditing', 'undoReviewingRestoreUnverified', 'runAcceptTrackedStarted', 'runAcceptTrackedFailed', 'undoResult', 'undoTrackedResult', 'undoNativeResult', 'runAcceptTrackedResult'];
612
+ if (I18n?.t) outer: for (const key of keys) for (const locale of ['en', 'zh']) {
613
+ const template = I18n.t(locale, key, { applied: '__APPLIED__', skipped: '__SKIPPED__' });
614
+ const pattern = template.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('__APPLIED__', '(\\d+)').replace('__SKIPPED__', '(\\d+)');
615
+ const match = new RegExp('^' + pattern + '$').exec(title);
616
+ if (match) { known = true; nextTitle = tr(key, { applied: match[1], skipped: match[2] }); break outer; }
617
+ }
618
+ let countedTitle;
619
+ const writeResult = /^(?:Write result: wrote (\d+) item\(s\), skipped (\d+)|写入结果:已写入 (\d+) 项,跳过 (\d+) 项)$/.exec(title);
620
+ const governance = /^(?:Project governance blocked (\d+) write\(s\) before review\.|项目治理规则在审核前阻止了 (\d+) 项写入。)$/.exec(title);
621
+ if (writeResult) {
622
+ const applied = writeResult[1] ?? writeResult[3], skipped = writeResult[2] ?? writeResult[4];
623
+ countedTitle = tx(`Write result: wrote ${applied} item(s), skipped ${skipped}`, `写入结果:已写入 ${applied} 项,跳过 ${skipped} 项`);
624
+ } else if (governance) {
625
+ const count = governance[1] ?? governance[2];
626
+ countedTitle = tx(`Project governance blocked ${count} write(s) before review.`, `项目治理规则在审核前阻止了 ${count} 项写入。`);
627
+ }
628
+ const labels = ['detailAccepted', 'detailRejected', 'detailUndone', 'detailSkipped', 'detailFile', 'detailRecord', 'detailReason'];
629
+ const project = (value, reason = false) => {
630
+ if (typeof value === 'string') return reason ? localizeVisibleReason(value) : value;
631
+ if (Array.isArray(value)) return value.map(item => project(item, reason));
632
+ if (!value || typeof value !== 'object') return value;
633
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => {
634
+ const label = labels.find(id => ['en', 'zh'].some(locale => I18n?.t?.(locale, id) === key));
635
+ return [label ? tr(label) : key, project(child, label === 'detailReason')];
636
+ }));
637
+ };
638
+ return { title: countedTitle || (known ? nextTitle : localizeCompileReason(localizeVisibleReason(title))),
639
+ detail: typeof detail === 'string' ? localizeVisibleReason(detail) : known ? project(detail) : detail };
640
+ }
641
+ function formatReviewResultFailure(failure) {
642
+ if (failure?.code === 'unknown_legacy_failure' && (!failure.evidence?.originalCode || failure.evidence.originalCode === 'unknown_legacy_failure')
643
+ && ['Unknown reason', '未知原因'].includes(failure.userMessage)) {
644
+ failure = { ...failure, userMessage: tr('unknownReason') };
645
+ }
646
+ return FailureReasons?.formatFailureReasonForDisplay(failure, getLocale?.(), key => { const value = tr(key, failure?.file ? undefined : { file: tr('unknownFile'), activeFile: tr('unknownFile') }); return value && value !== key ? value : undefined; });
647
+ }
549
648
  function renderActivityLine(input) {
550
649
  const event = sanitizeRunEventForRender(input);
551
650
  const row = document.createElement('div');
@@ -586,27 +685,48 @@
586
685
  // The result presenter owns parsing and current lifecycle metadata.
587
686
  const splitFlatCompletionReport = text => RunResultActions.splitFlatCompletionReport(text, tx);
588
687
  const projectCompletionMeta = (meta, run) => RunResultActions.projectCompletionMeta(meta, run, {
589
- tx, trackedChangeInFlight, isTrackedChangeLifecycleRun, projectRunSettlement
688
+ tx, getLocale, tr, trackedChangeInFlight, isTrackedChangeLifecycleRun, projectRunSettlement,
689
+ projectId: getCurrentProjectId?.(), accountScopeId: getAccountScopeId?.()
590
690
  });
591
691
 
592
692
  // The user's own open/closed choice per run survives card re-renders.
593
693
  const metaOpenChoices = new Map();
594
694
 
695
+ // The completion time lives with the other run facts, not in the action row.
696
+ // `classed` matches lists whose rows carry the meta label/value classes.
697
+ function appendFinishedRow(list, run, { classed = false } = {}) {
698
+ const finishedAt = new Date(String(run?.finishedAt || ''));
699
+ if (!Number.isFinite(finishedAt.getTime())) return;
700
+ const dt = Object.assign(document.createElement('dt'), { textContent: tx('Finished', '完成于') });
701
+ dt.dataset.metaKey = 'finishedAt';
702
+ const dd = Object.assign(document.createElement('dd'), {
703
+ textContent: new Intl.DateTimeFormat(getLocale(), { hour: 'numeric', minute: '2-digit' }).format(finishedAt),
704
+ title: finishedAt.toLocaleString(getLocale()) });
705
+ if (classed) {
706
+ dt.className = 'run-final-answer__meta-label';
707
+ dd.className = 'run-final-answer__meta-value';
708
+ }
709
+ list.append(dt, dd);
710
+ }
711
+
595
712
  // Renders the run-metadata rows (Why nothing changed / Write result / Undo /
596
713
  // Next) behind a one-line summary beneath the answer. Shared by the
597
714
  // structured and flat-fallback render paths so both demote identically.
598
- function appendCompletionMetaBlock(report, meta, run) {
715
+ function appendCompletionMetaBlock(report, meta, run, { quiet = false } = {}) {
599
716
  if (!Array.isArray(meta) || !meta.length) {
600
717
  return;
601
718
  }
602
- const rows = projectCompletionMeta(meta, run).filter(row => row?.label && row.value)
719
+ const noWrites = getFinishedRun(run)?.noWrites;
720
+ const rows = projectCompletionMeta(meta, run).filter(row => row?.label && row.value
721
+ && !(noWrites && ['writeResult', 'undo'].includes(row.key)))
603
722
  .map(row => ({ ...row, shown: failureNotice?.formatMetaValue(report, row) ?? row.value }));
604
723
  if (!rows.length) return;
605
724
  const summary = RunResultActions.summarizeCompletionMeta(rows, run, {
606
- tx, failed: report.dataset.status === 'failed', unconfirmed: rows.some(row => row.shown !== row.value)
725
+ tx, failed: report.dataset.status === 'failed' && !noWrites, unconfirmed: rows.some(row => row.shown !== row.value)
607
726
  });
608
727
  const metaBlock = document.createElement('dl');
609
728
  metaBlock.className = 'run-final-answer__meta';
729
+ appendFinishedRow(metaBlock, run, { classed: true });
610
730
  metaBlock.id = `codex-run-meta-${String(run?.id || 'report').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
611
731
  for (const row of rows) {
612
732
  const dt = document.createElement('dt');
@@ -619,6 +739,12 @@
619
739
  if (summary.warnKeys.has(row.key)) dt.dataset.tone = dd.dataset.tone = 'warn';
620
740
  metaBlock.append(dt, dd);
621
741
  }
742
+ if (quiet) {
743
+ const details = createOutcomeDetails(run);
744
+ details.querySelector('.run-outcome-details-content').append(metaBlock);
745
+ report.append(details);
746
+ return;
747
+ }
622
748
  const facts = document.createElement('div');
623
749
  facts.className = 'run-final-answer__facts';
624
750
  const toggle = document.createElement('button');
@@ -633,12 +759,18 @@
633
759
  toggle.append(item);
634
760
  });
635
761
  const more = Object.assign(document.createElement('span'), { className: 'run-final-answer__facts-more' });
636
- toggle.append(Object.assign(document.createElement('span'), { className: 'run-final-answer__facts-sep', textContent: '·' }), more);
762
+ // The chevron stays with the last fact, so a narrow row never leaves it alone on a line.
763
+ const tail = Object.assign(document.createElement('span'), { className: 'run-final-answer__facts-tail' });
764
+ if (toggle.lastElementChild) tail.append(toggle.lastElementChild);
765
+ tail.append(more);
766
+ toggle.append(tail);
637
767
  const setOpen = open => {
638
768
  facts.dataset.open = String(open);
639
769
  metaBlock.hidden = !open;
640
770
  toggle.setAttribute('aria-expanded', String(open));
641
- more.innerHTML = `${open ? tx('Hide', '收起') : tx('Details', '详情')}<svg class="codex-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m6 4 4 4-4 4"/></svg>`;
771
+ // The facts are the toggle; the chevron and the tooltip carry its state.
772
+ toggle.title = open ? tx('Hide details', '收起详情') : tx('Show details', '展开详情');
773
+ more.innerHTML = `<svg class="codex-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m6 4 4 4-4 4"/></svg>`;
642
774
  };
643
775
  setOpen(metaOpenChoices.get(run?.id) ?? summary.open);
644
776
  toggle.addEventListener('click', event => {
@@ -651,11 +783,130 @@
651
783
  report.append(facts);
652
784
  }
653
785
 
786
+ function createOutcomeDetails(run) {
787
+ const details = document.createElement('details');
788
+ details.className = 'run-outcome-details';
789
+ details.open = metaOpenChoices.get(run?.id) ?? false;
790
+ details.append(Object.assign(document.createElement('summary'), { textContent: tr('runOutcomeDetails') }));
791
+ details.append(Object.assign(document.createElement('div'), { className: 'run-outcome-details-content' }));
792
+ details.addEventListener('toggle', () => { if (run?.id) metaOpenChoices.set(run.id, details.open); });
793
+ return details;
794
+ }
795
+
796
+ function renderOutcomeCard(report, event, run, outcome) {
797
+ const card = document.createElement('div');
798
+ card.className = 'run-outcome';
799
+ card.dataset.outcome = outcome.kind;
800
+ const main = document.createElement('div');
801
+ main.className = 'run-final-answer';
802
+ const title = document.createElement('div');
803
+ title.className = 'run-final-heading';
804
+ title.textContent = sanitizeAssistantVisibleText(outcome.title);
805
+ main.append(title);
806
+ if (outcome.body) main.append(Object.assign(document.createElement('p'), {
807
+ className: 'run-outcome-body', textContent: sanitizeAssistantVisibleText(outcome.body)
808
+ }));
809
+ if (outcome.files.length) {
810
+ const files = document.createElement('ul');
811
+ files.className = 'run-outcome-files';
812
+ for (const item of outcome.files) {
813
+ const row = document.createElement('li');
814
+ const mark = document.createElement('span');
815
+ mark.className = 'run-outcome-file-icon';
816
+ mark.dataset.written = String(item.written);
817
+ mark.innerHTML = RunPresence.icon(item.written ? 'written' : 'skipped');
818
+ mark.setAttribute('aria-label', tr(item.written ? 'runOutcomeWritten' : 'runOutcomeSkipped'));
819
+ row.append(mark, Object.assign(document.createElement('span'), {
820
+ className: 'run-outcome-file-name', textContent: sanitizeAssistantVisibleText(item.file)
821
+ }));
822
+ if (item.reason) row.append(Object.assign(document.createElement('span'), {
823
+ className: 'run-outcome-file-reason', textContent: sanitizeAssistantVisibleText(item.reason)
824
+ }));
825
+ files.append(row);
826
+ }
827
+ main.append(files);
828
+ }
829
+ const next = document.createElement('dl');
830
+ next.className = 'run-outcome-next';
831
+ next.append(Object.assign(document.createElement('dt'), { textContent: tr('detailNext') }),
832
+ Object.assign(document.createElement('dd'), { textContent: sanitizeAssistantVisibleText(outcome.nextStep) }));
833
+ card.append(main, next);
834
+ report.append(card);
835
+ failureNotice?.append(report, event, run, card);
836
+ appendRecoveryActionForFailure(report, event, run);
837
+
838
+ const details = createOutcomeDetails(run);
839
+ const rows = projectCompletionMeta(event.detailStructured.meta || [], run)
840
+ .filter(row => row?.value && row.key !== 'nextStep'
841
+ && !(outcome.noWrites && ['writeResult', 'undo'].includes(row.key)));
842
+ if (outcome.failure?.code) rows.unshift({ label: tr('runOutcomeCode'), value: outcome.failure.code });
843
+ if (outcome.failure?.stage) rows.push({ label: tr('runOutcomeStage'), value: outcome.failure.stage });
844
+ if (outcome.noWrites) rows.push({ label: tr('runOutcomeWrites'), value: tr('runOutcomeNoneAttempted') });
845
+ const meta = document.createElement('dl');
846
+ meta.className = 'run-final-answer__meta';
847
+ appendFinishedRow(meta, run);
848
+ for (const row of rows) {
849
+ const label = Object.assign(document.createElement('dt'), { textContent: row.label });
850
+ if (row.key) label.dataset.metaKey = row.key;
851
+ meta.append(label, Object.assign(document.createElement('dd'), {
852
+ textContent: sanitizeAssistantVisibleText(failureNotice?.formatMetaValue(report, row) ?? row.value)
853
+ }));
854
+ }
855
+ const detailContent = details.querySelector('.run-outcome-details-content');
856
+ detailContent.append(meta);
857
+ // Preserve the original report body as optional context without repeating
858
+ // its skipped-item prose in the default outcome view.
859
+ if (event.detailStructured.body) {
860
+ const body = document.createElement('div');
861
+ renderMarkdownBlockText(body, RunResultActions.projectCompletionBody(event.detailStructured.body, tx));
862
+ detailContent.append(body);
863
+ }
864
+ card.append(details);
865
+ }
866
+
867
+ function formatCompileErrorNames(errors, totalCount) {
868
+ const names = [...new Set(errors.map(error => String(error)
869
+ .trim().replace(/^!\s*/, '').replace(/^(?:[A-Za-z]:)?[^:\r\n]+:\d+:\s*/, '').replace(/^!\s*/, '')
870
+ .split(/[ \t]+l\.\d+\b|[\r\n]/, 1)[0]
871
+ .trim().replace(/\.$/, '').replace(/\s+/g, ' ').trim()).filter(Boolean))];
872
+ const shown = names.slice(0, 3);
873
+ // History may retain fewer diagnostics than the original count. Preserve
874
+ // that omitted count without inventing names or changing the repair draft.
875
+ const more = Math.max(0, names.length - shown.length) + Math.max(0, totalCount - errors.length);
876
+ if (more) shown.push(tx(`+${more} more`, `另 ${more} 个`));
877
+ return shown.join(tx('; ', ';'));
878
+ }
879
+
654
880
  function renderCompletionReport(input, run) {
655
881
  const event = sanitizeRunEventForRender(input);
656
882
  const report = document.createElement('section');
657
883
  report.className = 'run-completion-report';
658
884
  report.dataset.status = event.status || 'completed';
885
+ const notStarted = getNotStartedRun(run);
886
+ if (notStarted) {
887
+ report.dataset.status = 'not-started';
888
+ const card = document.createElement('div');
889
+ card.className = 'run-outcome';
890
+ const main = document.createElement('div');
891
+ main.className = 'run-final-answer';
892
+ const title = document.createElement('div');
893
+ title.className = 'run-final-heading';
894
+ title.textContent = notStarted.title;
895
+ const body = document.createElement('p');
896
+ body.className = 'run-outcome-body';
897
+ body.textContent = notStarted.body;
898
+ main.append(title, body);
899
+ const next = document.createElement('dl');
900
+ next.className = 'run-outcome-next';
901
+ next.append(Object.assign(document.createElement('dt'), {
902
+ textContent: tr('detailNext')
903
+ }), Object.assign(document.createElement('dd'), {
904
+ textContent: notStarted.nextStep
905
+ }));
906
+ card.append(main, next);
907
+ report.append(card);
908
+ return report;
909
+ }
659
910
  // Compile errors captured post-write get a one-click fix loop: the
660
911
  // structured error list rides on the report event (compileErrors).
661
912
  const appendCompileFix = target => {
@@ -664,7 +915,7 @@
664
915
  return;
665
916
  }
666
917
  const fix = buildRecoveryButton('compile_errors',
667
- tx(`Fix ${compileErrors.length} compile error(s)`, `修复 ${compileErrors.length} 个编译错误`),
918
+ tx(`Fix ${compileErrors.length} compile error${compileErrors.length === 1 ? '' : 's'}`, `修复 ${compileErrors.length} 个编译错误`),
668
919
  tx('Prefills a task with @compile-log and the captured errors.', '自动组一条带 @compile-log 和错误列表的任务。'));
669
920
  fix.addEventListener('click', clickEvent => {
670
921
  clickEvent.stopPropagation();
@@ -702,6 +953,13 @@
702
953
  // visually demoted block beneath, with a separator and muted color so
703
954
  // they read as run metadata rather than part of the answer.
704
955
  const structured = event.detailStructured;
956
+ const outcome = getFinishedRun(run);
957
+ if (outcome && !['cancelled', 'compile-errors'].includes(outcome.kind)) {
958
+ renderOutcomeCard(report, event, run, outcome);
959
+ appendCompileFix(report);
960
+ appendRejectedRedo(report);
961
+ return report;
962
+ }
705
963
  const hasStructured = structured
706
964
  && (structured.conclusion || structured.body || (Array.isArray(structured.meta) && structured.meta.length));
707
965
  if (hasStructured) {
@@ -709,10 +967,11 @@
709
967
  main.className = 'run-final-answer';
710
968
  const mainSections = [];
711
969
  if (structured.conclusion) {
712
- mainSections.push(String(structured.conclusion));
970
+ mainSections.push(outcome?.kind === 'compile-errors' ? outcome.conclusion
971
+ : RunResultActions.projectCompletionConclusion?.(event, run, tx('en', 'zh'), { localizeCompileReason }) ?? String(structured.conclusion));
713
972
  }
714
973
  if (structured.body) {
715
- mainSections.push(structured.body);
974
+ mainSections.push(RunResultActions.projectCompletionBody?.(structured.body, tx) ?? structured.body);
716
975
  }
717
976
  if (mainSections.length) {
718
977
  renderMarkdownBlockText(main, mainSections.join('\n\n'));
@@ -721,8 +980,20 @@
721
980
  }
722
981
 
723
982
  failureNotice?.append(report, event, run);
724
- appendCompletionMetaBlock(report, structured.meta, run);
725
- appendCompileFix(report);
983
+ if (outcome?.kind === 'compile-errors') {
984
+ const compile = document.createElement('div');
985
+ compile.className = 'run-outcome run-outcome-compile';
986
+ const capturedErrors = Array.isArray(event.compileErrors) ? event.compileErrors.filter(Boolean) : [];
987
+ compile.append(Object.assign(document.createElement('strong'), { className: 'run-final-heading', textContent: outcome.title }),
988
+ Object.assign(document.createElement('p'), { className: 'run-outcome-body',
989
+ textContent: formatCompileErrorNames(capturedErrors.length ? capturedErrors : outcome.compileErrors, outcome.compileCount) }));
990
+ appendCompileFix(compile);
991
+ report.append(compile);
992
+ appendCompletionMetaBlock(report, structured.meta, run, { quiet: true });
993
+ } else {
994
+ appendCompletionMetaBlock(report, structured.meta, run);
995
+ appendCompileFix(report);
996
+ }
726
997
  appendRejectedRedo(report);
727
998
  appendRecoveryActionForFailure(report, event, run);
728
999
  return report;
@@ -737,7 +1008,8 @@
737
1008
  const split = splitFlatCompletionReport(flatText);
738
1009
  const body = document.createElement('div');
739
1010
  body.className = 'run-final-answer';
740
- renderMarkdownBlockText(body, split.body || flatText);
1011
+ renderMarkdownBlockText(body, RunResultActions.projectCompletionConclusion?.(event, run, tx('en', 'zh'),
1012
+ { flat: true, text: split.body || flatText, localizeCompileReason }) ?? (split.body || flatText));
741
1013
  decorateCompletionReport(body, { tx });
742
1014
  report.append(body);
743
1015
  appendCompileFix(report);
@@ -755,132 +1027,6 @@
755
1027
  // the normal cancel button is hidden because currentRunView is null).
756
1028
  // Recovery-action registry: failure codes whose "next step" is executable
757
1029
  // from the panel get a button, not prose. One primary action per failure.
758
- const RETRYABLE_FAILURE_CODES = new Set([
759
- 'native_request_failed', 'codex_timeout', 'codex_no_usable_result',
760
- 'codex_output_limit', 'stale_source_changed', 'patch_anchor_not_found',
761
- 'target_editor_not_ready', 'write_timeout', 'partial_write_needs_review',
762
- 'write_operation_failed'
763
- ]);
764
- const NATIVE_SETUP_FAILURE_CODES = new Set([
765
- 'native_bridge_unavailable', 'native_protocol_incompatible'
766
- ]);
767
- const OPEN_FILE_FAILURE_CODES = new Set([
768
- 'target_file_not_active', 'target_file_open_failed', 'target_file_not_found'
769
- ]);
770
- const STORAGE_FAILURE_CODES = new Set([
771
- 'storage_quota_exceeded', 'run_state_persist_failed'
772
- ]);
773
- // A missing `codex` binary is fixed in the terminal, not the panel — the
774
- // action opens the README troubleshooting section (PATH / login checks).
775
- const CODEX_INSTALL_FAILURE_CODES = new Set(['codex_not_found']);
776
- const CODEX_CLI_TROUBLESHOOTING_URL = 'https://github.com/Ghqqqq/codex-overleaf-link#faq-and-troubleshooting';
777
-
778
- function buildRecoveryButton(failureCode, label, title) {
779
- const button = document.createElement('button');
780
- button.type = 'button';
781
- button.className = 'run-final-answer__recovery-action';
782
- button.dataset.recoveryFor = failureCode;
783
- button.textContent = label;
784
- if (title) {
785
- button.title = title;
786
- }
787
- return button;
788
- }
789
-
790
- function appendEditAndResend(report, run, actionKey) {
791
- const retry = buildRecoveryButton(actionKey,
792
- tx('Edit & resend', '编辑后重发'),
793
- tx('Put this run\u2019s task back into the composer so you can adjust and resend it.', '把本轮任务填回输入框,修改后可直接重发。'));
794
- retry.addEventListener('click', clickEvent => {
795
- clickEvent.stopPropagation();
796
- refillComposerForRetry(run || null);
797
- });
798
- report.append(retry);
799
- }
800
-
801
- function appendRecoveryActionForFailure(report, event, run) {
802
- const recovery = failureNotice?.prepareRecovery(report, { openFile: openProjectFileForFailure });
803
- const failureCode = event?.failure?.code || '';
804
- if (recovery?.handled) return;
805
- report = recovery?.target || report;
806
- if (RETRYABLE_FAILURE_CODES.has(failureCode) && typeof refillComposerForRetry === 'function') {
807
- appendEditAndResend(report, run, failureCode);
808
- return;
809
- }
810
- if (NATIVE_SETUP_FAILURE_CODES.has(failureCode) && typeof showNativeSetupGuidance === 'function') {
811
- const fix = buildRecoveryButton(failureCode,
812
- tx('Fix the native host', '修复 native host'),
813
- tx('Opens the install/update guidance with the copyable command.', '打开安装/更新指引(含可复制命令)。'));
814
- fix.addEventListener('click', clickEvent => {
815
- clickEvent.stopPropagation();
816
- showNativeSetupGuidance();
817
- });
818
- report.append(fix);
819
- return;
820
- }
821
- if (OPEN_FILE_FAILURE_CODES.has(failureCode) && event?.failure?.file && typeof openProjectFileForFailure === 'function') {
822
- const open = buildRecoveryButton(failureCode,
823
- tx(`Open ${event.failure.file} in Overleaf`, `在 Overleaf 打开 ${event.failure.file}`), '');
824
- open.addEventListener('click', clickEvent => {
825
- clickEvent.stopPropagation();
826
- openProjectFileForFailure(event.failure.file);
827
- });
828
- report.append(open);
829
- return;
830
- }
831
- if (STORAGE_FAILURE_CODES.has(failureCode) && typeof openStorageSettings === 'function') {
832
- const clean = buildRecoveryButton(failureCode,
833
- tx('Open history & storage cleanup', '打开历史与存储清理'), '');
834
- clean.addEventListener('click', clickEvent => {
835
- clickEvent.stopPropagation();
836
- openStorageSettings();
837
- });
838
- report.append(clean);
839
- return;
840
- }
841
- if (CODEX_INSTALL_FAILURE_CODES.has(failureCode)) {
842
- const guide = buildRecoveryButton(failureCode,
843
- tx('Codex CLI troubleshooting guide', '查看 Codex CLI 排查指引'),
844
- tx('Opens the README section on fixing a missing `codex` command (PATH / login).', '打开 README 中「找不到 codex 命令」的排查说明(PATH/登录)。'));
845
- guide.addEventListener('click', clickEvent => {
846
- clickEvent.stopPropagation();
847
- window.open(CODEX_CLI_TROUBLESHOOTING_URL, '_blank', 'noopener');
848
- });
849
- report.append(guide);
850
- return;
851
- }
852
- if (failureCode !== 'codex_project_locked') return;
853
- const button = document.createElement('button');
854
- button.type = 'button';
855
- button.className = 'run-final-answer__recovery-action';
856
- button.dataset.recoveryFor = failureCode;
857
- button.textContent = tx('Force-release the stuck task', '强制释放卡住的任务');
858
- button.title = tx(
859
- 'Sends a force-cancel to the native host that drops the project lock so a new run can start. Use this when refreshing the tab did not free the project.',
860
- '向 native host 发送强制取消请求,释放当前 Overleaf 项目的占用,让新的任务可以启动。刷新页面没用时使用。'
861
- );
862
- button.addEventListener('click', event => {
863
- event.stopPropagation();
864
- button.disabled = true;
865
- button.textContent = tx('Releasing…', '正在释放…');
866
- forceCancelStuckTaskForCurrentProject().then(result => {
867
- if (result?.ok) {
868
- button.textContent = tx('Force-released — you can retry the run.', '已释放,可以重试本轮任务。');
869
- return;
870
- }
871
- // The helper resolves (never throws) with a non-ok result when the
872
- // request was not delivered. Report the real outcome instead of a
873
- // blanket success, and re-enable the button so the user can retry.
874
- button.disabled = false;
875
- button.textContent = tx('Release failed — refresh the tab, then try again.', '释放失败,请刷新页面后重试。');
876
- }).catch(() => {
877
- button.disabled = false;
878
- button.textContent = tx('Release failed — refresh the tab, then try again.', '释放失败,请刷新页面后重试。');
879
- });
880
- });
881
- report.append(button);
882
- }
883
-
884
1030
  // A run belongs to the tracked-change lifecycle iff trackedChangeStatus is set
885
1031
  // or it still has tracked-change refs. Every other run is a legacy-undo run.
886
1032
  function isTrackedChangeLifecycleRun(run) {
@@ -902,7 +1048,8 @@
902
1048
  && Boolean(String(run?.task || '').trim())
903
1049
  && typeof canEditRun === 'function' && canEditRun(run)
904
1050
  && typeof refillComposerForRetry === 'function';
905
- button.hidden = !available;
1051
+ const cardHasEdit = Boolean(root.querySelector('.run-outcome [data-recovery-action="edit-resend"]'));
1052
+ button.hidden = !available || cardHasEdit;
906
1053
  if (!available) return;
907
1054
  button.disabled = typeof options.running === 'boolean'
908
1055
  ? options.running : Boolean(getCurrentRunView?.());
@@ -950,19 +1097,24 @@
950
1097
  }
951
1098
 
952
1099
  const projection = RunResultActions.projectUndoAvailability(run, projectRunSettlement);
953
- if (!projection.canUndo && run.undoStatus !== 'applied') {
1100
+ if (!projection.canUndo && run.undoStatus !== 'applied' && !projection.undoUnavailable?.length) {
954
1101
  button.hidden = true;
955
1102
  return;
956
1103
  }
957
1104
 
958
1105
  button.hidden = false;
959
- button.disabled = run.undoStatus === 'running' || run.undoStatus === 'applied';
960
- button.textContent = run.undoStatus === 'applied'
1106
+ const inFlight = trackedChangeInFlight?.get(run.id) === 'reject';
1107
+ const applied = run.undoStatus === 'applied' && !inFlight;
1108
+ button.disabled = !projection.canUndo || inFlight || (run.undoStatus === 'running' && !run.undoRecoveryCheckRequired) || run.undoStatus === 'applied';
1109
+ button.textContent = applied
961
1110
  ? tr('undoApplied')
962
1111
  : (run.partialWriteback ? tr('undoPartialRun') : tr('undoRun'));
963
- button.title = run.undoStatus === 'applied'
1112
+ button.title = applied
964
1113
  ? tr('undoAppliedTitle')
965
1114
  : (run.partialWriteback ? tr('undoPartialRunTitle') : tr('undoRunTitle'));
1115
+ if (run.undoRecoveryUnavailable) button.title = tx('Undo recovery checks are unavailable. Reload this page after the connection recovers.', '撤销恢复检查暂不可用,请在连接恢复后重新加载此页面。');
1116
+ if (projection.undoUnavailable?.length) button.title = tr('failureReason_snapshot_undo_unavailable_user')
1117
+ + ' ' + projection.undoUnavailable.map(item => item.path).join(', ');
966
1118
  button.addEventListener('click', event => {
967
1119
  event.stopPropagation();
968
1120
  undoRun(run.id);
@@ -979,6 +1131,9 @@
979
1131
  const status = run.trackedChangeStatus || '';
980
1132
  const inFlight = trackedChangeInFlight.get(run.id);
981
1133
  const projection = projectRunSettlement(run);
1134
+ const undoTitle = projection.undoUnavailable?.length
1135
+ ? tr('failureReason_snapshot_undo_unavailable_user') + ' ' + projection.undoUnavailable.map(item => item.path).join(', ')
1136
+ : tr('undoRunTitle');
982
1137
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
983
1138
  // actionable. Branch placed before the terminal branches so a
984
1139
  // needs_review run is never treated as terminal.
@@ -986,7 +1141,7 @@
986
1141
  button.hidden = false;
987
1142
  button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
988
1143
  button.textContent = tr('undoRun');
989
- button.title = tr('undoRunTitle');
1144
+ button.title = undoTitle;
990
1145
  button.addEventListener('click', event => {
991
1146
  event.stopPropagation();
992
1147
  undoRun(run.id);
@@ -1006,7 +1161,7 @@
1006
1161
  button.hidden = false;
1007
1162
  button.disabled = true;
1008
1163
  button.textContent = tr('undoRun');
1009
- button.title = tr('undoRunTitle');
1164
+ button.title = tr('undoAcceptedTitle');
1010
1165
  return;
1011
1166
  }
1012
1167
  if (status !== 'pending' && !inFlight) {
@@ -1016,7 +1171,7 @@
1016
1171
  button.hidden = false;
1017
1172
  button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
1018
1173
  button.textContent = tr('undoRun');
1019
- button.title = tr('undoRunTitle');
1174
+ button.title = undoTitle;
1020
1175
  button.addEventListener('click', event => {
1021
1176
  event.stopPropagation();
1022
1177
  undoRun(run.id);
@@ -1085,7 +1240,7 @@
1085
1240
  button.hidden = false;
1086
1241
  button.disabled = true;
1087
1242
  button.textContent = tr('runAcceptTracked');
1088
- button.title = tr('runAcceptTrackedTitle');
1243
+ button.title = tr('undoAppliedTitle');
1089
1244
  return;
1090
1245
  }
1091
1246
  if (status !== 'pending' && !inFlight) {
@@ -1146,7 +1301,7 @@
1146
1301
  const log = getPanel()?.querySelector('[data-log]');
1147
1302
  const existing = log?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
1148
1303
  const run = findRunRecord(runId);
1149
- if (!log || !existing || !run || displayRun(run) === run) {
1304
+ if (!log || !existing || !run || (displayRun(run) === run && !getFinishedRun(run))) {
1150
1305
  return false;
1151
1306
  }
1152
1307
  const reading = scrollLayout?.snapshot(), open = existing.querySelector('[data-run-process]')?.open;
@@ -1158,7 +1313,43 @@
1158
1313
  return true;
1159
1314
  }
1160
1315
 
1316
+ function refreshLocale() {
1317
+ const log = getPanel()?.querySelector('[data-log]');
1318
+ if (!log) return;
1319
+ if (!getState().runs?.length) { renderRunHistory({ preserveScroll: true }); return; }
1320
+ const reading = scrollLayout?.snapshot() || { scrollTop: log.scrollTop };
1321
+ for (const run of getState().runs) {
1322
+ const existing = log.querySelector(`[data-run-id="${cssEscape(run.id)}"]`);
1323
+ if (!existing) continue;
1324
+ const current = getCurrentRunView();
1325
+ if (current?.root === existing) {
1326
+ // Streaming owners retain their DOM references and buffered model text.
1327
+ const status = existing.querySelector('[data-run-status]');
1328
+ if (status) status.textContent = getRunStatusText(run);
1329
+ configureAcceptButton(existing, run); configureUndoButton(existing, run);
1330
+ configureEditButton(existing, run); configureWritebackButton(existing, run);
1331
+ RunResultActions.configureResultActions(existing, run);
1332
+ const reportEvent = (run.events || []).filter(event => event.kind === 'report').at(-1);
1333
+ const report = current.report || existing.querySelector('[data-run-report]');
1334
+ if (reportEvent && report) refreshCompletionView(existing, report, displayLocalizedRun(run));
1335
+ activitySummary?.update(current);
1336
+ if (!activitySummary) RunGuidanceView.reconcileGuidance?.(existing.querySelector('[data-run-guidance]'),
1337
+ (run.events || []).concat(getPendingGuidance(run)), tx, { active: run.status === 'running' });
1338
+ continue;
1339
+ }
1340
+ const open = existing.querySelector('[data-run-process]')?.open;
1341
+ const next = renderRunCard(run);
1342
+ const process = next.querySelector('[data-run-process]');
1343
+ if (process && typeof open === 'boolean') process.open = open;
1344
+ existing.replaceWith(next);
1345
+ }
1346
+ log.scrollTop = reading.scrollTop;
1347
+ scrollLayout?.restore(reading);
1348
+ updateJumpToLatestButton(log);
1349
+ }
1350
+
1161
1351
  return {
1352
+ refreshLocale,
1162
1353
  refreshResolvedUndo: refreshRunCard,
1163
1354
  refreshActivitySummary: activitySummary?.update,
1164
1355
  appendActivityDetail,
@@ -1166,6 +1357,7 @@
1166
1357
  bindLogAutoFollow,
1167
1358
  bumpUnreadIfDetached,
1168
1359
  scrollLogToBottom,
1360
+ revealGuidance,
1169
1361
  collapseRunProcess,
1170
1362
  startRunElapsedTick,
1171
1363
  stopRunElapsedTick,
@@ -1184,5 +1376,5 @@
1184
1376
  };
1185
1377
  }
1186
1378
 
1187
- window.CodexOverleafRunTimelineView = { create };
1188
- })();
1379
+ return { create };
1380
+ });