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
@@ -11,6 +11,7 @@
11
11
  getPanel,
12
12
  getSessionDisplayTitle,
13
13
  getState,
14
+ icons = null,
14
15
  isDisplayableSession,
15
16
  isSessionRunning,
16
17
  startNewSession,
@@ -20,19 +21,20 @@
20
21
 
21
22
  function applySessionLabel() {
22
23
  const trigger = getPanel().querySelector('[data-session-menu-trigger]');
23
- if (trigger) {
24
- trigger.title = tr('sessionMenuOpen');
25
- trigger.setAttribute('aria-label', tr('sessionMenuOpen'));
26
- }
27
24
  const textNode = getPanel().querySelector('[data-session-label-text]');
28
25
  const deleteButton = getPanel().querySelector('[data-session-delete]');
29
26
  const renameButton = getPanel().querySelector('[data-session-rename]');
30
27
  const renameInput = getPanel().querySelector('[data-session-rename-input]');
31
28
  const active = getActiveSession(getState());
29
+ const label = active && isDisplayableSession(active)
30
+ ? getSessionDisplayTitle(active)
31
+ : tr('newSessionFallback');
32
32
  if (textNode) {
33
- textNode.textContent = active && isDisplayableSession(active)
34
- ? getSessionDisplayTitle(active)
35
- : tr('newSessionFallback');
33
+ textNode.textContent = label;
34
+ }
35
+ if (trigger) {
36
+ trigger.title = `${label} · ${tr('sessionMenuOpen')}`;
37
+ trigger.setAttribute('aria-label', `${tr('sessionMenuOpen')}: ${label}`);
36
38
  }
37
39
  if (deleteButton) {
38
40
  const running = active ? isSessionRunning(active) : false;
@@ -111,11 +113,15 @@
111
113
  .slice()
112
114
  .sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
113
115
  for (const session of sessions) {
114
- const row = document.createElement('button');
115
- row.type = 'button';
116
+ const row = document.createElement('div');
116
117
  row.className = 'codex-session-menu-item';
118
+ row.setAttribute('role', 'none');
117
119
  row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
118
120
  row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
121
+ const switchButton = document.createElement('button');
122
+ switchButton.type = 'button';
123
+ switchButton.className = 'codex-session-menu-switch';
124
+ switchButton.setAttribute('role', 'menuitem');
119
125
  const title = document.createElement('span');
120
126
  title.className = 'codex-session-menu-item-title';
121
127
  title.textContent = getSessionDisplayTitle(session);
@@ -127,26 +133,56 @@
127
133
  if (absoluteTs) {
128
134
  time.title = new Date(absoluteTs).toLocaleString();
129
135
  }
130
- row.append(title, time);
131
- row.addEventListener('click', event => {
136
+ switchButton.append(title, time);
137
+ switchButton.addEventListener('click', event => {
132
138
  event.stopPropagation();
133
139
  closeSessionMenu();
134
140
  if (session.id !== getState()?.activeSessionId) {
135
141
  switchSession(session.id);
136
142
  }
137
143
  });
144
+ const deleteButton = menuAction('codex-session-menu-item-delete', 'close', '',
145
+ row.dataset.running === 'true' ? tr('deleteRunningSession') : tr('deleteSession'),
146
+ () => deleteSessionWithConfirm(session.id));
147
+ deleteButton.setAttribute('role', 'menuitem');
148
+ deleteButton.disabled = row.dataset.running === 'true';
149
+ row.append(switchButton, deleteButton);
138
150
  menu.append(row);
139
151
  }
140
- const createButton = document.createElement('button');
141
- createButton.type = 'button';
142
- createButton.className = 'codex-session-menu-new';
143
- createButton.textContent = `+ ${tr('newSession')}`;
144
- createButton.addEventListener('click', event => {
152
+ const footer = document.createElement('div');
153
+ footer.className = 'codex-session-menu-footer';
154
+ const active = getActiveSession(state);
155
+ const running = active ? isSessionRunning(active) : false;
156
+ const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (state?.sessions || []).length <= 1;
157
+ const createButton = menuAction('codex-session-menu-new', 'plus', tr('sessionMenuNewShort'), tr('newSession'), () => startNewSession());
158
+ const renameButton = menuAction('codex-session-menu-rename', 'pencil', tr('sessionMenuRenameShort'), tr('renameSession'), () => beginActiveSessionRename());
159
+ renameButton.disabled = !active;
160
+ const deleteButton = menuAction('codex-session-menu-delete', 'close', tr('sessionMenuDeleteShort'),
161
+ running ? tr('deleteRunningSession') : tr('deleteSession'), () => {
162
+ if (getState()?.activeSessionId) deleteSessionWithConfirm(getState().activeSessionId);
163
+ });
164
+ deleteButton.disabled = !active || running || soleEmpty;
165
+ footer.append(createButton, renameButton, deleteButton);
166
+ menu.append(footer);
167
+ }
168
+
169
+ function menuAction(className, iconName, label, description, onSelect) {
170
+ const button = document.createElement('button');
171
+ button.type = 'button';
172
+ button.className = className;
173
+ button.title = description;
174
+ button.setAttribute('aria-label', description);
175
+ const glyph = icons?.svg?.(iconName);
176
+ if (glyph) button.insertAdjacentHTML('beforeend', glyph);
177
+ const text = document.createElement('span');
178
+ text.textContent = label;
179
+ button.append(text);
180
+ button.addEventListener('click', event => {
145
181
  event.stopPropagation();
146
182
  closeSessionMenu();
147
- startNewSession();
183
+ onSelect();
148
184
  });
149
- menu.append(createButton);
185
+ return button;
150
186
  }
151
187
 
152
188
  function beginActiveSessionRename() {
@@ -74,20 +74,40 @@
74
74
  .filter(record => !deleted.has(record.id))
75
75
  .map(record => [record.id, record]));
76
76
  const mergedRecords = sessionRecords.map(record => StorageDb.mergeSessionReviewState?.(
77
- record, existingById.get(record.id), options.reviewRunIds
77
+ record, existingById.get(record.id), options.reviewRunIds, { shared: StorageDb.sharedSessionsEnabled?.() === true }
78
78
  ) || record);
79
+ const reviewRunIds = new Set(options.reviewRunIds || []);
80
+ // Shared storage merges local edits and explicitly requested Undo markers
81
+ // against the page baseline. Legacy and ordinary stale saves keep the age guard.
79
82
  const writable = mergedRecords.filter(record => {
80
83
  const existing = existingById.get(record.id);
81
84
  if (!existing) return true;
82
- return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0);
85
+ return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0)
86
+ || (StorageDb.sharedSessionsEnabled?.() === true
87
+ && (StorageDb.hasUnpersistedSharedSession?.(record) === true
88
+ || (record.runs || []).some(run => reviewRunIds.has(run.id) && run.undoOwnerProtocol === 1)));
83
89
  });
84
90
  const cleanedExisting = existingSessions.filter((record, index) =>
85
91
  record !== rawExistingSessions[index] && !writable.some(item => item.id === record.id)
86
92
  );
87
93
  if (cleanedExisting.length || writable.length) {
88
- const written = await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable], {
89
- reviewRunIds: options.reviewRunIds
90
- });
94
+ const reviewReceipts = [];
95
+ if (StorageDb.sharedSessionsEnabled?.() === true && reviewRunIds.size) {
96
+ for (const record of writable) {
97
+ const ids = (record.runs || []).filter(run => reviewRunIds.has(run.id)).map(run => run.id);
98
+ if (ids.length) reviewReceipts.push(await StorageDb.putSessionReviewState(record, ids));
99
+ }
100
+ }
101
+ let written;
102
+ try {
103
+ written = await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable], {
104
+ reviewRunIds: options.reviewRunIds, reviewReceipts
105
+ });
106
+ } catch (error) {
107
+ if (error.code === 'shared_session_conflict' && reviewReceipts.length)
108
+ Object.defineProperty(error, 'reviewReceipts', { value: reviewReceipts, configurable: true });
109
+ throw error;
110
+ }
91
111
  // Check the actual durable result, including a decision already saved
