codex-overleaf-link 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -15,11 +15,56 @@
15
15
  return JSON.parse(JSON.stringify(value));
16
16
  }
17
17
 
18
- async function applyReviewTransition({ getState, runId, transition, persist }) {
18
+ // Review owns recovery progress and its settlement decision. Log events,
19
+ // compilation/mirror evidence, and later draft/display data have other writers.
20
+ const REVIEW_FIELDS = ['appliedOperations', 'undoOperations', 'undoBaseFiles', 'undoTrackedChanges',
21
+ 'undoExpectedFiles', 'undoStatus', 'trackedChangeStatus', 'trackedChangeCaptures', 'interruptedDraft',
22
+ 'retryWriteback', 'saveCheck'];
23
+ const REVIEW_FACT_FIELDS = ['documentEffect', 'fileSettlements', 'failures', 'recoveryAvailability', 'recoveryDisposition'];
24
+ const AUXILIARY_EVIDENCE = ['mirrored', 'compiled'];
25
+ const REVIEW_SOURCE_FIELDS = ['id', 'runProjectId', 'runAccountScopeId', 'startedAt', 'nativeEventSeq',
26
+ 'codexThreadId', 'codexTurnId', 'executionSnapshot', 'forkSnapshot'];
27
+ const factsOf = record => record?.settlement || record?.settlementFacts || {};
28
+ function reviewSnapshot(record, SessionState) {
29
+ const normalized = SessionState?.normalizeRuns?.([record])?.[0] || record;
30
+ const facts = factsOf(normalized);
31
+ return JSON.stringify([
32
+ [...REVIEW_SOURCE_FIELDS, ...REVIEW_FIELDS].map(key => normalized[key]),
33
+ REVIEW_FACT_FIELDS.map(key => facts[key]),
34
+ Object.entries(facts.evidence || {}).filter(([key]) => !AUXILIARY_EVIDENCE.includes(key)).sort(([a], [b]) => a.localeCompare(b))
35
+ ]);
36
+ }
37
+ function restoreReviewFields(record, previous, staged, legacyUndo) {
38
+ for (const key of REVIEW_FIELDS) {
39
+ if (Object.hasOwn(previous, key)) record[key] = cloneSnapshot(previous[key]);
40
+ else delete record[key];
41
+ }
42
+ Object.assign(record, legacyUndo ? { undoStatus: 'partial' } : { trackedChangeStatus: 'needs_review' });
43
+ const currentFacts = factsOf(record), stagedFacts = factsOf(staged);
44
+ const restored = cloneSnapshot(factsOf(previous));
45
+ const preserveLateFields = (target, current, initial, excluded) => {
46
+ for (const key of new Set([...Object.keys(current), ...Object.keys(initial)])) {
47
+ if (excluded.includes(key) || JSON.stringify(current[key]) === JSON.stringify(initial[key])) continue;
48
+ if (Object.hasOwn(current, key)) target[key] = cloneSnapshot(current[key]); else delete target[key];
49
+ }
50
+ };
51
+ preserveLateFields(restored, currentFacts, stagedFacts, [...REVIEW_FACT_FIELDS, 'evidence']);
52
+ const evidence = { ...(restored.evidence || {}) };
53
+ const currentEvidence = currentFacts.evidence || {}, stagedEvidence = stagedFacts.evidence || {};
54
+ preserveLateFields(evidence, currentEvidence, stagedEvidence,
55
+ [...Object.keys(currentEvidence), ...Object.keys(stagedEvidence)].filter(key => !AUXILIARY_EVIDENCE.includes(key)));
56
+ if (Object.keys(evidence).length) restored.evidence = evidence; else delete restored.evidence;
57
+ delete record.settlementFacts;
58
+ if (Object.keys(restored).length) record.settlement = restored; else delete record.settlement;
59
+ }
60
+
61
+ async function applyReviewTransition({ getState, runId, transition, persist, legacyUndo = false, assertCurrent, SessionState }) {
19
62
  const targets = () => Array.from(new Set([
20
63
  ...(getState().runs || []),
21
64
  ...(getState().sessions || []).flatMap(session => session.runs || [])
22
65
  ])).filter(run => run.id === runId);
66
+ assertCurrent?.();
67
+ const capturedState = getState(), capturedSessionId = capturedState.activeSessionId;
23
68
  const records = targets();
24
69
  const sessions = (getState().sessions || []).filter(session =>
25
70
  (session.runs || []).some(run => run.id === runId));
@@ -31,15 +76,31 @@
31
76
  (Date.parse(session.lastActivityAt) || 0) + 1)).toISOString();
