codex-overleaf-link 2.2.1 → 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 (71) 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/nativeTransportEnvelope.js +148 -0
  70. package/native-host/src/taskRunnerRuntime.js +3 -3
  71. package/package.json +13 -5
@@ -17,11 +17,13 @@
17
17
  sendBackgroundNative,
18
18
  PANEL_STATE_BASE_KEY,
19
19
  PROJECT_EDITOR_RESERVED_IDS,
20
- STATUS_BADGE_CLASS
20
+ STATUS_BADGE_CLASS,
21
+ ProjectSessionCleanup, SessionPersistence, SessionState, StorageDb, StorageKeys, StorageMigration
21
22
  } = deps;
22
- const projectSessionCleanup = window.CodexOverleafProjectSessionCleanup.create({
23
+ const projectSessionCleanup = ProjectSessionCleanup.create({
23
24
  tr, getAccountScopeId: getCachedAccountScopeId, showPluginConfirm, showPluginToast,
24
- sendBackgroundNative, mutateProjectPanelState, renderRecentProjectsVariant
25
+ sendBackgroundNative, mutateProjectPanelState, renderRecentProjectsVariant,
26
+ SessionState, StorageDb, StorageMigration
25
27
  });
26
28
 
27
29
  // chrome.storage.local cache key (spec §5.6.3). The cache is keyed by
@@ -496,7 +498,6 @@
496
498
  }
497
499
 
498
500
  function sessionRecordDisplayTitle(record) {
499
- var SessionState = window.CodexOverleafSessionState;
500
501
  if (record && typeof record.title === 'string' && record.title.trim()) {
501
502
  return record.title.trim();
502
503
  }
@@ -505,12 +506,11 @@
505
506
  }
506
507
 
507
508
  async function loadProjectSessionRecords(projectId) {
508
- var StorageDb = window.CodexOverleafStorageDb;
509
509
  if (!StorageDb) {
510
510
  return [];
511
511
  }
512
512
  var records = await StorageDb.getAllByIndex('sessions', 'projectId', projectId);
513
- var Persistence = window.CodexOverleafSessionPersistence;
513
+ var Persistence = SessionPersistence;
514
514
  var deletedIds = await Persistence.getDeletedSessionIds(projectId);
515
515
  var scope = getCachedAccountScopeId();
516
516
  return (records || [])
@@ -521,7 +521,6 @@
521
521
  }
522
522
 
523
523
  async function renderProjectSessions(sessionsEl, projectId) {
524
- var StorageDb = window.CodexOverleafStorageDb;
525
524
  sessionsEl.innerHTML = '';
526
525
  var sessionsLoading = document.createElement('div');
527
526
  sessionsLoading.className = 'recent-projects-loading';
@@ -544,7 +543,6 @@
544
543
  }
545
544
 
546
545
  function renderProjectSessionRow(sessionsEl, projectId, record) {
547
- var StorageDb = window.CodexOverleafStorageDb;
548
546
  var running = Boolean(StorageDb)
549
547
  && settleDashboardRunStatus(StorageDb.derivePrimaryStatusBadge(record), record.updatedAt || record.lastActivityAt) === 'running';
550
548
  var row = document.createElement('div');
@@ -614,17 +612,21 @@
614
612
  }
615
613
 
616
614
  async function activateSessionAndOpenProject(projectId, record) {
617
- var Migration = window.CodexOverleafStorageMigration;
615
+ var Migration = StorageMigration;
618
616
  try {
619
617
  if (Migration && Migration.loadPrefs && Migration.savePrefs) {
620
- var prefs = await Migration.loadPrefs();
618
+ var accountScopeId = getCachedAccountScopeId();
619
+ if (!accountScopeId) {
620
+ throw new Error('Account scope is unavailable.');
621
+ }
622
+ var prefs = await Migration.loadPrefs(accountScopeId, projectId);
621
623
  prefs = prefs && typeof prefs === 'object' ? prefs : {};
622
624
  var map = prefs.activeSessionByProject && typeof prefs.activeSessionByProject === 'object'
623
625
  ? prefs.activeSessionByProject
624
626
  : {};
625
627
  map[projectId] = record.id;
626
628
  prefs.activeSessionByProject = map;
627
- await Migration.savePrefs(prefs);
629
+ await Migration.savePrefs(prefs, accountScopeId, projectId);
628
630
  }
629
631
  } catch (_error) { /* fall through: opening the project still works */ }
630
632
  openProjectFromRow(projectId);
@@ -636,7 +638,6 @@
636
638
  // panel-state blob is deliberately left alone — an empty projectId would
637
639
  // map getProjectStorageKey onto the global legacy key.
638
640
  async function cleanupDeadProjectEntry(projectId) {
639
- var StorageDb = window.CodexOverleafStorageDb;
640
641
  if (!StorageDb) {
641
642
  return;
642
643
  }
@@ -684,7 +685,6 @@
684
685
  }
685
686
 
686
687
  function projectPanelStateKey(projectId) {
687
- var StorageKeys = window.CodexOverleafStorageKeys;
688
688
  return StorageKeys.getProjectStorageKey(PANEL_STATE_BASE_KEY, 'https://www.overleaf.com/project/' + projectId);
689
689
  }
690
690
 
@@ -694,7 +694,6 @@
694
694
  // NOTE: if the project is open in another tab, that tab's in-memory state
695
695
  // wins on its next save; the IndexedDB record mutation below still holds.
696
696
  async function mutateProjectPanelState(projectId, mutate) {
697
- var SessionState = window.CodexOverleafSessionState;
698
697
  var key = projectPanelStateKey(projectId);
699
698
  var stored = await chrome.storage.local.get(key);
700
699
  var blob = stored && stored[key];
@@ -709,8 +708,6 @@
709
708
  }
710
709
 
711
710
  async function deleteDashboardSession(sessionsEl, projectId, record) {
712
- var StorageDb = window.CodexOverleafStorageDb;
713
- var SessionState = window.CodexOverleafSessionState;
714
711
  // v1.8.1 P1b: ANY record whose stored badge is 'running' gets the
715
712
  // stronger confirm — even one the 30-minute heuristic settled to
716
713
  // interrupted. Activity timestamps are only bumped once a minute during
@@ -731,7 +728,7 @@
731
728
  if (!approved) {
732
729
  return;
733
730
  }
734
- await window.CodexOverleafSessionPersistence.addTombstones(projectId, [record.id]);
731
+ await SessionPersistence.addTombstones(projectId, [record.id]);
735
732
  try {
736
733
  await mutateProjectPanelState(projectId, function (state) {
737
734
  return SessionState.deleteSession(state, record.id);
@@ -773,8 +770,6 @@
773
770
  }
774
771
 
775
772
  function beginDashboardSessionRename(sessionsEl, projectId, record, row, titleEl) {
776
- var SessionState = window.CodexOverleafSessionState;
777
- var StorageDb = window.CodexOverleafStorageDb;
778
773
  if (row.querySelector('input')) {
779
774
  return;
780
775
  }
@@ -816,8 +811,6 @@
816
811
  }
817
812
 
818
813
  async function commitDashboardSessionRename(sessionsEl, projectId, record, rawTitle) {
819
- var SessionState = window.CodexOverleafSessionState;
820
- var StorageDb = window.CodexOverleafStorageDb;
821
814
  if (!SessionState) {
822
815
  return;
823
816
  }
@@ -963,9 +956,8 @@
963
956
  // there really is more than one page of projects.
964
957
  var pageLimit = 10;
965
958
  try {
966
- var StorageDb = window.CodexOverleafStorageDb;
967
959
  if (StorageDb) {
968
- var deletedSessionIdsByProject = await window.CodexOverleafSessionPersistence.loadTombstones();
960
+ var deletedSessionIdsByProject = await SessionPersistence.loadTombstones();
969
961
  rows = await StorageDb.listRecentProjectsAcrossAccount({
970
962
  accountScopeId: accountScopeId,
971
963
  limit: showAll ? 500 : pageLimit + 1,
@@ -47,7 +47,7 @@
47
47
  }
48
48
 
49
49
  function appendToView(event, view) {
50
- const text = window.CodexOverleafRunGuidanceView?.getGuidanceText(event);
50
+ const text = deps.RunGuidanceView?.getGuidanceText(event);
51
51
  if (!text) return false;
52
52
  const guidance = view.guidance || view.root?.querySelector('[data-run-guidance]');
53
53
  const target = view.terminalStatus ? guidance : view.events;
@@ -2,7 +2,7 @@
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(require('../shared/runInputQueue'));
4
4
  } else {
5
- root.CodexOverleafRunQueueScheduler = factory(root.CodexOverleafRunInputQueue);
5
+ root.CodexOverleafModuleRegistry.define('RunQueueScheduler', ['RunInputQueue'], factory);
6
6
  }
7
7
  })(typeof window !== 'undefined' ? window : globalThis, function runQueueSchedulerFactory(Queue) {
8
8
  'use strict';
@@ -19,18 +19,49 @@
19
19
  }
20
20
 
21
21
  async function afterSettlement({ sessionId, status, queueItemId = '', continueQueue = false } = {}) {
22
- let queue = options.getQueue?.(sessionId) || [];
23
- if (status === 'completed') {
24
- queue = dequeue(queue, queueItemId, { sessionId, reason: 'completed' });
25
- } else if (continueQueue) {
26
- queue = dequeue(queue, queueItemId, { sessionId, reason: 'continue_queue' });
27
- } else if (queue.length) {
28
- queue = Queue.pauseAll(queue, status || 'run_did_not_complete');
22
+ const originalQueue = options.getQueue?.(sessionId) || [];
23
+ const shouldContinue = status === 'completed' || continueQueue;
24
+ const shouldRemove = Boolean(queueItemId && shouldContinue);
25
+ const claimedItem = shouldRemove
26
+ ? originalQueue.find(item => item?.id === queueItemId)
27
+ : null;
28
+ if (shouldRemove) {
29
+ await options.persist?.({
30
+ queueMutation: {
31
+ type: 'remove',
32
+ sessionId,
33
+ itemId: queueItemId,
34
+ claimToken: claimedItem?.claimToken
35
+ }
36
+ });
37
+ const latestQueue = options.getQueue?.(sessionId) || originalQueue;
38
+ options.setQueue?.(
39
+ sessionId,
40
+ dequeue(latestQueue, queueItemId, {
41
+ sessionId,
42
+ reason: status === 'completed' ? 'completed' : 'continue_queue'
43
+ })
44
+ );
45
+ } else if (!shouldContinue) {
46
+ const paused = originalQueue.length
47
+ ? Queue.pauseAll(originalQueue, status || 'run_did_not_complete')
48
+ : originalQueue;
49
+ options.setQueue?.(sessionId, paused);
50
+ try {
51
+ await options.persist?.({
52
+ queueMutation: { type: 'transition', sessionId }
53
+ });
54
+ } catch (error) {
55
+ const latestQueue = options.getQueue?.(sessionId) || paused;
56
+ options.setQueue?.(
57
+ sessionId,
58
+ Queue.pauseAll(latestQueue, status || 'run_did_not_complete')
59
+ );
60
+ throw error;
61
+ }
29
62
  }
30
- options.setQueue?.(sessionId, queue);
31
- await options.persist?.();
32
63
  options.onChange?.(sessionId);
33
- if (status === 'completed' || continueQueue) {
64
+ if (shouldContinue) {
34
65
  scheduleOne(sessionId);
35
66
  }
36
67
  }
@@ -39,15 +70,33 @@
39
70
  if (scheduling || options.isRunning?.() || options.canStart?.(sessionId) === false) {
40
71
  return false;
41
72
  }
42
- const claimed = Queue.claimNext(options.getQueue?.(sessionId) || [], {
43
- randomUUID: options.randomUUID
44
- });
45
- if (!claimed.ok) {
73
+ const initialQueue = options.getQueue?.(sessionId) || [];
74
+ if (!initialQueue.some(item => item?.status === 'queued')) {
46
75
  return false;
47
76
  }
48
77
  scheduling = true;
49
- options.setQueue?.(sessionId, claimed.queue);
50
- Promise.resolve(options.persist?.())
78
+ let claimed = null;
79
+ Promise.resolve(options.prepareClaim?.(sessionId))
80
+ .then(() => {
81
+ claimed = Queue.claimNext(options.getQueue?.(sessionId) || initialQueue, {
82
+ randomUUID: options.randomUUID,
83
+ resolveExecutionSnapshot: options.resolveExecutionSnapshot
84
+ });
85
+ if (!claimed.ok) {
86
+ const error = new Error(claimed.error?.message || claimed.error?.code || 'queue_claim_failed');
87
+ error.code = claimed.error?.code || 'queue_claim_failed';
88
+ throw error;
89
+ }
90
+ options.setQueue?.(sessionId, claimed.queue);
91
+ return options.persist?.({
92
+ queueMutation: {
93
+ type: 'claim',
94
+ sessionId,
95
+ itemId: claimed.item.id,
96
+ claimToken: claimed.item.claimToken
97
+ }
98
+ });
99
+ })
51
100
  .then(() => {
52
101
  options.onChange?.(sessionId);
53
102
  scheduling = false;
@@ -55,27 +104,64 @@
55
104
  })
56
105
  .catch(error => {
57
106
  scheduling = false;
58
- const paused = Queue.pauseAll(options.getQueue?.(sessionId) || claimed.queue, error?.code || 'start_failed');
107
+ const paused = Queue.pauseAll(
108
+ options.getQueue?.(sessionId) || claimed?.queue || initialQueue,
109
+ error?.code || 'start_failed'
110
+ );
59
111
  options.setQueue?.(sessionId, paused);
60
112
  options.onChange?.(sessionId);
61
- return options.persist?.();
113
+ return Promise.resolve(options.persist?.({
114
+ queueMutation: claimed?.item?.id
115
+ ? {
116
+ type: 'release',
117
+ sessionId,
118
+ itemId: claimed.item.id,
119
+ claimToken: claimed.item.claimToken
120
+ }
121
+ : { type: 'transition', sessionId }
122
+ })).catch(() => undefined);
62
123
  });
63
124
  return true;
64
125
  }
65
126
 
66
127
  async function markExecuting(sessionId, itemId, _runId) {
67
128
  const queue = options.getQueue?.(sessionId) || [];
129
+ const item = queue.find(entry => entry?.id === itemId);
130
+ try {
131
+ await options.persist?.({
132
+ queueMutation: { type: 'remove', sessionId, itemId, claimToken: item?.claimToken }
133
+ });
134
+ } catch (error) {
135
+ const latestQueue = options.getQueue?.(sessionId) || queue;
136
+ options.setQueue?.(
137
+ sessionId,
138
+ Queue.pauseAll(latestQueue, error?.code || 'queue_promotion_persistence_failed')
139
+ );
140
+ options.onChange?.(sessionId);
141
+ try {
142
+ await options.persist?.({
143
+ queueMutation: { type: 'restore', sessionId, itemId, claimToken: item?.claimToken }
144
+ });
145
+ } catch (recoveryError) {
146
+ if (error && typeof error === 'object') {
147
+ error.persistenceRecoveryError = recoveryError?.message || String(recoveryError);
148
+ }
149
+ }
150
+ throw error;
151
+ }
152
+ const latestQueue = options.getQueue?.(sessionId) || queue;
68
153
  options.setQueue?.(
69
154
  sessionId,
70
- dequeue(queue, itemId, { sessionId, runId: _runId, reason: 'executing' })
155
+ dequeue(latestQueue, itemId, { sessionId, runId: _runId, reason: 'executing' })
71
156
  );
72
- await options.persist?.();
73
157
  options.onChange?.(sessionId);
74
158
  }
75
159
 
76
160
  async function resume(sessionId) {
77
161
  options.setQueue?.(sessionId, Queue.resumeAll(options.getQueue?.(sessionId) || []));
78
- await options.persist?.();
162
+ await options.persist?.({
163
+ queueMutation: { type: 'transition', sessionId }
164
+ });
79
165
  options.onChange?.(sessionId);
80
166
  scheduleOne(sessionId);
81
167
  }
@@ -88,7 +174,7 @@
88
174
  const scheduler = create({
89
175
  getQueue: sessionId => options.getSession?.(sessionId)?.pendingInputs || [],
90
176
  setQueue: (sessionId, pendingInputs) => options.setQueue?.(sessionId, pendingInputs),
91
- persist: () => options.save?.(),
177
+ persist: persistenceOptions => options.save?.(persistenceOptions),
92
178
  isRunning: () => Boolean(options.getCurrentRun?.()),
93
179
  canStart: sessionId => options.canStart?.(sessionId) !== false,
94
180
  start: (item, sessionId) => startQueuedInput(item, sessionId),
@@ -97,6 +183,8 @@
97
183
  sessionId: context.sessionId,
98
184
  recordId: item.sourceRunId || ''
99
185
  }),
186
+ prepareClaim: options.prepareClaim,
187
+ resolveExecutionSnapshot: options.resolveExecutionSnapshot,
100
188
  randomUUID: options.randomUUID
101
189
  });
102
190
 
@@ -128,7 +216,9 @@
128
216
  // active turn, or until the scheduler promotes it to the next run.
129
217
  options.clearTask?.();
130
218
  render();
131
- options.saveSoon?.();
219
+ options.saveSoon?.({
220
+ queueMutation: { type: 'enqueue', sessionId: session.id, itemId: result.item.id }
221
+ });
132
222
  }
133
223
 
134
224
  async function guide(itemId) {
@@ -142,7 +232,9 @@
142
232
  }
143
233
  options.setQueue?.(session.id, Queue.markSteering(session.pendingInputs, itemId));
144
234
  render();
145
- await options.save?.();
235
+ await options.save?.({
236
+ queueMutation: { type: 'transition', sessionId: session.id, itemId }
237
+ });
146
238
  try {
147
239
  const response = await options.sendSteer?.({
148
240
  requestId: run.nativeRequestId,
@@ -174,7 +266,11 @@
174
266
  options.toast?.(response?.error?.message || options.tr?.('queuedInputGuideFailed'), 'warning');
175
267
  }
176
268
  render();
177
- await options.save?.();
269
+ await options.save?.({
270
+ queueMutation: response?.ok
271
+ ? { type: 'remove', sessionId: session.id, itemId }
272
+ : { type: 'transition', sessionId: session.id, itemId }
273
+ });
178
274
  } catch (error) {
179
275
  const latest = options.getSession?.(session.id);
180
276
  const paused = (latest?.pendingInputs || []).map(entry => entry.id === itemId
@@ -183,7 +279,9 @@
183
279
  options.setQueue?.(session.id, Queue.normalizeQueue(paused));
184
280
  options.toast?.(error?.message || options.tr?.('queuedInputGuideFailed'), 'warning');
185
281
  render();
186
- await options.save?.();
282
+ await options.save?.({
283
+ queueMutation: { type: 'transition', sessionId: session.id, itemId }
284
+ });
187
285
  }
188
286
  }
189
287
 
@@ -201,7 +299,9 @@
201
299
  }
202
300
  options.setQueue?.(session.id, Queue.remove(session.pendingInputs || [], itemId));
203
301
  render();
204
- options.saveSoon?.();
302
+ options.saveSoon?.({
303
+ queueMutation: { type: 'remove', sessionId: session.id, itemId }
304
+ });
205
305
  }
206
306
 
207
307
  function resume() {
@@ -242,7 +342,7 @@
242
342
  error.code = 'queue_session_not_active';
243
343
  throw error;
244
344
  }
245
- options.applyQueuedInput?.(item);
345
+ await options.applyQueuedInput?.(item);
246
346
  await options.run?.(item);
247
347
  }
248
348
 
@@ -0,0 +1,51 @@
1
+ (function initCodexOverleafRunSettlementPersistence(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafRunSettlementPersistence = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function runSettlementPersistenceFactory() {
6
+ 'use strict';
7
+
8
+ function create(options = {}) {
9
+ const Settlement = options.writebackSettlement;
10
+
11
+ function findTarget(recordId, sessionId) {
12
+ return typeof options.findRunRecord === 'function'
13
+ ? options.findRunRecord(recordId, sessionId)
14
+ : null;
15
+ }
16
+
17
+ function applyToCurrentRun(settlement) {
18
+ const view = options.getCurrentRunView?.();
19
+ if (!settlement || !view?.recordId) return;
20
+ const record = findTarget(view.recordId, view.sessionId);
21
+ if (!record) return;
22
+ Object.assign(record, Settlement.applySettlementTransition(record, settlement));
23
+ }
24
+
25
+ function settleMirrorRefresh(result = {}) {
26
+ const record = findTarget(result.recordId, result.sessionId);
27
+ if (!record) return;
28
+ if (result.runProjectId && result.runProjectId !== options.getCurrentProjectId?.()) return;
29
+ const existing = record.settlement && typeof record.settlement === 'object'
30
+ ? record.settlement
31
+ : record.settlementFacts;
32
+ if (!existing || typeof existing !== 'object') return;
33
+ record.settlement = Settlement.compactSettlementFacts({
34
+ ...existing,
35
+ evidence: {
36
+ ...(existing.evidence || {}),
37
+ mirrored: result.state
38
+ }
39
+ });
40
+ delete record.settlementFacts;
41
+ options.saveStateSoon?.();
42
+ }
43
+
44
+ return Object.freeze({
45
+ applyToCurrentRun,
46
+ settleMirrorRefresh
47
+ });
48
+ }
49
+
50
+ return Object.freeze({ create });
51
+ });
@@ -1,7 +1,6 @@
1
1
  (function initCodexOverleafRunTimelineView() {
2
2
  'use strict';
3
3
 
4
- const RunGuidanceView = window.CodexOverleafRunGuidanceView;
5
4
  // Run-timeline view — the timeline render pipeline carved out of
6
5
  // contentRuntime.js (v1.4.6 structural-debt phase 2): the scroll engine +
7
6
  // jump-to-latest button, the live-elapsed tick and collapsed-header summary,
@@ -13,6 +12,7 @@
13
12
  // that owns it.
14
13
  function create(deps = {}) {
15
14
  const {
15
+ RunGuidanceView,
16
16
  tr,
17
17
  tx,
18
18
  getLocale,
@@ -40,7 +40,8 @@
40
40
  showNativeSetupGuidance,
41
41
  openProjectFileForFailure,
42
42
  openStorageSettings,
43
- trackedChangeInFlight
43
+ trackedChangeInFlight,
44
+ projectRunSettlement
44
45
  } = deps;
45
46
 
46
47
  let logAutoFollow = true;
@@ -884,8 +885,8 @@
884
885
  return;
885
886
  }
886
887
 
887
- const undoCount = getRunUndoCount(run);
888
- if (!undoCount && run.undoStatus !== 'applied') {
888
+ const projection = projectRunSettlement(run);
889
+ if (!projection.canUndo && run.undoStatus !== 'applied') {
889
890
  button.hidden = true;
890
891
  return;
891
892
  }
@@ -912,13 +913,14 @@
912
913
  // same executable state as `pending`.
913
914
  function configureLifecycleUndoButton(button, run) {
914
915
  const status = run.trackedChangeStatus || '';
916
+ const projection = projectRunSettlement(run);
915
917
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
916
918
  // actionable. Branch placed before the terminal branches so a
917
919
  // needs_review run is never treated as terminal.
918
920
  if (status === 'needs_review') {
919
921
  const inFlight = trackedChangeInFlight.get(run.id);
920
922
  button.hidden = false;
921
- button.disabled = inFlight === 'reject' || inFlight === 'accept';
923
+ button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
922
924
  button.textContent = tr('undoRun');
923
925
  button.title = tr('undoRunTitle');
924
926
  button.addEventListener('click', event => {
@@ -949,7 +951,7 @@
949
951
  }
950
952
  const inFlight = trackedChangeInFlight.get(run.id);
951
953
  button.hidden = false;
952
- button.disabled = inFlight === 'reject' || inFlight === 'accept';
954
+ button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
953
955
  button.textContent = tr('undoRun');
954
956
  button.title = tr('undoRunTitle');
955
957
  button.addEventListener('click', event => {
@@ -980,13 +982,14 @@
980
982
  }
981
983
 
982
984
  const status = run.trackedChangeStatus || '';
985
+ const projection = projectRunSettlement(run);
983
986
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
984
987
  // actionable. It remains an internal retryable proof state, while the
985
988
  // primary button label stays in the same executable state as `pending`.
986
989
  if (status === 'needs_review') {
987
990
  const inFlight = trackedChangeInFlight.get(run.id);
988
991
  button.hidden = false;
989
- button.disabled = inFlight === 'accept' || inFlight === 'reject';
992
+ button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
990
993
  if (inFlight === 'accept') {
991
994
  button.textContent = tr('runAcceptTrackedConfirming');
992
995
  button.title = tr('runAcceptTrackedConfirming');
@@ -1020,7 +1023,7 @@
1020
1023
 
1021
1024
  const inFlight = trackedChangeInFlight.get(run.id);
1022
1025
  button.hidden = false;
1023
- button.disabled = inFlight === 'accept' || inFlight === 'reject';
1026
+ button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
1024
1027
  if (inFlight === 'accept') {
1025
1028
  button.textContent = tr('runAcceptTrackedConfirming');
1026
1029
  button.title = tr('runAcceptTrackedConfirming');
@@ -0,0 +1,100 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(
4
+ require('./scopedPersistencePanelState'),
5
+ require('./scopedPersistenceDetachedCommit')
6
+ );
7
+ } else {
8
+ root.CodexOverleafModuleRegistry.define(
9
+ 'ScopedPersistenceCoordinator',
10
+ ['ScopedPersistencePanelState', 'ScopedPersistenceDetachedCommit'],
11
+ factory
12
+ );
13
+ }
14
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function (PanelState, DetachedCommit) {
15
+ 'use strict';
16
+
17
+ function createController(options) {
18
+ options = options || {};
19
+ var Transaction = options.Transaction;
20
+ if (!Transaction || typeof Transaction.createCoordinator !== 'function') return null;
21
+ var writerId = getStableWriterId(options.sessionStorage);
22
+ var coordinator = Transaction.createCoordinator({
23
+ adapter: Transaction.createBrowserAdapter({ chromeApi: options.chromeApi }),
24
+ writerId: writerId
25
+ });
26
+ var commitDetached = (options.DetachedCommit || DetachedCommit)?.create?.({
27
+ Transaction: Transaction, chromeApi: options.chromeApi, writerId: writerId
28
+ });
29
+ var activeScope = null;
30
+
31
+ function scope(projectId) {
32
+ var accountScopeId = '';
33
+ try {
34
+ accountScopeId = typeof options.getAccountScopeId === 'function'
35
+ ? options.getAccountScopeId() || ''
36
+ : '';
37
+ } catch (_error) {
38
+ accountScopeId = '';
39
+ }
40
+ return { accountScopeId: accountScopeId, projectId: projectId };
41
+ }
42
+
43
+ return Object.freeze({
44
+ async beginHydration(projectId) {
45
+ var nextScope = scope(projectId);
46
+ if (!nextScope.accountScopeId) {
47
+ activeScope = null;
48
+ return null;
49
+ }
50
+ var token = await coordinator.beginHydration(nextScope);
51
+ if (coordinator.isCurrent(token)) activeScope = nextScope;
52
+ return token;
53
+ },
54
+ isCurrent: coordinator.isCurrent,
55
+ async commit(projectId, commitOptions, action) {
56
+ var nextScope = scope(projectId);
57
+ if (!nextScope.accountScopeId) {
58
+ return { ok: false, reason: 'account_scope_unavailable' };
59
+ }
60
+ if (!activeScope
61
+ || activeScope.accountScopeId !== nextScope.accountScopeId
62
+ || activeScope.projectId !== nextScope.projectId) {
63
+ var hydration = await coordinator.beginHydration(nextScope);
64
+ if (!coordinator.isCurrent(hydration)) {
65
+ return { ok: false, reason: 'stale_view' };
66
+ }
67
+ activeScope = nextScope;
68
+ }
69
+ var view = await coordinator.captureCommitView(nextScope, {
70
+ detached: Boolean(commitOptions && commitOptions.detached)
71
+ });
72
+ return coordinator.commit(view, action, {
73
+ rebase: !commitOptions || commitOptions.rebase !== false,
74
+ queueMutation: commitOptions && commitOptions.queueMutation,
75
+ queueMutations: commitOptions && commitOptions.queueMutations,
76
+ sessionTombstones: commitOptions && commitOptions.sessionTombstones
77
+ });
78
+ },
79
+ commitDetached: commitDetached
80
+ });
81
+ }
82
+ function getStableWriterId(storage) {
83
+ var key = 'codex-overleaf-scoped-writer-id';
84
+ try {
85
+ var existing = storage?.getItem?.(key);
86
+ if (existing) return existing;
87
+ var created = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
88
+ ? crypto.randomUUID()
89
+ : 'writer-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
90
+ storage?.setItem?.(key, created);
91
+ return created;
92
+ } catch (_error) {
93
+ return '';
94
+ }
95
+ }
96
+ return Object.freeze({
97
+ createController: createController,
98
+ persistPanelState: PanelState?.persistPanelState || null
99
+ });
100
+ });