codex-overleaf-link 2.3.0 → 2.3.2

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 (50) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +20 -0
  4. package/extension/src/content/assetTransferBroker.js +169 -0
  5. package/extension/src/content/composerPanel.js +0 -2
  6. package/extension/src/content/contentRuntime.js +135 -111
  7. package/extension/src/content/generated/content.bundle.js +1066 -345
  8. package/extension/src/content/generated/content.bundle.meta.json +7 -3
  9. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  10. package/extension/src/content/markdownText.js +12 -10
  11. package/extension/src/content/mathText.js +155 -9
  12. package/extension/src/content/modelPicker.js +2 -2
  13. package/extension/src/content/modelPickerSupport.js +3 -3
  14. package/extension/src/content/nativeCompatibilityController.js +2 -2
  15. package/extension/src/content/otWarmMirror.js +1 -1
  16. package/extension/src/content/pageBridgeClient.js +1 -0
  17. package/extension/src/content/runResultActions.js +114 -0
  18. package/extension/src/content/runTimelineView.js +23 -0
  19. package/extension/src/content/sessionForkController.js +120 -0
  20. package/extension/src/content/writebackController.js +3 -0
  21. package/extension/src/content/writebackOrchestrator.js +5 -50
  22. package/extension/src/page/binaryAssetUploader.js +343 -0
  23. package/extension/src/page/trackedChangesLifecycle.js +12 -4
  24. package/extension/src/page/writebackRouter.js +6 -2
  25. package/extension/src/pageBridge.js +30 -1
  26. package/extension/src/shared/agentTranscript.js +10 -3
  27. package/extension/src/shared/compatibility.js +4 -2
  28. package/extension/src/shared/i18n.js +18 -4
  29. package/extension/src/shared/models.js +13 -2
  30. package/extension/src/shared/pageRpcContract.js +6 -2
  31. package/extension/src/shared/runExecutionSnapshot.js +14 -4
  32. package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
  33. package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
  34. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  35. package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
  36. package/extension/src/shared/sessionState.js +58 -15
  37. package/extension/src/shared/storageDb.js +6 -5
  38. package/extension/src/shared/storageRunActions.js +11 -1
  39. package/extension/src/shared/writebackOperationEvidence.js +16 -6
  40. package/extension/src/shared/writebackSettlement.js +28 -6
  41. package/extension/styles/panel.css +79 -8
  42. package/native-host/src/codexModels.js +6 -2
  43. package/native-host/src/codexPromptAssembly.js +1 -1
  44. package/native-host/src/codexSessionRunner.js +5 -1
  45. package/native-host/src/codexThreadFork.js +103 -0
  46. package/native-host/src/mirrorWorkspace.js +5 -19
  47. package/native-host/src/nativeAssetTransfer.js +183 -0
  48. package/native-host/src/taskRunnerRuntime.js +33 -81
  49. package/native-host/src/updateManager.js +36 -1
  50. package/package.json +1 -1
@@ -50,7 +50,6 @@
50
50
  'codex.providers.clear-secret',
51
51
  'codex.providers.delete',
52
52
  'task.run',
53
- 'task.confirm',
54
53
  'mirror.sync',
55
54
  'mirror.patchFiles',
56
55
  'mirror.confirmWriteback',
@@ -183,6 +182,7 @@
183
182
  callPageBridge,
184
183
  getCurrentProjectId,
185
184
  getCurrentProjectReferenceFiles,
185
+ getCurrentProjectMathSources,
186
186
  showPluginToast,
187
187
  recordRenderingDiagnostic: detail => appendTechnicalEvent({ type: 'rendering.diagnostic', detail })
188
188
  });
@@ -398,8 +398,24 @@
398
398
  openStorageSettings
399
399
  } = panelMaintenance;
400
400
 
