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
@@ -12,6 +12,7 @@
12
12
  plus: '<path d="M8 3.2v9.6"/><path d="M3.2 8h9.6"/>',
13
13
  panel: '<rect x="2.3" y="2.3" width="11.4" height="11.4" rx="2"/><path d="M9.2 2.5v11"/><path d="M5.1 6.1h1.5M5.1 8h1.5M5.1 9.9h1.5"/>',
14
14
  collapse: '<path d="m6.1 3.3 4.7 4.7-4.7 4.7"/>',
15
+ chevronDown: '<path d="m4.4 6.4 3.6 3.6 3.6-3.6"/>',
15
16
  settings: '<path d="M3.4 5.2h5.9"/><path d="M11.7 5.2h.9"/><path d="M10.1 3.6v3.2"/><path d="M3.4 10.8h1"/><path d="M6.8 10.8h5.8"/><path d="M5.9 9.2v3.2"/>'
16
17
  };
17
18
  const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : 'settings';
@@ -53,8 +54,15 @@
53
54
  <div class="codex-panel-resize-handle" data-panel-resize-handle title="Drag to resize the Codex panel. Double click to reset." aria-label="Drag to resize the Codex panel. Double click to reset." role="separator"></div>
54
55
  <div class="codex-vscode-head" data-panel-header>
55
56
  <div class="codex-vscode-title">CODEX</div>
57
+ <div class="codex-thread-title codex-head-session" data-session-label>
58
+ <button type="button" class="codex-thread-title-trigger" data-session-menu-trigger aria-haspopup="menu" aria-expanded="false">
59
+ <span class="codex-thread-title-text" data-session-label-text></span>
60
+ <span class="codex-thread-title-chevron">${codexIcon('chevronDown')}</span>
61
+ </button>
62
+ <input type="text" class="codex-thread-title-input" data-session-rename-input maxlength="80" hidden>
63
+ <div class="codex-session-menu" data-session-menu role="menu" hidden></div>
64
+ </div>
56
65
  <div class="codex-vscode-head-actions" aria-label="Codex actions">
57
- <button type="button" data-refresh title="Refresh current file status. This will not sync or modify files." aria-label="Refresh current file status. This will not sync or modify files.">${codexIcon('refresh')}</button>
58
66
  <div data-diagnostics-slot></div>
59
67
  <button type="button" data-new-session title="New Session" aria-label="New Session">${codexIcon('plus')}</button>
60
68
  <button type="button" data-custom-instructions-settings title="Project Settings" aria-label="Project Settings" aria-expanded="false">${codexIcon('settings')}</button>
@@ -86,7 +94,6 @@
86
94
 
87
95
  bind(instance, panelEl, 'click', event => event.stopPropagation());
88
96
  bind(instance, panelEl, 'mousedown', event => event.stopPropagation());
89
- bind(instance, panelEl.querySelector('[data-refresh]'), 'click', () => callbacks.onRefresh?.());
90
97
  bind(instance, panelEl.querySelector('[data-new-session]'), 'click', () => callbacks.onNewSession?.());
91
98
  bind(instance, panelEl.querySelector('[data-custom-instructions-settings]'), 'click', () => callbacks.onSettingsClick?.());
92
99
  bind(instance, panelEl.querySelector('[data-close-panel]'), 'click', () => callbacks.onClosePanel?.());
@@ -117,6 +124,12 @@
117
124
  sessionSlot: instance.sessionSlot,
118
125
  composerSlot: instance.composerSlot,
119
126
  setView: view => { panelEl.dataset.view = view; },
127
+ setLocale: labels => {
128
+ launcherEl.title = labels.launcher;
129
+ launcherEl.setAttribute('aria-label', labels.launcher);
130
+ const close = panelEl.querySelector('[data-close-panel]');
131
+ if (close) { close.title = labels.close; close.setAttribute('aria-label', labels.close); }
132
+ },
120
133
  destroy: () => destroy(instance),
121
134
  _instance: instance
122
135
  };
@@ -314,7 +327,7 @@
314
327
  }
315
328
  const panel = options.getPanel?.();
316
329
  if (!panel) {
317
- return Promise.resolve(false);
330
+ return Promise.resolve(config.choices?.length ? 'cancel' : false);
318
331
  }
