codex-overleaf-link 2.5.1 → 2.6.5

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 (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -11,6 +11,7 @@
11
11
  const {
12
12
  RunActivitySummary,
13
13
  RunPresence,
14
+ PanelIcons,
14
15
  RunActivityModel,
15
16
  SubagentActivityView,
16
17
  RunGuidanceView,
@@ -20,6 +21,7 @@
20
21
  RunFailureNotice,
21
22
  I18n,
22
23
  FailureReasons,
24
+ projectContextLimitEvent = value => value,
23
25
  localizeVisibleReason = value => value,
24
26
  localizeCompileReason = value => value,
25
27
  tr,
@@ -87,16 +89,18 @@
87
89
  openStorageSettings, forceCancelStuckTaskForCurrentProject
88
90
  });
89
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 });
90
94
  const displayRun = run => RunFailureNotice?.projectResolvedUndo?.(run) || run;
91
95
  const displayLocalizedRun = run => {
92
96
  const displayed = displayRun(run);
93
97
  return displayed ? { ...displayed, events: (displayed.events || []).map(sanitizeRunEventForRender) } : displayed;
94
98
  };
95
- const activitySummary = RunActivitySummary?.create({ RunActivityModel, RunGuidanceView, getPendingGuidance, SubagentActivityView, tx, findRunRecord: (...args) => displayLocalizedRun(findRunRecord(...args)), getPanel, cssEscape,
99
+ const activitySummary = RunActivitySummary?.create({ RunActivityModel, RunGuidanceView, getPendingGuidance, SubagentActivityView, PanelIcons, tx, findRunRecord: (...args) => displayLocalizedRun(findRunRecord(...args)), getPanel, cssEscape,
96
100
  projectReviewResultEvent: sanitizeRunEventForRender,
97
101
  formatReviewResultFailure,
98
102
  formatEventTime, formatElapsed, renderMarkdownBlockText, renderRunEvent, formatEventDetail,
99
- presence, classifyStatus: classifyRunPresence, getProjectId: () => getCurrentProjectId?.() || '',
103
+ presence, getNotStartedRun, getFinishedRun, classifyStatus: classifyRunPresence, getProjectId: () => getCurrentProjectId?.() || '',
100
104
  summarizeCommand: RunPresence?.commandSummary,
101
105
  isGuidance: event => Boolean(RunGuidanceView.getGuidanceText(event)), sanitizeAssistantVisibleText });
102
106
 
@@ -180,7 +184,7 @@
180
184
  ? tx(`↓ Latest · ${unreadSinceDetach}`, `↓ 最新 · ${unreadSinceDetach}`)
181
185
  : tx('↓ Latest', '↓ 最新');
182
186
  button.setAttribute('aria-label', unreadSinceDetach > 0
183
- ? tx(`Jump to latest, ${unreadSinceDetach} new steps`, `跳到最新,${unreadSinceDetach} 个新步骤`)
187
+ ? tx(`Jump to latest, ${unreadSinceDetach} new step${unreadSinceDetach === 1 ? '' : 's'}`, `跳到最新,${unreadSinceDetach} 个新步骤`)
184
188
  : tx('Jump to latest', '跳到最新'));
185
189
  }
186
190
 
@@ -285,6 +289,8 @@
285
289
  scroller.scrollTop = scroller.scrollHeight;
286
290
  }
287
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);
288
294
  if (activitySummary) { activitySummary.settle(view, statusText); return; }
289
295
  const runProcess = view?.runProcess || view?.root?.querySelector('[data-run-process]');
290
296
  if (runProcess) {
@@ -297,11 +303,22 @@
297
303
  const record = view?.recordId ? findRunRecord(view.recordId, view.sessionId) : null;
298
304
  const stepCount = (record?.events || []).filter(event => (event.kind || 'activity') === 'activity').length;
299
305
  statusEl.textContent = stepCount > 0
300
- ? `${statusText} · ${tx(`${stepCount} steps`, `${stepCount} 步`)}`
306
+ ? `${statusText} · ${tx(`${stepCount} step${stepCount === 1 ? '' : 's'}`, `${stepCount} 步`)}`
301
307
  : statusText;
302
308
  }
303
309
  }
304
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
+
305
322
  // Keep elapsed labels live without creating new activity records.