401
+ const sessionForkController = Modules.SessionForkController.create({
402
+ SessionState: Modules.SessionState,
403
+ getState: () => state,
404
+ setState: next => { state = next; },
405
+ sendBackgroundNative,
406
+ saveState,
407
+ applyStateToPanel,
408
+ showPluginToast,
409
+ tr
410
+ });
411
+ const runResultActions = Modules.RunResultActions.create({
412
+ tr,
413
+ getLocale,
414
+ forkRunFromNode: sessionForkController.forkRunFromNode
415
+ });
401
416
  const runTimelineView = Modules.RunTimelineView.create({
402
417
  RunGuidanceView: Modules.RunGuidanceView,
418
+ RunResultActions: runResultActions,
403
419
  tr,
404
420
  tx,
405
421
  getLocale,
@@ -516,6 +532,7 @@
516
532
  // lives in writebackOrchestrator.js. Every dep below is either a hoisted
517
533
  // function declaration, an already-initialized const above, or a lazy
518
534
  // accessor thunk — safe in the top-of-init() wiring zone.
535
+ const assetTransferBroker = Modules.AssetTransferBroker.create({ callPageBridge, sendBackgroundNative });
519
536
  const writebackOrchestrator = Modules.WritebackOrchestrator.create({
520
537
  tr,
521
538
  tx,
@@ -526,7 +543,6 @@
526
543
  appendOperationsPreview,
527
544
  appendPartialWritebackWarning,
528
545
  appendApplyResult,
529
- renderDiffReview,
530
546
  renderReadOnlyDiffReview,
531
547
  showPluginConfirm,
532
548
  callPageBridge,
@@ -556,6 +572,7 @@
556
572
  confirmBinaryOperations,
557
573
  filterSyncChangesByOperations,
558
574
  writebackController,
575
+ assetTransferBroker,
559
576
  writebackSettlement: WritebackSettlement,
560
577
  compileAdapter: Modules.CompileAdapter,
561
578
  RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
@@ -1512,9 +1529,6 @@
1512
1529
  if (mode === 'ask') {
1513
1530
  return 'Ask';
1514
1531
  }
1515
- if (mode === 'confirm') {
1516
- return 'Confirm';
1517
- }
1518
1532
  if (mode === 'auto') {
1519
1533
  return 'Auto';
1520
1534
  }
@@ -1877,7 +1891,7 @@
1877
1891
  if (!useExistingMirror) {
1878
1892
  appendLog(formatProjectSnapshotUserLog(project));
1879
1893
  }
1880
- currentRunView.projectFiles = captureProjectReferenceFiles(project);
1894
+ persistCurrentProjectReferenceFiles(project);
1881
1895
  const snapshotWarnings = useExistingMirror
1882
1896
  ? { blocking: [], nonBlocking: [] }
1883
1897
  : getProjectSnapshotWarnings(project);
@@ -1924,7 +1938,7 @@
1924
1938
  skippedFiles: [],
1925
1939
  blockedFiles: snapshotWarnings.blocking.map(reason => ({ path: 'project', reason }))
1926
1940
  });
1927
- finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
1941
+ await finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
1928
1942
  return;
1929
1943
  }
1930
1944
 
@@ -2011,7 +2025,7 @@
2011
2025
  reason: finding.detectorId || 'sensitive'
2012
2026
  }))
2013
2027
  });
2014
- finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
2028
+ await finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
2015
2029
  return;
2016
2030
  }
2017
2031
  if (sensitiveFindings.findings.length) {
@@ -2049,7 +2063,7 @@
2049
2063
  return;
2050
2064
  }
2051
2065
  project = staleRetry.project;
2052
- currentRunView.projectFiles = captureProjectReferenceFiles(project);
2066
+ persistCurrentProjectReferenceFiles(project);
2053
2067
  useExistingMirror = false;
2054
2068
  fileOverlays = null;
2055
2069
  otWarmStart = false;
@@ -2104,7 +2118,7 @@
2104
2118
  } else {
2105
2119
  appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
2106
2120
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
2107
- finishRunView(tx('Cancelled', '已取消'), 'rejected');
2121
+ await finishRunView(tx('Cancelled', '已取消'), 'rejected');
2108
2122
  return;
2109
2123
  }
2110
2124
  }
@@ -2112,7 +2126,7 @@
2112
2126
  if (!response.ok) {
2113
2127
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2114
2128
  appendRunCancelledReport();
2115
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2129
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2116
2130
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2117
2131
  return;
2118
2132
  }
@@ -2199,7 +2213,7 @@
2199
2213
  sensitiveFindings: sensitiveFindings.findings,