92
112
  // from another origin, instead of acknowledging only the proposed view.
93
113
  return written.filter(record => writable.some(candidate => candidate.id === record.id));
@@ -9,8 +9,10 @@
9
9
 
10
10
  function codexIcon(name) {
11
11
  const icons = {
12
+ archive: '<rect x="2.5" y="3" width="11" height="3.2" rx=".8"/><path d="M3.5 6.2v6.3a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V6.2"/><path d="M6.5 9h3"/>',
12
13
  appearance: '<path d="M8 2.2v1.1"/><path d="M8 12.7v1.1"/><path d="M2.2 8h1.1"/><path d="M12.7 8h1.1"/><path d="m3.9 3.9.8.8"/><path d="m11.3 11.3.8.8"/><path d="m12.1 3.9-.8.8"/><path d="m4.7 11.3-.8.8"/><circle cx="8" cy="8" r="2.4"/>',
13
14
  bolt: '<path d="M8.9 1.8 3.8 8.8h3.3l-.5 5.4 5.5-7.5H8.8z"/>',
15
+ close: '<path d="m4.6 4.6 6.8 6.8M11.4 4.6l-6.8 6.8"/>',
14
16
  database: '<ellipse cx="8" cy="4" rx="4.5" ry="1.9"/><path d="M3.5 4v4c0 1 2 1.9 4.5 1.9s4.5-.9 4.5-1.9V4"/><path d="M3.5 8v4c0 1 2 1.9 4.5 1.9s4.5-.9 4.5-1.9V8"/>',
15
17
  flask: '<path d="M6 2.5h4"/><path d="M7 2.5v4.1l-3.4 5.3A1.4 1.4 0 0 0 4.8 14h6.4a1.4 1.4 0 0 0 1.2-2.1L9 6.6V2.5"/><path d="M5.3 10.5h5.4"/>',
16
18
  history: '<path d="M4.2 3.4h7.6"/><path d="M4.2 8h7.6"/><path d="M4.2 12.6h4.6"/><path d="M2.2 3.4h.1"/><path d="M2.2 8h.1"/><path d="M2.2 12.6h.1"/>',
@@ -36,7 +38,9 @@
36
38
  container,
37
39
  callbacks: options.callbacks || {},
38
40
  i18n: options.i18n || null,
39
- button: options.button || null
41
+ button: options.button || null,
42
+ // Names the open project in project-page scope lines; may return ''.
43
+ getProjectName: typeof options.getProjectName === 'function' ? options.getProjectName : null
40
44
  };
41
45
 
42
46
  container.innerHTML = `
@@ -211,6 +215,7 @@
211
215
  <p class="codex-set-row-help" data-update-settings-summary data-i18n="softwareUpdatesHelp">Automatically checks signed stable releases for the Extension and Native Host.</p>
212
216
  <p class="codex-set-note" data-update-settings-state aria-live="polite" hidden></p>
213
217
  <button type="button" class="codex-set-btn" data-check-updates data-i18n="checkForUpdates">Check for updates</button>
218
+ <button type="button" class="codex-set-btn" data-restore-previous-version hidden>Restore previous version</button>
214
219
  </div>
215
220
  </div>
216
221
  </details>
@@ -242,7 +247,7 @@
242
247
  </label>
243
248
  <p class="codex-set-row-help" data-i18n="loadCodexLocalSkillsHelp">Pull skills from your local Codex installation.</p>
244
249
  <button type="button" class="codex-skills-entry" data-skills-entry>
245
- <span class="codex-skills-entry-label" data-i18n="codexOverleafSkillsEntry">Codex Overleaf skills</span>
250
+ <span class="codex-skills-entry-label" data-i18n="skillsManageEntry">Manage skills</span>
246
251
  <span class="codex-skills-entry-summary" data-skills-entry-summary></span>
247
252
  <span class="codex-skills-entry-chevron" aria-hidden="true">›</span>
248
253
  </button>
@@ -252,9 +257,9 @@
252
257
  </section>
253
258
  <section class="codex-custom-instructions-panel codex-skills-panel" data-skills-screen>
254
259
  <div class="codex-custom-instructions-head">
255
- <button type="button" data-skills-back title="Back" aria-label="Back">‹</button>
260
+ <button type="button" data-skills-back data-i18n-title="settingsBack" data-i18n-aria-label="settingsBack" title="Back" aria-label="Back">‹</button>
256
261
  <div>
257
- <div class="codex-custom-instructions-title" data-i18n="codexOverleafSkillsTitle">Codex Overleaf skills</div>
262
+ <div class="codex-custom-instructions-title" data-i18n="localSkillsTitle">Skills</div>
258
263
  </div>
259
264
  <span class="codex-set-saved" data-set-saved hidden><svg class="codex-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m3.5 8 3 3 6-6"/></svg><span data-i18n="settingsSaved">Saved</span></span>
260
265
  </div>
@@ -266,6 +271,9 @@
266
271
  </div>
267
272
  <div class="codex-skills-divider"></div>
268
273
  <div class="codex-local-skill-list" data-local-skill-list></div>
274
+ <div class="codex-skills-divider"></div>
275
+ <div class="codex-wb-skills-list-title codex-wb-skills-caption" data-i18n="codexLocalSkillsTitle">Local Codex skills</div>
276
+ <div class="codex-local-skill-list" data-codex-local-skill-list></div>
269
277
  </section>
270
278
  `;
271
279
 
@@ -331,6 +339,7 @@
331
339
  return host || null;
332
340
  },
