codex-overleaf-link 2.5.1 → 2.6.5

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 (111) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +148 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -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?.());
@@ -221,7 +228,10 @@
221
228
  return compact;
222
229
  }
223
230
 
224
- function setVisible(panelEl, visible) {
231
+ function setVisible(target, visible) {
232
+ const instance = target?._instance;
233
+ const panelEl = instance?.panelEl || target;
234
+ if (visible && instance) updateCompactMode(instance);
225
235
  panelEl?.classList?.toggle('is-open', Boolean(visible));
226
236
  document.documentElement.classList.toggle(ROOT_CLASS, Boolean(visible));
227
237
  const launcherEl = panelEl?.ownerDocument?.getElementById?.('codex-overleaf-launcher');
@@ -320,7 +330,7 @@
320
330
  }
321
331
  const panel = options.getPanel?.();
322
332
  if (!panel) {
323
- return Promise.resolve(false);
333
+ return Promise.resolve(config.choices?.length ? 'cancel' : false);
324
334
  }
325
335
  dismiss(false);
326
336
  const tr = options.tr || (key => key);
@@ -329,6 +339,8 @@
329
339
  const confirmLabel = config.confirmLabel || tr('confirmDefaultConfirm');
330
340
  const cancelLabel = config.cancelLabel || tr('confirmDefaultCancel');
331
341
  const destructive = config.destructive === true;
342
+ const choices = Array.isArray(config.choices) && config.choices.length ? config.choices : null;
343
+ const cancelled = choices ? 'cancel' : false;
332
344
  const doc = panel.ownerDocument || document;
333
345
 
334
346
  return new Promise(resolve => {
@@ -369,54 +381,69 @@
369
381
  body.textContent = String(message);
370
382
  const actions = doc.createElement('div');
371
383
  actions.className = 'codex-plugin-confirm-actions';
372
- const cancel = doc.createElement('button');
373
- cancel.type = 'button';
374
- cancel.className = 'codex-plugin-confirm-cancel';
375
- cancel.textContent = cancelLabel;
376
- const confirm = doc.createElement('button');
377
- confirm.type = 'button';
378
- confirm.className = 'codex-plugin-confirm-confirm';
379
- if (destructive) confirm.dataset.destructive = 'true';
380
- confirm.textContent = confirmLabel;
381
- actions.append(cancel, confirm);
384
+ const buttons = (choices || [
385
+ { value: false, label: cancelLabel }, { value: true, label: confirmLabel }
386
+ ]).map(choice => {
387
+ const button = doc.createElement('button');
388
+ button.type = 'button';
389
+ const cancel = choice.value === false || choice.value === 'cancel';
390
+ // Only one primary action: other affirmative choices can ask for the secondary style.
391
+ button.className = cancel || choice.secondary === true ? 'codex-plugin-confirm-cancel' : 'codex-plugin-confirm-confirm';
392
+ if (destructive && !cancel) button.dataset.destructive = 'true';
393
+ button.textContent = choice.label;
394
+ button.addEventListener('click', () => cleanup(choice.value));
395
+ actions.append(button);
396
+ return button;
397
+ });
382
398
  card.append(head, body, actions);
383
399
  overlay.append(card);
384
400
  panel.append(overlay);
385
401
  overlay.showModal?.();
386
402
 
403
+ let timeout, ownerCheck, removalObserver;
404
+ const onAbort = () => cleanup(cancelled);
387
405
  const cleanup = value => {
388
406
  if (settled) return;
389
407
  settled = true;
390
408
  activeResolve = null;
391
409
  doc.removeEventListener('keydown', onKeydown, true);
410
+ config.signal?.removeEventListener('abort', onAbort);
411
+ clearTimeout(timeout);
412
+ clearInterval(ownerCheck);
413
+ removalObserver?.disconnect();
392
414
  overlay.close?.();
393
415
  overlay.remove();
394
416
  returnFocus?.focus?.();
395
- resolve(value);
417
+ resolve(choices && !choices.some(choice => choice.value === value) ? cancelled : value);
396
418
  };
397
419
  activeResolve = cleanup;
398
420
  const onKeydown = event => {
399
421
  if (event.key === 'Escape') {
400
- event.preventDefault();
401
- cleanup(false);
422
+ event.preventDefault(); cleanup(cancelled);
402
423
  } else if (event.key === 'Enter') {
403
424
  event.preventDefault();
404
- if (!destructive && event.target !== cancel) cleanup(true);
405
- else if (event.target === cancel) cleanup(false);
406
- else if (event.target === confirm) cleanup(true);
425
+ if (buttons.includes(event.target)) event.target.click();
426
+ else if (!choices && !destructive) cleanup(true);
407
427
  } else if (event.key === 'Tab') {
408
428
  event.preventDefault();
409
- (event.target === cancel ? confirm : cancel).focus();
429
+ const index = buttons.indexOf(event.target);
430
+ buttons[(index + (event.shiftKey ? buttons.length - 1 : 1)) % buttons.length].focus();
410
431
  }
411
432
  };
412
- overlay.addEventListener('click', event => {
413
- if (event.target === overlay) cleanup(false);
414
- });
415
- overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(false); });
416
- cancel.addEventListener('click', () => cleanup(false));
417
- confirm.addEventListener('click', () => cleanup(true));
433
+ overlay.addEventListener('click', event => { if (event.target === overlay) cleanup(cancelled); });
434
+ overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(cancelled); });
435
+ overlay.addEventListener('close', () => cleanup(cancelled));
418
436
  doc.addEventListener('keydown', onKeydown, true);