32
77
  session.lastActivityAt = session.updatedAt;
33
78
  }
79
+ const snapshot = record => reviewSnapshot(record, SessionState);
80
+ const stagedRecords = records.map(cloneSnapshot), staged = records.map(snapshot);
81
+ const sameRecord = (original, record) => original === record
82
+ || (assertCurrent && SessionState?.sameRunIdentity?.(original, record));
83
+ const stillStaged = record => records.some((original, index) =>
84
+ sameRecord(original, record) && snapshot(record) === staged[index]);
85
+ const sameView = () => getState().activeSessionId === capturedSessionId
86
+ && (getState() === capturedState || (assertCurrent && SessionState?.sameRunIdentity));
34
87
  try {
35
- await persist({ preserveRunActionPayload: true, reviewRunIds: [runId] });
88
+ await persist({ preserveRunActionPayload: true, reviewRunIds: [runId], requireDurable: true });
89
+ assertCurrent?.();
90
+ const current = targets();
91
+ if (!sameView() || !current.length || current.some(record => !stillStaged(record))
92
+ || records.some(original => !current.some(record => sameRecord(original, record))))
93
+ throw reviewPersistenceError();
36
94
  } catch (error) {
95
+ try { assertCurrent?.(); } catch (_stale) { throw error; }
96
+ // The envelope still failed closed, but an exact run-scoped readback
97
+ // already acknowledges this outcome. Keep the error/notice, not a false retry.
98
+ if (error.code === 'shared_session_conflict' && error.durableReviewRunIds?.includes(runId)) throw error;
37
99
  // The remote action may have succeeded. Keep its recovery data and expose uncertainty.
38
100
  for (const record of targets()) {
39
- Object.assign(record, cloneSnapshot(previous), { trackedChangeStatus: 'needs_review' });
40
- for (const field of ['settlement', 'settlementFacts']) {
41
- if (!Object.prototype.hasOwnProperty.call(previous, field)) delete record[field];
42
- }
101
+ if (!sameView() || !stillStaged(record)) continue;
102
+ const index = records.findIndex((original, index) => sameRecord(original, record) && snapshot(record) === staged[index]);
103
+ restoreReviewFields(record, previous, stagedRecords[index], legacyUndo);
43
104
  }
44
105
  throw error;
45
106
  }
@@ -51,6 +112,28 @@
51
112
  });
52
113
  }
53
114
 
115
+ function verifyRecoveredRuns(recoveredRuns, proposed, written) {
116
+ for (var target of recoveredRuns || []) {
117
+ var expectedSession = proposed.find(session => session.id === target.sessionId);
118
+ var storedSession = (written || []).find(session => session.id === target.sessionId
119
+ && session.projectId === expectedSession?.projectId
120
+ && session.accountScopeId === expectedSession?.accountScopeId);
121
+ var expected = expectedSession?.runs?.find(run => run.id === target.runId);
122
+ var stored = storedSession?.runs?.find(run => run.id === target.runId);
123
+ var retained = expected && stored
124
+ && expected.runProjectId === target.projectId && stored.runProjectId === target.projectId
125
+ && Number(stored.nativeEventSeq || 0) >= Number(expected.nativeEventSeq || 0)
126
+ && stored.codexThreadId === expected.codexThreadId
127
+ && stored.codexTurnId === expected.codexTurnId
128
+ && JSON.stringify(stored.interruptedDraft) === JSON.stringify(expected.interruptedDraft)
129
+ && (expected.events || []).every(event => (stored.events || [])
130
+ .some(saved => JSON.stringify(saved) === JSON.stringify(event)));
131
+ if (!retained) throw Object.assign(new Error('Recovered run was not durably saved; its replay journal is retained.'), {
132
+ code: 'recovery_state_not_persisted'
133
+ });
134
+ }
135
+ }
136
+
54
137
  async function persistPanelState(input) {
55
138
  var state = cloneSnapshot(input.state || {});
56
139
  var compactState = cloneSnapshot(input.compactState || {});
@@ -71,6 +154,12 @@
71
154
  code: 'stale_view'
72
155
  });