306
323
  function startRunElapsedTick() {
307
324
  stopRunElapsedTick();
@@ -431,7 +448,6 @@
431
448
  <div class="run-result-actions" hidden>
432
449
  <button type="button" class="run-copy-action" data-run-copy hidden></button>
433
450
  <button type="button" class="run-fork-action" data-run-fork hidden></button>
434
- <span class="run-completed-time" data-run-completed-time hidden></span>
435
451
  </div>
436
452
  <div class="run-report" data-run-report aria-live="polite" hidden></div>
437
453
  </div>
@@ -463,7 +479,7 @@
463
479
  if (event.kind === 'report') {
464
480
  report.hidden = false;
465
481
  report.replaceChildren(renderCompletionReport(event, run));
466
- } else if (activitySummary || event.kind === 'technical') {
482
+ } else if (getNotStartedRun(run) || activitySummary || event.kind === 'technical') {
467
483
  continue;
468
484
  } else if (RunGuidanceView.getGuidanceText(event)) {
469
485
  continue;
@@ -484,6 +500,10 @@
484
500
  }
485
501
 
486
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;
487
507
  if (run.retryingWriteback) return tx('Checking file sync', '正在检查文件同步');
488
508
  if (['Stopped tracking after a page refresh', '页面刷新后已停止跟踪'].includes(run.statusText)) return tr('restoredRunStoppedStatus');
489
509
  const knownStatus = /^(?:Done|Failed|Cancelled|Running|已处理|处理失败|已取消|处理中)(?:\s+[\d.]+(?:ms|s|m|h|秒|分钟)(?:\s+[\d.]+(?:ms|s|m|h|秒|分钟))*)?$/.test(run.statusText || '');
@@ -567,6 +587,7 @@
567
587
  }
568
588
 
569
589
  function sanitizeRunEventForRender(event = {}) {
590
+ event = projectContextLimitEvent(event);
570
591
  const streamRole = sanitizeAssistantVisibleText(event.streamRole);
571
592
  const title = sanitizeAssistantVisibleText(event.title);
572
593
  const activity = !streamRole && (!event.kind || event.kind === 'activity') && !event.guidanceId;
@@ -671,21 +692,41 @@
671
692
  // The user's own open/closed choice per run survives card re-renders.
672
693
  const metaOpenChoices = new Map();
673
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
+
674
712
  // Renders the run-metadata rows (Why nothing changed / Write result / Undo /
675
713
  // Next) behind a one-line summary beneath the answer. Shared by the
676
714
  // structured and flat-fallback render paths so both demote identically.
677
- function appendCompletionMetaBlock(report, meta, run) {
715
+ function appendCompletionMetaBlock(report, meta, run, { quiet = false } = {}) {
678
716
  if (!Array.isArray(meta) || !meta.length) {
679
717
  return;
680
718
  }
681
- 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)))
682
722
  .map(row => ({ ...row, shown: failureNotice?.formatMetaValue(report, row) ?? row.value }));
683
723
  if (!rows.length) return;
684
724
  const summary = RunResultActions.summarizeCompletionMeta(rows, run, {
685
- 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)
686
726
  });
687
727
  const metaBlock = document.createElement('dl');
688
728
  metaBlock.className = 'run-final-answer__meta';
729
+ appendFinishedRow(metaBlock, run, { classed: true });
689
730
  metaBlock.id = `codex-run-meta-${String(run?.id || 'report').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
690
731
  for (const row of rows) {
691
732
  const dt = document.createElement('dt');
@@ -698,6 +739,12 @@
698
739
  if (summary.warnKeys.has(row.key)) dt.dataset.tone = dd.dataset.tone = 'warn';
699
740
  metaBlock.append(dt, dd);
700
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
+ }
701
748
  const facts = document.createElement('div');
702
749
  facts.className = 'run-final-answer__facts';
703
750
  const toggle = document.createElement('button');
@@ -712,12 +759,18 @@
712
759
  toggle.append(item);
713
760
  });
714
761
  const more = Object.assign(document.createElement('span'), { className: 'run-final-answer__facts-more' });
715
- 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);
716
767
  const setOpen = open => {
717
768
  facts.dataset.open = String(open);
718
769
  metaBlock.hidden = !open;
719
770
  toggle.setAttribute('aria-expanded', String(open));
720
- 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>`;
721
774
  };
