codex-overleaf-link 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) 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/composerPanel.js +0 -2
  5. package/extension/src/content/contentRuntime.js +108 -110
  6. package/extension/src/content/generated/content.bundle.js +566 -331
  7. package/extension/src/content/generated/content.bundle.meta.json +4 -3
  8. package/extension/src/content/markdownText.js +12 -10
  9. package/extension/src/content/mathText.js +155 -9
  10. package/extension/src/content/nativeCompatibilityController.js +2 -2
  11. package/extension/src/content/otWarmMirror.js +1 -1
  12. package/extension/src/content/runTimelineView.js +6 -0
  13. package/extension/src/content/writebackOrchestrator.js +2 -49
  14. package/extension/src/page/trackedChangesLifecycle.js +10 -4
  15. package/extension/src/page/writebackRouter.js +6 -2
  16. package/extension/src/shared/agentTranscript.js +10 -3
  17. package/extension/src/shared/compatibility.js +1 -1
  18. package/extension/src/shared/i18n.js +2 -4
  19. package/extension/src/shared/pageRpcContract.js +2 -2
  20. package/extension/src/shared/runExecutionSnapshot.js +14 -4
  21. package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
  22. package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
  23. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  24. package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
  25. package/extension/src/shared/sessionState.js +46 -11
  26. package/extension/src/shared/storageDb.js +6 -5
  27. package/extension/src/shared/writebackOperationEvidence.js +16 -6
  28. package/extension/src/shared/writebackSettlement.js +28 -6
  29. package/extension/styles/panel.css +18 -8
  30. package/native-host/src/codexPromptAssembly.js +1 -1
  31. package/native-host/src/taskRunnerRuntime.js +11 -81
  32. package/native-host/src/updateManager.js +36 -1
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>Codex Overleaf Link</h1>
4
4
  <p><strong>Empower Overleaf with Codex.</strong></p>
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-2.3.0-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-2.3.1-blue" alt="version">
7
7
  <img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
8
8
  <img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
@@ -38,14 +38,14 @@ One command installs the managed native host **and** managed extension runtime.
38
38
  macOS / Linux:
39
39
 
40
40
  ```bash
41
- CODEX_OVERLEAF_REF=v2.3.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.0/install.sh)"
41
+ CODEX_OVERLEAF_REF=v2.3.1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.1/install.sh)"
42
42
  ```
43
43
 
44
44
  Windows PowerShell:
45
45
 
46
46
  ```powershell
47
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.0/install.ps1 -OutFile install.ps1
48
- $env:CODEX_OVERLEAF_REF='v2.3.0'
47
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.1/install.ps1 -OutFile install.ps1
48
+ $env:CODEX_OVERLEAF_REF='v2.3.1'
49
49
  powershell -ExecutionPolicy Bypass -File install.ps1
50
50
  ```
51
51
 
@@ -56,7 +56,7 @@ Then, in the `chrome://extensions` tab the script opened: enable **Developer mod
56
56
  `npm exec` installs the same managed native host and extension runtime without keeping a source checkout. Use it if you prefer a pinned npm package.
57
57
 
58
58
  ```bash
59
- npm exec --yes codex-overleaf-link@2.3.0 -- install-managed
59
+ npm exec --yes codex-overleaf-link@2.3.1 -- install-managed
60
60
  ```
61
61
 
