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
@@ -0,0 +1,256 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafFileMover = factory();
4
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
5
+ 'use strict';
6
+
7
+ function create(deps = {}) {
8
+ const { window, document, treeOperations, snapshotRouter, textFileCreator } = deps;
9
+ const normalizePath = deps.normalizeSafeProjectPath || window.CodexOverleafProjectFiles?.normalizeSafeProjectPath;
10
+ const delay = deps.delay || (ms => new Promise(resolve => window.setTimeout(resolve, ms)));
11
+ const error = (code, message) => Object.assign(new Error(message), { code });
12
+ const parent = path => path.split('/').slice(0, -1).join('/');
13
+ const basename = path => path.split('/').pop();
14
+ const join = (folder, name) => [folder, name].filter(Boolean).join('/');
15
+ function entities() {
16
+ treeOperations.invalidateDomProjectPathCache?.();
17
+ const root = document.querySelector('[data-testid="file-tree-list-root"], .file-tree-list[role="tree"]');
18
+ const unique = new Map();
19
+ for (const row of root?.querySelectorAll?.('[role="treeitem"]') || []) {
20
+ const entity = row.querySelector?.(':scope > .entity[data-file-id]')
21
+ || row.querySelector?.('[data-file-type][data-file-id]');
22
+ const id = entity?.dataset?.fileId || entity?.getAttribute?.('data-file-id');
23
+ const type = entity?.dataset?.fileType || entity?.getAttribute?.('data-file-type');
24
+ const path = normalizePath?.(treeOperations.readProjectPathFromNode?.(row));
25
+ if (id && path && ['doc', 'file', 'folder'].includes(type)) unique.set(type + ':' + id + ':' + path, { id, path, type });
26
+ }
27
+ return [...unique.values()];
28
+ }
29
+ function at(path, type) {
30
+ const found = entities().filter(entry => entry.path === path && (!type || entry.type === type));
31
+ return found.length === 1 ? found[0] : null;
32
+ }
33
+ function folderId(path, projectId) {
34
+ if (path) return at(path, 'folder')?.id || '';
35
+ for (const root of [window._ide, window.Overleaf, window.overleaf, window.OL]) {
36
+ const project = root?.project;
37
+ if ((project?._id || project?.id) !== projectId) continue;
38
+ const folder = Array.isArray(project.rootFolder) ? project.rootFolder[0] : project.rootFolder;
39
+ if (folder?._id || folder?.id) return folder._id || folder.id;
40
+ }
41
+ return '';
42
+ }
43
+ async function digest(file, kind) {
44
+ let bytes;
45
+ if (kind === 'text' && typeof file?.content === 'string') bytes = new TextEncoder().encode(file.content);
46
+ else if (kind === 'binary' && typeof file?.contentBase64 === 'string') {
47
+ bytes = Uint8Array.from((window.atob || globalThis.atob)(file.contentBase64), char => char.charCodeAt(0));
48
+ } else throw error('file_move_source_unavailable', 'The source contents could not be verified.');
49
+ const hash = await (window.crypto || globalThis.crypto).subtle.digest('SHA-256', bytes);
50
+ return [...new Uint8Array(hash)].map(byte => byte.toString(16).padStart(2, '0')).join('');
51
+ }
52
+ function csrfToken() {
53
+ const meta = document.querySelector('meta[name="ol-csrfToken"], meta[name="csrf-token"], meta[name="_csrf"]');
54
+ let value = meta ? meta.content : window.csrfToken || window._csrf;
55
+ if (['json', 'number'].includes(meta?.dataset?.type)) {
56
+ try { value = JSON.parse(value); } catch (_error) { return ''; }
57
+ }
58
+ if (meta?.dataset?.type === 'boolean') return '';
59
+ return typeof value === 'string' && value.length > 0 && value.length <= 4096 && !/[\0\r\n]/.test(value) ? value : '';
60
+ }
61
+
62
+ // Compatibility consumer for identity-bound moves recorded by older builds.
63
+ // New mirror diffs never produce metadata moves.
64
+ async function moveFile(operation, options = {}) {
65
+ const source = normalizePath?.(operation?.path), target = normalizePath?.(operation?.to);
66
+ const proof = operation?.moveProof;
67
+ const projectId = options.runProjectId;
68
+ const entity = value => typeof value === 'string' && value.length > 0 && value.length <= 160 && !/[\0\r\n]/.test(value);
69
+ if (options.noTraceUndo !== true || !entity(proof?.sourceEntityId) || !entity(proof?.targetEntityId)
70
+ || proof.sourceEntityId !== proof.targetEntityId) return { ok: false, code: 'file_tree_controls_unavailable', changedDocument: false,
71
+ reason: 'A recorded same-entity move is required before its location can be restored.' };
72
+ const deadline = Math.min(Date.now() + 90000, Number.isFinite(options.deadlineAt) ? options.deadlineAt : Infinity);
73
+ const createdDirectories = [], completed = [];
74
+ let entityId = '', entityType = '', attempted = false, currentPath = source;
75
+ const assertCurrent = () => {
76
+ if (!projectId || treeOperations.getProjectId() !== projectId) throw error('aborted_project_changed', 'The project changed during the file move.');
77
+ if (options.isCurrent?.() === false) throw error('codex_cancelled', 'The file move was cancelled.');
78
+ if (Date.now() >= deadline) throw error('file_move_deadline_exceeded', 'The file move verification deadline was reached.');
79
+ };
80
+ const inspectRemainingDirectories = async () => {
81
+ if (!Array.isArray(proof.createdDirectories) || !proof.createdDirectories.length) return {};
82
+ try {
83
+ const directoryCleanup = await textFileCreator?.cleanupCreatedDirectories?.(proof.createdDirectories, {
84
+ projectId, isCurrent: () => { assertCurrent(); return true; }
85
+ });
86
+ if (directoryCleanup) return { directoryCleanup };
87
+ } catch (_error) { /* The verified file move remains complete; only its directory inspection is unresolved. */ }
88
+ return { directoryCleanup: { ok: false, removed: [], remaining: proof.createdDirectories, requiresExplicitCleanup: true } };
89
+ };
90
+ const observedPath = () => {
91
+ if (treeOperations.getProjectId() !== projectId) return '';
92
+ const found = entities().filter(entry => entry.id === entityId && entry.type === entityType);
93
+ return found.length === 1 ? found[0].path : '';
94
+ };
95
+ const snapshot = async () => {
96
+ assertCurrent(); snapshotRouter.invalidateCache?.();
97
+ const result = await snapshotRouter.fetchProjectZipSnapshot({ force: true, maxAgeMs: 0, serverOnly: true,
98
+ includeContent: true, includeBinaryFiles: true, writebackVerification: true,
99
+ zipTimeoutMs: Math.max(1, Math.min(30000, deadline - Date.now())) });
100
+ assertCurrent();
101
+ const skipped = [...(result?.skipped || []), ...(result?.capabilities?.skipped || [])];
102
+ if (!result?.ok || !Array.isArray(result.files) || result.partialSnapshot === true
103
+ || result.pathIdentityComplete === false || result.capabilities?.pathIdentityComplete === false
104
+ || skipped.some(file => file?.unknownPath || ['zip_path_invalid', 'zip_path_duplicate'].includes(file?.reason))
105
+ || result.capabilities?.fullProjectSnapshot === false || (result.id && result.id !== projectId)) {
106
+ throw error('file_move_source_unavailable', 'A complete server source snapshot is required to move a file.');
107
+ }
108
+ return result;
109
+ };
110
+ const contains = (data, path) => data.files.some(file => file.path === path)
111
+ || [...(data.skipped || []), ...(data.capabilities?.skipped || [])].some(file => file.path === path);
112
+ const verify = async (path, data) => {
113
+ assertCurrent();
114
+ const entity = at(path);
115
+ if (!entity || !['doc', 'file'].includes(entity.type) || (entityId && entity.id !== entityId)
116
+ || (entityType && entity.type !== entityType) || (proof.entityType && entity.type !== proof.entityType)
117
+ || (proof.sourceEntityId && entity.id !== proof.sourceEntityId)) {
118
+ throw error('file_move_identity_changed', 'The exact source file identity changed or is unavailable.');
119
+ }
120
+ const file = data.files.find(file => file.path === path);
121
+ if (await digest(file, proof.kind) !== proof.sha256.toLowerCase()) {
122
+ throw error('stale_source_changed', 'The source file changed after Codex read it.');
123
+ }
124
+ assertCurrent();
125
+ if (at(path)?.id !== entity.id || at(path)?.type !== entity.type) throw error('file_move_identity_changed', 'The source file identity changed while checking its contents.');
126
+ return entity;
127
+ };
128
+ const checkStep = async step => {
129
+ const data = await snapshot(); await verify(step.from, data);
130
+ if (contains(data, step.to) || at(step.to)) throw error('file_move_target_exists', 'The destination or an intermediate move path is occupied.');
131
+ if (step.action === 'move' && folderId(parent(step.to), projectId) !== step.folderId) {
132
+ throw error('file_move_folder_changed', 'The destination folder identity changed.');
133
+ }
134
+ assertCurrent();
135
+ };
136
+ const confirmStep = async step => {
137
+ for (let i = 0; i < 8; i++) {
138
+ const data = await snapshot();
139
+ if (!contains(data, step.from) && contains(data, step.to) && at(step.to)?.id === entityId) {
140
+ await verify(step.to, data); currentPath = step.to; return true;
141
+ }
142
+ if (i < 7) await delay(Math.min(250 * (i + 1), 1000));
143
+ }
144
+ return false;
145
+ };
146
+ const send = async step => {
147
+ await checkStep(step);
148
+ const csrf = csrfToken();
149
+ if (!csrf) throw error('file_move_identity_unavailable', 'The page request identity is unavailable. Reload Overleaf before moving files.');
150
+ const Controller = window.AbortController || globalThis.AbortController;
151
+ const controller = new Controller();
152
+ const timer = window.setTimeout(() => controller.abort(), Math.max(1, deadline - Date.now()));
153
+ let response, transportError;
154
+ try {
155
+ assertCurrent(); attempted = true;
156
+ response = await window.fetch(`/project/${encodeURIComponent(projectId)}/${entityType}/${encodeURIComponent(entityId)}/${step.action}`, {
157
+ method: 'POST', credentials: 'same-origin', signal: controller.signal,
158
+ headers: { 'Content-Type': 'application/json', 'X-Csrf-Token': csrf, Accept: 'application/json' },
159
+ body: JSON.stringify(step.action === 'rename' ? { name: basename(step.to) } : { folder_id: step.folderId })
160
+ });
161
+ } catch (caught) { transportError = caught; }
162
+ finally { window.clearTimeout(timer); }
163
+ assertCurrent();
164
+ if (await confirmStep(step)) return;
165
+ if (response && !response.ok && response.status >= 400 && response.status < 500 && response.status !== 408) {
166
+ const rejected = error('file_move_rejected', 'Overleaf rejected the file move.');
167
+ rejected.definitive = true; throw rejected;
168
+ }
169
+ const unknown = error('file_move_unconfirmed', transportError
170
+ ? 'The move response was lost. Check the file location before retrying.'
171
+ : 'The move result could not be confirmed. Check the file location before retrying.');
172
+ unknown.receiptUnconfirmed = true; throw unknown;
173
+ };
174
+ try {
175
+ assertCurrent();
176
+ if (!source || !target || source === target || operation.type !== 'move' || proof?.v !== 1
177
+ || !['text', 'binary'].includes(proof.kind) || !/^[a-f0-9]{64}$/i.test(proof.sha256 || '')) {
178
+ throw error('invalid_project_path', 'A verified source, different destination and content hash are required for a move.');
179
+ }
180
+ if (parent(source)) await textFileCreator?.prepareUploadParent?.(parent(source), { createMissing: false, isCurrent: () => { assertCurrent(); return true; } });
181
+ let data = await snapshot();
182
+ if (!contains(data, source) && proof.sourceEntityId && at(target)?.id === proof.sourceEntityId) {
183
+ entityId = proof.sourceEntityId; entityType = at(target).type;
184
+ await verify(target, data);
185
+ return { ok: true, verified: true, verification: 'overleaf-zip', idempotent: true, changedDocument: false,
186
+ method: 'overleaf.entity-move', moveProof: { ...proof, targetEntityId: entityId }, ...await inspectRemainingDirectories() };
187
+ }
188
+ const sourceEntity = await verify(source, data); entityId = sourceEntity.id; entityType = sourceEntity.type;
189
+ if (contains(data, target) || at(target)) throw error('file_move_target_exists', 'The move destination already exists.');
190
+ const originalParent = parent(source), destinationParent = parent(target);
191
+ const originalFolderId = originalParent === destinationParent ? '' : folderId(originalParent, projectId);
192
+ if (originalParent !== destinationParent && !originalFolderId) throw error('file_move_folder_changed', 'The original folder identity is unavailable.');
193
+ if (destinationParent && originalParent !== destinationParent && !folderId(destinationParent, projectId)) {
194
+ await textFileCreator?.prepareUploadParent?.(destinationParent, { createMissing: true, isCurrent: () => { assertCurrent(); return true; },
195
+ onCreatedDirectory: proof => createdDirectories.push(proof) });
196
+ }
197
+ const destinationFolderId = originalParent === destinationParent ? '' : folderId(destinationParent, projectId);
198
+ if (originalParent !== destinationParent && !destinationFolderId) throw error('file_move_folder_changed', 'The destination folder identity is unavailable.');
199
+ data = await snapshot(); await verify(source, data);
200
+ const steps = [];
201
+ const rename = to => { steps.push({ action: 'rename', from: currentPath, to }); currentPath = to; };
202
+ const move = to => { steps.push({ action: 'move', from: currentPath, to, folderId: destinationFolderId, fromFolderId: originalFolderId }); currentPath = to; };
203
+ if (originalParent === destinationParent) rename(target);
204
+ else if (basename(source) === basename(target)) move(target);
205
+ else if (!contains(data, join(originalParent, basename(target))) && !at(join(originalParent, basename(target)))) {
206
+ rename(join(originalParent, basename(target))); move(target);
207
+ } else if (!contains(data, join(destinationParent, basename(source))) && !at(join(destinationParent, basename(source)))) {
208
+ move(join(destinationParent, basename(source))); rename(target);
209
+ } else {
210
+ const token = window.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 16);
211
+ if (!token) throw error('file_move_target_exists', 'An unoccupied intermediate name could not be reserved safely.');
212
+ const extension = basename(source).match(/\.[^.]+$/)?.[0] || '';
213
+ const temporary = 'codex-move-' + token + extension;
214
+ if ([originalParent, destinationParent].some(folder => contains(data, join(folder, temporary)) || at(join(folder, temporary)))) {
215
+ throw error('file_move_target_exists', 'The temporary move path is occupied. Retry on a fresh project snapshot.');
216
+ }
217
+ rename(join(originalParent, temporary)); move(join(destinationParent, temporary)); rename(target);
218
+ }
219
+ currentPath = source;
220
+ for (const step of steps) { await send(step); completed.push(step); }
221
+ return { ok: true, verified: true, verification: 'overleaf-zip', changedDocument: true, method: 'overleaf.entity-move',
222
+ moveProof: { v: 1, kind: proof.kind, sha256: proof.sha256.toLowerCase(), sourceEntityId: entityId, targetEntityId: entityId, entityType },
223
+ ...await inspectRemainingDirectories(),
224
+ ...(createdDirectories.length ? { createdDirectories } : {}) };
225
+ } catch (caught) {
226
+ let rolledBack = false;
227
+ // Only a definite refusal permits compensation. A lost response may
228
+ // still be in flight; never replay or reverse that uncertain mutation.
229
+ if (caught.definitive && completed.length && treeOperations.getProjectId() === projectId && options.isCurrent?.() !== false) {
230
+ try {
231
+ for (const prior of completed.slice().reverse()) {
232
+ await send({ action: prior.action, from: prior.to, to: prior.from, folderId: prior.fromFolderId });
233
+ }
234
+ rolledBack = true;
235
+ } catch (_rollbackError) { /* Preserve the verified entity and report its location for recovery. */ }
236
+ }
237
+ const observed = entityId ? observedPath() : '';
238
+ const changedDocument = attempted || createdDirectories.length > 0;
239
+ const reason = rolledBack ? 'The move was not completed; the original file location was restored.' : caught.message;
240
+ const moveRecovery = attempted && !rolledBack ? { v: 1, projectId, kind: proof?.kind, sha256: proof?.sha256,
241
+ entityId, entityType, sourcePath: source, targetPath: target, observedPath: observed } : null;
242
+ return { ok: false, code: rolledBack ? 'file_move_rolled_back' : caught.code || 'file_move_failed', reason,
243
+ changedDocument, rolledBack, receiptUnconfirmed: caught.receiptUnconfirmed === true,
244
+ ...(createdDirectories.length ? { createdDirectories } : {}),
245
+ ...(moveRecovery ? { moveRecovery } : {}),
246
+ failure: { code: caught.code || 'file_move_failed', stage: attempted ? 'verify' : 'preflight',
247
+ severity: changedDocument ? 'warning' : 'blocked', terminalState: changedDocument ? 'needs_review' : 'blocked',
248
+ changedDocument, retryable: false, file: observed || source,
249
+ ...(moveRecovery ? { evidence: { moveRecovery } } : {}),
250
+ userMessage: reason, nextAction: changedDocument ? 'Check the file location before retrying.' : 'Read the project again before retrying.' } };
251
+ }
252
+ }
253
+ return { moveFile };
254
+ }
255
+ return { create };
256
+ });
@@ -16,6 +16,7 @@
16
16
  const checkpointWrite = hasCheckpointCapability(deps);