722
775
  setOpen(metaOpenChoices.get(run?.id) ?? summary.open);
723
776
  toggle.addEventListener('click', event => {
@@ -730,11 +783,130 @@
730
783
  report.append(facts);
731
784
  }
732
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
+
733
880
  function renderCompletionReport(input, run) {
734
881
  const event = sanitizeRunEventForRender(input);
735
882
  const report = document.createElement('section');
736
883
  report.className = 'run-completion-report';
737
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
+ }
738
910
  // Compile errors captured post-write get a one-click fix loop: the
739
911
  // structured error list rides on the report event (compileErrors).
740
912
  const appendCompileFix = target => {
@@ -743,7 +915,7 @@
743
915
  return;
744
916
  }
745
917
  const fix = buildRecoveryButton('compile_errors',
746
- tx(`Fix ${compileErrors.length} compile error(s)`, `修复 ${compileErrors.length} 个编译错误`),
918
+ tx(`Fix ${compileErrors.length} compile error${compileErrors.length === 1 ? '' : 's'}`, `修复 ${compileErrors.length} 个编译错误`),
747
919
  tx('Prefills a task with @compile-log and the captured errors.', '自动组一条带 @compile-log 和错误列表的任务。'));
748
920
  fix.addEventListener('click', clickEvent => {
749
921
  clickEvent.stopPropagation();
@@ -781,6 +953,13 @@
781
953
  // visually demoted block beneath, with a separator and muted color so
782
954
  // they read as run metadata rather than part of the answer.
783
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
+ }
784
963
  const hasStructured = structured
785
964
  && (structured.conclusion || structured.body || (Array.isArray(structured.meta) && structured.meta.length));
786
965
  if (hasStructured) {
@@ -788,7 +967,8 @@
788
967
  main.className = 'run-final-answer';
789
968
  const mainSections = [];
790
969
  if (structured.conclusion) {
791
- 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));
792
972
  }
793
973
  if (structured.body) {
794
974
  mainSections.push(RunResultActions.projectCompletionBody?.(structured.body, tx) ?? structured.body);
@@ -800,8 +980,20 @@
800
980
  }
801
981
 
802
982
  failureNotice?.append(report, event, run);
803
- appendCompletionMetaBlock(report, structured.meta, run);
804
- 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
+ }
805
997
  appendRejectedRedo(report);
806
998
  appendRecoveryActionForFailure(report, event, run);
807
999
  return report;
@@ -816,7 +1008,8 @@
816
1008
  const split = splitFlatCompletionReport(flatText);
817
1009
  const body = document.createElement('div');
818
1010
  body.className = 'run-final-answer';
819
- 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));
820
1013
  decorateCompletionReport(body, { tx });
821
1014
  report.append(body);
822
1015
  appendCompileFix(report);
@@ -855,7 +1048,8 @@
855
1048
  && Boolean(String(run?.task || '').trim())
856
1049
  && typeof canEditRun === 'function' && canEditRun(run)
857
1050
  && typeof refillComposerForRetry === 'function';
858
- button.hidden = !available;
1051
+ const cardHasEdit = Boolean(root.querySelector('.run-outcome [data-recovery-action="edit-resend"]'));
1052
+ button.hidden = !available || cardHasEdit;
859
1053
  if (!available) return;
860
1054
  button.disabled = typeof options.running === 'boolean'
861
1055
  ? options.running : Boolean(getCurrentRunView?.());
@@ -903,7 +1097,7 @@
903
1097
  }
904
1098
 
905
1099
  const projection = RunResultActions.projectUndoAvailability(run, projectRunSettlement);