333
341
  setProviderCloseGuard: guard => instance.workbench?.setProviderCloseGuard(guard),
342
+ revealGroup: key => instance.workbench?.revealGroup(key) || null,
334
343
  loadState: state => loadState(instance, state),
335
344
  readState: () => readState(instance),
336
345
  setStatus: (text, status) => setStatus(instance, text, status),
@@ -338,6 +347,7 @@
338
347
  setSkillsSummary: text => setSkillsSummary(instance, text),
339
348
  setProviderSummary: summary => setProviderSummary(instance, summary),
340
349
  refreshNotes: () => updateGovernanceNotes(instance),
350
+ refreshLocale: () => instance.workbench?.refresh(),
341
351
  destroy: () => destroy(instance),
342
352
  _instance: instance
343
353
  };
@@ -460,7 +470,9 @@
460
470
  };
461
471
  }
462
472
 
463
- function setStatus(target, text, status = 'info') {
473
+ // options.page names the workbench page a message belongs to; the workbench
474
+ // shows it only while that page is active. Untagged messages show everywhere.
475
+ function setStatus(target, text, status = 'info', options = {}) {
464
476
  const instance = target?._instance || target;
465
477
  const element = instance?.container?.querySelector('[data-project-settings-status]');
466
478
  if (!element) {
@@ -468,6 +480,9 @@
468
480
  }
469
481
  element.textContent = text || '';
470
482
  element.dataset.status = status;
483
+ if (options.page) element.dataset.statusPage = options.page;
484
+ else delete element.dataset.statusPage;
485
+ instance.workbench?.refresh();
471
486
  }
472
487
 
473
488
  function clearStatus(target) {
@@ -478,6 +493,8 @@
478
493
  }
479
494
  element.textContent = '';
480
495
  delete element.dataset.status;
496
+ delete element.dataset.statusPage;
497
+ instance.workbench?.refresh();
481
498
  }