73
156
  }
157
+ if ((input.recoveryRuns || []).some(target => target.projectId !== projectId
158
+ || target.accountScopeId !== accountScopeId)) {
159
+ throw Object.assign(new Error('Journal recovery scope changed before persistence.'), {
160
+ code: 'recovery_scope_changed'
161
+ });
162
+ }
74
163
  var reviewRuns = (input.reviewRunIds || []).map(id =>
75
164
  (state.sessions || []).flatMap(session => session.runs || []).find(run => run.id === id));
76
165
  if (reviewRuns.some(run => !run)) throw reviewPersistenceError();
@@ -92,6 +181,8 @@
92
181
  latestPrefs && latestPrefs.experimentalOtByProject || {},
93
182
  { [projectId]: input.normalizeExperimentalOtByProject(state.experimentalOtByProject)[projectId] === true }
94
183
  );
184
+ prefs.reviewingAvailabilityByProject = Object.assign({}, latestPrefs?.reviewingAvailabilityByProject || {},
185
+ state.reviewingAvailabilityByProject || {});
95
186
  prefs.governanceRulesByProject = Object.assign(
96
187
  {},
97
188
  latestPrefs && latestPrefs.governanceRulesByProject || {},
@@ -129,25 +220,48 @@
129
220
  focusFiles: Array.isArray(session.focusFiles) ? session.focusFiles : [],
130
221
  createdAt: session.createdAt,
131
222
  updatedAt: session.updatedAt,
132
- pendingInputs: (Array.isArray(session.pendingInputs) ? session.pendingInputs : [])
223
+ pendingInputs: (Array.isArray(session.pendingInputs) ? session.pendingInputs : [])
133
224
  .filter(function (item) { return !queueTombstones[item?.id]; })
134
225
  }), { preserveRunActionPayload: true });
135
226
  });
136
- var written = await input.SessionPersistence.writeSessions({
137
- Migration: Migration,
138
- StorageDb: StorageDb,
139
- projectId: projectId,
140
- accountScopeId: accountScopeId,
141
- deletedSessionIds: deletedSessionIds,
142
- sessionRecords: sessionRecords,
143
- reviewRunIds: input.reviewRunIds,
144
- queueClaims: persistenceMeta.queueClaims || {},
145
- queueTombstones: queueTombstones,
146
- writerId: input.persistenceContext?.writerId || ''
147
- });
227
+ sessionRecords = StorageDb.compactSessionBatchActions?.(sessionRecords, { activeSessionId: state.activeSessionId }) || sessionRecords;
228
+ var written;
229
+ try {
230
+ written = await input.SessionPersistence.writeSessions({
231
+ Migration: Migration,
232
+ StorageDb: StorageDb,
233
+ projectId: projectId,
234
+ accountScopeId: accountScopeId,
235
+ deletedSessionIds: deletedSessionIds,
236
+ sessionRecords: sessionRecords,
237
+ reviewRunIds: input.reviewRunIds,
238
+ queueClaims: persistenceMeta.queueClaims || {},
239
+ queueTombstones: queueTombstones,
240
+ writerId: input.persistenceContext?.writerId || ''
241
+ });
242
+ } catch (error) {
243
+ if (error.code === 'shared_session_conflict' && error.reviewReceipts && StorageDb.reviewStateSnapshot) {
244
+ error.durableReviewRunIds = (input.reviewRunIds || []).filter(id => sessionRecords.some(expected => {
245
+ const proposed = expected.runs?.find(run => run.id === id);
246
+ const saved = error.reviewReceipts.find(record => record.id === expected.id
247
+ && record.accountScopeId === expected.accountScopeId && record.projectId === expected.projectId)
248
+ ?.runs?.find(run => run.id === id);
249
+ return proposed && saved && StorageDb.reviewStateSnapshot(proposed) === StorageDb.reviewStateSnapshot(saved);
250
+ }));
251
+ delete error.reviewReceipts;
252
+ }
253
+ throw error;
254
+ }
148
255
  if (!reviewRuns.every(reviewRun => (written || []).some(session => (session.runs || []).some(run =>
149
- run.id === reviewRun.id && run.trackedChangeStatus === reviewRun.trackedChangeStatus
256
+ run.id === reviewRun.id
257
+ && (!reviewRun.trackedChangeStatus && ['applied', 'partial'].includes(reviewRun.undoStatus)
258
+ ? run.undoStatus === reviewRun.undoStatus && (reviewRun.undoStatus === 'applied'
259
+ || ['undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges']
260
+ .every(field => JSON.stringify(run[field] || []) === JSON.stringify(reviewRun[field] || [])))
261
+ : run.trackedChangeStatus === reviewRun.trackedChangeStatus)
150
262
  )))) throw reviewPersistenceError();