2200
2214
  blockedFiles: [{ path: 'codex.run', reason: response.error?.code || response.error?.message || 'native_error' }]
2201
2215
  });
2202
- finishRunView(response.error?.code === 'project_locked'
2216
+ await finishRunView(response.error?.code === 'project_locked'
2203
2217
  ? tx('Codex task already running', 'Codex 任务正在运行')
2204
2218
  : tx('Local Codex error', '本地 Codex 错误'), 'failed');
2205
2219
  return;
@@ -2249,7 +2263,7 @@
2249
2263
  sensitiveFindings: sensitiveFindings.findings,
2250
2264
  blockedFiles: [{ path: 'codex.run', reason: 'codex_no_usable_result' }]
2251
2265
  });
2252
- finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
2266
+ await finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
2253
2267
  return;
2254
2268
  }
2255
2269
  const writebackProject = useExistingMirror
@@ -2272,7 +2286,7 @@
2272
2286
  resultStatus: syncOutcome.hasSkippedOperations ? 'completed_with_skips' : 'completed'
2273
2287
  });
2274
2288
 
2275
- finishRunView(
2289
+ await finishRunView(
2276
2290
  syncOutcome.hasSkippedOperations ? tx('Sync completed with skipped items', '同步完成但有跳过项') : tx('Sync completed', '同步完成'),
2277
2291
  syncOutcome.hasSkippedOperations ? 'failed' : 'completed'
2278
2292
  );
@@ -2366,7 +2380,7 @@
2366
2380
  } catch (error) {
2367
2381
  if (runCancellationRequested || isRunCancellationError(error)) {
2368
2382
  appendRunCancelledReport();
2369
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2383
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2370
2384
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2371
2385
  return;
2372
2386
  }
@@ -2416,7 +2430,7 @@
2416
2430
  resultStatus: 'failed',
2417
2431
  blockedFiles: [{ path: 'task', reason: error.message }]
2418
2432
  });
2419
- finishRunView(tx('Task failed', '任务失败'), 'failed');
2433
+ await finishRunView(tx('Task failed', '任务失败'), 'failed');
2420
2434
  } finally {
2421
2435
  const settledView = currentRunView;
2422
2436
  const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
@@ -2489,7 +2503,7 @@
2489
2503
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2490
2504
  appendRunCancelledReport();
2491
2505
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2492
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2506
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2493
2507
  return;
2494
2508
  }
2495
2509
  const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale() });
@@ -2522,7 +2536,7 @@
2522
2536
  resultStatus: 'failed',
2523
2537
  blockedFiles: [{ path: 'skill-installer', reason: response.error?.code || response.error?.message || 'native_error' }]
2524
2538
  });
2525
- finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
2539
+ await finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
2526
2540
  return;
2527
2541
  }
2528
2542
 
@@ -2536,7 +2550,7 @@
2536
2550
  mode: submittedMode
2537
2551
  });
2538
2552
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'completed' });
2539
- finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
2553
+ await finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
2540
2554
 
2541
2555
  const runSessionForHistory = findSessionById(runSessionId) || getActiveSession(state);
2542
2556
  const rawAssistantMessage = assistantMessage;
@@ -2659,7 +2673,7 @@
2659
2673
  const finishTitle = requireReviewing
2660
2674
  ? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
2661
2675
  : tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
2662
- finishRunView(finishTitle, 'failed');
2676
+ await finishRunView(finishTitle, 'failed');
2663
2677
  return {
2664
2678
  ok: false,
2665
2679
  reason,
@@ -3857,10 +3871,6 @@
3857
3871
  return diffReviewPanel.createDiffReviewElement(syncChanges, options);
3858
3872
  }
3859
3873
 
3860
- function renderDiffReview(syncChanges) {
3861
- return diffReviewPanel.renderDiffReview(syncChanges);
3862
- }
3863
-
3864
3874
  function renderReadOnlyDiffReview(syncChanges, title = tr('diffWrittenChangesTitle')) {
3865
3875
  return diffReviewPanel.renderReadOnlyDiffReview(syncChanges, title);
3866
3876
  }
@@ -4623,80 +4633,11 @@
4623
4633
  mode,
