codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  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 +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  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 +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -1,10 +1,10 @@
1
1
  (function initCodexOverleafOtWarmMirrorController(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('../shared/projectFiles'));
4
4
  } else {
5
- root.CodexOverleafOtWarmMirrorController = factory();
5
+ root.CodexOverleafModuleRegistry.define('OtWarmMirrorController', ['ProjectFiles'], factory);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function otWarmMirrorControllerFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function otWarmMirrorControllerFactory(projectFiles) {
8
8
  'use strict';
9
9
 
10
10
  const OT_POLL_INTERVAL_MS = 1000;
@@ -17,6 +17,7 @@
17
17
  const OT_MAX_BATCH_BYTES = 2 * 1024 * 1024;
18
18
  const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : null;
19
19
  const PAUSE_STATES = new Set(['running', 'writing', 'undoing', 'compiling']);
20
+ const normalizeLiteralPath = projectFiles.normalizeSafeProjectPath;
20
21
 
21
22
  function buildPatchFilesRequest({ projectId, events, nativeCompatibility } = {}) {
22
23
  const files = [];
@@ -24,8 +25,8 @@
24
25
  if (files.length >= OT_MAX_PATCH_BATCH) {
25
26
  break;
26
27
  }
27
- const filePath = normalizePath(event?.path);
28
- if (!isSafeRelativePath(filePath) || typeof event?.baseHash !== 'string') {
28
+ const filePath = normalizeLiteralPath(event?.path);
29
+ if (!filePath || typeof event?.baseHash !== 'string') {
29
30
  continue;
30
31
  }
31
32
  const baseHash = event.baseHash.trim();
@@ -73,7 +74,7 @@
73
74
  return { pause: false };
74
75
  }
75
76
 
76
- function canUseOtWarmStart({ enabled, focusFiles, mirrorStatus, now = Date.now() } = {}) {
77
+ function canUseOtWarmStart({ enabled, focusFiles, focusFilesAreLiteral = false, mirrorStatus, now = Date.now() } = {}) {
77
78
  if (!enabled) {
78
79
  return { ok: false, reason: 'disabled' };
79
80
  }
@@ -82,7 +83,10 @@
82
83
  }
83
84
  if (mirrorStatus.dirty === true) return { ok: false, reason: 'mirror_dirty' };
84
85
 
85
- const normalizedFocusFiles = normalizePaths(focusFiles);
86
+ if (focusFilesAreLiteral === true && Array.isArray(focusFiles) && focusFiles.some(path => !normalizeLiteralPath(path))) {
87
+ return { ok: false, reason: 'invalid_focus_paths' };
88
+ }
89
+ const normalizedFocusFiles = normalizePaths(focusFiles, focusFilesAreLiteral === true);
86
90
  if (!normalizedFocusFiles.length) {
87
91
  return { ok: false, reason: 'no_focus_files' };
88
92
  }
@@ -94,7 +98,7 @@
94
98
  }
95
99
  const age = now - Date.parse(file.observedAt ?? file.lastPatchAt ?? '');
96
100
  if (!Number.isFinite(age) || age < 0 || age > OT_FRESHNESS_MAX_AGE_MS) continue;
97
- const filePath = normalizePath(file.path);
101
+ const filePath = normalizeLiteralPath(file.path);
98
102
  if (filePath) {
99
103
  freshFiles.add(filePath);
100
104
  }
@@ -124,9 +128,9 @@
124
128
  let bytes = next.reduce((sum, event) => sum + patchBytes(event), 0);
125
129
  if (next.length > OT_MAX_PENDING_EVENTS || bytes > OT_MAX_PENDING_BYTES) return { ok: false, reason: 'ot_queue_limit' };
126
130
  for (const input of events) {
127
- const path = normalizePath(input?.path);
131
+ const path = normalizeLiteralPath(input?.path);
128
132
  const time = typeof input?.observedAt === 'number' ? input.observedAt : Date.parse(input?.observedAt || '');
129
- if (!isSafeRelativePath(path) || path.length > 1024 || typeof input?.nextContent !== 'string' ||
133
+ if (!path || path.length > 1024 || typeof input?.nextContent !== 'string' ||
130
134
  typeof input.baseHash !== 'string' || !input.baseHash || input.baseHash.length > 128) {
131
135
  return { ok: false, reason: 'ot_event_invalid' };
132
136
  }
@@ -165,7 +169,7 @@
165
169
  }
166
170
  if (!events.length || Number(result.appliedCount) !== events.length || !Array.isArray(result.appliedFiles) ||
167
171
  result.appliedFiles.length !== events.length || !result.appliedFiles.every((file, index) =>
168
- normalizePath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
172
+ normalizeLiteralPath(file?.path) === events[index].path && (!events[index].nextHash || file.hash === events[index].nextHash))) {
169
173
  return { ok: false, reason: 'mirror_patch_invalid_result' };
170
174
  }
171
175
  if (events.some(event => { const time = Date.parse(event.observedAt);
@@ -220,11 +224,11 @@
220
224
  .replace(/^\/+/, '');
221
225
  }
222
226
 
223
- function normalizePaths(value) {
227
+ function normalizePaths(value, literal = false) {
224
228
  const seen = new Set();
225
229
  const paths = [];
226
230
  for (const item of Array.isArray(value) ? value : []) {
227
- const filePath = normalizePath(item);
231
+ const filePath = literal ? normalizeLiteralPath(item) : normalizePath(item);
228
232
  if (!filePath || seen.has(filePath)) {
229
233
  continue;
230
234
  }
@@ -234,13 +238,6 @@
234
238
  return paths;
235
239
  }
236
240
 
237
- function isSafeRelativePath(filePath) {
238
- if (!filePath) {
239
- return false;
240
- }
241
- return !filePath.split('/').some(part => part === '.' || part === '..');
242
- }
243
-
244
241
  function isPrimitiveMetadata(value) {
245
242
  return ['string', 'number', 'boolean'].includes(typeof value);
246
243
  }
@@ -21,14 +21,17 @@
21
21
  ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
22
22
  ['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
23
23
  ['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
24
+ ['src/page/ownedDirectoryCleanup.js', 'codex-overleaf-owned-directory-cleanup-script', true],
24
25
  ['src/page/textFileCreator.js', 'codex-overleaf-text-file-creator-script', true],
25
26
  ['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
26
27
  ['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
27
28
  ['src/page/referenceProjects.js', 'codex-overleaf-reference-projects-script', true],
29
+ ['src/page/fileMover.js', 'codex-overleaf-file-mover-script', true],
28
30
  ['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
29
31
  ['src/page/trackedChangeOwnership.js', 'codex-overleaf-tracked-change-ownership-script', true],
30
32
  ['src/page/trackedChangeCapture.js', 'codex-overleaf-tracked-change-capture-script', true],
31
33
  ['src/page/trackedChangeReplay.js', 'codex-overleaf-tracked-change-replay-script', true],
34
+ ['src/page/reviewContext.js', 'codex-overleaf-review-context-script', true],
32
35
  ['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
33
36
  ['src/page/writebackRouter.js', 'codex-overleaf-writeback-router-script', true],
34
37
  ['src/page/writebackReceiptJournal.js', 'codex-overleaf-writeback-receipt-script', true],
@@ -75,6 +78,11 @@
75
78
  const requestTimeoutMs = Number.isFinite(Number(policy.timeoutMs))
76
79
  ? Number(policy.timeoutMs)
77
80
  : Contract.resolveTimeoutMs(method, timeoutOverrides);
81
+ // Review preparation and its nested writes share the transport budget.
82
+ if (Contract.getMethod(method)?.timeoutClass === 'lifecycle') {
83
+ params = { ...params, deadlineAt: params?.deadlineAt == null ? now() + requestTimeoutMs
84
+ : Number.isFinite(params.deadlineAt) ? Math.min(params.deadlineAt, now() + requestTimeoutMs) : params.deadlineAt };
85
+ }
78
86
  const remainingMs = params?.deadlineAt == null ? Infinity
79
87
  : Number.isFinite(params.deadlineAt) ? params.deadlineAt - now() : 0;
80
88
  if (remainingMs <= 0) {
@@ -84,7 +92,7 @@
84
92
  }
85
93
  const timeoutMs = Math.min(requestTimeoutMs, remainingMs);
86
94
  const cancellable = policy.cancellation === 'content_abort';
87
- let settled = false;
95
+ let settled = false, dispatched = false;
88
96
  let timeout = null;
89
97
  function cleanup() {
90
98
  if (timeout !== null) windowRef.clearTimeout(timeout);
@@ -107,8 +115,13 @@
107
115
  const error = new Error('Codex run was cancelled by the user');
108
116
  error.code = 'codex_cancelled';
109
117
  error.cancelled = true;
118
+ error.requestDispatched = dispatched;
119
+ error.changedDocument = dispatched && Contract.getMethod(method)?.mutation === 'document';
120
+ if (dispatched && method === 'applyOperations') { error.requestId = id; error.rpcMethod = method; }
110
121
  rejectOnce(error);
111
122
  }
123
+ // Cancellation stops new requests; receipt settlement has its own non-cancellable policy.
124
+ if (cancellable && options.isCancellationRequested?.()) { cancelRequest(); return; }
112
125
  timeout = windowRef.setTimeout(() => {
113
126
  resolveOnce({ ok: false, error: 'Page bridge timed out',
114
127
  ...(method === 'applyOperations' ? { code: 'page_bridge_timeout', rpcMethod: method, requestId: id } : {}) });
@@ -126,11 +139,11 @@
126
139
  }
127
140
  if (cancellable) {
128
141
  activeCancellationHandlers.set(id, cancelRequest);
129
- if (options.isCancellationRequested?.()) windowRef.queueMicrotask(cancelRequest);
130
142
  }
131
143
  windowRef.addEventListener('message', onMessage);
132
144
  // Every low-level request remains one attempt. Higher layers must opt
133
145
  // into replay so a document mutation is never duplicated implicitly.
146
+ dispatched = true;
134
147
  windowRef.postMessage({
135
148
  source: 'codex-overleaf/content',
136
149
  id,
@@ -24,6 +24,7 @@
24
24
  openCustomInstructionsSettings,
25
25
  onHistoryRowJump,
26
26
  resetProjectNameCache,
27
+ clearHistoryComposer,
27
28
  getPanel,
28
29
  getState,
29
30
  setState,
@@ -110,27 +111,54 @@
110
111
  if (!confirmed) {
111
112
  return;
112
113
  }
114
+ if (getCurrentRunView()) {
115
+ showPluginToast(tx('A run started while confirmation was open. Finish or cancel it before clearing history.', '确认期间有任务开始运行,请先完成或取消任务后再清理历史。'));
116
+ return;
117
+ }
118
+ let historyClearedAt;
113
119
  try {
114
- await StorageDb?.clearAllStores?.();
120
+ await StorageDb.clearAllStores();
121
+ historyClearedAt = Date.now();
115
122
  historyView.invalidate();
116
- // v1.8.1: the dashboard's project-name cache lives in
117
- // chrome.storage.local, outside the IndexedDB stores — clear it too so
118
- // no (potentially sensitive) project titles outlive the wipe.
119
- await resetProjectNameCache?.();
120
123
  } catch (error) {
121
124
  showPluginToast(tx(`Could not clear history: ${error.message}`, `清空历史失败:${error.message}`));
122
125
  return;
123
126
  }
124
- // Reset the in-getPanel() getState() to match the emptied store and re-render
125
- // EXPLICITLY. startNewSession() is unusable here: its empty-session reuse
126
- // guard matches the fresh session that normalization mints and returns
127
- // before applyStateToPanel(), leaving the deleted cards on screen.
128
- setState(normalizePanelState({ ...getState(), sessions: [], runs: [], activeSessionId: '' }));
129
- await saveState();
127
+ const cleanupErrors = [];
128
+ try {
129
+ // Project names live outside the history store. Failure here must not
130
+ // leave deleted history in memory for a later autosave to replay.
131
+ await resetProjectNameCache?.();
132
+ } catch (error) {
133
+ cleanupErrors.push(error.message);
134
+ }
135
+ // Clear the compatibility mirror as well as the session list; otherwise
136
+ // normalization can reconstruct old summary runs and the Native thread.
137
+ const clearedState = normalizePanelState({
138
+ ...getState(), session: null, sessions: [], runs: [], activeSessionId: '',
139
+ task: '', focusFiles: [], selectionContext: null, composerAttachments: []
140
+ });
141
+ const freshSession = clearedState.sessions[0];
142
+ // The shared store rejects createdAt <= its clear marker. A new session
143
+ // in the same millisecond needs the next tick, at most 1 ms ahead of now.
144
+ freshSession.createdAt = freshSession.updatedAt = new Date(Math.max(Date.now(), historyClearedAt + 1)).toISOString();
145
+ setState(clearedState);
146
+ try {
147
+ clearHistoryComposer?.();
148
+ if (await saveState({ requireDurable: true }) !== true) {
149
+ throw new Error(tx('The blank session save was not confirmed.', '空白会话的保存结果尚未确认。'));
150
+ }
151
+ } catch (error) {
152
+ cleanupErrors.push(error.message);
153
+ }
154
+ // The store has already been cleared, including when the fresh-session
155
+ // save fails. Always remove old cards and draft inputs from the panel.
130
156
  applyStateToPanel();
131
157
  refreshStorageUsageSummary();
132
158
  void renderAuditHistoryPanel();
133
- showPluginToast(tx('Local history cleared. Overleaf files are unchanged.', '本地历史已清理,Overleaf 文件保持不变。'));
159
+ showPluginToast(cleanupErrors.length
160
+ ? tx(`Saved history was removed, but cleanup or blank-session saving needs attention: ${cleanupErrors.join('; ')}`, `已删除保存的历史,但清理或空白会话保存仍有问题:${cleanupErrors.join(';')}`)
161
+ : tx('Local history cleared. Overleaf files are unchanged.', '本地历史已清理,Overleaf 文件保持不变。'));
134
162
  getPanel()?.querySelector('[data-task]')?.focus();
135
163
  }
136
164
 
@@ -117,6 +117,12 @@
117
117
  sessionSlot: instance.sessionSlot,
118
118
  composerSlot: instance.composerSlot,
119
119
  setView: view => { panelEl.dataset.view = view; },
120
+ setLocale: labels => {
121
+ launcherEl.title = labels.launcher;
122
+ launcherEl.setAttribute('aria-label', labels.launcher);
123
+ const close = panelEl.querySelector('[data-close-panel]');
124
+ if (close) { close.title = labels.close; close.setAttribute('aria-label', labels.close); }
125
+ },
120
126
  destroy: () => destroy(instance),
121
127
  _instance: instance
122
128
  };
@@ -2,12 +2,20 @@
2
2
  'use strict';
3
3
 
4
4
  function create(deps = {}) {
5
- async function confirmSwitch({ providerName } = {}) {
5
+ function assertSwitchAllowed() {
6
6
  if ((deps.getRunningSessionIds?.() || []).length) {
7
- deps.showToast?.(deps.tx(
7
+ throw Object.assign(new Error(deps.tx(
8
8
  'Wait for the running task to finish before switching providers.',
9
9
  '请等待正在运行的任务结束后再切换模型服务。'
10
- ), { status: 'warning' });
10
+ )), { code: 'provider_switch_running' });
11
+ }
12
+ }
13
+
14
+ async function confirmSwitch({ providerName } = {}) {
15
+ try {
16
+ assertSwitchAllowed();
17
+ } catch (error) {
18
+ deps.showToast?.(error.message, { status: 'warning' });
11
19
  return false;
12
20
  }
13
21
  return deps.showConfirm?.({
@@ -33,6 +41,7 @@
33
41
  const providerConfigurationChanged = nextProviderId === (state.providerId || 'builtin')
34
42
  && nextProviderRevision !== Number(state.providerRevision || 0);
35
43
  const resetProviderThread = providerChanged || providerConfigurationChanged;
44
+ if (resetProviderThread) assertSwitchAllowed();
36
45
  const sessions = (state.sessions || []).map(session => ({
37
46
  ...session,
38
47
  providerId: nextProviderId,
@@ -53,7 +62,7 @@
53
62
  deps.applyStateToPanel?.();
54
63
  }
55
64
 
56
- return { confirmSwitch, commit };
65
+ return { assertSwitchAllowed, confirmSwitch, commit };
57
66
  }
58
67
 
59
68
  window.CodexOverleafProjectProviderSelection = { create };
@@ -6,6 +6,7 @@
6
6
  function create(deps = {}) {
7
7
  const {
8
8
  tr,
9
+ tx = en => en,
9
10
  getAccountScopeId,
10
11
  showPluginConfirm,
11
12
  showPluginToast,
@@ -22,79 +23,125 @@
22
23
  return !Number.isFinite(touched) || (Date.now() - touched) <= FRESH_RUNNING_WINDOW_MS;
23
24
  }
24
25
 
25
- async function clearProjectSessions(projectId, projectName) {
26
+ async function clearProjectSessions(projectId, projectName, options = {}) {
26
27
  const StorageDb = deps.StorageDb;
27
28
  const SessionState = deps.SessionState;
28
29
  if (!StorageDb || !SessionState) {
29
30
  return;
30
31
  }
31
- const scope = getAccountScopeId();
32
- const records = (await StorageDb.getAllByIndex('sessions', 'projectId', projectId))
33
- .filter(record => record && (!scope || record.accountScopeId === scope));
34
- if (!records.length) {
35
- await renderRecentProjectsVariant();
36
- return;
37
- }
38
- if (records.some(record => isFreshRunningRecord(StorageDb, record))) {
39
- showPluginToast(tr('recentProjects_clearProject_running'), { status: 'warning', sticky: true });
40
- return;
41
- }
42
- const approved = await showPluginConfirm({
43
- title: tr('recentProjects_clearProject_title', { project: projectName }),
44
- message: tr('recentProjects_clearProject_message', { count: String(records.length), project: projectName }),
45
- confirmLabel: tr('recentProjects_clearProject_confirm'),
46
- cancelLabel: tr('confirmDefaultCancel'),
47
- destructive: true
48
- });
49
- if (!approved) {
50
- return;
51
- }
52
- const Migration = deps.StorageMigration;
53
- if (Migration?.addSessionTombstones) {
54
- await Migration.addSessionTombstones(projectId, records.map(record => record.id));
55
- }
32
+ const scope = options.accountScopeId ?? getAccountScopeId();
33
+ const scopeChanged = error => error?.code === 'history_cleanup_scope_changed'
34
+ || error?.code === 'account_scope_unavailable';
35
+ const assertCurrent = () => {
36
+ if (!scope || getAccountScopeId() !== scope || options.isCurrent?.() === false
37
+ || options.generation !== undefined && deps.getViewGeneration?.() !== options.generation) {
38
+ throw Object.assign(new Error('History cleanup identity changed.'), { code: 'history_cleanup_scope_changed' });
39
+ }
40
+ };
41
+ const readRecords = async () => {
42
+ assertCurrent();
43
+ const records = await StorageDb.getAllByIndex('sessions', 'projectId', projectId);
44
+ assertCurrent();
45
+ return records.filter(record => record && record.accountScopeId === scope);
46
+ };
56
47
  try {
57
- await mutateProjectPanelState(projectId, state => records.reduce(
58
- (nextState, record) => SessionState.deleteSession(nextState, record.id),
59
- state
60
- ));
61
- } catch (_error) { /* IndexedDB deletion remains authoritative for the dashboard. */ }
62
-
63
- let removed = 0;
64
- let historyFailures = 0;
65
- for (const record of records) {
66
- try {
67
- await StorageDb.deleteRecord('sessions', record.id);
68
- removed++;
69
- } catch (_error) { /* The remaining record stays visible and can be retried. */ }
48
+ let records = await readRecords();
49
+ if (!records.length) {
50
+ await renderRecentProjectsVariant();
51
+ return;
52
+ }
53
+ if (records.some(record => isFreshRunningRecord(StorageDb, record))) {
54
+ showPluginToast(tr('recentProjects_clearProject_running'), { status: 'warning', sticky: true });
55
+ return;
56
+ }
57
+ const approved = await showPluginConfirm({
58
+ title: tr('recentProjects_clearProject_title', { project: projectName }),
59
+ message: tr('recentProjects_clearProject_message', { count: String(records.length), project: projectName }),
60
+ confirmLabel: tr('recentProjects_clearProject_confirm'),
61
+ cancelLabel: tr('confirmDefaultCancel'),
62
+ destructive: true
63
+ });
64
+ if (!approved) {
65
+ return;
66
+ }
67
+ // Re-read after the destructive dialog: another tab can have started a
68
+ // run or added a conversation while the original count was displayed.
69
+ const approvedIds = new Set(records.map(record => record.id));
70
+ const latest = await readRecords();
71
+ if (latest.some(record => isFreshRunningRecord(StorageDb, record))) {
72
+ showPluginToast(tr('recentProjects_clearProject_running'), { status: 'warning', sticky: true });
73
+ return;
74
+ }
75
+ records = latest.filter(record => approvedIds.has(record.id));
76
+ if (!records.length) { await renderRecentProjectsVariant(); return; }
77
+ const Migration = deps.StorageMigration;
78
+ if (Migration?.addSessionTombstones) {
79
+ assertCurrent();
80
+ await Migration.addSessionTombstones(projectId, records.map(record => record.id));
81
+ assertCurrent();
82
+ }
70
83
  try {
71
- const response = await sendBackgroundNative({
72
- method: 'codex.history.clearPlugin',
73
- params: { sessionId: record.id, threadId: record.codexThreadId || '' }
74
- });
75
- if (!response || !response.ok) {
84
+ assertCurrent();
85
+ await mutateProjectPanelState(projectId, state => records.reduce(
86
+ (nextState, record) => SessionState.deleteSession(nextState, record.id),
87
+ state
88
+ ), { assertCurrent });
89
+ assertCurrent();
90
+ } catch (error) {
91
+ if (scopeChanged(error)) throw error;
92
+ assertCurrent(); // Other legacy-blob failures do not replace shared deletion.
93
+ }
94
+
95
+ let removed = 0;
96
+ let historyFailures = 0;
97
+ for (const record of records) {
98
+ try {
99
+ assertCurrent();
100
+ await StorageDb.deleteRecord('sessions', record.id, { accountScopeId: scope, projectId });
101
+ assertCurrent();
102
+ removed++;
103
+ } catch (error) {
104
+ if (scopeChanged(error)) throw error;
105
+ assertCurrent(); // The remaining record can be retried in the same scope.
106
+ }
107
+ try {
108
+ assertCurrent();
109
+ const response = await sendBackgroundNative({
110
+ method: 'codex.history.clearPlugin',
111
+ params: { sessionId: record.id, threadId: record.codexThreadId || '' }
112
+ });
113
+ assertCurrent();
114
+ if (!response || !response.ok) {
115
+ historyFailures++;
116
+ }
117
+ } catch (error) {
118
+ if (scopeChanged(error)) throw error;
119
+ assertCurrent();
76
120
  historyFailures++;
77
121
  }
78
- } catch (_error) {
79
- historyFailures++;
80
122
  }
123
+ if (removed < records.length) {
124
+ showPluginToast(tr('recentProjects_clearProject_partial', {
125
+ removed: String(removed),
126
+ count: String(records.length)
127
+ }), { status: 'warning', sticky: true });
128
+ } else if (historyFailures) {
129
+ showPluginToast(tr('recentProjects_clearProject_historyPartial', {
130
+ count: String(historyFailures)
131
+ }), { status: 'warning', sticky: true });
132
+ } else {
133
+ showPluginToast(tr('recentProjects_clearProject_done', {
134
+ count: String(removed),
135
+ project: projectName
136
+ }), { status: 'completed' });
137
+ }
138
+ await renderRecentProjectsVariant();
139
+ } catch (error) {
140
+ if (!scopeChanged(error)) throw error;
141
+ showPluginToast(tx('History view or account changed. Reopen the history list before deleting sessions.',
142
+ '历史视图或账号已变化,请重新打开历史列表后再清理会话。'), { status: 'warning', sticky: true });
143
+ return false;
81
144
  }
82
- if (removed < records.length) {
83
- showPluginToast(tr('recentProjects_clearProject_partial', {
84
- removed: String(removed),
85
- count: String(records.length)
86
- }), { status: 'warning', sticky: true });
87
- } else if (historyFailures) {
88
- showPluginToast(tr('recentProjects_clearProject_historyPartial', {
89
- count: String(historyFailures)
90
- }), { status: 'warning', sticky: true });
91
- } else {
92
- showPluginToast(tr('recentProjects_clearProject_done', {
93
- count: String(removed),
94
- project: projectName
95
- }), { status: 'completed' });
96
- }
97
- await renderRecentProjectsVariant();
98
145
  }
99
146
 
100
147
  return { clearProjectSessions };
@@ -140,9 +140,13 @@
140
140
  if (!settingsPanelInstance) {
141
141
  return;
142
142
  }
143
- const summary = getSkillLoadingSettings().loadCodexOverleafSkills === false
143
+ const settings = getSkillLoadingSettings();
144
+ const localCount = settings.loadCodexLocalSkills === false ? 0
145
+ : (deps.getCodexLocalSkills?.() || []).filter(skill => isCodexLocalSkillEnabled(skill.id, skill)).length;
146
+ const overleafCount = settings.loadCodexOverleafSkills === false ? 0 : countEnabledCodexOverleafSkills();
147
+ const summary = settings.loadCodexLocalSkills === false && settings.loadCodexOverleafSkills === false
144
148
  ? tr('codexOverleafSkillsSummaryOff')
145
- : tr('codexOverleafSkillsSummaryCount', { count: countEnabledCodexOverleafSkills() });
149
+ : tr('codexOverleafSkillsSummaryCount', { count: localCount + overleafCount });
146
150
  SettingsPanel.setSkillsSummary(settingsPanelInstance, summary);
147
151
  }
148
152
 
@@ -296,6 +300,36 @@
296
300
  deps.updateGlobalPreferences?.({ codexOverleafSkillEnabled: { [skillId]: Boolean(enabled) } })?.catch(() => {});
297
301
  }
298
302
 
303
+ function getCodexLocalSkillEnabled() {
304
+ const map = getState()?.codexLocalSkillEnabled;
305
+ return map && typeof map === 'object' && !Array.isArray(map) ? map : {};
306
+ }
307
+
308
+ function isCodexLocalSkillEnabled(skillId, skill) {
309
+ const map = getCodexLocalSkillEnabled();
310
+ return Object.prototype.hasOwnProperty.call(map, skillId) ? map[skillId] !== false : skill?.enabled !== false;
311
+ }
312
+
313
+ function setCodexLocalSkillEnabled(skillId, enabled) {
314
+ if (deps.updateGlobalPreferences) {
315
+ try {
316
+ const pending = deps.updateGlobalPreferences({ codexLocalSkillEnabled: { [skillId]: Boolean(enabled) } });
317
+ Promise.resolve(pending).then(() => saveStateSoon()).catch(() => {
318
+ renderLocalSkillList();
319
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
320
+ });
321
+ } catch (_error) {
322
+ renderLocalSkillList();
323
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
324
+ }
325
+ } else {
326
+ setState({ ...getState(), codexLocalSkillEnabled: { ...getCodexLocalSkillEnabled(), [skillId]: Boolean(enabled) } });
327
+ saveStateSoon();
328
+ renderLocalSkillList();
329
+ }
330
+ deps.onSkillsChange?.();
331
+ }
332
+
299
333
  function readSkillLoadingSettingsFromSettings() {
300
334
  return SettingsPanel.readState(getSettingsPanelInstance()).skillToggles;
301
335
  }
@@ -337,6 +371,9 @@
337
371
  closeCustomInstructionsSettings,
338
372
  closeSkillsView,
339
373
  getCodexOverleafSkillEnabled,
374
+ getCodexLocalSkillEnabled,
375
+ isCodexLocalSkillEnabled,
376
+ setCodexLocalSkillEnabled,
340
377
  getCustomInstructionsForCurrentProject,
341
378
  getGovernanceRulesForCurrentProject,
342
379
  getSkillLoadingSettings,