419
- (destructive ? cancel : confirm).focus();
437
+ config.signal?.addEventListener('abort', onAbort, { once: true });
438
+ if (config.timeoutMs > 0) timeout = setTimeout(onAbort, config.timeoutMs);
439
+ if (config.isCurrent) ownerCheck = setInterval(() => { if (!config.isCurrent()) onAbort(); }, 100);
440
+ const Observer = doc.defaultView?.MutationObserver;
441
+ if (choices && Observer) {
442
+ removalObserver = new Observer(() => { if (!overlay.isConnected) onAbort(); });
443
+ removalObserver.observe(panel, { childList: true });
444
+ }
445
+ (choices ? buttons[0] : buttons[destructive ? 0 : 1]).focus();
446
+ if (config.signal?.aborted || config.isCurrent?.() === false) onAbort();
420
447
  });
421
448
  }
422
449
 
@@ -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;
@@ -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() {
@@ -242,6 +245,25 @@
242
245
  });
243
246
  }
244
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
+
245
267
  function getThemePreference() {
246
268
  return CodexOverleafTheme.normalizeThemePreference(getState()?.theme);
247
269
  }
@@ -316,11 +338,11 @@
316
338
  const pending = deps.updateGlobalPreferences({ codexLocalSkillEnabled: { [skillId]: Boolean(enabled) } });
317
339
  Promise.resolve(pending).then(() => saveStateSoon()).catch(() => {
318
340
  renderLocalSkillList();
319
- setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
341
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
320
342
  });
321
343
  } catch (_error) {
322
344
  renderLocalSkillList();
323
- setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
345
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
324
346
  }
325
347
  } else {
326
348
  setState({ ...getState(), codexLocalSkillEnabled: { ...getCodexLocalSkillEnabled(), [skillId]: Boolean(enabled) } });
@@ -348,8 +370,8 @@
348
370
  renderLocalSkillList();
349
371
  }
350
372
 
351
- function setProjectSettingsStatus(text, status = 'info') {
352
- SettingsPanel.setStatus(getSettingsPanelInstance(), text, status);
373
+ function setProjectSettingsStatus(text, status = 'info', options) {
374
+ SettingsPanel.setStatus(getSettingsPanelInstance(), text, status, options);
353
375
  }
354
376
 
355
377
  function clearProjectSettingsStatus() {
@@ -378,6 +400,8 @@
378
400
  getGovernanceRulesForCurrentProject,
379
401
  getSkillLoadingSettings,
380
402
  getThemePreference,
403
+ getReviewingAvailability,
404
+ observeReviewingAvailability,
381
405
  isCodexOverleafSkillEnabled,
382
406
  normalizeCustomInstructionsByProject,
383
407
  normalizeGovernanceRules,
@@ -613,18 +613,7 @@
613
613
  actions.innerHTML = instance.embedded ? '' : `<button type="button" class="codex-provider-secondary-button" data-provider-action="close">${escapeHtml(tx('Close', '关闭'))}</button>`;
614
614
  return;
615
615
  }
616
- const active = provider.id && provider.id === getCurrentProjectProviderId(instance);
617
- const isNew = !provider.id;
618
- const destinationChanged = active && getCurrentBaseUrl(instance) !== provider.endpointDisclosureBaseUrl;
619
- const actionState = getFooterActionState({
620
- isNew,
621
- active,
622
- dirty: instance.dirty,
623
- destinationChanged,
624
- canSave: canSaveCurrentDraft(instance),
625
- hasProject: instance.callbacks.hasCurrentProject?.() !== false,
626
- canActivate: canActivateCurrentProvider(instance)
627
- });
616
+ const actionState = getCurrentFooterActionState(instance);
628
617
  const saveDisabled = actionState.saveEnabled ? '' : ' disabled aria-disabled="true"';
629
618
  const useDisabled = actionState.useEnabled ? '' : ' disabled aria-disabled="true"';
630
619
  actions.innerHTML = `
@@ -638,6 +627,21 @@
638
627
  `;
639
628
  }