263
+ verifyRecoveredRuns(input.recoveryRuns, sessionRecords, written);
264
+ return written;
151
265
  }
152
266
 
153
267
  return Object.freeze({ persistPanelState: persistPanelState, applyReviewTransition: applyReviewTransition });
@@ -293,7 +293,7 @@
293
293
  toolbar = null;
294
294
  }
295
295
 
296
- return { attach, capture, clear, render, start, destroy };
296
+ return { attach, capture, clear, render, start, destroy, getRevision: () => generation };
297
297
  }
298
298
 
299
299
  return { create, buildChip, renderSent };
@@ -0,0 +1,153 @@
1
+ (function initCodexOverleafSensitivePreflight(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafSensitivePreflight = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function sensitivePreflightFactory() {
8
+ 'use strict';
9
+
10
+ // Report evidence admits detector identities only, never matched text or previews.
11
+ const REPORT_DETECTOR_IDS = new Set(['private-key', 'bearer-token', 'api-token', 'aws-access-key',
12
+ 'google-api-key', 'huggingface-token', 'gitlab-token', 'stripe-live-secret', 'jwt-token', 'secret-assignment']);
13
+
14
+ function create({
15
+ SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
16
+ getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
17
+ }) {
18
+ async function runSensitivePreflight(options) {
19
+ options = options || {};
20
+ const { task, project, rules, useExistingMirror = false, guidance = false } = options;
21
+ const normalizedRules = normalizeGovernanceRules(rules);
22
+ if (normalizedRules.sensitiveCheckEnabled === false) {
23
+ return { blocked: false, findings: [] };
24
+ }
25
+ if (!SensitiveScan?.scanSensitiveInputs && !useExistingMirror) {
26
+ return { blocked: false, findings: [] };
27
+ }
28
+ let findings = SensitiveScan?.scanSensitiveInputs
29
+ ? SensitiveScan.scanSensitiveInputs({
30
+ task,
31
+ files: guidance ? [] : (project?.files || []).filter(isTextSnapshotFile)
32
+ })
33
+ : [];
34
+ if (useExistingMirror && !guidance) {
35
+ findings = dedupeSensitiveFindings([
36
+ ...findings,
37
+ ...await scanNativeMirrorSensitiveFindings()
38
+ ]);
39
+ }
40
+ if (!findings.length) {
41
+ return { blocked: false, findings: [] };
42
+ }
43
+ options.assertCurrent?.();
44
+ appendRunEvent({
45
+ title: tx(
46
+ `Sensitive content check found ${findings.length} item(s).`,
47
+ `敏感内容检查发现 ${findings.length} 项。`
48
+ ),
49
+ status: 'failed',
50
+ detail: findings.slice(0, 8).map(finding => ({
51
+ [tr('detailFile')]: finding.path || finding.source || 'task',
52
+ [tr('detailReason')]: finding.detectorId,
53
+ [tx('Preview', '预览')]: finding.preview || '[REDACTED]'
54
+ }))
55
+ });
56
+ if (normalizedRules.sensitiveConfirmAllowed === true) {
57
+ const approved = await showPluginConfirm({
58
+ title: tr('sensitiveConfirmTitle'),
59
+ message: formatSensitiveConfirmMessage(findings),
60
+ confirmLabel: guidance ? tx('Send guidance', '发送指导') : tr('sensitiveConfirmRun'),
61
+ cancelLabel: tr('confirmDefaultCancel'),
62
+ destructive: true
63
+ });
64
+ options.assertCurrent?.();
65
+ return { blocked: !approved, findings };
66
+ }
67
+ // Blocking an extra message must not finish the task already in progress.
68
+ if (guidance) return { blocked: true, findings };
69
+ const detectorIds = [...new Set(findings.map(finding => finding?.detectorId))];
70
+ appendCompletionReport({ conclusionKey: 'sensitive_blocked',
71
+ conclusion: tx(
72
+ 'This run was blocked before Codex received project context because sensitive content was detected.',
73
+ '检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
74
+ ),
75
+ status: 'blocked',
76
+ unchangedReason: tx('Sensitive content was found before sending.', '发送前发现了敏感内容。'),
77
+ failure: { code: 'sensitive_content_blocked', stage: 'preflight', severity: 'blocked',
78
+ retryable: false, changedDocument: false,
79
+ evidence: {
80
+ // An unknown type makes the cause generic, even alongside one known type.
81
+ detectorIds: detectorIds.every(id => REPORT_DETECTOR_IDS.has(id)) ? detectorIds.slice(0, 5) : [],
82
+ findingCount: findings.length
83
+ } },
84
+ operations: [],
85
+ applyResults: [],
86
+ nextStep: tx(
87
+ 'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.',
88
+ '请移除敏感内容、缩小 @context,或在项目设置里允许敏感内容显式确认。'
89
+ )
90
+ });
91
+ return { blocked: true, findings };
92
+ }
93
+
94
+ function formatSensitiveConfirmMessage(findings = []) {
95
+ const visible = findings.slice(0, 8).map(finding => {
96
+ const path = finding.path || finding.source || 'task';
97
+ return `${path}: ${finding.detectorId || 'sensitive'} (${finding.preview || '[REDACTED]'})`;
98
+ });
99
+ return [
100
+ tr('sensitiveConfirmMessage'),
101
+ '',
102
+ ...visible,
103
+ findings.length > visible.length ? tx(`${findings.length - visible.length} more finding(s).`, `另有 ${findings.length - visible.length} 项。`) : ''
104
+ ].filter(Boolean).join('\n');
105
+ }
106
+
107
+ async function scanNativeMirrorSensitiveFindings() {
108
+ try {
109
+ const response = await sendBackgroundNative({
110
+ method: 'mirror.scanSensitive',
111
+ params: { projectId: getCurrentProjectId() }
112
+ });
113
+ if (response?.ok) {
114
+ return Array.isArray(response.result?.findings) ? response.result.findings : [];
115
+ }
116
+ return [{
117
+ detectorId: 'mirror-sensitive-scan-unavailable',
118
+ source: 'local-mirror',
119
+ preview: response?.error?.message || 'Could not scan the local mirror before Codex runs.'
120
+ }];
121
+ } catch (error) {
122
+ return [{
123
+ detectorId: 'mirror-sensitive-scan-unavailable',
124
+ source: 'local-mirror',
125
+ preview: error?.message || 'Could not scan the local mirror before Codex runs.'
126
+ }];
127
+ }
128
+ }
129
+
130
+ function dedupeSensitiveFindings(findings = []) {
131
+ const seen = new Set();
132
+ const deduped = [];
133
+ for (const finding of findings || []) {
134
+ const key = [
135
+ finding?.detectorId || '',
136
+ finding?.source || '',
137
+ finding?.path || '',
138
+ finding?.preview || ''
139
+ ].join('\u0000');
140
+ if (seen.has(key)) {
141
+ continue;
142
+ }
143
+ seen.add(key);
144
+ deduped.push(finding);
145
+ }
146
+ return deduped;
147
+ }
148
+
149
+ return { run: runSensitivePreflight };
150
+ }
151
+
152
+ return { create };
153
+ });
@@ -7,15 +7,32 @@
7
7
 
8
8
  function create(deps = {}) {
9
9
  const {
10
- SessionState, getState, setState, sendBackgroundNative, saveState,
10
+ SessionState, getState, setState, getScope, sendBackgroundNative, saveState,
11
11
  applyStateToPanel, showPluginToast, tr
12
12
  } = deps;
13
13
 
14
14
  async function forkRunFromNode(runId) {
15
15
  const previousState = getState();
16
+ const skillPreferences = {
17
+ loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
18
+ loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false,
19
+ codexLocalSkillEnabled: { ...previousState.codexLocalSkillEnabled }
20
+ };
16
21
  const source = findSource(previousState, runId);
17
22
  if (!source) return failFork('session_fork_source_missing', tr('forkRunUnavailable'));
18
- const { session, run, runIndex } = source;
23
+ const scope = getScope ? { ...getScope() } : null;
24
+ const requestedActiveId = previousState.activeSessionId;
25
+ // The Native request and copied history must describe the same origin,
26
+ // even when the source session is edited while the request is pending.
27
+ const { session, run, runIndex } = JSON.parse(JSON.stringify(source));
28
+ const sameScope = () => {
29
+ const current = getScope?.();
30
+ return !scope || ['accountScopeId', 'projectId', 'generation'].every(key => current?.[key] === scope[key]);
31
+ };
32
+ const currentSource = () => {
33
+ const current = findSource(getState(), runId);
34
+ return current?.session.id === session.id && current.run.codexTurnId === run.codexTurnId;
35
+ };
19
36
  const draftThreadId = run.codexTurnId && run.interruptedDraft?.turnId === run.codexTurnId
20
37
  ? run.interruptedDraft.threadId : '';
21
38
  // Provider switches replace the session thread, not the selected node's origin.
@@ -25,25 +42,39 @@
25
42
  }
26
43
  let response;
27
44
  try {
45
+ const resolveLocalSkills = deps.captureLocalSkillIdsForFork || deps.getEnabledCodexLocalSkillIds;
46
+ const enabledCodexLocalSkillIds = skillPreferences.loadCodexLocalSkills
47
+ ? captureLocalSkillIds(await resolveLocalSkills?.(skillPreferences)) : [];
48
+ if (!sameScope() || !currentSource()) return null;
49
+ if (resolveLocalSkills && !Array.isArray(enabledCodexLocalSkillIds)) {
50
+ throw codedError('codex_local_skill_unavailable', tr('forkRunFailed'));
51
+ }
28
52
  response = await sendBackgroundNative({
29
53
  method: 'codex.thread.fork',
30
54
  params: {
31
55
  threadId: sourceThreadId,
32
56
  lastTurnId: run.codexTurnId,
33
57
  speedTier: session.speedTier,
34
- loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
35
- loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false
58
+ loadCodexLocalSkills: skillPreferences.loadCodexLocalSkills,
59
+ loadCodexOverleafSkills: skillPreferences.loadCodexOverleafSkills,
60
+ enabledCodexLocalSkillIds
36
61
  }
37
62
  });
38
63
  } catch (error) {
64
+ if (!sameScope() || !currentSource()) return null;
39
65
  return failFork(error?.code || 'session_fork_failed', error?.message || tr('forkRunFailed'));
40
66
  }
67
+ if (!sameScope() || !currentSource()) {
68
+ if (response?.ok && response.result?.threadId) await clearFork(response.result.threadId);
69
+ return null;
70
+ }
41
71
  if (!response?.ok || !response.result?.threadId) {
42
72
  return failFork(response?.error?.code || 'session_fork_failed', response?.error?.message || tr('forkRunFailed'));
43
73
  }
44
74
  const copiedRuns = session.runs.slice(0, runIndex + 1).map(copyRunSnapshot);
45
75
  const titleBase = String(session.title || copiedRuns[0]?.task || tr('newSessionFallback')).trim();
46
- const forkTitle = buildUniqueForkTitle(previousState.sessions, titleBase);
76
+ const mergeBase = getState();
77
+ const forkTitle = buildUniqueForkTitle(mergeBase.sessions, titleBase);
47
78
  const forked = SessionState.createSession({
48
79
  title: forkTitle,
49
80
  titleSource: 'manual',
@@ -62,27 +93,55 @@
62
93
  forkedFromSessionId: session.id,
63
94
  forkedFromRunId: run.id
64
95
  });
65
- setState(SessionState.normalizePanelState({
66
- ...previousState,
67
- sessions: [...(previousState.sessions || []), forked],
68
- activeSessionId: forked.id
69
- }));
96
+ const activeId = mergeBase.activeSessionId === requestedActiveId
97
+ ? forked.id : mergeBase.activeSessionId;
98
+ const installed = SessionState.setActiveSession({
99
+ ...mergeBase, sessions: [...(mergeBase.sessions || []), forked]
100
+ }, activeId);
101
+ const installedSnapshot = JSON.stringify(installed);
102
+ const forkSnapshot = JSON.stringify(forked);
103
+ setState(installed);
70
104
  try {
105
+ // Keep the visible composer and active-session owner aligned during save.
106
+ applyStateToPanel();
71
107
  await saveState();
72
108
  } catch (error) {
73
- setState(previousState);
74
- await sendBackgroundNative({
75
- method: 'codex.history.clearPlugin',
76
- params: { sessionId: forked.id, threadId: response.result.threadId }
77
- }).catch(() => null);
78
- showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
109
+ if (sameScope()) {
110
+ const current = getState();
111
+ const currentFork = current.sessions?.find(item => item.id === forked.id);
112
+ // Never erase work entered into the fork after its optimistic insert.
113
+ if (!currentFork || JSON.stringify(currentFork) === forkSnapshot) {
114
+ if (currentFork) {
115
+ if (current === installed && JSON.stringify(current) === installedSnapshot) {
116
+ setState(mergeBase);
117
+ } else {
118
+ const remaining = current.sessions.filter(item => item.id !== forked.id);
119
+ const fallbackId = remaining.some(item => item.id === mergeBase.activeSessionId)
120
+ ? mergeBase.activeSessionId : remaining[remaining.length - 1]?.id;
121
+ setState(SessionState.setActiveSession({ ...current, sessions: remaining },
122
+ current.activeSessionId === forked.id ? fallbackId : current.activeSessionId));
123
+ }
124
+ try { applyStateToPanel(); } catch (_renderError) { /* Preserve the original failure and finish cleanup. */ }
125
+ }
126
+ await clearFork(response.result.threadId, forked.id);
127
+ }
128
+ if (sameScope()) showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
129
+ }
79
130
  throw error;
80
131
  }
81
- applyStateToPanel();
132
+ // Persistence owns its captured scope. A late result cannot render a new
133
+ // view or delete a thread that may already have been saved in the old one.
134
+ if (!sameScope() || !getState().sessions?.some(item => item.id === forked.id)) return null;
82
135
  showPluginToast(tr('forkRunDone'), { status: 'completed' });
83
136
  return forked;
84
137
  }
85
138
 
139
+ async function clearFork(threadId, sessionId) {
140
+ try {
141
+ await sendBackgroundNative({ method: 'codex.history.clearPlugin', params: { sessionId, threadId } });
142
+ } catch (_error) { /* Best-effort cleanup must not restore a stale view. */ }
143
+ }
144
+
86
145
  function failFork(code, message) {
87
146
  showPluginToast(message, { status: 'failed', sticky: true });
88
147
  throw codedError(code, message);
@@ -91,6 +150,10 @@
91
150
  return { forkRunFromNode };
92
151
  }
93
152
 
153
+ function captureLocalSkillIds(value) {
154
+ return Array.isArray(value) ? [...value] : undefined;
155
+ }
156
+
94
157
  function buildUniqueForkTitle(sessions = [], sourceTitle = '') {
95
158
  const fallback = String(sourceTitle || 'New Session').trim() || 'New Session';
96
159
  const baseTitle = fallback
@@ -11,7 +11,8 @@
11
11
  const {
12
12
  SessionPanel,
13
13
  SessionState,
14
- SessionMenuView
14
+ SessionMenuView,
15
+ PanelIcons
15
16
  } = deps;
16
17
  const {
17
18
  createSession,
@@ -30,6 +31,9 @@
30
31
  saveState,
31
32
  saveStateSoon,
32
33
  readPanelInputs,
34
+ flushComposerDraft,
35
+ setSessionTransition,
36
+ getDraftScope,
33
37
  applyStateToPanel,
34
38
  syncSessionProvider,
35
39
  getPanel,
@@ -47,6 +51,7 @@
47
51
  getActiveSession,
48
52
  getPanel,
49
53
  getSessionDisplayTitle,
54
+ icons: PanelIcons,
50
55
  getState,
51
56
  isDisplayableSession,
52
57
  isSessionRunning,
@@ -60,7 +65,31 @@
60
65
  closeSessionMenu
61
66
  } = sessionMenuView;
62
67
 
63
- async function startNewSession() {
68
+ let transitionToken = null;
69
+
70
+ async function withSessionTransition(change) {
71
+ if (transitionToken) return;
72
+ const token = {};
73
+ transitionToken = token;
74
+ const scope = getDraftScope?.();
75
+ try {
76
+ // Lock before the first wait, including an outstanding IME commit.
77
+ await setSessionTransition?.(true);
78
+ if (getDraftScope?.() !== scope) return;
79
+ return await change(scope);
80
+ } finally {
81
+ if (transitionToken === token) {
82
+ transitionToken = null;
83
+ setSessionTransition?.(false);
84
+ }
85
+ }
86
+ }
87
+
88
+ function startNewSession() {
89
+ return withSessionTransition(createNewSession);
90
+ }
91
+
92
+ async function createNewSession(draftScope) {
64
93
  // Leaving a still-running session is allowed but never silent: the run
65
94
  // keeps going in the background and lands in its own session.
66
95
  const runningActive = getActiveSession(getState());
@@ -74,7 +103,10 @@
74
103
  return;
75
104
  }
76
105
  }
106
+ if (getDraftScope?.() !== draftScope) return;
77
107
  readPanelInputs();
108
+ await flushComposerDraft?.();
109
+ if (getDraftScope?.() !== draftScope) return;
78
110
  // Reuse an already-empty, idle active session instead of minting a ghost.
79
111
  // Empty sessions are invisible in the list yet still consume the storage cap
80
112
  // (slice(-20) / maxSessions), so spamming "New Session" could silently evict
@@ -99,6 +131,7 @@
99
131
  if (!evictionApproved) {
100
132
  return;
101
133
  }
134
+ if (getDraftScope?.() !== draftScope) return;
102
135
  }
103
136
  const session = createSession({
104
137
  mode: getState().mode,
@@ -114,15 +147,22 @@
114
147
  activeSessionId: session.id
115
148
  }));
