codex-overleaf-link 2.3.4 → 2.3.6

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 (41) hide show
  1. package/README.md +325 -211
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +9 -0
  4. package/extension/src/backgroundUpdateCoordinator.js +88 -4
  5. package/extension/src/content/composerPanel.js +1 -1
  6. package/extension/src/content/contentRuntime.js +51 -35
  7. package/extension/src/content/diagnosticsController.js +13 -3
  8. package/extension/src/content/diagnosticsPanel.js +1 -1
  9. package/extension/src/content/generated/content.bundle.js +1233 -298
  10. package/extension/src/content/generated/content.bundle.meta.json +8 -3
  11. package/extension/src/content/globalPreferencesController.js +125 -0
  12. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  13. package/extension/src/content/otWarmMirror.js +95 -65
  14. package/extension/src/content/otWarmMirrorController.js +102 -1
  15. package/extension/src/content/panelMaintenance.js +26 -15
  16. package/extension/src/content/panelRenderer.js +6 -1
  17. package/extension/src/content/projectSettingsCoordinator.js +1 -0
  18. package/extension/src/content/providerSettingsCoordinator.js +16 -0
  19. package/extension/src/content/providerSettingsDialog.js +24 -15
  20. package/extension/src/content/runFailureNotice.js +104 -0
  21. package/extension/src/content/runScrollLayout.js +124 -0
  22. package/extension/src/content/runTimelineView.js +36 -10
  23. package/extension/src/content/settingsPanel.js +32 -13
  24. package/extension/src/content/themeController.js +3 -0
  25. package/extension/src/content/updateNotice.js +24 -4
  26. package/extension/src/page/overleafRealtimeObserver.js +116 -24
  27. package/extension/src/shared/compatibility.js +1 -1
  28. package/extension/src/shared/globalPreferences.js +131 -0
  29. package/extension/src/shared/i18n.js +79 -39
  30. package/extension/src/shared/managedUpdateProjection.js +1 -1
  31. package/extension/src/shared/otText.js +0 -1
  32. package/extension/src/shared/sessionState.js +17 -21
  33. package/extension/src/shared/sessionTitle.js +120 -0
  34. package/extension/src/shared/updateRuntimeIdentity.js +32 -0
  35. package/extension/styles/panel.css +90 -16
  36. package/native-host/src/mirrorWorkspace.js +20 -1
  37. package/native-host/src/releaseProxy.js +177 -0
  38. package/native-host/src/releaseTransport.js +154 -0
  39. package/native-host/src/updateManager.js +40 -39
  40. package/package.json +1 -1
  41. package/scripts/install-managed.mjs +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Codex Overleaf Link",
4
- "version": "2.3.4",
4
+ "version": "2.3.6",
5
5
  "description": "Connect the active Overleaf project to a local Codex bridge.",