4624
4634
  operations: [],
4625
4635
  applyResults: [],
4626
- nextStep: tx('Continue the conversation, or switch to Suggest/Auto to let Codex edit files.', '可以继续追问,或切换到建议修改/自动写入后让 Codex 修改文件。')
4636
+ nextStep: tx('Continue the conversation, or switch to Auto to let Codex edit files.', '可以继续追问,或切换到自动写入后让 Codex 修改文件。')
4627
4637
  });
4628
4638
  return { status: tr('modeAsk'), summaryLine };
4629
4639
  }
4630
4640
 
4631
- if (result.status === 'requires_task_confirmation') {
4632
- appendPlannedChangeSummary(result.summary, tx('Preparing changes', '准备修改'));
4633
- const approved = await showPluginConfirm({
4634
- title: tx('Apply these changes?', '应用这些修改?'),
4635
- message: formatSummary(tx('Change Summary', '修改摘要'), result.summary),
4636
- confirmLabel: tx('Apply changes', '应用修改'),
4637
- cancelLabel: tr('confirmDefaultCancel')
4638
- });
4639
- if (!approved) {
4640
- appendLog(tx('Cancelled: Codex did not write any files.', '已取消:Codex 没有写入任何文件。'));
4641
- const summaryLine = appendChangeSummary({ notes, summary: result.summary, status: 'rejected' });
4642
- appendCompletionReport({
4643
- conclusion: tx('You cancelled this change. Codex did not write files.', '你取消了这轮修改,Codex 没有写入文件。'),
4644
- status: 'rejected',
4645
- notes,
4646
- summary: result.summary,
4647
- userReport: result.userReport,
4648
- mode,
4649
- operations: [],
4650
- applyResults: [],
4651
- nextStep: tx('Adjust the task and run again, or switch to Ask first so Codex can explain the plan.', '可以调整任务描述后重新运行,或切到只问不改先让 Codex 解释方案。')
4652
- });
4653
- return { status: 'rejected', summaryLine };
4654
- }
4655
- const confirmed = result.planId
4656
- ? await sendNative({ method: 'task.confirm', params: { planId: result.planId } })
4657
- : { ok: true, result: { operations: result.operations || [] } };
4658
- if (!confirmed.ok) {
4659
- appendLog(`Confirm failed: ${confirmed.error.message}`);
4660
- const summaryLine = appendChangeSummary({ notes, operations: [], status: 'confirm failed' });
4661
- appendCompletionReport({
4662
- conclusion: tx('No writable changes were returned after confirmation.', '确认后没有拿到可写入的修改。'),
4663
- status: 'confirm failed',
4664
- notes,
4665
- userReport: result.userReport,
4666
- mode,
4667
- operations: [],
4668
- applyResults: [],
4669
- nextStep: confirmed.error.message
4670
- });
4671
- return { status: 'confirm failed', summaryLine };
4672
- }
4673
- const operations = confirmed.result.operations || [];
4674
- appendOperationsPreview(operations, tx('Confirmed; preparing to write', '用户已确认,准备写入'));
4675
- const applied = await applyTaskOperations(project, operations, { allowHighRisk: true });
4676
- appendApplyResult(applied);
4677
- recordUndoFromApply(project, applied);
4678
- const summaryLine = appendChangeSummary({
4679
- notes,
4680
- operations,
4681
- applyResults: [applied],
4682
- status: 'confirmed and applied'
4683
- });
4684
- appendCompletionReport({
4685
- conclusion: notes || tx('Changes were written after your confirmation.', '已按你的确认写入修改。'),
4686
- status: 'confirmed and applied',
4687
- notes,
4688
- userReport: result.userReport,
4689
- mode,
4690
- operations,
4691
- applyResults: [applied]
4692
- });
4693
- return {
4694
- status: 'confirmed and applied',
4695
- summaryLine,
4696
- hasSkippedOperations: hasSkippedApplyOperations([applied])
4697
- };
4698
- }
4699
-
4700
4641
  if (result.status === 'delete_plan_required') {
4701
4642
  const operations = result.operations || [];
4702
4643
  const applyResults = [];
@@ -4769,7 +4710,7 @@
4769
4710
 
4770
4711
  const operations = result.operations || [];
4771
4712
  appendOperationsPreview(operations, tx('Preparing to write', '准备写入'));
4772
- const applied = await applyTaskOperations(project, operations, { allowHighRisk: mode === 'confirm' });
4713
+ const applied = await applyTaskOperations(project, operations);
4773
4714
  appendApplyResult(applied);
4774
4715
  recordUndoFromApply(project, applied);
4775
4716
  appendLog(mode === 'auto' ? tx('Auto write task completed.', '自动写入任务完成。') : tx('Task completed.', '任务完成。'));
@@ -5089,9 +5030,6 @@
5089
5030
  if (mode === 'ask') {
5090
5031
  return tr('modeAsk');
5091
5032
  }
5092
- if (mode === 'confirm') {
5093
- return tr('modeConfirm');
5094
- }
5095
5033
  if (mode === 'auto') {
5096
5034
  return tr('modeAuto');
5097
5035
  }
@@ -5103,8 +5041,6 @@
5103
5041
  const labels = {
5104
5042
  completed: tx('Completed', '已完成'),
5105
5043
  rejected: tx('Cancelled', '已取消'),
5106
- 'confirm failed': tx('Confirmation failed', '确认失败'),
5107
- 'confirmed and applied': tx('Suggested changes applied', '已应用建议修改'),
5108
5044
  'applied with delete plan': tx('Applied with confirmed deletes', '已应用并删除确认项'),
5109
5045
  'applied without deletes': tx('Applied without deletes', '已应用非删除修改'),
5110
5046
  '只问不改': tr('modeAsk')
@@ -5559,6 +5495,7 @@
5559
5495
  speedTier: prefs.speedTier || '',
5560
5496
  mode: prefs.mode || '',
5561
5497
  locale: prefs.locale || '',
5498
+ theme: ['dark', 'light', 'auto'].includes(prefs.theme) ? prefs.theme : 'dark',
5562
5499
  requireReviewing: prefs.requireReviewing !== false,
5563
5500
  autoRecompile: prefs.autoRecompile !== false,
5564
5501
  loadCodexLocalSkills: prefs.loadCodexLocalSkills !== false,
@@ -5581,7 +5518,7 @@
5581
5518
  history: Array.isArray(session.history) ? session.history : [],
5582
5519
  task: typeof session.task === 'string' ? session.task : '',
5583
5520
  pendingInputs: Array.isArray(session.pendingInputs) ? session.pendingInputs : [],
5584
- mode: session.mode || prefs.mode || 'confirm', providerId: session.providerId || 'builtin',
5521
+ mode: session.mode || prefs.mode || 'ask', providerId: session.providerId || 'builtin',
5585
5522
  model: session.model || prefs.model || 'gpt-5.4',
5586
5523
  reasoningEffort: session.reasoningEffort || prefs.reasoningEffort || 'high',
5587
5524
  speedTier: session.speedTier || prefs.speedTier || 'standard',
@@ -5609,6 +5546,7 @@
5609
5546
  reasoningEffort: fallback.reasoningEffort || '',
5610
5547
  speedTier: fallback.speedTier || '',
5611
5548
  locale: fallback.locale || '',
5549
+ theme: ['dark', 'light', 'auto'].includes(fallback.theme) ? fallback.theme : 'dark',
5612
5550
  requireReviewing: fallback.requireReviewing !== false,
5613
5551
  autoOpen: fallback.autoOpen !== false,
5614
5552
  loadCodexLocalSkills: fallback.loadCodexLocalSkills !== false,
@@ -5958,7 +5896,13 @@
5958
5896
  saveStateSoon();
5959
5897
  return;
5960
5898
  }
5961
- if (event?.target?.matches?.('[data-speed]')) renderReasoningOptions(getRenderedModelEntries());
5899
+ if (event?.target?.matches?.('[data-speed]')) {
5900
+ // Capability rendering consults state.speedTier. Capture the user's
5901
+ // choice before rebuilding the select, otherwise the rebuild projects
5902
+ // the previous tier back into the DOM and makes Fast appear inert.
5903
+ state.speedTier = event.target.value || 'standard';
5904
+ renderReasoningOptions(getRenderedModelEntries());
5905
+ }
5962
5906
  renderSpeedOptions(getRenderedModelEntries());
5963
5907
  readPanelInputs();
5964
5908
  renderModelConfigChoices();
@@ -5984,7 +5928,7 @@
5984
5928
  }