62
62
  Then, in `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the managed extension path printed by the command. The Release extension zip remains available for explicitly unmanaged/manual installations.
@@ -88,9 +88,9 @@ npm installs, updates, and uninstalls the coordinated managed extension/native p
88
88
 
89
89
  | Action | Command |
90
90
  |--------|---------|
91
- | Install / update | `npm exec --yes codex-overleaf-link@2.3.0 -- install-managed` |
92
- | Diagnose | `npm exec --yes codex-overleaf-link@2.3.0 -- doctor` |
93
- | Uninstall | `npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed` |
91
+ | Install / update | `npm exec --yes codex-overleaf-link@2.3.1 -- install-managed` |
92
+ | Diagnose | `npm exec --yes codex-overleaf-link@2.3.1 -- doctor` |
93
+ | Uninstall | `npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed` |
94
94
 
95
95
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
96
96
 
@@ -104,13 +104,13 @@ Managed installations discover signed stable updates automatically and apply the
104
104
 
105
105
  v2.2.0 introduces Bootstrap protocol 2. Existing v2.1.x managed installations must run the pinned `install-managed` command once, then reload the extension and Overleaf. Later protocol-2 releases use the in-product signed updater for ordinary runtime, style, vendor, and Native Host feature changes.
106
106
 
107
- The v2.3.0 GitHub Release contains:
107
+ The v2.3.1 GitHub Release contains:
108
108
 
109
- - `codex-overleaf-link-extension-v2.3.0.zip`: loadable Chrome extension package for manual unpacked installation.
110
- - `codex-overleaf-native-host-v2.3.0.tar.gz`: native host runtime files used by the installer and release verification.
111
- - `codex-overleaf-link-2.3.0.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
112
- - `install.sh`: release-pinned macOS / Linux installer that defaults to `v2.3.0` when run directly from the release artifact.
113
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v2.3.0` when run directly from the release artifact.
109
+ - `codex-overleaf-link-extension-v2.3.1.zip`: loadable Chrome extension package for manual unpacked installation.
110
+ - `codex-overleaf-native-host-v2.3.1.tar.gz`: native host runtime files used by the installer and release verification.
111
+ - `codex-overleaf-link-2.3.1.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
112
+ - `install.sh`: release-pinned macOS / Linux installer that defaults to `v2.3.1` when run directly from the release artifact.
113
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v2.3.1` when run directly from the release artifact.
114
114
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
115
115
  - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
116
116
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
@@ -121,7 +121,7 @@ The v2.3.0 GitHub Release contains:
121
121
  Remove the native host (use `--browser chromium` on Linux Chromium):
122
122
 
123
123
  ```bash
124
- npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed
124
+ npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed
125
125
  ```
126
126
 
127
127
  The same command works on Windows PowerShell. If you installed from a manual checkout or source installer, you can also run `npm run uninstall:native` inside the repo, use `node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs` on macOS / Linux, or use `node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs` on Windows PowerShell.
@@ -156,13 +156,13 @@ Then remove the extension from `chrome://extensions`. To delete local data: on m
156
156
  Linux Chromium install or update:
157
157
 
158
158
  ```bash
159
- CODEX_OVERLEAF_REF=v2.3.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.0/install.sh)" -- --browser chromium
159
+ CODEX_OVERLEAF_REF=v2.3.1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.1/install.sh)" -- --browser chromium
160
160
  ```
161
161
 
162
162
  Linux Chromium uninstall:
163
163
 
164
164
  ```bash
165
- npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed --browser chromium
165
+ npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed --browser chromium
166
166
  ```
167
167
 
168
168
  ## Features