6
6
  "icons": {
7
7
  "16": "assets/icons/icon16.png",
@@ -15,6 +15,15 @@ if (!globalThis.CodexOverleafNativeRequestIdentity) {
15
15
  : 'shared/nativeRequestIdentity.js';
16
16
  importScripts(runtimeBase ? chrome.runtime.getURL(identityPath) : identityPath);
17
17
  }
18
+ if (!globalThis.CodexOverleafGlobalPreferences) {
19
+ const runtimeBase = String(globalThis.__CODEX_OVERLEAF_RUNTIME_BASE__ || '');
20
+ importScripts(runtimeBase ? chrome.runtime.getURL(`${runtimeBase}/src/shared/globalPreferences.js`) : 'shared/globalPreferences.js');
21
+ }
22
+ globalThis.CodexOverleafGlobalPreferences?.installBackground(chrome);
23
+ if (!globalThis.CodexOverleafUpdateRuntimeIdentity) {
24
+ const runtimeBase = String(globalThis.__CODEX_OVERLEAF_RUNTIME_BASE__ || '');
25
+ importScripts(runtimeBase ? chrome.runtime.getURL(`${runtimeBase}/src/shared/updateRuntimeIdentity.js`) : 'shared/updateRuntimeIdentity.js');
26
+ }
18
27
 
19
28
  (function initBackground() {
20
29
  'use strict';
@@ -4,6 +4,7 @@
4
4
  const UPDATE_STATE_KEY = 'codex-overleaf-managed-update-state-v1';
5
5
  const CONSENT_STATE_KEY = 'codex-overleaf-update-consent-v1';
6
6
  const UPDATE_RELOAD_TABS_KEY = 'codex-overleaf-managed-update-tabs-v1';
7
+ const MANUAL_RELOAD_TABS_KEY = 'codex-overleaf-manual-runtime-reload-v1';
7
8
  const CHECK_ALARM = 'codex-overleaf-consent-update-check';
8
9
  const IDLE_ALARM = 'codex-overleaf-consent-update-idle';
9
10
  const WATCHDOG_ALARM = 'codex-overleaf-consent-update-watchdog';
@@ -12,7 +13,7 @@
12
13
  const CHECK_INTERVAL_MINUTES = 24 * 60;
13
14
  const SNOOZE_MS = 24 * 60 * 60 * 1000;
14
15
  const CANDIDATE_MAX_AGE_MS = 5 * 60 * 1000;
15
- const CHECK_REQUEST_TIMEOUT_MS = 15 * 1000;
16
+ const CHECK_REQUEST_TIMEOUT_MS = 55 * 1000;
16
17
  const ACTIVATION_TIMEOUT_MS = 20 * 1000;
17
18
  const ACTIVATION_POLL_MS = 250;
18
19
  const FAST_IDLE_RETRY_MS = 4000;
@@ -31,7 +32,8 @@
31
32
  'codex-overleaf/consent-update-check',
32
33
  'codex-overleaf/consent-update-install',
33
34
  'codex-overleaf/consent-update-later',
34
- 'codex-overleaf/consent-update-dismiss'
35
+ 'codex-overleaf/consent-update-dismiss',
36
+ 'codex-overleaf/consent-update-reload'
35
37
  ]);
36
38
 
37
39
  const policy = root.CodexOverleafUpdateConsent;
@@ -41,6 +43,10 @@
41
43
  let policyTail = Promise.resolve();
42
44
  let idleRetryTimer = null;
43
45
  let activationPromise = null;
46
+ let runtimeStatus = null;
47
+ let runtimeStatusAt = 0;
48
+ let runtimeStatusRead = null;
49
+ let runtimeReloadMessage = '';
44
50
 
45
51
  function init(options = {}) {
46
52
  if (initialized || !policy || !revocation) return;
@@ -92,6 +98,7 @@
92
98
  await reconcileRecoveryState();
93
99
  await settleTerminalConsent();
94
100
  await armLegacyGuard();
101
+ await finishManualRuntimeReload();
95
102
  await publishView();
96
103
  const updateState = await getUpdateState();
97
104
  if (['staged', 'waiting_for_idle'].includes(updateState.state)) {
@@ -129,6 +136,8 @@
129
136
  return postponeUpdate();
130
137
  case 'codex-overleaf/consent-update-dismiss':
131
138
  return dismissCompletedUpdate();
139
+ case 'codex-overleaf/consent-update-reload':
140
+ return reloadInstalledRuntime();
132
141
  default:
133
142
  throw codedError('unknown_update_action', 'Unknown update action.');
134
143
  }
@@ -648,11 +657,86 @@
648
657
  }
649
658
 
650
659
  async function getView() {
651
- const [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
652
- return policy.deriveViewModel(state, consent, {
660
+ let [state, consent] = await Promise.all([getUpdateState(), getConsentState()]);
661
+ const busy = state.state === 'checking' || policy.isExecutionState(state.state) || consent.authorizationId;
662
+ const runtime = busy ? { state: 'transaction_active' } : await readRuntimeStatus();
663
+ if (runtime.state === 'aligned' && ['failed', 'rolled_back'].includes(state.state) &&
664
+ policy.compareStableVersions(state.latestVersion || state.currentVersion, runtime.installedVersion) <= 0) {
665
+ state = await setUpdateState({ ...state, state: 'idle', currentVersion: runtime.installedVersion,
666
+ latestVersion: runtime.installedVersion, code: '', message: '', transactionId: '', blocker: '', blockers: [] });
667
+ }
668
+ const view = policy.deriveViewModel(state, consent, {
653
669
  currentVersion: currentVersion(),
654
670
  now: Date.now()
655
671
  });
672
+ view.runtime = runtime;
673
+ const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
674
+ const pagesPending = runtime.state === 'aligned' && pending?.targetVersion === runtime.installedVersion && pending.tabIds?.length;
675
+ if (runtime.state === 'reload_required' || pagesPending) {
676
+ view.showPanel = true;
677
+ view.state = { ...state, state: pagesPending ? 'reload_tabs_required' : 'reload_required',
678
+ currentVersion: runtime.extensionVersion, latestVersion: runtime.installedVersion,
679
+ code: 'update_reload_required', message: runtimeReloadMessage };
680
+ view.badge = { text: 'UP', color: '#3578bd' };
681
+ }
682
+ return view;
683
+ }
684
+
685
+ async function readRuntimeStatus(force = false) {
686
+ if (!root.CodexOverleafUpdateRuntimeIdentity) return { state: 'unknown' };
687
+ if (!force && runtimeStatus && Date.now() - runtimeStatusAt < 5000) return runtimeStatus;
688
+ if (runtimeStatusRead) return runtimeStatusRead;
689
+ runtimeStatusRead = withTimeout(requestNative('update.status'), 4000,
690
+ codedError('update_status_timeout', 'Installed version inspection timed out.')).then(status => {
691
+ runtimeStatusAt = Date.now();
692
+ runtimeStatus = root.CodexOverleafUpdateRuntimeIdentity.inspectInstalledRuntime(status, {
693
+ extensionVersion: chrome.runtime.getManifest().version, runtimeVersion: currentVersion()
694
+ });
695
+ return runtimeStatus;
696
+ }).catch(() => ({ state: 'unknown' })).finally(() => { runtimeStatusRead = null; });
697
+ return runtimeStatusRead;
698
+ }
699
+
700
+ async function requireReloadSafePoint() {
701
+ const tabs = await chrome.tabs.query({ url: OVERLEAF_MATCHES });
702
+ const probes = await Promise.all(tabs.filter(isUsableEditorTab).map(tab => probeTabIdle(tab.id)));
703
+ const nativeGate = await requestNative('update.canApply').then(result => ({ ok: true, result }))
704
+ .catch(error => ({ ok: false, error: safeError(error) }));
705
+ const blockers = root.CodexOverleafUpdateStatus?.collectBlockers(probes, nativeGate) || ['busy'];
706
+ if (nativeBridge?.getPendingState?.().executionRequests > 0) blockers.push('background_execution_pending');
707
+ if (blockers.length) {
708
+ runtimeReloadMessage = 'Reload is waiting for Overleaf to be saved and idle (' + blockers.join(', ') + ').';
709
+ throw codedError('update_reload_busy', runtimeReloadMessage);
710
+ }
711
+ runtimeReloadMessage = '';
712
+ return tabs.filter(tab => Number.isInteger(tab.id) && !tab.discarded && tab.status !== 'unloaded');
713
+ }
714
+
715
+ async function reloadInstalledRuntime() {
716
+ const runtime = await readRuntimeStatus(true);
717
+ if (runtime.state === 'aligned') { await finishManualRuntimeReload(); return getView(); }
718
+ if (runtime.state !== 'reload_required') throw codedError('update_reload_unavailable', 'No verified installed update is awaiting reload.');
719
+ const tabs = await requireReloadSafePoint();
720
+ await chrome.storage.local.set({ [MANUAL_RELOAD_TABS_KEY]: {
721
+ targetVersion: runtime.installedVersion, tabIds: tabs.map(tab => tab.id)
722
+ } });
723
+ setTimeout(() => chrome.runtime.reload(), 0);
724
+ return getView();
725
+ }
726
+
727
+ async function finishManualRuntimeReload() {
728
+ const pending = (await chrome.storage.local.get(MANUAL_RELOAD_TABS_KEY))?.[MANUAL_RELOAD_TABS_KEY];
729
+ if (!pending || !Array.isArray(pending.tabIds)) return;
730
+ const runtime = await readRuntimeStatus(true);
731
+ if (runtime.state !== 'aligned' || runtime.installedVersion !== pending.targetVersion) return;
732
+ let tabs;
733
+ try { tabs = await requireReloadSafePoint(); } catch (_error) { return; }
734
+ const remaining = [];
735
+ for (const tab of tabs.filter(tab => pending.tabIds.includes(tab.id))) {
736
+ try { await chrome.tabs.reload(tab.id); } catch (_error) { remaining.push(tab.id); }
737
+ }
738
+ if (remaining.length) await chrome.storage.local.set({ [MANUAL_RELOAD_TABS_KEY]: { ...pending, tabIds: remaining } });
739
+ else await chrome.storage.local.remove(MANUAL_RELOAD_TABS_KEY);
656
740
  }
657
741
 
658
742
  async function publishView() {
@@ -110,7 +110,7 @@
110
110
  </span>
111
111
  </div>
112
112
  <div class="codex-context-selection" data-context-selection></div>
113
- <div class="codex-context-status" data-context-status>Type @ to add context: @file, @compile-log, @current-section.</div>
113
+ <div class="codex-context-status" data-context-status>Type @ to choose files or add @compile-log. Name the section you want in your request.</div>
114
114
  <div class="codex-context-files" data-context-file-list></div>
115
115
  </div>
116
116
  </form>
@@ -217,6 +217,7 @@
217
217
  renderAuditHistoryPanel: () => renderAuditHistoryPanel(),
218
218
  renderRecentProjectsVariant: (...args) => renderRecentProjectsVariant(...args),
219
219
  saveStateSoon: () => saveStateSoon(),
220
+ updateGlobalPreferences: patch => getGlobalPreferences().update(patch),
220
221
  setState: next => { state = next; },
221
222
  tr,
222
223
  tx,
@@ -417,6 +418,8 @@
417
418
  const runTimelineView = Modules.RunTimelineView.create({
418
419
  RunGuidanceView: Modules.RunGuidanceView,
419
420
  RunResultActions: runResultActions,
421
+ RunScrollLayout: Modules.RunScrollLayout,
422
+ RunFailureNotice: Modules.RunFailureNotice,
420
423
  tr,
421
424
  tx,
422
425
  getLocale,
@@ -664,6 +667,7 @@
664
667
  const providerSettingsCoordinator = Modules.ProviderSettingsCoordinator.create({
665
668
  ProviderProfiles: Modules.ProviderProfiles,
666
669
  ProviderSettingsDialog: Modules.ProviderSettingsDialog,
670
+ hasCurrentProject: () => Boolean(getCurrentProjectId()),
667
671
  tx,
668
672
  sendBackgroundNative,
669
673
  document,
@@ -833,6 +837,7 @@
833
837
  });
834
838
 
835
839
  let panel = null;
840
+ let probeStatusSnapshot = null;
836
841
  let panelRendererInstance = null;
837
842
  let sessionPanelInstance = null;
838
843
  let settingsPanelInstance = null;
@@ -840,6 +845,7 @@
840
845
  let composerPanelInstance = null;
841
846
  let state = null;
842
847
  let storageKey = LEGACY_STORAGE_KEY;
848
+ let globalPreferencesController = null;
843
849
  let currentRunView = null;
844
850
  let pendingRetryReplacement = null;
845
851
  let runQueueScheduler = null;
@@ -975,9 +981,10 @@
975
981
  });
976
982
 
977
983
  async function init() {
984
+ await getGlobalPreferences().initialize();
978
985
  await refreshAccountScopeId();
979
986
  storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
980
- state = normalizePanelState(await loadStoredState(), { restoreRunningRuns: true });
987
+ state = normalizePanelState(getGlobalPreferences().overlay(await loadStoredState()), { restoreRunningRuns: true });
981
988
  initializeRunQueueScheduler();
982
989
  recoverInterruptedRunJournals()
983
990
  .then(() => applyStateToPanel())
@@ -999,6 +1006,7 @@
999
1006
  recentProjects.mountDashboardShell();
1000
1007
  }
1001
1008
  applyStateToPanel();
1009
+ getGlobalPreferences().refreshView();
1002
1010
  // Welcome-panel + write-guard: seed the lifecycle
1003
1011
  // module-locals from the current URL, install the SPA route hook so
1004
1012
  // future Overleaf navigations swap variants, and warm the account-scope
@@ -1026,7 +1034,7 @@
1026
1034
  providerSettingsCoordinator.syncSessionProvider().catch(() => {});
1027
1035
  syncOtWarmMirrorController().catch(error => {
1028
1036
  updateOtStatusDisplay('unavailable');
1029
- appendPlainLog(tx(`Failed to sync experimental OT warm mirror: ${error.message}`, `同步实验性 OT 预热镜像失败:${error.message}`));
1037
+ appendPlainLog(tx(`Failed to sync OT warm mirror: ${error.message}`, `同步 OT 预热镜像失败:${error.message}`));
1030
1038
  });
1031
1039
  await refreshProbe({ quiet: true });
1032
1040
  }
@@ -1263,12 +1271,13 @@
1263
1271
  button: panel.querySelector('[data-custom-instructions-settings]'),
1264
1272
  callbacks: {
1265
1273
  onBack: () => closeCustomInstructionsSettings(),
1266
- onInputChange: () => persistPanelInputs(),
1274
+ onInputChange: event => persistPanelInputs(event),
1267
1275
  onSkillsOpen: () => openSkillsView(),
1268
1276
  onSkillsBack: () => closeSkillsView(),
1269
1277
  onProvidersOpen: () => providerSettingsCoordinator.open(),
1270
1278
  onClearAllHistory: () => clearAllHistoryWithConfirm(),
1271
1279
  onHistoryOpen: () => renderAuditHistoryPanel(),
1280
+ onStorageOpen: () => refreshStorageUsageSummary(),
1272
1281
  onHistoryFilter: () => applyAuditHistoryFilter(),
1273
1282
  // Experimental OT mirror: a single visible switch whose click is
1274
1283
  // intercepted so the confirm-before-enable flow still runs.
@@ -1439,9 +1448,6 @@
1439
1448
  function renderAttachmentPreviewList(attachments = [], container, options = {}) {
1440
1449
  return composerAttachmentController.renderAttachmentPreviewList(attachments, container, options);
1441
1450
  }
1442
-
1443
-
1444
-
1445
1451
  function applyLocaleToPanel() {
1446
1452
  if (!panel) {
1447
1453
  return;
@@ -1530,6 +1536,17 @@
1530
1536
  updateModelDisplay();
1531
1537
  renderSessionList();
1532
1538
  renderContextSelection();
1539
+ const probeStatus = panel.querySelector('[data-probe-status]');
1540
+ if (probeStatus?.dataset.refreshing === 'true') probeStatus.textContent = tr('refreshProbeLoading');
1541
+ else if (probeStatus && probeStatusSnapshot?.projectId === getCurrentProjectId()) {
1542
+ const { failed, probe, userInitiated } = probeStatusSnapshot;
1543
+ const text = failed ? tr('refreshProbeFailed') : formatProbeStatusBar(probe);
1544
+ probeStatus.textContent = userInitiated && !failed ? tr('refreshProbeDone', { status: text }) : text;
1545
+ if (!failed) updateExistingProbeNotice(probe);
1546
+ }
1547
+ updateSkillsEntrySummary();
1548
+ providerSettingsCoordinator.renderSummary();
1549
+ if (settingsPanelInstance?.container?.querySelector('[data-storage-card]')?.open) refreshStorageUsageSummary();
1533
1550
  }
1534
1551
 
1535
1552
  function setElementTitleAndAria(selector, title, ariaLabel) {
@@ -2388,7 +2405,7 @@
2388
2405
  }
2389
2406
 
2390
2407
  await saveState();
2391
- applyStateToPanel();
2408
+ applyStateToPanel({ preserveScroll: true });
2392
2409
  } catch (persistenceError) {
2393
2410
  appendRunEvent({
2394
2411
  title: tx('Codex result was generated, but saving local session history failed.', 'Codex 结果已生成,但保存本地会话记录失败。'),
@@ -4332,6 +4349,19 @@
4332
4349
  return scopedPersistenceCoordinator;
4333
4350
  }
4334
4351
 
4352
+ function getGlobalPreferences() {
4353
+ if (!globalPreferencesController) globalPreferencesController = Modules.GlobalPreferencesController.create({
4354
+ chromeApi: chrome, getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
4355
+ applyTheme: applyPanelTheme, applyLocale: () => { applyLocaleToPanel(); refreshDashboardSoon(); },
4356
+ onPreloadChange: enabled => contextTrayController.scheduleContextPrefetch({ enabled }),
4357
+ onSkillsChange: () => { renderLocalSkillList(); updateSkillsEntrySummary(); },
4358
+ onError: error => showPluginToast(tx(
4359
+ `Global settings could not be saved or loaded: ${error.message}. Please retry.`,
4360
+ `全局设置未能保存或读取:${error.message}。请重试。`), { status: 'failed', sticky: true })
4361
+ });
4362
+ return globalPreferencesController;
4363
+ }
4364
+
4335
4365
  async function reloadProjectRunHistory(newProjectId) {
4336
4366
  // Rebind the storage key to the new project and reload the panel state
4337
4367
  // from chrome.storage.local so the per-project session list reflects
@@ -4339,7 +4369,7 @@
4339
4369
  try {
4340
4370
  storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
4341
4371
  const reloaded = await loadStoredState();
4342
- state = normalizePanelState(reloaded, { restoreRunningRuns: true });
4372
+ state = normalizePanelState(getGlobalPreferences().overlay(reloaded), { restoreRunningRuns: true });
4343
4373
  applyStateToPanel();
4344
4374
  } catch (_error) {
4345
4375
  // Reload failures fall back to the in-memory state; the next saveState
@@ -4925,6 +4955,7 @@
4925
4955
  }
4926
4956
 
4927
4957
  async function refreshProbe(options = {}) {
4958
+ const projectId = getCurrentProjectId();
4928
4959
  const userInitiated = options.userInitiated === true;
4929
4960
  if (userInitiated) {
4930
4961
  setRefreshProbeLoading(true);
@@ -4940,6 +4971,8 @@
4940
4971
  const probe = await callPageBridge('probe', {
4941
4972
  manualOverride: state?.requireReviewing === false
4942
4973
  });
4974
+ if (projectId !== getCurrentProjectId()) return null;
4975
+ probeStatusSnapshot = { projectId, probe, userInitiated };
4943
4976
  const status = panel?.querySelector('[data-probe-status]');
4944
4977
  if (status) {
4945
4978
  status.textContent = userInitiated
@@ -4958,6 +4991,8 @@
4958
4991
  if (!userInitiated) {
4959
4992
  throw error;
4960
4993
  }
4994
+ if (projectId !== getCurrentProjectId()) return null;
4995
+ probeStatusSnapshot = { projectId, failed: true };
4961
4996
  const status = panel?.querySelector('[data-probe-status]');
4962
4997
  if (status) {
4963
4998
  status.textContent = tr('refreshProbeFailed');
@@ -5077,8 +5112,8 @@
5077
5112
  return fileItems.length
5078
5113
  ? fileItems.join(', ')
5079
5114
  : tx(
5080
- '@whole-project (default); add @file, @compile-log, or @current-section',
5081
- '@whole-project(默认);可添加 @file、@compile-log、@current-section'
5115
+ '@whole-project (default); choose files or add @compile-log',
5116
+ '@whole-project(默认);可选择文件或添加 @compile-log'
5082
5117
  );
5083
5118
  }
5084
5119
 
@@ -5911,6 +5946,8 @@
5911
5946
  }
5912
5947
 
5913
5948
  async function persistPanelInputs(event) {
5949
+ const globalWrite = getGlobalPreferences().handleInput(event);
5950
+ if (globalWrite) return globalWrite;
5914
5951
  // Typing in the task box is the hottest write path: a full saveState per
5915
5952
  // keystroke serializes every session/run/event and rewrites IndexedDB, so
5916
5953
  // panels get slower as history grows. Drafts take the debounced saver;
@@ -6009,29 +6046,8 @@
6009
6046
  setCustomInstructionsForProject(projectId, customInstructionsInput.value);
6010
6047
  }
6011
6048
  setGovernanceRulesForCurrentProject(readGovernanceRulesFromSettings());
6012
- const newSettings = readSkillLoadingSettingsFromSettings();
6013
- const prevOverleafSkills = getSkillLoadingSettings().loadCodexOverleafSkills;
6014
- setSkillLoadingSettings(newSettings);
6015
- // Theme is a global preference; read it from the settings select (which
6016
- // lives inside the panel, like the other settings inputs), persist it on
6017
- // state, and apply immediately. applyPanelTheme normalizes the value.
6018
- const themePref = panel?.querySelector('[data-theme-select]')?.value || 'dark';
6019
- if (state) {
6020
- state.theme = themePref;
6021
- }
6022
- applyPanelTheme(themePref);
6023
- // Language is a global UI preference (moved here from the diagnostics
6024
- // menu); re-translate the panel when it changes.
6025
- const languagePref = panel?.querySelector('[data-language-select]')?.value;
6026
- if (languagePref && state && languagePref !== getLocale()) {
6027
- state.locale = i18n?.normalizeLocale?.(languagePref) || languagePref;
6028
- applyLocaleToPanel();
6029
- }
6030
- // Re-render skill list when master toggle changes so per-skill toggles update their disabled state.
6031
- if (prevOverleafSkills !== getSkillLoadingSettings().loadCodexOverleafSkills) {
6032
- renderLocalSkillList();
6033
- }
6034
- updateSkillsEntrySummary();
6049
+ // Global controls are handled as field patches by getGlobalPreferences;
6050
+ // unrelated session autosaves must not read stale settings controls.
6035
6051
  }
6036
6052
  }
6037
6053
 
@@ -6070,7 +6086,7 @@
6070
6086
  saveStateSoon();
6071
6087
  }
6072
6088
 
6073
- function applyStateToPanel() {
6089
+ function applyStateToPanel(options = {}) {
6074
6090
  applyPanelTheme(getThemePreference());
6075
6091
  if ((state.providerId || 'builtin') === 'builtin' && !modelSelectHasOption(state.model)) {
6076
6092
  renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model);
@@ -6106,7 +6122,7 @@
6106
6122
  syncModeControls();
6107
6123
  applySessionLabel();
6108
6124
  renderSessionList();
6109
- renderRunHistory();
6125
+ renderRunHistory(options);
6110
6126
  renderContextSelection();
6111
6127
  renderContextSummary();
6112
6128
  // v1.8.0 C4: refresh recovery — small attachments persisted in state are
@@ -359,7 +359,7 @@
359
359
  if (!options.collectOnly) {
360
360
  showDiagnosticsLoading(tr('diagnosticsOtTitle'), tr('diagnosticsLoading'));
361
361
  }
362
- // OT warm mirror is experimental and off by default. When it is disabled
362
+ // OT warm mirror is opt-in and off by default. When it is disabled
363
363
  // there is nothing live to probe, so report a clean, healthy "disabled"
364
364
  // state instead of querying (then warning about) absent OT metadata — or
365
365
  // degrading to the generic "could not run" error if a probe throws. A
@@ -374,6 +374,14 @@
374
374
  nextStep: '',
375
375
  technical: ''
376
376
  };
377
+ const mirrorState = getOtWarmMirrorState?.() || {};
378
+ if (mirrorState.failureActive && mirrorState.lastFailure) {
379
+ const failure = mirrorState.lastFailure;
380
+ disabledResult.status = 'warning';
381
+ disabledResult.summary = tr(otWarmMirrorController.getFailureMessageKey(failure.code));
382
+ disabledResult.nextStep = tr('otFailureNextStep');
383
+ disabledResult.technical = `lastErrorCode: ${failure.code}\nphase: ${failure.phase}\nfailedAt: ${new Date(failure.at).toISOString()}`;
384
+ }
377
385
  if (options.collectOnly) {
378
386
  return disabledResult;
379
387
  }
@@ -434,6 +442,7 @@
434
442
  const queuedEventCount = normalizeOtDiagnosticsCount(otStatus?.queuedEventCount);
435
443
  const lastEventAt = formatOtDiagnosticValue(otStatus?.lastEventAt, tr('noneValue'));
436
444
  const warmMirrorState = getOtWarmMirrorState() || {};
445
+ const failureActive = warmMirrorState.failureActive === true;
437
446
  const lastOtPatchAt = formatOtDiagnosticValue(mirrorStatus?.lastOtPatchAt || warmMirrorState.lastPatchAt, tr('noneValue'));
438
447
  const lastOtErrorCode = formatOtDiagnosticValue(mirrorStatus?.lastOtErrorCode || warmMirrorState.lastErrorCode, tr('noneValue'));
439
448
  const lastErrorCode = formatOtDiagnosticValue(otStatus?.lastErrorCode || otStatus?.reason || otStatus?.error, tr('noneValue'));
@@ -445,13 +454,14 @@
445
454
  const bridgeFailed = otStatus?.ok === false;
446
455
  const focusFiles = getActiveFocusFiles();
447
456
  const otWarmStart = otWarmMirrorController?.canUseOtWarmStart?.({ enabled, focusFiles, mirrorStatus }) || { ok: false };
448
- const fallback = !enabled || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
457
+ const fallback = !enabled || failureActive || statusValue !== 'observing' || bridgeFailed || otWarmStart?.ok !== true;
449
458
 
450
459
  return {
451
460
  title: tr('diagnosticsOtTitle'),
452
461
  subtitle: tr('diagnosticsOtSubtitle'),
453
462
  status: enabled && !fallback && !bridgeFailed ? 'completed' : (enabled || bridgeFailed ? 'warning' : 'completed'),
454
- summary: tr(enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
463
+ summary: tr(failureActive ? otWarmMirrorController.getFailureMessageKey(warmMirrorState.lastErrorCode)
464
+ : enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
455
465
  bullets: [
456
466
  `${tr('otStatus')}: ${formatOtStatusLabel(statusValue)}`,
457
467
  `${tr('otFreshFiles')}: ${otFreshFileCount}`,
@@ -38,7 +38,7 @@
38
38
  <small data-i18n="diagnosticsSnapshotSubtitle">Full project, assets, read source</small>
39
39
  </button>
40
40
  <button type="button" data-diagnostics-ot>
41
- <span data-i18n="diagnosticsOtTitle">Check Experimental OT Mirror</span>
41
+ <span data-i18n="diagnosticsOtTitle">Check OT Mirror</span>
42
42
  <small data-i18n="diagnosticsOtSubtitle">Status, fresh files, fallback</small>
43
43
  </button>
44
44
  <button type="button" data-diagnostics-export>