5985
5929
 
5986
5930
  async function selectMode(mode) {
5987
- if (!['ask', 'confirm', 'auto'].includes(mode)) {
5931
+ if (!['ask', 'auto'].includes(mode)) {
5988
5932
  return;
5989
5933
  }
5990
5934
  const modeSelect = panel?.querySelector('[data-mode]');
@@ -6268,7 +6212,11 @@
6268
6212
  events: root.querySelector('[data-run-events]'),
6269
6213
  report: root.querySelector('[data-run-report]'),
6270
6214
  status: root.querySelector('[data-run-status]'),
6271
- projectFiles: captureProjectReferenceFiles(arguments[0]?.project),
6215
+ // A run starts before warm-mirror resolution has produced its lightweight
6216
+ // project snapshot. Seed references from the already-prefetched project
6217
+ // inventory so early stream events and fast Ask responses can still
6218
+ // resolve project-relative locations.
6219
+ projectFiles: getCurrentProjectReferenceFiles(),
6272
6220
  startedAt: Date.now(),
6273
6221
  queueItemId,
6274
6222
  nativeRequestId: '',
@@ -6289,7 +6237,7 @@
6289
6237
  return updatedAt;
6290
6238
  }
6291
6239
 
6292
- function finishRunView(text, status) {
6240
+ async function finishRunView(text, status) {
6293
6241
  if (!currentRunView) {
6294
6242
  return;
6295
6243
  }
@@ -6316,7 +6264,11 @@
6316
6264
  const navigationDivergent = currentRunView.runProjectId
6317
6265
  && activeProjectId !== currentRunView.runProjectId;
6318
6266
  if (!navigationDivergent) {
6319
- saveStateSoon();
6267
+ // A terminal badge is a durability promise: once the user can see
6268
+ // Done/Failed/Cancelled, an immediate reload must hydrate that same
6269
+ // terminal state. Drain any older running snapshot and persist the
6270
+ // latest record before exposing the terminal UI.
6271
+ await flushQueuedSaveState().catch(() => {});
6320
6272
  }
6321
6273
  renderSessionList();
6322
6274
  }
@@ -6636,6 +6588,39 @@
6636
6588
 
6637
6589
 
6638
6590
 
6591
+ function getCurrentProjectMathSources() {
6592
+ const sources = [];
6593
+ const seen = new Set();
6594
+ let remainingChars = 256 * 1024;
6595
+ const add = item => {
6596
+ if (!item || typeof item !== 'object' || typeof item.content !== 'string') return;
6597
+ const path = normalizeReferencePathForRuntime(item.path || item.filePath || '');
6598
+ if (!path || seen.has(path) || hasUnsafeRuntimePathSegments(path)
6599
+ || !/\.(?:tex|sty|cls|ltx)$/i.test(path) || remainingChars <= 0) return;
6600
+ const content = item.content.slice(0, Math.min(64 * 1024, remainingChars));
6601
+ remainingChars -= content.length;
6602
+ seen.add(path);
6603
+ sources.push({ path, content });
6604
+ };
6605
+ for (const collection of [
6606
+ currentRunView?.projectFiles,
6607
+ currentRunView?.files,
6608
+ currentRunView?.project?.files,
6609
+ currentRunView?.snapshot?.files,
6610
+ currentRunView?.projectSnapshot?.files,
6611
+ currentRunView?.contextFiles,
6612
+ state?.projectFiles,
6613
+ state?.files,
6614
+ state?.project?.files,
6615
+ state?.snapshot?.files,
6616
+ state?.projectSnapshot?.files,
6617
+ state?.contextFiles
6618
+ ]) {
6619
+ if (Array.isArray(collection)) collection.forEach(add);
6620
+ }
6621
+ return sources;
6622
+ }
6623
+
6639
6624
  function getCurrentProjectReferenceFiles() {
6640
6625
  const files = [];
6641
6626
  const seen = new Set();
@@ -6686,7 +6671,14 @@
6686
6671
  }
6687
6672
  };
6688
6673
 
6674
+ const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
6689
6675
  addFiles(currentRunView?.projectFiles);
6676
+ addFiles(activeSession?.projectReferenceFiles);
6677
+ // ContextTray owns the exact, background-prefetched project file list.
6678
+ // Warm-mirror runs intentionally carry only the active/focused overlay, so
6679
+ // omitting this inventory makes valid references to every other project
6680
+ // file render as plain text.
6681
+ addFiles(contextTrayController?.getContextProject?.()?.files);
6690
6682
  addFiles(currentRunView?.files);
6691
6683
  addFiles(currentRunView?.project?.files);
6692
6684
  addFiles(currentRunView?.snapshot?.files);
@@ -6700,14 +6692,26 @@
6700
6692
  addFiles(state?.contextFiles);
6701
6693
  addTextPaths(state?.focusFiles);
6702
6694
  addTextPaths(state?.session?.focusFiles);
6703
- const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
6704
6695
  addTextPaths(activeSession?.focusFiles);
6705
6696
  for (const session of Array.isArray(state?.sessions) ? state.sessions : []) {
6697
+ addFiles(session?.projectReferenceFiles);
6706
6698
  addTextPaths(session?.focusFiles);
6707
6699
  }
6708
6700
  return files;
6709
6701
  }
