codex-overleaf-link 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +25 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/runtime-manifest.json +5 -71
  4. package/extension/src/background.js +53 -69
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/contentRuntime.js +650 -1449
  8. package/extension/src/content/contextTray.js +4 -6
  9. package/extension/src/content/diagnosticsController.js +1 -2
  10. package/extension/src/content/diffReviewPanel.js +14 -13
  11. package/extension/src/content/generated/content.bundle.js +41370 -0
  12. package/extension/src/content/generated/content.bundle.meta.json +102 -0
  13. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  14. package/extension/src/content/markdownText.js +5 -7
  15. package/extension/src/content/modelPicker.js +1 -1
  16. package/extension/src/content/modelPickerSupport.js +10 -5
  17. package/extension/src/content/moduleRegistryKernel.js +125 -0
  18. package/extension/src/content/nativeCompatibilityController.js +313 -0
  19. package/extension/src/content/otWarmMirror.js +7 -5
  20. package/extension/src/content/pageBridgeClient.js +224 -0
  21. package/extension/src/content/panelMaintenance.js +8 -3
  22. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  23. package/extension/src/content/projectProviderSelection.js +4 -12
  24. package/extension/src/content/projectSessionCleanup.js +3 -3
  25. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  26. package/extension/src/content/providerSettingsDialog.js +30 -18
  27. package/extension/src/content/recentProjects.js +15 -23
  28. package/extension/src/content/runGuidanceController.js +1 -1
  29. package/extension/src/content/runQueueScheduler.js +129 -29
  30. package/extension/src/content/runSettlementPersistence.js +51 -0
  31. package/extension/src/content/runTimelineView.js +11 -8
  32. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  33. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  34. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  35. package/extension/src/content/sessionManager.js +15 -12
  36. package/extension/src/content/sessionPersistence.js +67 -8
  37. package/extension/src/content/updateNotice.js +10 -4
  38. package/extension/src/content/writebackOrchestrator.js +91 -23
  39. package/extension/src/contentScript.js +5 -1
  40. package/extension/src/page/trackedChangesLifecycle.js +6 -1
  41. package/extension/src/pageBridge.js +47 -98
  42. package/extension/src/shared/agentTranscript.js +8 -5
  43. package/extension/src/shared/compatibility.js +1 -1
  44. package/extension/src/shared/governanceRules.js +1 -1
  45. package/extension/src/shared/managedUpdateProjection.js +337 -0
  46. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  47. package/extension/src/shared/otText.js +0 -19
  48. package/extension/src/shared/pageRpcContract.js +136 -0
  49. package/extension/src/shared/runExecutionSnapshot.js +177 -0
  50. package/extension/src/shared/runExecutionSnapshotCodec.js +168 -0
  51. package/extension/src/shared/runInputQueue.js +81 -34
  52. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  53. package/extension/src/shared/scopedPersistenceQueuePolicy.js +181 -0
  54. package/extension/src/shared/scopedPersistenceTransaction.js +238 -0
  55. package/extension/src/shared/sessionState.js +64 -48
  56. package/extension/src/shared/settlementFacts.js +351 -0
  57. package/extension/src/shared/storageDb.js +19 -5
  58. package/extension/src/shared/storageMigration.js +275 -61
  59. package/extension/src/shared/storageSessionClaims.js +60 -0
  60. package/extension/src/shared/updateConsent.js +4 -0
  61. package/extension/src/shared/updateRevocationIntent.js +109 -0
  62. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  63. package/extension/src/shared/writebackOperationEvidence.js +143 -0
  64. package/extension/src/shared/writebackSettlement.js +705 -0
  65. package/native-host/src/codexRuntimeIdentity.js +1 -1
  66. package/native-host/src/codexSessionRunner.js +7 -13
  67. package/native-host/src/index.js +3 -69
  68. package/native-host/src/managedInstall.js +50 -8
  69. package/native-host/src/mirrorWorkspace.js +15 -8
  70. package/native-host/src/nativeTransportEnvelope.js +148 -0
  71. package/native-host/src/taskRunnerRuntime.js +3 -3
  72. package/package.json +13 -5