@@ -298,7 +298,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
298
298
  Re-run any [native host installer](#install), reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
299
299
 
300
300
  ```bash
301
- npm exec --yes codex-overleaf-link@2.3.0 -- install-managed
301
+ npm exec --yes codex-overleaf-link@2.3.1 -- install-managed
302
302
  ```
303
303
 
304
304
  **The Windows popup or panel shows a Bash recovery command**
@@ -347,8 +347,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
347
347
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
348
348
  | Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
349
349
  | Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
350
- | Installer/update command | `npm exec --yes codex-overleaf-link@2.3.0 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- install-managed --browser chromium` |
351
- | Uninstall command | `npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-managed --browser chromium` |
350
+ | Installer/update command | `npm exec --yes codex-overleaf-link@2.3.1 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- install-managed --browser chromium` |
351
+ | Uninstall command | `npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-managed --browser chromium` |
352
352
  | Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
353
353
  | Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
354
354
  | Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
@@ -384,7 +384,7 @@ Contributions are welcome. Please open an issue before submitting large changes
384
384
  The recommended v1.9 installation owns one stable unpacked-extension directory and a versioned native-host directory:
385
385
 
386
386
  ```bash
387
- npm exec --yes codex-overleaf-link@2.3.0 -- install-managed
387
+ npm exec --yes codex-overleaf-link@2.3.1 -- install-managed
388
388
  ```
389
389
 
390
390
  Load the printed extension path once from `chrome://extensions`. Starting with the next signed stable release, Codex Overleaf Link checks GitHub Releases automatically, downloads and verifies the coordinated extension/native-host bundle, waits until every connected Overleaf tab is saved and idle, and then updates both components together. A failed health check restores the previous version automatically.
@@ -394,11 +394,11 @@ The stable updater ignores drafts and prereleases, performs no telemetry, and ca
394
394
  The legacy native-only installer remains available for source or unmanaged extension directories:
395
395
 
396
396
  ```bash
397
- npm exec --yes codex-overleaf-link@2.3.0 -- install-native
397
+ npm exec --yes codex-overleaf-link@2.3.1 -- install-native
398
398
  ```
399
399
 
400
400
  Legacy native-only installations can be removed with:
401
401
 
402
402
  ```bash
403
- npm exec --yes codex-overleaf-link@2.3.0 -- uninstall-native
403
+ npm exec --yes codex-overleaf-link@2.3.1 -- uninstall-native
404
404
  ```
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Codex Overleaf Link",
4
- "version": "2.3.0",
4
+ "version": "2.3.1",
5
5
  "description": "Connect the active Overleaf project to a local Codex bridge.",
6
6
  "icons": {
7
7
  "16": "assets/icons/icon16.png",
@@ -21,6 +21,7 @@ if (!globalThis.CodexOverleafNativeRequestIdentity) {
21
21
 
22
22
  const HOST_NAME = 'com.codex.overleaf';
23
23
  const MANAGED_UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
24
+ const MANAGED_UPDATE_CONSENT_KEY = 'codex-overleaf-update-consent-v1';
24
25
  const MANAGED_UPDATE_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
25
26
  const MANAGED_OVERLEAF_MATCHES = [
26
27
  'https://www.overleaf.com/project',
@@ -406,6 +407,25 @@ if (!globalThis.CodexOverleafNativeRequestIdentity) {
406
407
  }
407
408
 
408
409
  const transaction = response.result?.transaction || null;
410
+ if (transaction?.state === 'superseded') {
411
+ const activeVersion = response.result?.activeVersion || chrome.runtime.getManifest().version;
412
+ await chrome.storage.local.remove([
413
+ MANAGED_UPDATE_CONSENT_KEY,
414
+ MANAGED_UPDATE_TABS_KEY
415
+ ]);
416
+ await setManagedUpdateState({
417
+ state: 'idle',
418
+ managed: true,
419
+ currentVersion: activeVersion,
420
+ latestVersion: activeVersion,
421
+ transactionId: '',
422
+ blocker: '',
423
+ blockers: [],
424
+ code: '',
425
+ message: ''
426
+ });
427
+ return;
428
+ }
409
429
  const state = await getManagedUpdateState();
410
430
  const reconciled = ManagedUpdateProjection.reconcile(state, transaction, {
411
431
  currentVersion: chrome.runtime.getManifest().version
@@ -29,12 +29,10 @@
29
29
  <span class="codex-mode-label" data-i18n="mode">Mode</span>
30
30
  <div class="codex-mode-switch" role="group" aria-label="Write mode">
31
31
  <button type="button" data-mode-choice="ask" aria-pressed="false" title="Read and analyze only. Do not write to Overleaf.">Ask</button>
32
- <button type="button" data-mode-choice="confirm" aria-pressed="false" title="Show a change plan first, then write after approval.">Suggest</button>
33
32
  <button type="button" data-mode-choice="auto" aria-pressed="false" title="Write directly after authorization. Deletes still require confirmation.">Auto</button>
34
33
  </div>
35
34
  <select data-mode aria-label="Mode" hidden>
36
35
  <option value="ask">Ask</option>
37
- <option value="confirm">Suggest</option>
38
36
  <option value="auto">Auto</option>
39
37
  </select>
40
38
  </div>
@@ -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
  });
@@ -526,7 +526,6 @@
526
526
  appendOperationsPreview,
527
527
  appendPartialWritebackWarning,
528
528
  appendApplyResult,
529
- renderDiffReview,
530
529
  renderReadOnlyDiffReview,
531
530
  showPluginConfirm,
532
531
  callPageBridge,
@@ -1512,9 +1511,6 @@
1512
1511
  if (mode === 'ask') {
1513
1512
  return 'Ask';
1514
1513
  }
1515
- if (mode === 'confirm') {
1516
- return 'Confirm';
1517
- }
1518
1514
  if (mode === 'auto') {
1519
1515
  return 'Auto';
1520
1516
  }
@@ -1877,7 +1873,7 @@
1877
1873
  if (!useExistingMirror) {
1878
1874
  appendLog(formatProjectSnapshotUserLog(project));
1879
1875
  }
1880
- currentRunView.projectFiles = captureProjectReferenceFiles(project);
1876
+ persistCurrentProjectReferenceFiles(project);
1881
1877
  const snapshotWarnings = useExistingMirror
1882
1878
  ? { blocking: [], nonBlocking: [] }
1883
1879
  : getProjectSnapshotWarnings(project);
@@ -1924,7 +1920,7 @@
1924
1920
  skippedFiles: [],
1925
1921
  blockedFiles: snapshotWarnings.blocking.map(reason => ({ path: 'project', reason }))
1926
1922
  });
1927
- finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
1923
+ await finishRunView(tx('Blocked: full project was not read', '已阻止:没有读到完整项目'), 'failed');
1928
1924
  return;
1929
1925
  }
1930
1926
 
@@ -2011,7 +2007,7 @@
2011
2007
  reason: finding.detectorId || 'sensitive'
2012
2008
  }))