17
17
  const compileCapture = Boolean(deps.compileState?.capturedRequestTemplate);
18
18
  const reviewingControl = Boolean(deps.findReviewingActivationControl?.());
19
+ const upgradeModal = Boolean(deps.findUpgradeModal?.());
19
20
 
20
21
  if (!editorRead) {
21
22
  warnings.push('没有识别到当前 Overleaf 编辑器,Codex 不能读取当前文件。');
@@ -47,7 +48,9 @@
47
48
  trigger: compileCapture
48
49
  },
49
50
  reviewing: {
50
- control: reviewingControl
51
+ control: reviewingControl,
52
+ upgradeModal,
53
+ confirmed: !upgradeModal && deps.reviewingConfirmed === true
51
54
  },
52
55
  warnings
53
56
  };
@@ -0,0 +1,94 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafOwnedDirectoryCleanup = factory();
4
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
5
+ 'use strict';
6
+ function create(deps = {}) {
7
+ const { document, treeOperations, delay, now, readServerSnapshot,
8
+ assertVerificationOwner, normalizeDomText, normalizeSafeProjectPath } = deps;
9
+ const { invalidateDomProjectPathCache } = treeOperations;
10
+ function folderIdentity(row) {
11
+ const entity = row?.querySelector?.('[data-file-type="folder"]');
12
+ return ['data-file-id', 'data-folder-id', 'data-entity-id']
13
+ .map(name => entity?.getAttribute?.(name) || row?.getAttribute?.(name) || '').find(Boolean) || '';
14
+ }
15
+ // Resolve every path component within its direct native child group. A
16
+ // collapsed/ambiguous tree is unknown, never proof that a folder is absent.
17
+ function locateOwnedFolder(path) {
18
+ let group = document.querySelector('[data-testid="file-tree-list-root"], .file-tree-list[role="tree"]');
19
+ const parts = path.split('/');
20
+ for (let index = 0; index < parts.length; index++) {
21
+ if (!group?.children) throw new Error('The native folder tree is unavailable.');
22
+ const inner = Array.from(group.children).find(node => node.classList?.contains('file-tree-folder-list-inner')) || group;
23
+ const matches = Array.from(inner.children).filter(node => node.getAttribute?.('role') === 'treeitem'
24
+ && node.querySelector?.('[data-file-type="folder"]') && normalizeDomText(node.getAttribute('aria-label')) === parts[index]);
25
+ if (matches.length > 1) throw new Error('The folder path is ambiguous.');
26
+ if (!matches.length) return null;
27
+ const row = matches[0];
28
+ if (index === parts.length - 1) return row;
29
+ if (row.getAttribute('aria-expanded') !== 'true') throw new Error('The ancestor folder must be expanded before cleanup.');
30
+ const sibling = row.nextElementSibling;
31
+ group = sibling?.getAttribute('role') === 'tree' ? sibling : row.querySelector('[role="tree"]');
32
+ }
33
+ return null;
34
+ }
35
+ // Native folder Delete can remove a subtree. Read/empty checks cannot
36
+ // make that mutation conditional on the subtree staying empty. Retain the
37
+ // proof for explicit native cleanup; never click Delete automatically.
38
+ async function cleanupCreatedDirectories(proofs, options = {}) {
39
+ const items = Array.isArray(proofs) ? proofs : [];
40
+ const removed = [], completed = new Set(), failures = [], review = [];
41
+ for (const proof of items.slice().sort((a, b) => String(b?.path || '').split('/').length - String(a?.path || '').split('/').length)) {
42
+ try {
43
+ const path = normalizeSafeProjectPath(proof?.path);
44
+ if (proof?.v !== 1 || !path || path !== proof.path || !proof.projectId || !proof.nativeFolderId
45
+ || (options.projectId && proof.projectId !== options.projectId)) throw new Error('The created folder identity is not proven.');
46
+ const assertCurrent = () => assertVerificationOwner(proof.projectId, options.isCurrent);
47
+ const exactRow = () => {
48
+ const row = locateOwnedFolder(path);
49
+ if (row && folderIdentity(row) !== proof.nativeFolderId) throw new Error('The created folder identity changed.');
50
+ return row;
51
+ };
52
+ assertCurrent();
53
+ for (let depth = 1; depth <= path.split('/').length; depth++) {
54
+ const prefix = path.split('/').slice(0, depth).join('/'), row = locateOwnedFolder(prefix);
55
+ if (!row) break;
56
+ if (prefix === path && folderIdentity(row) !== proof.nativeFolderId) throw new Error('The created folder identity changed.');
57
+ if (row.getAttribute('aria-expanded') === 'true') continue;
58
+ const toggle = row.querySelector('.folder-expand-collapse-button');
59
+ if (!toggle) throw new Error('The folder cannot be expanded safely.');
60
+ assertCurrent(); toggle.click();
61
+ const deadline = now() + 5000;
62
+ while (locateOwnedFolder(prefix)?.getAttribute('aria-expanded') !== 'true') {
63
+ assertCurrent(); if (now() >= deadline) throw new Error('The folder expansion was not confirmed.');
64
+ await delay(100); invalidateDomProjectPathCache();
65
+ }
66
+ }
67
+ exactRow();
68
+ const snapshot = await readServerSnapshot({ projectId: proof.projectId, isCurrent: options.isCurrent, includeBinaryFiles: true });
69
+ assertCurrent();
70
+ const row = exactRow();
71
+ const entries = [...snapshot.files, ...(snapshot.skipped || []), ...(snapshot.capabilities?.skipped || [])];
72
+ const serverHasEntries = entries.some(entry => String(entry?.path || '').startsWith(path + '/'));
73
+ if (!row) {
74
+ if (serverHasEntries) throw new Error('Server entries remain under the folder path; absence is unconfirmed.');
75
+ completed.add(proof); removed.push({ ...proof, alreadyAbsent: true, verification: 'overleaf-native-tree-and-zip' }); continue;
76
+ }
77
+ const sibling = row.nextElementSibling;
78
+ const group = sibling?.getAttribute('role') === 'tree' ? sibling : row.querySelector('[role="tree"]');
79
+ if (row.getAttribute('aria-expanded') !== 'true' || !group?.children) throw new Error('The folder contents are unknown.');
80
+ const inner = Array.from(group.children).find(node => node.classList?.contains('file-tree-folder-list-inner')) || group;
81
+ const empty = inner.children.length === 0 && !serverHasEntries;
82
+ review.push({ ...proof, empty, requiresExplicitCleanup: true });
83
+ failures.push({ path, code: empty ? 'directory_requires_explicit_cleanup' : 'created_directory_not_empty',
84
+ reason: empty ? 'The new empty folder remains. Review and remove it through Overleaf if appropriate.'
85
+ : 'The created folder contains entries or unread files and was left untouched.' });
86
+ } catch (error) { failures.push({ path: proof?.path || '', code: error?.code || 'created_directory_cleanup_unconfirmed', reason: error?.message || '' }); }
87
+ }
88
+ return { ok: completed.size === items.length, removed, remaining: items.filter(item => !completed.has(item)),
89
+ failures, review, requiresExplicitCleanup: review.length > 0, changedDocument: false };
90
+ }
91
+ return { folderIdentity, cleanupCreatedDirectories };
92
+ }
93
+ return { create };
94
+ });
@@ -113,6 +113,7 @@
113
113
  }, { projectId, signal: controller.signal });