906
- if (!projection.canUndo && run.undoStatus !== 'applied') {
1100
+ if (!projection.canUndo && run.undoStatus !== 'applied' && !projection.undoUnavailable?.length) {
907
1101
  button.hidden = true;
908
1102
  return;
909
1103
  }
@@ -911,7 +1105,7 @@
911
1105
  button.hidden = false;
912
1106
  const inFlight = trackedChangeInFlight?.get(run.id) === 'reject';
913
1107
  const applied = run.undoStatus === 'applied' && !inFlight;
914
- button.disabled = inFlight || (run.undoStatus === 'running' && !run.undoRecoveryCheckRequired) || run.undoStatus === 'applied';
1108
+ button.disabled = !projection.canUndo || inFlight || (run.undoStatus === 'running' && !run.undoRecoveryCheckRequired) || run.undoStatus === 'applied';
915
1109
  button.textContent = applied
916
1110
  ? tr('undoApplied')
917
1111
  : (run.partialWriteback ? tr('undoPartialRun') : tr('undoRun'));
@@ -919,6 +1113,8 @@
919
1113
  ? tr('undoAppliedTitle')
920
1114
  : (run.partialWriteback ? tr('undoPartialRunTitle') : tr('undoRunTitle'));
921
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(', ');
922
1118
  button.addEventListener('click', event => {
923
1119
  event.stopPropagation();
924
1120
  undoRun(run.id);
@@ -935,6 +1131,9 @@
935
1131
  const status = run.trackedChangeStatus || '';
936
1132
  const inFlight = trackedChangeInFlight.get(run.id);
937
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');
938
1137
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
939
1138
  // actionable. Branch placed before the terminal branches so a
940
1139
  // needs_review run is never treated as terminal.
@@ -942,7 +1141,7 @@
942
1141
  button.hidden = false;
943
1142
  button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
944
1143
  button.textContent = tr('undoRun');
945
- button.title = tr('undoRunTitle');
1144
+ button.title = undoTitle;
946
1145
  button.addEventListener('click', event => {
947
1146
  event.stopPropagation();
948
1147
  undoRun(run.id);
@@ -962,7 +1161,7 @@
962
1161
  button.hidden = false;
963
1162
  button.disabled = true;
964
1163
  button.textContent = tr('undoRun');
965
- button.title = tr('undoRunTitle');
1164
+ button.title = tr('undoAcceptedTitle');
966
1165
  return;
967
1166
  }
968
1167
  if (status !== 'pending' && !inFlight) {
@@ -972,7 +1171,7 @@
972
1171
  button.hidden = false;
973
1172
  button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
974
1173
  button.textContent = tr('undoRun');
975
- button.title = tr('undoRunTitle');
1174
+ button.title = undoTitle;
976
1175
  button.addEventListener('click', event => {
977
1176
  event.stopPropagation();
978
1177
  undoRun(run.id);
@@ -1041,7 +1240,7 @@
1041
1240
  button.hidden = false;
1042
1241
  button.disabled = true;
1043
1242
  button.textContent = tr('runAcceptTracked');
1044
- button.title = tr('runAcceptTrackedTitle');
1243
+ button.title = tr('undoAppliedTitle');
1045
1244
  return;
1046
1245
  }
1047
1246
  if (status !== 'pending' && !inFlight) {
@@ -1102,7 +1301,7 @@
1102
1301
  const log = getPanel()?.querySelector('[data-log]');
1103
1302
  const existing = log?.querySelector(`[data-run-id="${cssEscape(runId)}"]`);
1104
1303
  const run = findRunRecord(runId);
1105
- if (!log || !existing || !run || displayRun(run) === run) {
1304
+ if (!log || !existing || !run || (displayRun(run) === run && !getFinishedRun(run))) {
1106
1305
  return false;
1107
1306
  }
1108
1307
  const reading = scrollLayout?.snapshot(), open = existing.querySelector('[data-run-process]')?.open;
@@ -1132,7 +1331,7 @@
1132
1331
  RunResultActions.configureResultActions(existing, run);
1133
1332
  const reportEvent = (run.events || []).filter(event => event.kind === 'report').at(-1);
1134
1333
  const report = current.report || existing.querySelector('[data-run-report]');
1135
- if (reportEvent && report) report.replaceChildren(renderCompletionReport(reportEvent, displayLocalizedRun(run)));
1334
+ if (reportEvent && report) refreshCompletionView(existing, report, displayLocalizedRun(run));
1136
1335
  activitySummary?.update(current);
1137
1336
  if (!activitySummary) RunGuidanceView.reconcileGuidance?.(existing.querySelector('[data-run-guidance]'),
1138
1337
  (run.events || []).concat(getPendingGuidance(run)), tx, { active: run.status === 'running' });