319
332
  dismiss(false);
320
333
  const tr = options.tr || (key => key);
@@ -323,6 +336,8 @@
323
336
  const confirmLabel = config.confirmLabel || tr('confirmDefaultConfirm');
324
337
  const cancelLabel = config.cancelLabel || tr('confirmDefaultCancel');
325
338
  const destructive = config.destructive === true;
339
+ const choices = Array.isArray(config.choices) && config.choices.length ? config.choices : null;
340
+ const cancelled = choices ? 'cancel' : false;
326
341
  const doc = panel.ownerDocument || document;
327
342
 
328
343
  return new Promise(resolve => {
@@ -363,54 +378,69 @@
363
378
  body.textContent = String(message);
364
379
  const actions = doc.createElement('div');
365
380
  actions.className = 'codex-plugin-confirm-actions';
366
- const cancel = doc.createElement('button');
367
- cancel.type = 'button';
368
- cancel.className = 'codex-plugin-confirm-cancel';
369
- cancel.textContent = cancelLabel;
370
- const confirm = doc.createElement('button');
371
- confirm.type = 'button';
372
- confirm.className = 'codex-plugin-confirm-confirm';
373
- if (destructive) confirm.dataset.destructive = 'true';
374
- confirm.textContent = confirmLabel;
375
- actions.append(cancel, confirm);
381
+ const buttons = (choices || [
382
+ { value: false, label: cancelLabel }, { value: true, label: confirmLabel }
383
+ ]).map(choice => {
384
+ const button = doc.createElement('button');
385
+ button.type = 'button';
386
+ const cancel = choice.value === false || choice.value === 'cancel';
387
+ // Only one primary action: other affirmative choices can ask for the secondary style.
388
+ button.className = cancel || choice.secondary === true ? 'codex-plugin-confirm-cancel' : 'codex-plugin-confirm-confirm';
389
+ if (destructive && !cancel) button.dataset.destructive = 'true';
390
+ button.textContent = choice.label;
391
+ button.addEventListener('click', () => cleanup(choice.value));
392
+ actions.append(button);
393
+ return button;
394
+ });
376
395
  card.append(head, body, actions);
377
396
  overlay.append(card);
378
397
  panel.append(overlay);
379
398
  overlay.showModal?.();
380
399
 
400
+ let timeout, ownerCheck, removalObserver;
401
+ const onAbort = () => cleanup(cancelled);
381
402
  const cleanup = value => {
382
403
  if (settled) return;
383
404
  settled = true;
384
405
  activeResolve = null;
385
406
  doc.removeEventListener('keydown', onKeydown, true);
407
+ config.signal?.removeEventListener('abort', onAbort);
408
+ clearTimeout(timeout);
409
+ clearInterval(ownerCheck);
410
+ removalObserver?.disconnect();
386
411
  overlay.close?.();
387
412
  overlay.remove();
388
413
  returnFocus?.focus?.();
389
- resolve(value);
414
+ resolve(choices && !choices.some(choice => choice.value === value) ? cancelled : value);
390
415
  };
391
416
  activeResolve = cleanup;
392
417
  const onKeydown = event => {
393
418
  if (event.key === 'Escape') {
394
- event.preventDefault();
395
- cleanup(false);
419
+ event.preventDefault(); cleanup(cancelled);
396
420
  } else if (event.key === 'Enter') {
397
421
  event.preventDefault();
398
- if (!destructive && event.target !== cancel) cleanup(true);
399
- else if (event.target === cancel) cleanup(false);
400
- else if (event.target === confirm) cleanup(true);
422
+ if (buttons.includes(event.target)) event.target.click();
423
+ else if (!choices && !destructive) cleanup(true);
401
424
  } else if (event.key === 'Tab') {
402
425
  event.preventDefault();
403
- (event.target === cancel ? confirm : cancel).focus();
426
+ const index = buttons.indexOf(event.target);
427
+ buttons[(index + (event.shiftKey ? buttons.length - 1 : 1)) % buttons.length].focus();
404
428
  }
405
429
  };
406
- overlay.addEventListener('click', event => {
407
- if (event.target === overlay) cleanup(false);
408
- });
409
- overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(false); });
410
- cancel.addEventListener('click', () => cleanup(false));
411
- confirm.addEventListener('click', () => cleanup(true));
430
+ overlay.addEventListener('click', event => { if (event.target === overlay) cleanup(cancelled); });
431
+ overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(cancelled); });
432
+ overlay.addEventListener('close', () => cleanup(cancelled));
412
433
  doc.addEventListener('keydown', onKeydown, true);