2013
2009
  });
2014
- finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
2010
+ await finishRunView(tx('Blocked: sensitive content review required', '已阻止:需要处理敏感内容'), 'failed');
2015
2011
  return;
2016
2012
  }
2017
2013
  if (sensitiveFindings.findings.length) {
@@ -2049,7 +2045,7 @@
2049
2045
  return;
2050
2046
  }
2051
2047
  project = staleRetry.project;
2052
- currentRunView.projectFiles = captureProjectReferenceFiles(project);
2048
+ persistCurrentProjectReferenceFiles(project);
2053
2049
  useExistingMirror = false;
2054
2050
  fileOverlays = null;
2055
2051
  otWarmStart = false;
@@ -2104,7 +2100,7 @@
2104
2100
  } else {
2105
2101
  appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
2106
2102
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
2107
- finishRunView(tx('Cancelled', '已取消'), 'rejected');
2103
+ await finishRunView(tx('Cancelled', '已取消'), 'rejected');
2108
2104
  return;
2109
2105
  }
2110
2106
  }
@@ -2112,7 +2108,7 @@
2112
2108
  if (!response.ok) {
2113
2109
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2114
2110
  appendRunCancelledReport();
2115
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2111
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2116
2112
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2117
2113
  return;
2118
2114
  }
@@ -2199,7 +2195,7 @@
2199
2195
  sensitiveFindings: sensitiveFindings.findings,
2200
2196
  blockedFiles: [{ path: 'codex.run', reason: response.error?.code || response.error?.message || 'native_error' }]
2201
2197
  });
2202
- finishRunView(response.error?.code === 'project_locked'
2198
+ await finishRunView(response.error?.code === 'project_locked'
2203
2199
  ? tx('Codex task already running', 'Codex 任务正在运行')
2204
2200
  : tx('Local Codex error', '本地 Codex 错误'), 'failed');
2205
2201
  return;
@@ -2249,7 +2245,7 @@
2249
2245
  sensitiveFindings: sensitiveFindings.findings,
2250
2246
  blockedFiles: [{ path: 'codex.run', reason: 'codex_no_usable_result' }]
2251
2247
  });