640
629
 
630
+ function getCurrentFooterActionState(instance) {
631
+ const provider = getSelectedProvider(instance);
632
+ const active = Boolean(provider?.id && provider.id === getCurrentProjectProviderId(instance));
633
+ return getFooterActionState({
634
+ isNew: !provider?.id,
635
+ active,
636
+ dirty: instance.dirty,
637
+ destinationChanged: active && getCurrentBaseUrl(instance) !== provider.endpointDisclosureBaseUrl,
638
+ disclosureAccepted: instance.root.querySelector('[data-provider-disclosure]')?.checked === true,
639
+ canSave: canSaveCurrentDraft(instance),
640
+ hasProject: instance.callbacks.hasCurrentProject?.() !== false,
641
+ canActivate: canActivateCurrentProvider(instance)
642
+ });
643
+ }
644
+
641
645
  function getFooterActionState(options = {}) {
642
646
  const isNew = options.isNew === true;
643
647
  const active = options.active === true;
@@ -645,9 +649,9 @@
645
649
  return {
646
650
  showSave: isNew || dirty,
647
651
  showSaveAndUse: options.hasProject !== false && (isNew || dirty) && (!active || options.destinationChanged === true),
648
- showUse: options.hasProject !== false && !isNew && !active && !dirty,
652
+ showUse: options.hasProject !== false && !isNew && (!active || options.destinationChanged === true) && !dirty,
649
653
  saveEnabled: options.canSave === true,
650
- useEnabled: options.canActivate === true
654
+ useEnabled: options.canActivate === true && (!options.destinationChanged || options.disclosureAccepted === true)
651
655
  };
652
656
  }
653
657
 
@@ -837,6 +841,11 @@
837
841
  refreshTestState(instance);
838
842
  return;
839
843
  }