482
499
 
483
500
  function setSkillsSummary(target, text) {
@@ -4,21 +4,32 @@
4
4
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
5
5
  'use strict';
6
6
 
7
- const CATEGORIES = [
8
- ['general', 'settingsWorkbenchGeneral', 'appearance'],
9
- ['providers', 'settingsWorkbenchModels', 'bolt'],
10
- ['context', 'settingsWorkbenchContext', 'database'],
11
- ['instructions', 'settingsWorkbenchInstructions', 'pen'],
12
- ['protection', 'settingsWorkbenchProtection', 'shield'],
13
- ['history', 'settingsWorkbenchHistory', 'history'],
14
- ['updates', 'softwareUpdatesTitle', 'software']
7
+ // The sidebar is split by scope; a page's section decides whether its groups
8
+ // need an open project. Pages: [id, label key, icon].
9
+ const SECTIONS = [
10
+ ['project', 'settingsScopeProjectTitle', [
11
+ ['writing', 'settingsWorkbenchWriting', 'pen'],
12
+ ['protection', 'settingsWorkbenchProtection', 'shield'],
13
+ ['cache', 'otSettingsTitle', 'flask'],
14
+ ['history', 'settingsWorkbenchChanges', 'history']
15
+ ]],
16
+ ['global', 'settingsScopeGlobalTitle', [
17
+ ['appearance', 'appearanceTitle', 'appearance'],
18
+ ['providers', 'settingsWorkbenchModels', 'bolt'],
19
+ ['context', 'settingsWorkbenchContextSkills', 'database'],
20
+ ['storage', 'storageTitle', 'archive'],
21
+ ['updates', 'softwareUpdatesTitle', 'software']
22
+ ]]
15
23
  ];
16
24
  const GROUP_PAGES = {
17
- appearance: 'general', 'writing-style': 'general', providers: 'providers', 'context-loading': 'context',
18
- experimental: 'context', personalization: 'instructions', protection: 'protection',
19
- privacy: 'protection', history: 'history', storage: 'history', updates: 'updates'
25
+ 'writing-style': 'writing', personalization: 'writing', protection: 'protection', privacy: 'protection',
26
+ experimental: 'cache', history: 'history', appearance: 'appearance', providers: 'providers',
27
+ 'context-loading': 'context', skills: 'context', storage: 'storage', updates: 'updates'
20
28
  };
21
- const PROJECT_GROUPS = new Set(['writing-style', 'personalization', 'protection', 'privacy', 'experimental', 'history']);
29
+ // Groups without a mapping keep their original scope.
30
+ const FALLBACK_PAGES = { project: 'writing', global: 'context' };
31
+ // The skills list opens in place of the page that holds the Skills group.
32
+ const SKILLS_PAGE = 'context';
22
33
 
23
34
  function mount(instance, options = {}) {
24
35
  const container = instance.container, doc = container.ownerDocument;
@@ -50,93 +61,94 @@
50
61
  const status = settings.querySelector('[data-project-settings-status]');
51
62
  const dialog = make('dialog', 'codex-settings-workbench');
52
63
  dialog.setAttribute('aria-labelledby', 'codex-settings-workbench-title');
53
- const header = make('header', 'codex-wb-header');
54
- const heading = make('div', 'codex-wb-heading');
55
- const title = make('h2', '', 'settingsWorkbenchTitle');
64
+ const title = make('h2', 'codex-wb-title', 'settingsWorkbenchTitle');
56
65
  title.id = 'codex-settings-workbench-title';
57
- heading.append(title, make('p', '', 'settingsWorkbenchSubtitle'));
58
66
  closeButton.classList.add('codex-wb-close');
59
- closeButton.textContent = '\u00d7';
60
- header.append(heading, closeButton);
67
+ if (options.icon) closeButton.innerHTML = options.icon('close'); // Static, repository-owned icon path.
68
+ else closeButton.textContent = '\u00d7';
61
69
  originalHead?.remove();
62
70
 
63
71
  const layout = make('div', 'codex-wb-layout');
64
72
  const sidebar = make('aside', 'codex-wb-sidebar');
65
- const filters = make('div', 'codex-wb-filters');
66
- filters.setAttribute('role', 'group');
67
- const allFilter = button('', 'settingsWorkbenchAll');
68
- const projectFilter = button('', 'settingsScopeProjectTitle');
69
- filters.append(allFilter, projectFilter);
70
73
  const nav = make('nav', 'codex-wb-nav');
71
74
  nav.setAttribute('role', 'tablist');
72
75
  nav.setAttribute('aria-orientation', 'vertical');
73
- const brand = make('div', 'codex-wb-brand');
74
- brand.textContent = 'Codex Overleaf Link';
75
- sidebar.append(filters, nav, brand);
76
+ sidebar.append(title, nav);
76
77
  const main = make('div', 'codex-wb-main');
77
78
  const body = make('div', 'codex-wb-body');
78
79
  const footer = make('footer', 'codex-wb-footer');
79
80
  if (status) { status.setAttribute('aria-live', 'polite'); footer.append(status); }
80
81
  main.append(body, footer);
81
82
  layout.append(sidebar, main);
82
- dialog.append(header, layout);
83
- const pages = new Map(), navButtons = new Map(), groups = [];
84
- let active = 'general', filter = 'all', returnFocus = null, destroyed = false;
83
+ // The close button floats over the content's top-right corner.
84
+ dialog.append(layout, closeButton);
85
+ const pages = new Map(), navButtons = new Map(), pageScopes = new Map(), sectionLabels = new Map(), groups = [];
86
+ let active = 'writing', returnFocus = null, destroyed = false;
85
87
  let providerRequested = false, providerCloseGuard = null, closing = null;
86
88
 
87
- for (const [id, key, icon] of CATEGORIES) {
88
- const page = make('section', 'codex-wb-page');
89
- page.id = 'codex-settings-page-' + id;
90
- page.dataset.workbenchPage = id;
91
- page.setAttribute('role', 'tabpanel');
92
- page.setAttribute('aria-labelledby', 'codex-settings-nav-' + id);
93
- page.hidden = true;
94
- page.append(make('h3', 'codex-wb-page-title', key));
95
- pages.set(id, page);
96
- const tab = button('codex-wb-nav-button');
97
- tab.id = 'codex-settings-nav-' + id;
98
- tab.setAttribute('role', 'tab');
99
- tab.setAttribute('aria-controls', page.id);
100
- if (options.icon) {
101
- const glyph = make('span', 'codex-wb-nav-icon');
102
- glyph.innerHTML = options.icon(icon); // Static, repository-owned icon paths only.
103
- tab.append(glyph);
89
+ for (const [scope, sectionKey, items] of SECTIONS) {
90
+ // Visual section heading; each tab references it as its description.
91
+ const label = make('div', 'codex-wb-nav-section', sectionKey);
92
+ label.id = 'codex-settings-nav-section-' + scope;
93
+ label.setAttribute('aria-hidden', 'true');
94
+ nav.append(label);
95
+ sectionLabels.set(scope, label);
96
+ for (const [id, key, icon] of items) {
97
+ const page = make('section', 'codex-wb-page');
98
+ page.id = 'codex-settings-page-' + id;
99
+ page.dataset.workbenchPage = id;
100
+ page.setAttribute('role', 'tabpanel');
101
+ page.setAttribute('aria-labelledby', 'codex-settings-nav-' + id);
102
+ page.hidden = true;
103
+ page.append(make('h3', 'codex-wb-page-title', key), make('p', 'codex-wb-page-scope'));
104
+ pages.set(id, page);
105
+ pageScopes.set(id, scope);
106
+ const tab = button('codex-wb-nav-button');
107
+ tab.id = 'codex-settings-nav-' + id;
108
+ tab.setAttribute('role', 'tab');
109
+ tab.setAttribute('aria-controls', page.id);
110
+ tab.setAttribute('aria-describedby', label.id);
111
+ if (options.icon) {
112
+ const glyph = make('span', 'codex-wb-nav-icon');
113
+ glyph.innerHTML = options.icon(icon); // Static, repository-owned icon paths only.
114
+ tab.append(glyph);
115
+ }
116
+ tab.append(make('span', '', key));
117
+ listen(tab, 'click', () => select(id));
118
+ nav.append(tab);
119
+ navButtons.set(id, tab);
104
120
  }
105
- tab.append(make('span', '', key));
106
- listen(tab, 'click', () => select(id));
107
- nav.append(tab);
108
- navButtons.set(id, tab);
109
121
  }
110
- function addGroup(node, category, projectOnly) {
122
+ function addGroup(node, category) {
123
+ const projectOnly = pageScopes.get(category) === 'project';
111
124
  node.hidden = true;
112
125
  node.dataset.workbenchScope = projectOnly ? 'project' : 'global';
113
126
  const summary = node.querySelector('summary');
114
- const badge = make('span', 'codex-wb-scope-label',
115
- projectOnly ? 'settingsScopeProjectTitle' : 'settingsScopeGlobalTitle');
116
- (summary || node).append(badge);
127
+ if (summary) {
128
+ // Groups are plain sections: always open while their page is shown.
129
+ summary.tabIndex = -1;
130
+ listen(summary, 'click', event => event.preventDefault());
131
+ }
117
132
  pages.get(category).append(node);
118
- groups.push({ node, category, projectOnly, expanded: true });
133
+ groups.push({ node, category, projectOnly });
119
134
  }
135
+ const fallbackPage = scope => FALLBACK_PAGES[
136
+ scope && !scope.classList.contains('codex-project-settings-scope--global') ? 'project' : 'global'];
120
137
  // Move existing controls rather than copying them. Their listeners, unsaved
121
138
  // values, identity, validation and persistence callbacks remain attached.
122
139
  for (const group of Array.from(settings.querySelectorAll('[data-set-group]'))) {
123
140
  const key = group.dataset.setGroup;
124
- const originalScope = group.closest('.codex-project-settings-scope');
125
- const projectOnly = PROJECT_GROUPS.has(key)
126
- || (!GROUP_PAGES[key] && originalScope && !originalScope.classList.contains('codex-project-settings-scope--global'));
127
- addGroup(group, GROUP_PAGES[key] || 'instructions', Boolean(projectOnly));
141
+ addGroup(group, GROUP_PAGES[key] || fallbackPage(group.closest('.codex-project-settings-scope')));
128
142
  }
129
- // Preserve non-card controls, including the legacy skill-loading entry.
130
- // Scope headings are replaced by labels on the actual groups.
143
+ // Preserve non-card controls. The sidebar sections replace the scope headings.
131
144
  for (const scope of Array.from(settings.querySelectorAll('.codex-project-settings-scope'))) {
132
145
  scope.querySelectorAll('.codex-project-settings-scope-title').forEach(node => node.remove());
133
- if (scope.children.length) {
134
- addGroup(scope, 'instructions', !scope.classList.contains('codex-project-settings-scope--global'));
135
- } else scope.remove();
146
+ if (scope.children.length) addGroup(scope, fallbackPage(scope));
147
+ else scope.remove();
136
148
  }
137
149
  for (const node of Array.from(settings.children)) {
138
150
  if (node === status) continue;
139
- addGroup(node, 'instructions', false);
151
+ addGroup(node, FALLBACK_PAGES.global);
140
152
  }
141
153
  const providerHost = make('div', 'codex-wb-provider-host');
142
154
  providerHost.dataset.providerSettingsHost = '';
@@ -218,31 +230,62 @@
218
230
  && /^\/project\/[^/]+/.test(win?.location?.pathname || '');
219
231
  }
220
232
  function available(item) {
221
- return (!item.projectOnly || hasProject()) && (filter !== 'project' || item.projectOnly);
233
+ return !item.projectOnly || hasProject();
234
+ }
235
+ // A page whose only visible group repeats the page title hides that group's
236
+ // title; the group header keeps only its Saved mark.
237
+ function refreshSoleSections() {
238
+ for (const page of pages.values()) {
239
+ const pageGroups = Array.from(page.querySelectorAll('.codex-set-group'));
240
+ const visibleGroups = pageGroups.filter(node => !node.hidden);
241
+ const pageTitle = page.querySelector('.codex-wb-page-title')?.textContent.trim();
242
+ for (const node of pageGroups) {
243
+ const groupTitle = node.querySelector('.codex-set-group-head .codex-set-group-title');
244
+ const sole = visibleGroups.length === 1 && visibleGroups[0] === node
245
+ && groupTitle && groupTitle.textContent.trim() === pageTitle;
246
+ if (sole) node.dataset.soleSection = 'true';
247
+ else delete node.dataset.soleSection;
248
+ }
249
+ }
250
+ }
251
+ // Each page states its scope once, under its title.
252
+ function renderPageScope(page, scope, projectName) {
253
+ const line = page.querySelector('.codex-wb-page-scope');
254
+ if (!line) return;
255
+ const text = t(scope === 'project' ? 'settingsPageScopeProject' : 'settingsPageScopeGlobal');
256
+ if (scope !== 'project' || !projectName) { line.textContent = text; return; }
257
+ const name = doc.createElement('b');
258
+ name.textContent = projectName;
259
+ line.replaceChildren(text + ' \u00b7 ', name);
222
260
  }
223
261
  function refresh() {
224
262
  if (destroyed) return;
225
- if (!hasProject()) filter = 'all';
226
- if (panel.dataset.view === 'skills') { active = 'instructions'; filter = 'all'; }
263
+ if (panel.dataset.view === 'skills') active = SKILLS_PAGE;
227
264
  const availablePages = new Set(groups.filter(available).map(item => item.category));
228
- if (!availablePages.has(active)) active = CATEGORIES.find(([id]) => availablePages.has(id))?.[0] || 'general';
265
+ if (!availablePages.has(active)) active = Array.from(navButtons.keys()).find(id => availablePages.has(id)) || 'appearance';
229
266
  for (const node of dialog.querySelectorAll('[data-i18n]')) {
230
267
  // Existing controls use their established translation pass; update the
231
268
  // newly owned chrome only, without replacing controls or skill rows.
232
- if (node.closest('.codex-wb-header, .codex-wb-nav, .codex-wb-filters, .codex-wb-footer')
269
+ if (node.closest('.codex-wb-nav, .codex-wb-footer')
270
+ || node.classList.contains('codex-wb-title')
233
271
  || node.classList.contains('codex-wb-page-title')
234
- || node.classList.contains('codex-wb-scope-label')
235
272
  || node.classList.contains('codex-wb-theme-caption')
236
273
  || node.classList.contains('codex-wb-skills-caption')) node.textContent = t(node.dataset.i18n);
237
274
  }
238
- projectFilter.disabled = !hasProject();
239
- projectFilter.title = hasProject() ? t('settingsWorkbenchProjectFilter') : t('settingsWorkbenchOpenProject');
240
- filters.setAttribute('aria-label', t('settingsWorkbenchFilter'));
275
+ const projectName = hasProject() ? String(instance.getProjectName?.() || '').trim() : '';
276
+ for (const [id, page] of pages) renderPageScope(page, pageScopes.get(id), projectName);
277
+ for (const [scope, label] of sectionLabels) {
278
+ label.hidden = !Array.from(pageScopes).some(([id, pageScope]) => pageScope === scope && availablePages.has(id));
279
+ }
241
280
  nav.setAttribute('aria-label', t('settingsWorkbenchCategories'));
242
- allFilter.setAttribute('aria-pressed', String(filter === 'all'));
243
- projectFilter.setAttribute('aria-pressed', String(filter === 'project'));
244
281
  closeButton.title = t('settingsWorkbenchClose');
245
282
  closeButton.setAttribute('aria-label', t('settingsWorkbenchClose'));
283
+ const skillsBack = skills?.querySelector('[data-skills-back]');
284
+ if (skillsBack) {
285
+ skillsBack.title = t('settingsBack');
286
+ skillsBack.setAttribute('aria-label', t('settingsBack'));
287
+ }
288
+ if (status) status.hidden = Boolean(status.dataset.statusPage) && status.dataset.statusPage !== active;
246
289
  for (const [id, tab] of navButtons) {
247
290
  tab.hidden = !availablePages.has(id);
248
291
  tab.setAttribute('aria-selected', String(id === active));
@@ -251,12 +294,11 @@
251
294
  }
252
295
  for (const item of groups) {
253
296
  const visible = available(item) && item.category === active && panel.dataset.view !== 'skills';
254
- if (item.node.tagName === 'DETAILS' && item.node.hidden !== !visible) {
255
- if (!visible) item.expanded = item.node.open;
256
- item.node.open = visible && item.expanded;
257
- }
297
+ // Opening a group when its page is shown keeps the lazy toggle loaders (history, storage).
298
+ if (item.node.tagName === 'DETAILS' && item.node.open !== visible) item.node.open = visible;
258
299
  item.node.hidden = !visible;
259
300
  }
301
+ refreshSoleSections();
260
302
  updateThemes();
261
303
  }
262
304
  function requestProviders() {
@@ -272,9 +314,17 @@
272
314
  try { Promise.resolve(instance.callbacks.onProvidersOpen()).catch(failed); }
273
315
  catch (error) { failed(error); }
274
316
  }
317
+ // Shows the page that holds a group and returns the group, or null when the
318
+ // group is unavailable (for example a project group outside a project).
319
+ function revealGroup(key) {
320
+ const item = groups.find(entry => entry.node.dataset?.setGroup === key);
321
+ if (!item || !available(item)) return null;
322
+ active = item.category;
323
+ refresh();
324
+ return item.node;
325
+ }
275
326
  function revealProviders() {
276
327
  active = 'providers';
277
- filter = 'all';
278
328
  providerRequested = true;
279
329
  pages.get('providers').dataset.providerEmbedded = 'true';
280
330
  panel.dataset.view = 'settings';
@@ -325,13 +375,6 @@
325
375
  if (['settings', 'skills'].includes(panel.dataset.view)) show();
326
376
  else hide();
327
377
  };
328
- listen(allFilter, 'click', () => { filter = 'all'; refresh(); });
329
- listen(projectFilter, 'click', () => {
330
- filter = 'project';
331
- if (panel.dataset.view === 'skills') instance.callbacks.onSkillsBack?.();
332
- refresh();
333
- body.scrollTop = 0;
334
- });
335
378
  listen(nav, 'keydown', event => {
336
379
  if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
337
380
  const tabs = Array.from(navButtons.values()).filter(tab => !tab.hidden);
@@ -358,7 +401,7 @@
358
401
  observer.observe(panel, { attributes: true, attributeFilter: ['data-view', 'data-settings-scope', 'data-theme'] });
359
402
  refresh();
360
403
  return {
361
- show, hide, refresh, revealProviders,
404
+ show, hide, refresh, revealGroup, revealProviders,
362
405
  setProviderCloseGuard(guard) { providerCloseGuard = guard; },
363
406
  destroy() {
364
407
  destroyed = true;