2252
- finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
2248
+ await finishRunView(tx('Local Codex returned nothing usable', '本地 Codex 没有返回可用结果'), 'failed');
2253
2249
  return;
2254
2250
  }
2255
2251
  const writebackProject = useExistingMirror
@@ -2272,7 +2268,7 @@
2272
2268
  resultStatus: syncOutcome.hasSkippedOperations ? 'completed_with_skips' : 'completed'
2273
2269
  });
2274
2270
 
2275
- finishRunView(
2271
+ await finishRunView(
2276
2272
  syncOutcome.hasSkippedOperations ? tx('Sync completed with skipped items', '同步完成但有跳过项') : tx('Sync completed', '同步完成'),
2277
2273
  syncOutcome.hasSkippedOperations ? 'failed' : 'completed'
2278
2274
  );
@@ -2366,7 +2362,7 @@
2366
2362
  } catch (error) {
2367
2363
  if (runCancellationRequested || isRunCancellationError(error)) {
2368
2364
  appendRunCancelledReport();
2369
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2365
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2370
2366
  void finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2371
2367
  return;
2372
2368
  }
@@ -2416,7 +2412,7 @@
2416
2412
  resultStatus: 'failed',
2417
2413
  blockedFiles: [{ path: 'task', reason: error.message }]
2418
2414
  });
2419
- finishRunView(tx('Task failed', '任务失败'), 'failed');
2415
+ await finishRunView(tx('Task failed', '任务失败'), 'failed');
2420
2416
  } finally {
2421
2417
  const settledView = currentRunView;
2422
2418
  const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
@@ -2489,7 +2485,7 @@
2489
2485
  if (runCancellationRequested || isRunCancellationError(response.error)) {
2490
2486
  appendRunCancelledReport();
2491
2487
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'cancelled' });
2492
- finishRunView(tx('Cancelled', '已中断'), 'rejected');
2488
+ await finishRunView(tx('Cancelled', '已中断'), 'rejected');
2493
2489
  return;
2494
2490
  }
2495
2491
  const translated = translateRawError(response.error.message, { mode: submittedMode, locale: getLocale() });
@@ -2522,7 +2518,7 @@
2522
2518
  resultStatus: 'failed',
2523
2519
  blockedFiles: [{ path: 'skill-installer', reason: response.error?.code || response.error?.message || 'native_error' }]
2524
2520
  });
2525
- finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
2521
+ await finishRunView(tx('Skill installer failed', 'Skill installer 失败'), 'failed');
2526
2522
  return;
2527
2523
  }
2528
2524
 
@@ -2536,7 +2532,7 @@
2536
2532
  mode: submittedMode
2537
2533
  });
2538
2534
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'completed' });
2539
- finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
2535
+ await finishRunView(tx('Skill installer completed', 'Skill installer 已完成'), 'completed');
2540
2536
 
2541
2537
  const runSessionForHistory = findSessionById(runSessionId) || getActiveSession(state);
2542
2538
  const rawAssistantMessage = assistantMessage;
@@ -2659,7 +2655,7 @@
2659
2655
  const finishTitle = requireReviewing
2660
2656
  ? tx('Not started: could not enable Track Changes', '未开始:无法开启留痕')
2661
2657
  : tx('Not started: could not switch to Editing', '未开始:无法切换到 Editing');
2662
- finishRunView(finishTitle, 'failed');
2658
+ await finishRunView(finishTitle, 'failed');
2663
2659
  return {
2664
2660
  ok: false,
2665
2661
  reason,
@@ -3857,10 +3853,6 @@
3857
3853
  return diffReviewPanel.createDiffReviewElement(syncChanges, options);
3858
3854
  }
3859
3855
 