844
+ if (event.target.matches('[data-provider-disclosure]')) {
845
+ renderFooter(instance);
846
+ applyBusyState(instance);
847
+ return;
848
+ }
840
849
  if (event.target.matches('[data-provider-field]')) {
841
850
  const field = event.target.dataset.providerField;
842
851
  if (event.target.dataset.providerField === 'apiKey') {
@@ -1023,8 +1032,7 @@
1023
1032
 
1024
1033
  function applyBusyState(instance) {
1025
1034
  const busy = Boolean(instance.busy && instance.busy !== 'failed');
1026
- const canSave = canSaveCurrentDraft(instance);
1027
- const canActivate = canActivateCurrentProvider(instance);
1035
+ const actionState = getCurrentFooterActionState(instance);
1028
1036
  instance.root.dataset.busy = instance.busy || '';
1029
1037
  for (const element of instance.root.querySelectorAll('input, select, textarea, button')) {
1030
1038
  if (element.matches('[data-provider-confirm-answer]')) {
@@ -1034,9 +1042,9 @@
1034
1042
  } else if (element.matches('[data-provider-action="cancel-test"]')) {
1035
1043
  element.disabled = false;
1036
1044
  } else if (element.matches('[data-provider-action="save"], [data-provider-action="save-use"]')) {
1037
- element.disabled = busy || !canSave;
1045
+ element.disabled = busy || !actionState.saveEnabled;
1038
1046
  } else if (element.matches('[data-provider-action="use"]')) {
1039
- element.disabled = busy || !canActivate;
1047
+ element.disabled = busy || !actionState.useEnabled;
1040
1048
  } else {
1041
1049
  element.disabled = busy;
1042
1050
  }
@@ -1296,7 +1304,8 @@
1296
1304
 
1297
1305
  function renderTemplatePicker(instance, detail) {
1298
1306
  const tx = instance.tx;
1299
- detail.innerHTML = '<div class="codex-provider-template-head"><button type="button" class="codex-provider-inline-button" data-provider-action="templates-back">'
1307
+ detail.innerHTML = '<div class="codex-provider-template-head"><button type="button" class="codex-provider-inline-button codex-provider-back-button" data-provider-action="templates-back">'
1308
+ + '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m9.8 3.6-4.4 4.4 4.4 4.4"/></svg>'
1300
1309
  + escapeHtml(tx('Back', '返回')) + '</button><h3>' + escapeHtml(tx('Add a connection', '添加连接')) + '</h3></div>'
1301
1310
  + '<p class="codex-provider-help">' + escapeHtml(tx(
1302
1311
  'Service shortcuts default to Anthropic Messages. Chat Completions remains available; preset addresses follow the selected protocol. Add the matching API key and model IDs separately.',
@@ -199,10 +199,11 @@
199
199
  } catch (_error) { /* swallow */ }
200
200
  try {
201
201
  var docTitle = (document.title || '').trim();
202
- // Overleaf typically formats as "<Project Name> - Overleaf" or
203
- // "<Project Name> - Online LaTeX Editor"; strip the " - " suffix.
202
+ // Overleaf formats the title as "<Project Name> - Overleaf" or
203
+ // "<Project Name> - Online LaTeX Editor Overleaf" (localized); drop the
204
+ // last " - " segment when it names Overleaf.
204
205
  if (docTitle) {
205
- var stripped = docTitle.replace(/\s*[-–]\s*Overleaf.*$/i, '').trim();
206
+ var stripped = docTitle.replace(/\s+[-–|]\s+[^-–|]*Overleaf[^-–|]*$/i, '').trim();
206
207
  if (stripped && stripped.toLowerCase() !== 'overleaf') {
207
208
  return stripped;
208
209
  }
@@ -454,10 +455,10 @@
454
455
  };
455
456
  }
456
457
 
457
- async function refreshDashboardView() {
458
+ async function refreshDashboardView(options = {}) {
458
459
  var panelEl = getPanel();
459
460
  if (panelEl && panelEl.dataset.view === 'recent-projects') {
460
- await renderRecentProjectsVariant(dashboardViewOptions());
461
+ await renderRecentProjectsVariant(Object.assign(dashboardViewOptions(), options));
461
462
  }
462
463
  }
463
464
 
@@ -633,7 +634,7 @@
633
634
  : {};
634
635
  map[projectId] = record.id;
635
636
  prefs.activeSessionByProject = map;
636
- await Migration.savePrefs(prefs, accountScopeId, projectId);
637
+ await Migration.savePrefs({ activeSessionByProject: map }, accountScopeId, projectId);
637
638
  }
638
639
  } catch (_error) { /* fall through: opening the project still works */ }
639
640
  openProjectFromRow(projectId);
@@ -785,19 +786,31 @@
785
786
  input.focus();
786
787
  input.select();
787
788
  var settled = false;
788
- var finish = function (commit) {
789
+ var finish = async function (commit) {
789
790
  if (settled) {
790
791
  return;
791
792
  }
792
793
  settled = true;
793
794
  var nextRaw = input.value;
794
- input.remove();
795
- titleEl.hidden = false;
796
795
  // Unchanged input is a cancel: never rewrites the title source.
797
796
  if (!commit || nextRaw.trim() === seed.trim()) {
797
+ input.remove();
798
+ titleEl.hidden = false;
798
799
  return;
799
800
  }
800
- commitDashboardSessionRename(sessionsEl, projectId, record, nextRaw).catch(function () { /* swallow */ });
801
+ input.readOnly = true;
802
+ try {
803
+ await commitDashboardSessionRename(sessionsEl, projectId, record, nextRaw);
804
+ } catch (_error) {
805
+ settled = false;
806
+ input.readOnly = false;
807
+ showPluginToast(tr('recentProjects_renameFailed'), { status: 'failed', sticky: true });
808
+ input.focus();
809
+ return;
810
+ }
811
+ input.remove();
812
+ titleEl.hidden = false;
813
+ await refreshDashboardView().catch(function () { /* keep last render */ });
801
814
  };
802
815
  input.addEventListener('keydown', function (event) {
803
816
  if (event.key === 'Enter') {
@@ -832,15 +845,12 @@
832
845
  return SessionState.renameSession(state, record.id, rawTitle, { placeholderTitle: tr('newSessionFallback') });
833
846
  });
834
847
  } catch (_error) { /* blob may be absent; record update still proceeds */ }
835
- try {
836
- if (StorageDb) {
837
- await StorageDb.putRecord('sessions', StorageDb.buildSessionRecord(Object.assign({}, record, {
838
- title: next.title,
839
- titleSource: next.titleSource
840
- })));
841
- }
842
- } catch (_error) { /* swallow */ }
843
- await refreshDashboardView();
848
+ if (StorageDb) {
849
+ await StorageDb.putRecord('sessions', StorageDb.buildSessionRecord(Object.assign({}, record, {
850
+ title: next.title,
851
+ titleSource: next.titleSource
852
+ }), { preserveRunActionPayload: true }));
853
+ }
844
854
  }
845
855
 
846
856
  function ensureRecentProjectsRoot() {
@@ -1070,6 +1080,7 @@
1070
1080
  openProjectFromRow,
1071
1081
  renderRecentProjectRow,
1072
1082
  renderRecentProjectsVariant,
1083
+ refreshDashboardView,
1073
1084
  mountDashboardShell,
1074
1085
  resetProjectNameCache,
1075
1086
  renderPerProjectVariant