codex-overleaf-link 2.2.1 → 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 (79) 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 +67 -63
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/composerPanel.js +0 -2
  8. package/extension/src/content/contentRuntime.js +757 -1558
  9. package/extension/src/content/contextTray.js +4 -6
  10. package/extension/src/content/diagnosticsController.js +1 -2
  11. package/extension/src/content/diffReviewPanel.js +14 -13
  12. package/extension/src/content/generated/content.bundle.js +41605 -0
  13. package/extension/src/content/generated/content.bundle.meta.json +103 -0
  14. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  15. package/extension/src/content/markdownText.js +17 -17
  16. package/extension/src/content/mathText.js +155 -9
  17. package/extension/src/content/modelPicker.js +1 -1
  18. package/extension/src/content/modelPickerSupport.js +10 -5
  19. package/extension/src/content/moduleRegistryKernel.js +125 -0
  20. package/extension/src/content/nativeCompatibilityController.js +313 -0
  21. package/extension/src/content/otWarmMirror.js +8 -6
  22. package/extension/src/content/pageBridgeClient.js +224 -0
  23. package/extension/src/content/panelMaintenance.js +8 -3
  24. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  25. package/extension/src/content/projectProviderSelection.js +4 -12
  26. package/extension/src/content/projectSessionCleanup.js +3 -3
  27. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  28. package/extension/src/content/providerSettingsDialog.js +30 -18
  29. package/extension/src/content/recentProjects.js +15 -23
  30. package/extension/src/content/runGuidanceController.js +1 -1
  31. package/extension/src/content/runQueueScheduler.js +129 -29
  32. package/extension/src/content/runSettlementPersistence.js +51 -0
  33. package/extension/src/content/runTimelineView.js +17 -8
  34. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  35. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  36. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  37. package/extension/src/content/sessionManager.js +15 -12
  38. package/extension/src/content/sessionPersistence.js +67 -8
  39. package/extension/src/content/updateNotice.js +10 -4
  40. package/extension/src/content/writebackOrchestrator.js +89 -68
  41. package/extension/src/contentScript.js +5 -1
  42. package/extension/src/page/trackedChangesLifecycle.js +16 -5
  43. package/extension/src/page/writebackRouter.js +6 -2
  44. package/extension/src/pageBridge.js +47 -98
  45. package/extension/src/shared/agentTranscript.js +18 -8
  46. package/extension/src/shared/compatibility.js +1 -1
  47. package/extension/src/shared/governanceRules.js +1 -1
  48. package/extension/src/shared/i18n.js +2 -4
  49. package/extension/src/shared/managedUpdateProjection.js +337 -0
  50. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  51. package/extension/src/shared/otText.js +0 -19
  52. package/extension/src/shared/pageRpcContract.js +136 -0
  53. package/extension/src/shared/runExecutionSnapshot.js +187 -0
  54. package/extension/src/shared/runExecutionSnapshotCodec.js +171 -0
  55. package/extension/src/shared/runInputQueue.js +81 -34
  56. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  57. package/extension/src/shared/scopedPersistenceQueuePolicy.js +200 -0
  58. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  59. package/extension/src/shared/scopedPersistenceTransaction.js +241 -0
  60. package/extension/src/shared/sessionState.js +110 -59
  61. package/extension/src/shared/settlementFacts.js +351 -0
  62. package/extension/src/shared/storageDb.js +25 -10
  63. package/extension/src/shared/storageMigration.js +275 -61
  64. package/extension/src/shared/storageSessionClaims.js +60 -0
  65. package/extension/src/shared/updateConsent.js +4 -0
  66. package/extension/src/shared/updateRevocationIntent.js +109 -0
  67. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  68. package/extension/src/shared/writebackOperationEvidence.js +153 -0
  69. package/extension/src/shared/writebackSettlement.js +727 -0
  70. package/extension/styles/panel.css +18 -8
  71. package/native-host/src/codexPromptAssembly.js +1 -1
  72. package/native-host/src/codexRuntimeIdentity.js +1 -1
  73. package/native-host/src/codexSessionRunner.js +7 -13
  74. package/native-host/src/index.js +3 -69
  75. package/native-host/src/managedInstall.js +50 -8
  76. package/native-host/src/nativeTransportEnvelope.js +148 -0
  77. package/native-host/src/taskRunnerRuntime.js +14 -84
  78. package/native-host/src/updateManager.js +36 -1
  79. package/package.json +13 -5