114
114
  if (controller.signal.aborted) throw failure('reference_read_cancelled', 'Reference reading was cancelled.');
115
115
  if (!snapshot?.ok) throw failure('reference_project_unavailable', snapshot?.reason || 'The reference project could not be read.');
116
+ if (snapshot.pathIdentityComplete === false) throw failure('reference_project_incomplete', 'Some ZIP file identities could not be read unambiguously.');
116
117
  const files = (snapshot.files || []).filter(file => /\.tex$/i.test(file.path || ''));
117
118
  if (!files.length) throw failure('reference_project_empty', 'This project contains no readable TeX files.');
118
119
  if (files.length > 1000 || files.some(file => typeof file.content !== 'string')) {
@@ -0,0 +1,113 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafReviewContext = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
6
+ 'use strict';
7
+
8
+ const cancelledOwners = new Map();
9
+ function cancelOwner(token) {
10
+ if (typeof token !== 'string' || !token || token.length > 256) return false;
11
+ for (const [id, until] of cancelledOwners) if (until <= Date.now()) cancelledOwners.delete(id);
12
+ cancelledOwners.set(token, Date.now() + 120000); return true;
13
+ }
14
+
15
+ // One context per request. The small response reserve lets an expired review
16
+ // return its known partial evidence before the caller's transport timer fires.
17
+ function create(deps, input = {}) {
18
+ const now = deps.now || Date.now;
19
+ const timers = typeof deps.window?.setTimeout === 'function' ? deps.window : globalThis;
20
+ const started = now(), limit = input.deadlineAt == null ? started + 120000 : input.deadlineAt;
21
+ const deadlineAt = Number.isFinite(limit) ? limit - Math.min(250, Math.max(0, limit - started) / 10) : started;
22
+ const cancellation = deps.readWriteCancellationSequence?.();
23
+ const appliedLists = new Set(), observations = new Map();
24
+ let stopped = null, mutationStarted = false;
25
+ const params = { ...input, deadlineAt };
26
+ const failure = (code, reason) => ({ ok: false, code, reason, changedDocument: false,
27
+ failure: { code: code === 'codex_cancelled' ? 'codex_run_cancelled' : 'tracked_changes_remain',
28
+ stage: 'reviewing', severity: 'blocked', terminalState: 'blocked', changedDocument: false,
29
+ userMessage: reason, nextAction: 'Check the retained per-file results in Overleaf before retrying.',
30
+ evidence: { originalCode: code, writeStarted: false } } });
31
+ function check() {
32
+ if (stopped) return stopped;
33
+ if (params.reviewOwnerToken && cancelledOwners.has(params.reviewOwnerToken))
34
+ return stopped = failure('codex_cancelled', 'The Undo owner disconnected; no further review writes were started.');
35
+ if (!Number.isFinite(limit) || now() >= deadlineAt)
36
+ return stopped = failure('writeback_deadline_exceeded', 'The review time budget expired; no further review writes were started.');
37
+ if (typeof deps.readWriteCancellationSequence === 'function' && deps.readWriteCancellationSequence() !== cancellation)
38
+ return stopped = failure('codex_cancelled', 'The review was cancelled; no further review writes were started.');
39
+ const project = deps.getProjectId?.() || /\/project\/([^/?#]+)/.exec(deps.window?.location?.pathname || '')?.[1];
40
+ if (!params.runProjectId || (project && project !== params.runProjectId))
41
+ return stopped = failure('aborted_project_changed', 'The original review project is no longer active.');
42
+ const blocked = deps.checkWritebackRunProjectId?.(params);
43
+ if (blocked) return stopped = blocked.skipped?.[0]?.result || blocked;
44
+ return null;
45
+ }
46
+ function assert() {
47
+ const blocked = check();
48
+ if (blocked) throw Object.assign(new Error(blocked.reason || blocked.code), { reviewFailure: blocked });
49
+ }
50
+ async function wait(work) {
51
+ assert();
52
+ let timer;
53
+ try {
54
+ const timeout = new Promise((_, reject) => {
55
+ timer = timers.setTimeout(() => {
56
+ const blocked = check() || failure('writeback_deadline_exceeded', 'The review time budget expired.');
57
+ stopped = blocked;
58
+ reject(Object.assign(new Error(blocked.reason), { reviewFailure: blocked }));
59
+ }, Math.max(0, deadlineAt - now()));
60
+ });
61
+ const result = await Promise.race([Promise.resolve().then(() => { assert(); return work(); }), timeout]);
62
+ assert();
63
+ return result;
64
+ } finally { if (timer != null) timers.clearTimeout(timer); }
65
+ }
66
+ const remember = entry => {
67
+ if (entry.result?.changedDocument === true || entry.result?.failure?.changedDocument === true) mutationStarted = true;
68
+ const ref = entry.trackedChange || { path: entry.operation?.path || '', key: 'snapshot-undo:' + (entry.operation?.path || ''), id: '' };
69
+ observations.set(ref.path + '\0' + (ref.key || ref.id || ''), { trackedChange: ref, result: entry.result });
70
+ };
71
+ const scoped = { ...deps, reviewGuard: check, reviewAwait: work => wait(work),
72
+ reviewDeadlineAt: deadlineAt,
73
+ createReviewApplied() { const list = []; appliedLists.add(list); return list; } };
74
+ for (const name of ['openFileByPath', 'setReviewingEnabled', 'verifyActiveEditorText']) {
75
+ if (typeof deps[name] === 'function') scoped[name] = (...args) => wait(() => deps[name](...args));
76
+ }
77
+ scoped.delay = ms => wait(() => deps.delay(Math.min(ms, Math.max(0, deadlineAt - now()))));
78
+ if (typeof deps.readActiveEditorText === 'function') scoped.readActiveEditorText = (...args) => { assert(); return deps.readActiveEditorText(...args); };
79
+ if (typeof deps.clickNode === 'function') scoped.clickNode = (...args) => { assert(); mutationStarted = true; return deps.clickNode(...args); };
80
+ for (const name of ['applyOperationsCore', 'applyOperationsWithNoTraceUndo']) {
81
+ if (typeof deps[name] !== 'function') continue;
82
+ scoped[name] = async (operations, options = {}) => {
83
+ assert();
84
+ const result = await deps[name](operations, { ...options, deadlineAt,
85
+ reviewGuard: check, reviewAwait: work => wait(work),
86
+ onOperationResult(entry) { remember(entry); options.onOperationResult?.(entry); } });
87
+ for (const entry of result.applied || []) remember(entry);
88
+ // Preserve a completed in-flight result even if cancellation just arrived.
89
+ return result;
90
+ };
91
+ }
92
+ if (typeof deps.confirmReviewWriteback === 'function') scoped.confirmReviewWriteback = async (original, result) => {
93
+ for (const entry of result.applied || []) remember(entry);
94
+ return deps.confirmReviewWriteback({ ...original, deadlineAt, reviewGuard: check, reviewAwait: work => wait(work) }, result);
95
+ };
96
+ async function run(work) {
97
+ try { assert(); return await work(scoped, params); }
98
+ catch (error) {
99
+ if (!error?.reviewFailure) throw error;
100
+ for (const list of appliedLists) for (const entry of list) remember(entry);
101
+ const applied = [...observations.values()].filter(entry => entry.result?.ok !== false);
102
+ const path = deps.getActiveFilePath?.() || '';
103
+ const changedDocument = mutationStarted || applied.some(entry => entry.result?.changedDocument !== false);
104
+ const blocked = { ...error.reviewFailure, changedDocument,
105
+ failure: { ...error.reviewFailure.failure, changedDocument,
106
+ evidence: { ...error.reviewFailure.failure?.evidence, writeStarted: changedDocument } } };
107
+ return { ok: false, applied, skipped: [{ trackedChange: { path }, result: blocked }] };
108
+ }
109
+ }
110
+ return Object.freeze({ run, check, assert, wait, params, scoped });
111
+ }
112
+ return Object.freeze({ create, cancelOwner });
113
+ });
@@ -311,16 +311,18 @@
311
311
  expected.set(path, entry.result.verifiedContent);
312
312
  }