6710
6702
 
6703
+ function persistCurrentProjectReferenceFiles(project) {
6704
+ const projectFiles = captureProjectReferenceFiles(project);
6705
+ if (currentRunView) {
6706
+ currentRunView.projectFiles = projectFiles;
6707
+ }
6708
+ if (getActiveSession(state)) {
6709
+ state = updateActiveSession(state, { projectReferenceFiles: projectFiles });
6710
+ saveStateSoon();
6711
+ }
6712
+ return projectFiles;
6713
+ }
6714
+
6711
6715
  function captureProjectReferenceFiles(project) {
6712
6716
  const result = [];
6713
6717
  const seen = new Set();
@@ -7167,7 +7171,12 @@
7167
7171
  record.undoOperations = [];
7168
7172
  record.undoBaseFiles = [];
7169
7173
  record.undoTrackedChanges = combinedTrackedChanges;
7170
- record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(project, combinedAppliedOperations, combinedTrackedChanges);
7174
+ record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
7175
+ project,
7176
+ combinedAppliedOperations,
7177
+ combinedTrackedChanges,
7178
+ record.undoExpectedFiles
7179
+ );
7171
7180
  record.undoStatus = '';
7172
7181
  record.partialWriteback = skippedEntries.length > 0;
7173
7182
  // Recording tracked-change refs enters the run into the tracked-change