@@ -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;
@@ -273,6 +274,9 @@
273
274
  if (status === 'failed') {
274
275
  return tr('processedFailed', { elapsed });
275
276
  }
277
+ if (status === 'rejected' || status === 'cancelled') {
278
+ return tr('processedCancelled', { elapsed });
279
+ }
276
280
  if (status === 'running') {
277
281
  return tr('processing', { elapsed });
278
282
  }
@@ -410,6 +414,9 @@
410
414
  if (run.startedAt && run.finishedAt) {
411
415
  return formatProcessedSummary(run.status || 'completed', new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime());
412
416
  }
417
+ if (run.status === 'rejected' || run.status === 'cancelled') {
418
+ return tx('Cancelled', '已取消');
419
+ }
413
420
  return run.status === 'failed' ? tx('Failed', '处理失败') : tx('Done', '已处理');
414
421
  }
415
422
 
@@ -884,8 +891,8 @@
884
891
  return;
885
892
  }
886
893
 
887
- const undoCount = getRunUndoCount(run);
888
- if (!undoCount && run.undoStatus !== 'applied') {
894
+ const projection = projectRunSettlement(run);
895
+ if (!projection.canUndo && run.undoStatus !== 'applied') {
889
896
  button.hidden = true;
890
897
  return;
891
898
  }
@@ -912,13 +919,14 @@
912
919
  // same executable state as `pending`.
913
920
  function configureLifecycleUndoButton(button, run) {
914
921
  const status = run.trackedChangeStatus || '';
922
+ const projection = projectRunSettlement(run);
915
923
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
916
924
  // actionable. Branch placed before the terminal branches so a
917
925
  // needs_review run is never treated as terminal.
918
926
  if (status === 'needs_review') {
919
927
  const inFlight = trackedChangeInFlight.get(run.id);
920
928
  button.hidden = false;
921
- button.disabled = inFlight === 'reject' || inFlight === 'accept';
929
+ button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
922
930
  button.textContent = tr('undoRun');
923
931
  button.title = tr('undoRunTitle');
924
932
  button.addEventListener('click', event => {
@@ -949,7 +957,7 @@
949
957
  }
950
958
  const inFlight = trackedChangeInFlight.get(run.id);
951
959
  button.hidden = false;
952
- button.disabled = inFlight === 'reject' || inFlight === 'accept';
960
+ button.disabled = !projection.canUndo || inFlight === 'reject' || inFlight === 'accept';
953
961
  button.textContent = tr('undoRun');
954
962
  button.title = tr('undoRunTitle');
955
963
  button.addEventListener('click', event => {
@@ -980,13 +988,14 @@
980
988
  }
981
989
 
982
990
  const status = run.trackedChangeStatus || '';
991
+ const projection = projectRunSettlement(run);
983
992
  // §7 settlement matrix: needs_review keeps BOTH controls visible AND
984
993
  // actionable. It remains an internal retryable proof state, while the
985
994
  // primary button label stays in the same executable state as `pending`.
986
995
  if (status === 'needs_review') {
987
996
  const inFlight = trackedChangeInFlight.get(run.id);
988
997
  button.hidden = false;
989
- button.disabled = inFlight === 'accept' || inFlight === 'reject';
998
+ button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
990
999
  if (inFlight === 'accept') {
991
1000
  button.textContent = tr('runAcceptTrackedConfirming');
992
1001
  button.title = tr('runAcceptTrackedConfirming');
@@ -1020,7 +1029,7 @@
1020
1029
 
1021
1030
  const inFlight = trackedChangeInFlight.get(run.id);
1022
1031
  button.hidden = false;
1023
- button.disabled = inFlight === 'accept' || inFlight === 'reject';
1032
+ button.disabled = !projection.canAccept || inFlight === 'accept' || inFlight === 'reject';
1024
1033
  if (inFlight === 'accept') {
1025
1034
  button.textContent = tr('runAcceptTrackedConfirming');
1026
1035
  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
+ });
@@ -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
+ });