413
- (destructive ? cancel : confirm).focus();
434
+ config.signal?.addEventListener('abort', onAbort, { once: true });
435
+ if (config.timeoutMs > 0) timeout = setTimeout(onAbort, config.timeoutMs);
436
+ if (config.isCurrent) ownerCheck = setInterval(() => { if (!config.isCurrent()) onAbort(); }, 100);
437
+ const Observer = doc.defaultView?.MutationObserver;
438
+ if (choices && Observer) {
439
+ removalObserver = new Observer(() => { if (!overlay.isConnected) onAbort(); });
440
+ removalObserver.observe(panel, { childList: true });
441
+ }
442
+ (choices ? buttons[0] : buttons[destructive ? 0 : 1]).focus();
443
+ if (config.signal?.aborted || config.isCurrent?.() === false) onAbort();
414
444
  });
415
445
  }
416
446
 
@@ -115,7 +115,7 @@
115
115
  // Pull a queued message back into the composer. A typed draft is never overwritten.
116
116
  function pullBack(item) {
117
117
  const task = container?.parentElement?.querySelector('[data-task]');
118
- if (!task || (item.status || 'queued') !== 'queued') return;
118
+ if (!task || task.readOnly || (item.status || 'queued') !== 'queued') return;
119
119
  if (String(task.value || '').trim()) { options.onToast?.(tr('queuedInputEditBlocked')); return; }
120
120
  options.onRemove?.(item.id);
121
121
  task.value = item.text;
@@ -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 };
@@ -1,6 +1,9 @@
1
1
  (function initCodexOverleafProjectSettingsCoordinator() {
2
2
  'use strict';
3
3
 
4
+ // Skill messages belong to the settings workbench page that holds the Skills group.
5
+ const SKILLS_STATUS = Object.freeze({ page: 'context' });
6
+
4
7
  function create(deps = {}) {
5
8
  const {
6
9
  CodexOverleafTheme,
@@ -100,7 +103,7 @@
100
103
  refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
101
104
  `Could not list local skills: ${error.message}`,
102
105
  `无法列出本地技能:${error.message}`
103
- ), 'failed'));
106
+ ), 'failed', SKILLS_STATUS));
104
107
  }
105
108
 
106
109
  function toggleCustomInstructionsSettings() {
@@ -128,7 +131,7 @@
128
131
  refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
129
132
  `Could not list local skills: ${error.message}`,
130
133
  `无法列出本地技能:${error.message}`
131
- ), 'failed'));
134
+ ), 'failed', SKILLS_STATUS));
132
135
  }
133
136
 
134
137
  function closeSkillsView() {
@@ -140,9 +143,13 @@
140
143
  if (!settingsPanelInstance) {
141
144
  return;
142
145
  }
143
- const summary = getSkillLoadingSettings().loadCodexOverleafSkills === false
146
+ const settings = getSkillLoadingSettings();
147
+ const localCount = settings.loadCodexLocalSkills === false ? 0
148
+ : (deps.getCodexLocalSkills?.() || []).filter(skill => isCodexLocalSkillEnabled(skill.id, skill)).length;
149
+ const overleafCount = settings.loadCodexOverleafSkills === false ? 0 : countEnabledCodexOverleafSkills();
150
+ const summary = settings.loadCodexLocalSkills === false && settings.loadCodexOverleafSkills === false
144
151
  ? tr('codexOverleafSkillsSummaryOff')
145
- : tr('codexOverleafSkillsSummaryCount', { count: countEnabledCodexOverleafSkills() });
152
+ : tr('codexOverleafSkillsSummaryCount', { count: localCount + overleafCount });
146
153
  SettingsPanel.setSkillsSummary(settingsPanelInstance, summary);
147
154
  }
148
155
 
@@ -238,6 +245,25 @@
238
245
  });
239
246
  }
240
247
 