@@ -0,0 +1,47 @@
1
+ (function (root, factory) {
2
+ var api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafScopedPersistenceDetachedCommit = api;
5
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
6
+ 'use strict';
7
+
8
+ function create(options) {
9
+ options = options || {};
10
+ var Transaction = options.Transaction;
11
+ if (!Transaction
12
+ || typeof Transaction.createBrowserAdapter !== 'function'
13
+ || typeof Transaction.createCoordinator !== 'function') {
14
+ return null;
15
+ }
16
+ var adapter = Transaction.createBrowserAdapter({ chromeApi: options.chromeApi });
17
+ return async function commitDetached(projectId, accountScopeId, commitOptions, action) {
18
+ var nextScope = {
19
+ accountScopeId: String(accountScopeId || '').trim(),
20
+ projectId: String(projectId || '').trim()
21
+ };
22
+ if (!nextScope.accountScopeId) {
23
+ return { ok: false, reason: 'account_scope_unavailable' };
24
+ }
25
+ if (!nextScope.projectId) {
26
+ return { ok: false, reason: 'project_scope_unavailable' };
27
+ }
28
+ var coordinator = Transaction.createCoordinator({
29
+ adapter: adapter,
30
+ writerId: options.writerId
31
+ });
32
+ var hydration = await coordinator.beginHydration(nextScope);
33
+ if (!coordinator.isCurrent(hydration)) {
34
+ return { ok: false, reason: 'stale_view' };
35
+ }
36
+ var view = await coordinator.captureCommitView(nextScope, { detached: true });
37
+ return coordinator.commit(view, action, {
38
+ rebase: !commitOptions || commitOptions.rebase !== false,
39
+ queueMutation: commitOptions && commitOptions.queueMutation,
40
+ queueMutations: commitOptions && commitOptions.queueMutations,
41
+ sessionTombstones: commitOptions && commitOptions.sessionTombstones
42
+ });
43
+ };
44
+ }
45
+
46
+ return Object.freeze({ create: create });
47
+ });
@@ -0,0 +1,111 @@
1
+ (function (root, factory) {
2
+ var api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafScopedPersistencePanelState = api;
5
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
6
+ 'use strict';
7
+
8
+ function cloneSnapshot(value) {
9
+ if (!value || typeof value !== 'object') {
10
+ return value;
11
+ }
12
+ if (typeof structuredClone === 'function') {
13
+ return structuredClone(value);
14
+ }
15
+ return JSON.parse(JSON.stringify(value));
16
+ }
17
+
18
+ async function persistPanelState(input) {
19
+ var state = cloneSnapshot(input.state || {});
20
+ var compactState = cloneSnapshot(input.compactState || {});
21
+ var projectId = input.projectId;
22
+ var Migration = input.Migration;
23
+ var StorageDb = input.StorageDb;
24
+ var scope = input.persistenceContext?.scope || {};
25
+ var accountScopeId = typeof scope.accountScopeId === 'string'
26
+ ? scope.accountScopeId.trim()
27
+ : '';
28
+ if (!accountScopeId) {
29
+ throw Object.assign(new Error('Scoped persistence requires an account identity.'), {
30
+ code: 'account_scope_unavailable'
31
+ });
32
+ }
33
+ if (scope.projectId !== projectId) {
34
+ throw Object.assign(new Error('Scoped persistence project changed before commit.'), {
35
+ code: 'stale_view'
36
+ });
37
+ }
38
+ var latestPrefs = typeof Migration.loadPrefs === 'function'
39
+ ? await Migration.loadPrefs(accountScopeId, projectId)
40
+ : {};
41
+ var prefs = Object.assign(
42
+ {},
43
+ latestPrefs && typeof latestPrefs === 'object' ? latestPrefs : {},
44
+ StorageDb.extractLightweightPrefs(compactState, projectId)
45
+ );
46
+ prefs.activeSessionByProject = StorageDb.buildActiveSessionByProject(
47
+ latestPrefs && latestPrefs.activeSessionByProject || {},
48
+ projectId,
49
+ compactState.activeSessionId || state.activeSessionId || ''
50
+ );
51
+ prefs.experimentalOtByProject = Object.assign(
52
+ {},
53
+ latestPrefs && latestPrefs.experimentalOtByProject || {},
54
+ { [projectId]: input.normalizeExperimentalOtByProject(state.experimentalOtByProject)[projectId] === true }
55
+ );
56
+ prefs.governanceRulesByProject = Object.assign(
57
+ {},
58
+ latestPrefs && latestPrefs.governanceRulesByProject || {},
59
+ input.normalizeGovernanceRulesByProject(state.governanceRulesByProject)
60
+ );
61
+ var normalizedCustomProject = input.normalizeCustomInstructionsByProject({ [projectId]: '' });
62
+ var customProjectId = Object.keys(normalizedCustomProject)[0] || '';
63
+ var currentCustom = input.normalizeCustomInstructionsByProject(state.customInstructionsByProject);
64
+ prefs.customInstructionsByProject = Object.assign(
65
+ {},
66
+ latestPrefs && latestPrefs.customInstructionsByProject || {}
67
+ );
68
+ if (customProjectId) {
69
+ prefs.customInstructionsByProject[customProjectId] =
70
+ Object.prototype.hasOwnProperty.call(currentCustom, customProjectId)
71
+ ? currentCustom[customProjectId]
72
+ : '';
73
+ }
74
+ await Migration.savePrefs(prefs, accountScopeId, projectId);
75
+ var persistenceMeta = input.persistenceContext?.nextMeta || {};
76
+ var queueTombstones = persistenceMeta.queueTombstones || {};
77
+ var sessionTombstones = persistenceMeta.sessionTombstones || {};
78
+ var deletedSessionIds = Array.from(new Set([
79
+ ...(Array.isArray(input.deletedSessionIds) ? input.deletedSessionIds : []),
80
+ ...Object.keys(sessionTombstones)
81
+ ]));
82
+ var sessionRecords = (state.sessions || []).map(function (session) {
83
+ return StorageDb.buildSessionRecord(Object.assign({}, session, {
84
+ projectId: projectId,
85
+ accountScopeId: accountScopeId,
86
+ title: session.title || '',
87
+ titleSource: session.titleSource || 'auto',
88
+ codexThreadId: session.codexThreadId || '',
89
+ status: 'active',
90
+ focusFiles: Array.isArray(session.focusFiles) ? session.focusFiles : [],
91
+ createdAt: session.createdAt,
92
+ updatedAt: session.updatedAt,
93
+ pendingInputs: (Array.isArray(session.pendingInputs) ? session.pendingInputs : [])
94
+ .filter(function (item) { return !queueTombstones[item?.id]; })
95
+ }), { preserveRunActionPayload: true });
96
+ });
97
+ await input.SessionPersistence.writeSessions({
98
+ Migration: Migration,
99
+ StorageDb: StorageDb,
100
+ projectId: projectId,
101
+ accountScopeId: accountScopeId,
102
+ deletedSessionIds: deletedSessionIds,
103
+ sessionRecords: sessionRecords,
104
+ queueClaims: persistenceMeta.queueClaims || {},
105
+ queueTombstones: queueTombstones,
106
+ writerId: input.persistenceContext?.writerId || ''
107
+ });
108
+ }
109
+
110
+ return Object.freeze({ persistPanelState: persistPanelState });
111
+ });
@@ -1,17 +1,6 @@
1
1
  (function initCodexOverleafSessionManager() {
2
2
  'use strict';
3
3
 
4
- const SessionPanel = window.CodexOverleafSessionPanel;
5
- const {
6
- createSession,
7
- deleteSession,
8
- setActiveSession,
9
- getActiveSession,
10
- isDisplayableSession,
11
- normalizePanelState,
12
- deriveSessionTitle
13
- } = window.CodexOverleafSessionState;
14
-
15
4
  // Session manager — the session lifecycle + list/delete surface
16
5
  // carved out of contentRuntime.js (v1.4.7 structural-debt phase 3). Code
17
6
  // moved verbatim except that mutable runtime state is read through
@@ -19,6 +8,20 @@
19
8
  // setState() (sessions are immutable-update: every mutation rebuilds state
20
9
  // via the shared sessionState helpers and hands it back to the runtime).
21
10
  function create(deps = {}) {
11
+ const {
12
+ SessionPanel,
13
+ SessionState,
14
+ SessionMenuView
15
+ } = deps;
16
+ const {
17
+ createSession,
18
+ deleteSession,
19
+ setActiveSession,
20
+ getActiveSession,
21
+ isDisplayableSession,
22
+ normalizePanelState,
23
+ deriveSessionTitle
24
+ } = SessionState;
22
25
  const {
23
26
  tr,
24
27
  showPluginConfirm,
@@ -36,7 +39,7 @@
36
39
  formatSessionTime
37
40
  } = deps;
38
41
 
39
- const sessionMenuView = window.CodexOverleafSessionMenuView.create({
42
+ const sessionMenuView = SessionMenuView.create({
40
43
  commitSessionRename,
41
44
  deleteSessionWithConfirm,
42
45
  document,
@@ -1,8 +1,14 @@
1
- (function initCodexOverleafSessionPersistence() {
1
+ (function initCodexOverleafSessionPersistence(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(require('../shared/storageMigration'));
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define('SessionPersistence', ['StorageMigration'], factory);
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function sessionPersistenceFactory(Migration) {
2
8
  'use strict';
3
9
 
4
10
  function migration() {
5
- return window.CodexOverleafStorageMigration;
11
+ return Migration;
6
12
  }
7
13
 
8
14
  async function loadTombstones() {
@@ -21,11 +27,20 @@
21
27
  function isVisibleRecord(record, deletedIds, accountScopeId) {
22
28
  return Boolean(record)
23
29
  && !(deletedIds || []).includes(record.id)
24
- && (!accountScopeId || record.accountScopeId === accountScopeId);
30
+ && Boolean(accountScopeId)
31
+ && record.accountScopeId === accountScopeId;
25
32
  }
26
33
 
27
34
  async function writeSessions(options = {}) {
28
35
  const { StorageDb, projectId } = options;
36
+ const accountScopeId = typeof options.accountScopeId === 'string'
37
+ ? options.accountScopeId.trim()
38
+ : '';
39
+ if (!accountScopeId) {
40
+ throw Object.assign(new Error('Session persistence requires an account identity.'), {
41
+ code: 'account_scope_unavailable'
42
+ });
43
+ }
29
44
  const Migration = options.Migration || migration();
30
45
  const requested = (Array.isArray(options.deletedSessionIds) ? options.deletedSessionIds : [])
31
46
  .filter(id => typeof id === 'string' && id);
@@ -34,8 +49,23 @@
34
49
  : await Migration?.loadSessionTombstones?.() || {};
35
50
  const deleted = new Set(requested);
36
51
  for (const id of Migration?.getDeletedSessionIds?.(tombstones, projectId) || []) deleted.add(id);
37
- const sessionRecords = (options.sessionRecords || []).filter(record => !deleted.has(record.id));
38
- const existingSessions = await StorageDb.getAllByIndex('sessions', 'projectId', projectId);
52
+ const queueTombstones = options.queueTombstones && typeof options.queueTombstones === 'object'
53
+ ? options.queueTombstones
54
+ : {};
55
+ const rawSessionRecords = (options.sessionRecords || [])
56
+ .filter(record => !deleted.has(record.id) && record.accountScopeId === accountScopeId);
57
+ const rawExistingSessions = (await StorageDb.getAllByIndex('sessions', 'projectId', projectId))
58
+ .filter(record => record?.accountScopeId === accountScopeId);
59
+ const rawExistingById = new Map(rawExistingSessions.map(record => [record.id, record]));
60
+ const sessionRecords = rawSessionRecords
61
+ .map(record => applyQueuePolicy(
62
+ record,
63
+ rawExistingById.get(record.id),
64
+ options,
65
+ queueTombstones
66
+ ));
67
+ const existingSessions = rawExistingSessions
68
+ .map(record => applyQueuePolicy(record, record, options, queueTombstones));
39
69
  const deletedExisting = existingSessions.filter(record => deleted.has(record.id));
40
70
  if (deletedExisting.length) {
41
71
  await Promise.all(deletedExisting.map(record => StorageDb.deleteRecord('sessions', record.id)));
@@ -48,15 +78,44 @@
48
78
  if (!existing) return true;
49
79
  return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0);
50
80
  });
51
- if (writable.length) await StorageDb.putRecords('sessions', writable);
81
+ const cleanedExisting = existingSessions.filter((record, index) =>
82
+ record !== rawExistingSessions[index] && !writable.some(item => item.id === record.id)
83
+ );
84
+ if (cleanedExisting.length || writable.length) {
85
+ await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable]);
86
+ }
52
87
  return writable;
53
88
  }
54
89
 
55
- window.CodexOverleafSessionPersistence = {
90
+ function applyQueuePolicy(record, existingRecord, options, queueTombstones) {
91
+ const original = Array.isArray(record?.pendingInputs) ? record.pendingInputs : [];
92
+ let pendingInputs = original.filter(item => item?.id && !queueTombstones[item.id]);
93
+ const existingById = new Map(
94
+ (Array.isArray(existingRecord?.pendingInputs) ? existingRecord.pendingInputs : [])
95
+ .filter(item => item?.id && !queueTombstones[item.id])
96
+ .map(item => [item.id, item])
97
+ );
98
+ const claims = options.queueClaims && typeof options.queueClaims === 'object'
99
+ ? options.queueClaims
100
+ : {};
101
+ for (const claim of Object.values(claims)) {
102
+ if (!claim?.itemId || claim.ownerId === options.writerId) continue;
103
+ const index = pendingInputs.findIndex(item => item.id === claim.itemId);
104
+ const authoritative = existingById.get(claim.itemId);
105
+ if (authoritative && index >= 0) pendingInputs[index] = authoritative;
106
+ else if (authoritative) pendingInputs.push(authoritative);
107
+ else if (index >= 0) pendingInputs.splice(index, 1);
108
+ }
109
+ const unchanged = pendingInputs.length === original.length
110
+ && pendingInputs.every((item, index) => item === original[index]);
111
+ return unchanged ? record : { ...record, pendingInputs };
112
+ }
113
+
114
+ return {
56
115
  loadTombstones,
57
116
  getDeletedSessionIds,
58
117
  addTombstones,
59
118
  isVisibleRecord,
60
119
  writeSessions
61
120
  };
62
- })();
121
+ });
@@ -1,4 +1,10 @@
1
- (function initCodexOverleafUpdateNotice(root) {
1
+ (function initCodexOverleafUpdateNotice(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(require('../shared/managedUpdateProjection'));
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define('UpdateNotice', ['ManagedUpdateProjection'], factory);
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function updateNoticeFactory(UpdateProjection) {
2
8
  'use strict';
3
9
 
4
10
  let notice = null;
@@ -11,7 +17,7 @@
11
17
  let settingsActionInFlight = false;
12
18
  let restartWatchdog = null;
13
19
  let manualCommandCopied = false;
14
- const ACTIVE_UPDATE_STATES = new Set(['downloading', 'staged', 'waiting_for_idle', 'applying', 'awaiting_health']);
20
+ const ACTIVE_UPDATE_STATES = new Set(UpdateProjection.activePhases('panel'));
15
21
  const RESTART_WATCHDOG_MS = 30 * 1000;
16
22
 
17
23
  function mount(panelLike, options = {}) {
@@ -467,5 +473,5 @@
467
473
  return /^zh\b/i.test(locale) ? chinese : english;
468
474
  }
469
475
 
470
- root.CodexOverleafUpdateNotice = Object.freeze({ mount });
471
- })(window);
476
+ return Object.freeze({ mount });
477
+ });
@@ -53,11 +53,21 @@
53
53
  let buildReviewingBlockedApplyResult;
54
54
  let ensureReviewingBeforeWrite;
55
55
  let confirmBinaryOperations;
56
+ let compileAdapter;
56
57
  let filterSyncChangesByOperations;
57
58
  let writebackController;
58
59
  let RUN_SNAPSHOT_ZIP_TIMEOUT_MS;
59
60
  let getState;
60
61
  let getCurrentRunView;
62
+ let onMirrorRefreshSettled;
63
+ let writebackSettlement;
64
+ let injectedWritebackSettlement;
65
+
66
+ function buildSettlement(input = {}) {
67
+ return writebackSettlement?.settle instanceof Function
68
+ ? writebackSettlement.settle(input)
69
+ : null;
70
+ }
61
71
 
62
72
  async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
63
73
  const runMode = options.mode || getState().mode;
@@ -93,6 +103,10 @@
93
103
  return {
94
104
  summaryLine: assistantMessage || tx('Ask mode completed without Overleaf changes', 'Ask 模式已完成,未修改 Overleaf'),
95
105
  hasSkippedOperations: false,
106
+ settlement: buildSettlement({
107
+ operations: [],
108
+ applyResult: { ok: true, applied: [], skipped: [] }
109
+ }),
96
110
  audit: buildAuditSummaryFromApply({
97
111
  operations: [],
98
112
  resultStatus: 'ask_ignored_local_changes',
@@ -138,16 +152,23 @@
138
152
  conclusion: assistantMessage || tx('Codex finished locally. There are no changes to sync back to Overleaf.', 'Codex 已完成本地处理,没有需要同步回 Overleaf 的改动。'),
139
153
  status: runMode === 'ask' ? tr('modeAsk') : 'completed',
140
154
  operations: [],
141
- applyResults: [],
142
- unchangedReason: assistantMessage
143
- ? tx('No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。')
144
- : formatUnsupportedLocalChangeSummary(unsupportedChanges),
155
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
156
+ unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
157
+ || (assistantMessage ? tx('No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。') : formatUnsupportedLocalChangeSummary(unsupportedChanges)),
145
158
  mode: runMode,
146
- nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
159
+ nextStep: additionalSkippedEntries.length ? formatWritebackSkippedNextStep({ ok: false, applied: [], skipped: additionalSkippedEntries }) : tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
147
160
  });
148
161
  return {
149
162
  summaryLine: assistantMessage || tx('No changes to sync', '没有需要同步的改动'),
150
163
  hasSkippedOperations: additionalSkippedEntries.length > 0,
164
+ settlement: buildSettlement({
165
+ operations: buildSyncApplyOperations(syncChanges, project),
166
+ applyResult: {
167
+ ok: additionalSkippedEntries.length === 0,
168
+ applied: [],
169
+ skipped: additionalSkippedEntries
170
+ }
171
+ }),
151
172
  audit: buildAuditSummaryFromApply({
152
173
  operations: buildSyncApplyOperations(syncChanges, project),
153
174
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -182,6 +203,10 @@
182
203
  return {
183
204
  summaryLine: tx('Sync cancelled', '已取消同步'),
184
205
  hasSkippedOperations: additionalSkippedEntries.length > 0,
206
+ settlement: buildSettlement({
207
+ operations,
208
+ applyResult: { ok: false, applied: [], skipped: additionalSkippedEntries }
209
+ }),
185
210
  audit: buildAuditSummaryFromApply({
186
211
  operations,
187
212
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -239,14 +264,23 @@
239
264
  conclusion: assistantMessage || tx('No approved changes were written back to Overleaf.', '没有已确认的改动写回 Overleaf。'),
240
265
  status: runMode === 'ask' ? tr('modeAsk') : 'completed',
241
266
  operations: [],
242
- applyResults: [],
243
- unchangedReason: tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
267
+ applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
268
+ unchangedReason: additionalSkippedEntries.find(item => item?.result?.reason)?.result.reason
269
+ || tx('No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。'),
244
270
  mode: runMode,
245
- nextStep: tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
271
+ nextStep: additionalSkippedEntries.length ? formatWritebackSkippedNextStep({ ok: false, applied: [], skipped: additionalSkippedEntries }) : tx('You can continue the conversation, or adjust @context and run again.', '可以继续追问,或调整 @context 后重新运行。')
246
272
  });
247
273
  return {
248
274
  summaryLine: tx('No changes applied', '没有应用改动'),
249
275
  hasSkippedOperations: additionalSkippedEntries.length > 0,
276
+ settlement: buildSettlement({
277
+ operations: buildSyncApplyOperations(syncChanges, project),
278
+ applyResult: {
279
+ ok: additionalSkippedEntries.length === 0,
280
+ applied: [],
281
+ skipped: additionalSkippedEntries
282
+ }
283
+ }),
250
284
  audit: buildAuditSummaryFromApply({
251
285
  operations: buildSyncApplyOperations(syncChanges, project),
252
286
  applyResults: additionalSkippedEntries.length ? [{ ok: false, applied: [], skipped: additionalSkippedEntries }] : [],
@@ -283,6 +317,7 @@
283
317
  '已阻止写入:未确认 Overleaf Reviewing/Track Changes'
284
318
  ),
285
319
  hasSkippedOperations: true,
320
+ settlement: buildSettlement({ operations, applyResult: blocked }),
286
321
  audit: buildAuditSummaryFromApply({
287
322
  operations,
288
323
  applyResults: [blocked],
@@ -342,6 +377,7 @@
342
377
  // pending promise is exposed so undoRun and the next runTask barrier on
343
378
  // it — a mirror.sync landing AFTER an undo would otherwise push the
344
379
  // pre-undo snapshot as the local baseline.
380
+ const mirrorSettlementTarget = getCurrentRunView?.();
345
381
  pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification)
346
382
  .catch(error => {
347
383
  appendRunEvent({
@@ -351,12 +387,25 @@
351
387
  ),
352
388
  status: 'failed'
353
389
  });
390
+ return 'failed';
391
+ })
392
+ .then(state => {
393
+ if (typeof onMirrorRefreshSettled !== 'function') return;
394
+ return onMirrorRefreshSettled({
395
+ recordId: mirrorSettlementTarget?.recordId || '',
396
+ sessionId: mirrorSettlementTarget?.sessionId || '',
397
+ runProjectId: mirrorSettlementTarget?.runProjectId || '',
398
+ state: state || 'not-attempted'
399
+ });
354
400
  })
355
401
  .finally(() => {
356
402
  pendingMirrorRefresh = null;
357
403
  });
358
404
  const compileSummary = appliedPaths.length
359
- ? await autoRecompileAfterWriteback(appliedPaths, saveVerification).catch(error => {
405
+ ? await autoRecompileAfterWriteback(appliedPaths, saveVerification, {
406
+ autoRecompile: options.autoRecompile,
407
+ mode: runMode
408
+ }).catch(error => {
360
409
  appendRunEvent({
361
410
  title: tx(`Post-write compile failed: ${error.message}`, `写后编译出错:${error.message}`),
362
411
  status: 'failed'
@@ -419,6 +468,17 @@
419
468
  // Existing call sites that only read `summaryLine` / `hasSkippedOperations`
420
469
  // / `audit` are unaffected.
421
470
  applied,
471
+ settlement: buildSettlement({
472
+ operations,
473
+ applyResult: applied,
474
+ saveVerification,
475
+ mirror: {
476
+ state: appliedPaths.length && saveVerification?.state === 'verified_saved'
477
+ ? 'pending'
478
+ : 'not_started'
479
+ },
480
+ compile: compileSummary
481
+ }),
422
482
  audit: buildAuditSummaryFromApply({
423
483
  operations,
424
484
  applyResults: [applied],
@@ -447,8 +507,8 @@
447
507
  if (result?.ok === true && !result?.state) {
448
508
  return {
449
509
  ...result,
450
- ok: true,
451
- state: 'verified_saved'
510
+ ok: false,
511
+ state: 'verified_quiet'
452
512
  };
453
513
  }
454
514
  if (result?.state === 'unknown_timeout' || result?.state === 'unavailable') {
@@ -522,10 +582,10 @@
522
582
 
523
583
  async function refreshProjectMirrorAfterWriteback(project = {}, applied = {}, saveVerification = {}) {
524
584
  if (saveVerification?.state !== 'verified_saved') {
525
- return;
585
+ return 'not-attempted';
526
586
  }
527
587
  if (!Array.isArray(applied?.applied) || !applied.applied.length) {
528
- return;
588
+ return 'not-attempted';
529
589
  }
530
590
 
531
591
  if (await tryConfirmMirrorWriteback(applied)) {
@@ -536,7 +596,7 @@
536
596
  ),
537
597
  status: 'completed'
538
598
  });
539
- return;
599
+ return 'complete';
540
600
  }
541
601
 
542
602
  appendRunEvent({
@@ -567,7 +627,7 @@
567
627
  ),
568
628
  status: 'failed'
569
629
  });
570
- return;
630
+ return 'failed';
571
631
  }
572
632
 
573
633
  const syncedProject = mergeVerifiedAppliedFiles(freshProject, project, applied);
@@ -584,7 +644,7 @@
584
644
  title: tx('Local Codex workspace refreshed. The next run will start from the latest Overleaf content.', '已刷新本地 Codex workspace,下一轮会从最新 Overleaf 内容开始。'),
585
645
  status: 'completed'
586
646
  });
587
- return;
647
+ return 'complete';
588
648
  }
589
649
 
590
650
  const message = response?.error?.message || tx('native host did not respond', 'native host 没有响应');
@@ -595,6 +655,7 @@
595
655
  ),
596
656
  status: 'failed'
597
657
  });
658
+ return 'failed';
598
659
  }
599
660
 
600
661
  function mergeVerifiedAppliedFiles(freshProject = {}, originalProject = {}, applied = {}) {
@@ -615,11 +676,12 @@
615
676
  return writebackController.formatUnsupportedLocalChangeSummary(changes, getLocale());
616
677
  }
617
678
 
618
- async function autoRecompileAfterWriteback(writtenPaths = [], saveVerification = {}) {
619
- if (getState().autoRecompile === false) return null;
620
- if (getState().mode === 'ask') return null;
679
+ async function autoRecompileAfterWriteback(writtenPaths = [], saveVerification = {}, options = {}) {
680
+ if (options.autoRecompile === false
681
+ || (options.autoRecompile === undefined && getState().autoRecompile === false)) return null;
682
+ if ((options.mode || getState().mode) === 'ask') return null;
621
683
 
622
- const CompileAdapter = window.CodexOverleafCompileAdapter;
684
+ const CompileAdapter = compileAdapter;
623
685
  if (!CompileAdapter) return null;
624
686
 
625
687
  const hasCompilableFile = writtenPaths.some(filePath => CompileAdapter.isCompilableFile(filePath));
@@ -642,7 +704,8 @@
642
704
  const result = await callPageBridge('triggerCompile', {
643
705
  preferUiClick: true,
644
706
  waitForSaveMs: 5000,
645
- requireVerifiedSave: saveVerification?.state === 'verified_saved'
707
+ requireVerifiedSave: saveVerification?.state === 'verified_saved',
708
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
646
709
  });
647
710
  if (result?.ok) {
648
711
  let logResult = null;
@@ -650,7 +713,8 @@
650
713
  logResult = await callPageBridge('getCompileLog', {
651
714
  triggerIfStale: false,
652
715
  maxAgeMs: 30000,
653
- waitForSaveMs: 0
716
+ waitForSaveMs: 0,
717
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
654
718
  });
655
719
  } catch (_error) {
656
720
  logResult = null;
@@ -775,7 +839,8 @@
775
839
  const result = await callPageBridge('getCompileLog', {
776
840
  triggerIfStale: true,
777
841
  maxAgeMs: 30000,
778
- waitForSaveMs: 5000
842
+ waitForSaveMs: 5000,
843
+ runProjectId: getCurrentRunView()?.runProjectId || getCurrentProjectId()
779
844
  });
780
845
 
781
846
  if (!result?.ok) {
@@ -841,6 +906,9 @@
841
906
  getState,
842
907
  getCurrentRunView,
843
908
  } = deps);
909
+ onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
910
+ writebackSettlement = deps.writebackSettlement;
911
+ compileAdapter = deps.compileAdapter;
844
912
  return {
845
913
  applySyncChangesToOverleaf,
846
914
  resolveCompileLogContext,
@@ -4,5 +4,9 @@
4
4
  if (!window.CodexOverleafContentRuntime || typeof window.CodexOverleafContentRuntime.init !== 'function') {
5
5
  throw new Error('Codex Overleaf content runtime did not load.');
6
6
  }
7
- window.CodexOverleafContentRuntime.init();
7
+ const registry = window.CodexOverleafLegacyGlobalRegistry;
8
+ if (!registry || typeof registry.create !== 'function') {
9
+ throw new Error('Codex Overleaf dependency registry did not load.');
10
+ }
11
+ window.CodexOverleafContentRuntime.init(registry.create(window));
8
12
  })();
@@ -1008,7 +1008,12 @@
1008
1008
  result: {
1009
1009
  ok: true,
1010
1010
  method: 'overleaf-editor-undo',
1011
- undoClicks: result.clicks
1011
+ undoClicks: result.clicks,
1012
+ // undoEditorHistoryUntilContent returns ok only after the active
1013
+ // editor exactly matches this run's pre-write checkpoint. Preserve
1014
+ // that proof for the content-side lifecycle settlement.
1015
+ verified: true,
1016
+ verifiedContent: expectedByPath.get(path)
1012
1017
  }
1013
1018
  });
1014
1019
  }