313
313
  }
314
- const deadline = now() + Math.max(0, timeoutMs);
314
+ const deadline = Math.min(now() + Math.max(0, timeoutMs), params.deadlineAt == null ? Infinity
315
+ : Number.isFinite(params.deadlineAt) ? params.deadlineAt : now());
315
316
  let attempts = 0;
316
317
  if (expected.size && typeof readSnapshot === 'function') {
317
318
  do {
318
- if (!params.runProjectId || getProjectId() !== params.runProjectId) break;
319
+ if (now() >= deadline || params.reviewGuard?.() || !params.runProjectId || getProjectId() !== params.runProjectId) break;
319
320
  try {
320
- const snapshot = await readSnapshot({ force: true, maxAgeMs: 0,
321
+ const read = () => readSnapshot({ force: true, maxAgeMs: 0,
321
322
  zipTimeoutMs: Math.max(1, Math.min(8000, deadline - now())) });
323
+ const snapshot = params.reviewAwait ? await params.reviewAwait(read) : await read();
322
324
  attempts++;
323
- if (getProjectId() !== params.runProjectId) break;
325
+ if (now() >= deadline || params.reviewGuard?.() || getProjectId() !== params.runProjectId) break;
324
326
  if (snapshot?.ok === true && Array.from(expected).every(([path, content]) =>
325
327
  snapshot.files?.some(file => file.path === path && file.source === 'overleaf-zip'
326
328
  && file.content === content))) {