116
149
  applyStateToPanel();
150
+ const destinationScope = getDraftScope?.();
117
151
  await syncSessionProvider?.();
152
+ if (getDraftScope?.() !== destinationScope) return;
118
153
  await saveState();
154
+ if (getDraftScope?.() !== destinationScope) return;
119
155
  getPanel().querySelector('[data-task]')?.focus();
120
156
  }
121
157
 
122
- async function switchSession(sessionId) {
158
+ function switchSession(sessionId) {
123
159
  if (sessionId === getState().activeSessionId) {
124
160
  return;
125
161
  }
162
+ return withSessionTransition(scope => switchToSession(sessionId, scope));
163
+ }
164
+
165
+ async function switchToSession(sessionId, draftScope) {
126
166
  const runningActive = getActiveSession(getState());
127
167
  if (runningActive && isSessionRunning(runningActive)) {
128
168
  const approved = await showPluginConfirm({
@@ -134,10 +174,15 @@
134
174
  return;
135
175
  }
136
176
  }
177
+ if (getDraftScope?.() !== draftScope) return;
137
178
  readPanelInputs();
179
+ await flushComposerDraft?.();
180
+ if (getDraftScope?.() !== draftScope) return;
138
181
  setState(setActiveSession(getState(), sessionId));
139
182
  applyStateToPanel();
183
+ const destinationScope = getDraftScope?.();
140
184
  await syncSessionProvider?.();
185
+ if (getDraftScope?.() !== destinationScope) return;
141
186
  await saveState();
142
187
  }
143
188
 
@@ -201,7 +246,7 @@
201
246
  }
202
247
  });
203
248
  if (!response?.ok) {
204
- showPluginToast(tr('deleteSessionHistoryFailedToast', { message: response?.error?.message || 'native host did not return success' }), { status: 'warning', sticky: true });
249
+ showPluginToast(tr('deleteSessionHistoryFailedToast', { message: response?.error?.message || tr('deleteSessionNativeFailureDetail') }), { status: 'warning', sticky: true });
205
250
  } else if (response.result?.skipped) {
206
251
  showPluginToast(tr('deleteSessionNoThreadToast'), { status: 'info' });
207
252
  } else {
@@ -295,6 +340,7 @@
295
340
  bindActiveSessionHeader,
296
341
  startNewSession,
297
342
  switchSession,
343
+ isTransitioning: () => transitionToken !== null,
298
344
  deleteSessionWithConfirm,
299
345
  renderSessionList,
300
346
  commitSessionRename,