248
+ function getReviewingAvailability() {
249
+ return getState()?.reviewingAvailabilityByProject?.[getCurrentProjectId()] || null;
250
+ }
251
+
252
+ function observeReviewingAvailability(status, evidence) {
253
+ const projectId = getCurrentProjectId();
254
+ if (!projectId || !['available', 'unavailable', 'unknown'].includes(status)) return;
255
+ const previous = getReviewingAvailability();
256
+ // A remembered failure must not renew its own expiry.
257
+ if (previous?.status === status && previous.evidence === evidence && status === 'available') return;
258
+ // Preserve ordering even when a retry and its previous observation share a millisecond.
259
+ const observedAt = Math.max(Date.now(), Number.isFinite(previous?.observedAt) ? previous.observedAt + 1 : 0);
260
+ setState({ ...getState(), reviewingAvailabilityByProject: {
261
+ ...getState()?.reviewingAvailabilityByProject,
262
+ [projectId]: { status, evidence, observedAt }
263
+ } });
264
+ saveStateSoon(0);
265
+ }
266
+
241
267
  function getThemePreference() {
242
268
  return CodexOverleafTheme.normalizeThemePreference(getState()?.theme);
243
269
  }
@@ -296,6 +322,36 @@
296
322
  deps.updateGlobalPreferences?.({ codexOverleafSkillEnabled: { [skillId]: Boolean(enabled) } })?.catch(() => {});
297
323
  }
298
324
 
325
+ function getCodexLocalSkillEnabled() {
326
+ const map = getState()?.codexLocalSkillEnabled;
327
+ return map && typeof map === 'object' && !Array.isArray(map) ? map : {};
328
+ }
329
+
330
+ function isCodexLocalSkillEnabled(skillId, skill) {
331
+ const map = getCodexLocalSkillEnabled();
332
+ return Object.prototype.hasOwnProperty.call(map, skillId) ? map[skillId] !== false : skill?.enabled !== false;
333
+ }
334
+
335
+ function setCodexLocalSkillEnabled(skillId, enabled) {
336
+ if (deps.updateGlobalPreferences) {
337
+ try {
338
+ const pending = deps.updateGlobalPreferences({ codexLocalSkillEnabled: { [skillId]: Boolean(enabled) } });
339
+ Promise.resolve(pending).then(() => saveStateSoon()).catch(() => {
340
+ renderLocalSkillList();
341
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
342
+ });
343
+ } catch (_error) {
344
+ renderLocalSkillList();
345
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
346
+ }
347
+ } else {
348
+ setState({ ...getState(), codexLocalSkillEnabled: { ...getCodexLocalSkillEnabled(), [skillId]: Boolean(enabled) } });
349
+ saveStateSoon();
350
+ renderLocalSkillList();
351
+ }
352
+ deps.onSkillsChange?.();
353
+ }
354
+
299
355
  function readSkillLoadingSettingsFromSettings() {
300
356
  return SettingsPanel.readState(getSettingsPanelInstance()).skillToggles;
301
357
  }
@@ -314,8 +370,8 @@
314
370
  renderLocalSkillList();
315
371
  }
316
372
 
317
- function setProjectSettingsStatus(text, status = 'info') {
318
- SettingsPanel.setStatus(getSettingsPanelInstance(), text, status);
373
+ function setProjectSettingsStatus(text, status = 'info', options) {
374
+ SettingsPanel.setStatus(getSettingsPanelInstance(), text, status, options);
319
375
  }
320
376
 
321
377
  function clearProjectSettingsStatus() {
@@ -337,10 +393,15 @@
337
393
  closeCustomInstructionsSettings,
338
394
  closeSkillsView,
339
395
  getCodexOverleafSkillEnabled,
396
+ getCodexLocalSkillEnabled,
397
+ isCodexLocalSkillEnabled,
398
+ setCodexLocalSkillEnabled,
340
399
  getCustomInstructionsForCurrentProject,
341
400
  getGovernanceRulesForCurrentProject,
342
401
  getSkillLoadingSettings,
343
402
  getThemePreference,
403
+ getReviewingAvailability,
404
+ observeReviewingAvailability,
344
405
  isCodexOverleafSkillEnabled,
345
406
  normalizeCustomInstructionsByProject,
346
407
  normalizeGovernanceRules,