3860
- function renderDiffReview(syncChanges) {
3861
- return diffReviewPanel.renderDiffReview(syncChanges);
3862
- }
3863
-
3864
3856
  function renderReadOnlyDiffReview(syncChanges, title = tr('diffWrittenChangesTitle')) {
3865
3857
  return diffReviewPanel.renderReadOnlyDiffReview(syncChanges, title);
3866
3858
  }
@@ -4623,80 +4615,11 @@
4623
4615
  mode,
4624
4616
  operations: [],
4625
4617
  applyResults: [],
4626
- nextStep: tx('Continue the conversation, or switch to Suggest/Auto to let Codex edit files.', '可以继续追问,或切换到建议修改/自动写入后让 Codex 修改文件。')
4618
+ nextStep: tx('Continue the conversation, or switch to Auto to let Codex edit files.', '可以继续追问,或切换到自动写入后让 Codex 修改文件。')
4627
4619
  });
4628
4620
  return { status: tr('modeAsk'), summaryLine };
4629
4621
  }
4630
4622
 
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
4623
  if (result.status === 'delete_plan_required') {
4701
4624
  const operations = result.operations || [];
4702
4625
  const applyResults = [];
@@ -4769,7 +4692,7 @@
4769
4692
 
4770
4693
  const operations = result.operations || [];
4771
4694
  appendOperationsPreview(operations, tx('Preparing to write', '准备写入'));
4772
- const applied = await applyTaskOperations(project, operations, { allowHighRisk: mode === 'confirm' });
4695
+ const applied = await applyTaskOperations(project, operations);
4773
4696
  appendApplyResult(applied);
4774
4697
  recordUndoFromApply(project, applied);
4775
4698
  appendLog(mode === 'auto' ? tx('Auto write task completed.', '自动写入任务完成。') : tx('Task completed.', '任务完成。'));
@@ -5089,9 +5012,6 @@
5089
5012
  if (mode === 'ask') {
5090
5013
  return tr('modeAsk');
5091
5014
  }
5092
- if (mode === 'confirm') {
5093
- return tr('modeConfirm');
5094
- }
5095
5015
  if (mode === 'auto') {
5096
5016
  return tr('modeAuto');
5097
5017
  }
@@ -5103,8 +5023,6 @@
5103
5023
  const labels = {
5104
5024
  completed: tx('Completed', '已完成'),
5105
5025
  rejected: tx('Cancelled', '已取消'),
5106
- 'confirm failed': tx('Confirmation failed', '确认失败'),
5107
- 'confirmed and applied': tx('Suggested changes applied', '已应用建议修改'),
5108
5026
  'applied with delete plan': tx('Applied with confirmed deletes', '已应用并删除确认项'),
5109
5027
  'applied without deletes': tx('Applied without deletes', '已应用非删除修改'),
5110
5028
  '只问不改': tr('modeAsk')
@@ -5581,7 +5499,7 @@
5581
5499
  history: Array.isArray(session.history) ? session.history : [],
5582
5500
  task: typeof session.task === 'string' ? session.task : '',
5583
5501
  pendingInputs: Array.isArray(session.pendingInputs) ? session.pendingInputs : [],
5584
- mode: session.mode || prefs.mode || 'confirm', providerId: session.providerId || 'builtin',
5502
+ mode: session.mode || prefs.mode || 'ask', providerId: session.providerId || 'builtin',
5585
5503
  model: session.model || prefs.model || 'gpt-5.4',
5586
5504
  reasoningEffort: session.reasoningEffort || prefs.reasoningEffort || 'high',
5587
5505
  speedTier: session.speedTier || prefs.speedTier || 'standard',
@@ -5984,7 +5902,7 @@
5984
5902
  }
5985
5903
 
5986
5904
  async function selectMode(mode) {
5987
- if (!['ask', 'confirm', 'auto'].includes(mode)) {
5905
+ if (!['ask', 'auto'].includes(mode)) {
5988
5906
  return;
5989
5907
  }
5990
5908
  const modeSelect = panel?.querySelector('[data-mode]');
@@ -6268,7 +6186,11 @@
6268
6186
  events: root.querySelector('[data-run-events]'),
6269
6187
  report: root.querySelector('[data-run-report]'),
6270
6188
  status: root.querySelector('[data-run-status]'),
6271
- projectFiles: captureProjectReferenceFiles(arguments[0]?.project),
6189
+ // A run starts before warm-mirror resolution has produced its lightweight
6190
+ // project snapshot. Seed references from the already-prefetched project
6191
+ // inventory so early stream events and fast Ask responses can still
6192
+ // resolve project-relative locations.
6193
+ projectFiles: getCurrentProjectReferenceFiles(),
6272
6194
  startedAt: Date.now(),
6273
6195
  queueItemId,
6274
6196
  nativeRequestId: '',
@@ -6289,7 +6211,7 @@
6289
6211
  return updatedAt;
6290
6212
  }
6291
6213
 
6292
- function finishRunView(text, status) {
6214
+ async function finishRunView(text, status) {
6293
6215
  if (!currentRunView) {
6294
6216
  return;
6295
6217
  }
@@ -6316,7 +6238,11 @@
6316
6238
  const navigationDivergent = currentRunView.runProjectId
6317
6239
  && activeProjectId !== currentRunView.runProjectId;
6318
6240
  if (!navigationDivergent) {
6319
- saveStateSoon();
6241
+ // A terminal badge is a durability promise: once the user can see
6242
+ // Done/Failed/Cancelled, an immediate reload must hydrate that same
6243
+ // terminal state. Drain any older running snapshot and persist the
6244
+ // latest record before exposing the terminal UI.
6245
+ await flushQueuedSaveState().catch(() => {});
6320
6246
  }
6321
6247
  renderSessionList();
6322
6248
  }
@@ -6636,6 +6562,39 @@
6636
6562
 
6637
6563
 
6638
6564
 
6565
+ function getCurrentProjectMathSources() {
6566
+ const sources = [];
6567
+ const seen = new Set();
6568
+ let remainingChars = 256 * 1024;
6569
+ const add = item => {
6570
+ if (!item || typeof item !== 'object' || typeof item.content !== 'string') return;
6571
+ const path = normalizeReferencePathForRuntime(item.path || item.filePath || '');
6572
+ if (!path || seen.has(path) || hasUnsafeRuntimePathSegments(path)
6573
+ || !/\.(?:tex|sty|cls|ltx)$/i.test(path) || remainingChars <= 0) return;
6574
+ const content = item.content.slice(0, Math.min(64 * 1024, remainingChars));
6575
+ remainingChars -= content.length;
6576
+ seen.add(path);
6577
+ sources.push({ path, content });
6578
+ };
6579
+ for (const collection of [
6580
+ currentRunView?.projectFiles,
6581
+ currentRunView?.files,
6582
+ currentRunView?.project?.files,
6583
+ currentRunView?.snapshot?.files,
6584
+ currentRunView?.projectSnapshot?.files,
6585
+ currentRunView?.contextFiles,
6586
+ state?.projectFiles,
6587
+ state?.files,
6588
+ state?.project?.files,
6589
+ state?.snapshot?.files,
6590
+ state?.projectSnapshot?.files,
6591
+ state?.contextFiles
6592
+ ]) {
6593
+ if (Array.isArray(collection)) collection.forEach(add);
6594
+ }
6595
+ return sources;
6596
+ }
6597
+
6639
6598
  function getCurrentProjectReferenceFiles() {
6640
6599
  const files = [];
6641
6600
  const seen = new Set();
@@ -6686,7 +6645,14 @@
6686
6645
  }
6687
6646
  };
6688
6647
 
6648
+ const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
6689
6649
  addFiles(currentRunView?.projectFiles);
6650
+ addFiles(activeSession?.projectReferenceFiles);
6651
+ // ContextTray owns the exact, background-prefetched project file list.
6652
+ // Warm-mirror runs intentionally carry only the active/focused overlay, so
6653
+ // omitting this inventory makes valid references to every other project
6654
+ // file render as plain text.
6655
+ addFiles(contextTrayController?.getContextProject?.()?.files);
6690
6656
  addFiles(currentRunView?.files);
6691
6657
  addFiles(currentRunView?.project?.files);
6692
6658
  addFiles(currentRunView?.snapshot?.files);
@@ -6700,14 +6666,26 @@
6700
6666
  addFiles(state?.contextFiles);
6701
6667
  addTextPaths(state?.focusFiles);
6702
6668
  addTextPaths(state?.session?.focusFiles);
6703
- const activeSession = typeof getActiveSession === 'function' ? getActiveSession(state) : null;
6704
6669
  addTextPaths(activeSession?.focusFiles);
6705
6670
  for (const session of Array.isArray(state?.sessions) ? state.sessions : []) {
6671
+ addFiles(session?.projectReferenceFiles);
6706
6672
  addTextPaths(session?.focusFiles);
6707
6673
  }
6708
6674
  return files;
6709
6675
  }
6710
6676
 
6677
+ function persistCurrentProjectReferenceFiles(project) {
6678
+ const projectFiles = captureProjectReferenceFiles(project);
6679
+ if (currentRunView) {
6680
+ currentRunView.projectFiles = projectFiles;
6681
+ }
6682
+ if (getActiveSession(state)) {
6683
+ state = updateActiveSession(state, { projectReferenceFiles: projectFiles });
6684
+ saveStateSoon();
6685
+ }
6686
+ return projectFiles;
6687
+ }
6688
+
6711
6689
  function captureProjectReferenceFiles(project) {
6712
6690
  const result = [];
6713
6691
  const seen = new Set();
@@ -7167,7 +7145,12 @@
7167
7145
  record.undoOperations = [];
7168
7146
  record.undoBaseFiles = [];
7169
7147
  record.undoTrackedChanges = combinedTrackedChanges;
7170
- record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(project, combinedAppliedOperations, combinedTrackedChanges);
7148
+ record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
7149
+ project,
7150
+ combinedAppliedOperations,
7151
+ combinedTrackedChanges,
7152
+ record.undoExpectedFiles
7153
+ );
7171
7154
  record.undoStatus = '';
7172
7155
  record.partialWriteback = skippedEntries.length > 0;
7173
7156
  // Recording tracked-change refs enters the run into the tracked-change
@@ -7215,7 +7198,12 @@
7215
7198
  record.undoOperations = checkpoint.undoOperations;
7216
7199
  record.undoBaseFiles = checkpoint.undoBaseFiles;
7217
7200
  record.undoTrackedChanges = [];
7218
- record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(project, combinedAppliedOperations, []);
7201
+ record.undoExpectedFiles = selectExpectedFilesForTrackedUndo(
7202
+ project,
7203
+ combinedAppliedOperations,
7204
+ [],
7205
+ record.undoExpectedFiles
7206
+ );
7219
7207
  record.undoStatus = '';
7220
7208
  record.partialWriteback = skippedEntries.length > 0;
7221
7209
  refreshRunCardControls(record.id);
@@ -7235,8 +7223,18 @@
7235
7223
  return WritebackSettlement.normalizeApplyTrackedChanges(changes);
7236
7224
  }
7237
7225
 
7238
- function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = []) {
7239
- return WritebackSettlement.selectExpectedFilesForTrackedUndo(project, operations, trackedChanges);
7226
+ function selectExpectedFilesForTrackedUndo(
7227
+ project,
7228
+ operations = [],
7229
+ trackedChanges = [],
7230
+ previousExpectedFiles = []
7231
+ ) {
7232
+ return WritebackSettlement.selectExpectedFilesForTrackedUndo(
7233
+ project,
7234
+ operations,
7235
+ trackedChanges,
7236
+ previousExpectedFiles
7237
+ );
7240
7238
  }
7241
7239
 
7242
7240
  function buildTrackedUndoPostFiles(run) {