@@ -7215,7 +7224,12 @@
7215
7224
  record.undoOperations = checkpoint.undoOperations;
7216
7225
  record.undoBaseFiles = checkpoint.undoBaseFiles;
7217
7226
  record.undoTrackedChanges = [];
7218
- record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(project, combinedAppliedOperations, []);
7227
+ record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
7228
+ project,
7229
+ combinedAppliedOperations,
7230
+ [],
7231
+ record.undoExpectedFiles
7232
+ );
7219
7233
  record.undoStatus = '';
7220
7234
  record.partialWriteback = skippedEntries.length > 0;
7221
7235
  refreshRunCardControls(record.id);
@@ -7235,8 +7249,18 @@
7235
7249
  return WritebackSettlement.normalizeApplyTrackedChanges(changes);
7236
7250
  }
7237
7251
 
7238
- function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = []) {
7239
- return WritebackSettlement.selectExpectedFilesForTrackedUndo(project, operations, trackedChanges);
7252
+ function selectExpectedFilesForTrackedUndo(
7253
+ project,
7254
+ operations = [],
7255
+ trackedChanges = [],
7256
+ previousExpectedFiles = []
7257
+ ) {
7258
+ return WritebackSettlement.selectExpectedFilesForTrackedUndo(
7259
+ project,
7260
+ operations,
7261
+ trackedChanges,
7262
+ previousExpectedFiles
7263
+ );
7240
7264
  }
7241
7265
 
7242
7266
  function buildTrackedUndoPostFiles(run) {