codex-overleaf-link 1.8.6 → 1.9.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 (72) hide show
  1. package/README.md +45 -21
  2. package/bin/codex-overleaf-link.mjs +58 -0
  3. package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
  4. package/extension/assets/icons/codex-overleaf-icon.png +0 -0
  5. package/extension/assets/icons/icon128.png +0 -0
  6. package/extension/assets/icons/icon16.png +0 -0
  7. package/extension/assets/icons/icon32.png +0 -0
  8. package/extension/assets/icons/icon48.png +0 -0
  9. package/extension/bootstrap/background.js +404 -0
  10. package/extension/bootstrap/manifest.template.json +51 -0
  11. package/extension/bootstrap/popup.css +26 -0
  12. package/extension/bootstrap/popup.html +38 -0
  13. package/extension/bootstrap/popup.js +44 -0
  14. package/extension/bootstrap/runtimeContext.js +2 -0
  15. package/extension/runtime-manifest.json +60 -0
  16. package/extension/src/background.js +532 -0
  17. package/extension/src/content/applyResultFormatters.js +387 -0
  18. package/extension/src/content/composerAttachments.js +522 -0
  19. package/extension/src/content/composerPanel.js +254 -0
  20. package/extension/src/content/contentRuntime.js +8895 -0
  21. package/extension/src/content/contextTray.js +686 -0
  22. package/extension/src/content/diagnosticsController.js +659 -0
  23. package/extension/src/content/diagnosticsPanel.js +342 -0
  24. package/extension/src/content/diffReviewPanel.js +797 -0
  25. package/extension/src/content/localSkillsPanel.js +280 -0
  26. package/extension/src/content/markdownText.js +606 -0
  27. package/extension/src/content/mirrorHealth.js +92 -0
  28. package/extension/src/content/modelPicker.js +489 -0
  29. package/extension/src/content/nativeChannel.js +62 -0
  30. package/extension/src/content/otWarmMirror.js +964 -0
  31. package/extension/src/content/otWarmMirrorController.js +174 -0
  32. package/extension/src/content/panelMaintenance.js +367 -0
  33. package/extension/src/content/panelRenderer.js +253 -0
  34. package/extension/src/content/recentProjects.js +1063 -0
  35. package/extension/src/content/reviewHunks.js +247 -0
  36. package/extension/src/content/runController.js +242 -0
  37. package/extension/src/content/runTimelineView.js +1126 -0
  38. package/extension/src/content/sessionManager.js +487 -0
  39. package/extension/src/content/sessionPanel.js +263 -0
  40. package/extension/src/content/settingsPanel.js +527 -0
  41. package/extension/src/content/themeController.js +91 -0
  42. package/extension/src/content/updateIdle.js +67 -0
  43. package/extension/src/content/writebackController.js +300 -0
  44. package/extension/src/content/writebackOrchestrator.js +850 -0
  45. package/extension/src/contentScript.js +8 -0
  46. package/extension/src/page/compileBridge.js +460 -0
  47. package/extension/src/page/overleafCapabilities.js +100 -0
  48. package/extension/src/page/overleafEditor.js +282 -0
  49. package/extension/src/page/overleafProjectSnapshot.js +234 -0
  50. package/extension/src/page/overleafRealtimeObserver.js +491 -0
  51. package/extension/src/page/pageBridgeCapability.js +71 -0
  52. package/extension/src/page/snapshotRouter.js +948 -0
  53. package/extension/src/page/trackedChangesLifecycle.js +1506 -0
  54. package/extension/src/page/treeOperations.js +1584 -0
  55. package/extension/src/page/writeGuard.js +142 -0
  56. package/extension/src/page/writebackRouter.js +1934 -0
  57. package/extension/src/pageBridge.js +2224 -0
  58. package/extension/src/popup.js +247 -0
  59. package/extension/src/shared/compatibility.js +1 -1
  60. package/extension/styles/panel.css +4048 -0
  61. package/native-host/src/index.js +9 -2
  62. package/native-host/src/managedInstall.js +393 -0
  63. package/native-host/src/managedLauncherRuntime.js +71 -0
  64. package/native-host/src/nativeHostPlatform.js +16 -0
  65. package/native-host/src/runtimeInstaller.js +6 -1
  66. package/native-host/src/taskRunnerRuntime.js +8 -0
  67. package/native-host/src/updateArchive.js +253 -0
  68. package/native-host/src/updateManager.js +548 -0
  69. package/native-host/src/updateTrust.js +156 -0
  70. package/package.json +11 -2
  71. package/scripts/install-managed.mjs +64 -0
  72. package/scripts/uninstall-managed.mjs +48 -0
@@ -0,0 +1,527 @@
1
+ (function initCodexOverleafSettingsPanel() {
2
+ 'use strict';
3
+
4
+ function codexIcon(name) {
5
+ const icons = {
6
+ 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"/>',
7
+ bolt: '<path d="M8.9 1.8 3.8 8.8h3.3l-.5 5.4 5.5-7.5H8.8z"/>',
8
+ 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"/>',
9
+ 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"/>',
10
+ 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"/>',
11
+ lock: '<rect x="3.6" y="7" width="8.8" height="6.2" rx="1.2"/><path d="M5.6 7V5.3a2.4 2.4 0 0 1 4.8 0V7"/><path d="M8 9.6v1.2"/>',
12
+ pen: '<path d="M3.2 12.8 4 9.7l6.6-6.6a1.4 1.4 0 0 1 2 2L6 11.7z"/><path d="m9.6 4.1 2.3 2.3"/>',
13
+ shield: '<path d="M8 2.1 12.4 4v3.5c0 2.9-1.8 5.1-4.4 6.4-2.6-1.3-4.4-3.5-4.4-6.4V4z"/><path d="M6.1 8.1 7.5 9.5 10.2 6.6"/>'
14
+ };
15
+ const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : 'appearance';
16
+ return `<span class="codex-icon codex-icon-${safeName}" aria-hidden="true"><svg viewBox="0 0 16 16" focusable="false">${icons[safeName]}</svg></span>`;
17
+ }
18
+
19
+ function codexSetIcon(name) {
20
+ return `<span class="codex-set-group-icon">${codexIcon(name)}</span>`;
21
+ }
22
+
23
+ function create(options = {}) {
24
+ const container = options.container;
25
+ if (!container) {
26
+ throw new Error('CodexOverleafSettingsPanel requires a container');
27
+ }
28
+ const instance = {
29
+ container,
30
+ callbacks: options.callbacks || {},
31
+ i18n: options.i18n || null,
32
+ button: options.button || null
33
+ };
34
+
35
+ container.innerHTML = `
36
+ <section class="codex-custom-instructions-panel codex-project-settings-panel" data-custom-instructions-panel data-project-settings-panel>
37
+ <div class="codex-custom-instructions-head">
38
+ <button type="button" data-settings-back title="Back" aria-label="Back">‹</button>
39
+ <div>
40
+ <div class="codex-custom-instructions-title" data-i18n="projectSettingsTitle">Project Settings</div>
41
+ <div class="codex-custom-instructions-subtitle" data-i18n="projectSettingsSubtitle">Customize how Codex behaves in this and all projects.</div>
42
+ </div>
43
+ </div>
44
+ <div class="codex-project-settings-status" data-project-settings-status></div>
45
+ <div class="codex-project-settings-scope">
46
+ <div class="codex-project-settings-scope-title codex-set-eyebrow" data-i18n="settingsScopeProjectTitle">This project</div>
47
+ <details class="codex-set-group" data-set-group="personalization" open>
48
+ <summary class="codex-set-group-head">
49
+ <span class="codex-set-group-title">${codexSetIcon('pen')}<span data-i18n="personalizationConfig">Personalization</span></span>
50
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
51
+ </summary>
52
+ <div class="codex-set-card">
53
+ <p class="codex-set-row-help" data-i18n="personalizationHelp">Style, terminology, and LaTeX conventions Codex should follow in this project.</p>
54
+ <textarea id="codex-custom-instructions-input" class="codex-custom-instructions-input" data-custom-instructions-input rows="6" placeholder="Style, terminology, venue constraints, and LaTeX conventions for this project."></textarea>
55
+ </div>
56
+ </details>
57
+ <details class="codex-set-group" data-set-group="protection" open>
58
+ <summary class="codex-set-group-head">
59
+ <span class="codex-set-group-title">${codexSetIcon('shield')}<span data-i18n="fileProtectionTitle">File protection</span></span>
60
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
61
+ </summary>
62
+ <div class="codex-set-card">
63
+ <div class="codex-set-row">
64
+ <label class="codex-set-row-label" for="codex-governance-readonly-patterns" data-i18n="governanceReadonlyPatterns">Read-only patterns</label>
65
+ <p class="codex-set-row-help" data-i18n="governanceReadonlyHelp">Files Codex must never modify (one glob per line).</p>
66
+ <textarea id="codex-governance-readonly-patterns" class="codex-project-settings-textarea" data-governance-readonly-patterns rows="3" placeholder="paper/accepted/**&#10;main.tex"></textarea>
67
+ <div class="codex-set-note" data-governance-readonly-note hidden></div>
68
+ </div>
69
+ <div class="codex-set-row">
70
+ <label class="codex-set-row-label" for="codex-governance-writable-patterns" data-i18n="governanceWritablePatterns">Writable patterns</label>
71
+ <p class="codex-set-row-help" data-i18n="governanceWritableHelp">Files Codex may edit (one glob per line).</p>
72
+ <textarea id="codex-governance-writable-patterns" class="codex-project-settings-textarea" data-governance-writable-patterns rows="3" placeholder="sections/**&#10;figures/**"></textarea>
73
+ <div class="codex-set-note" data-governance-writable-note hidden></div>
74
+ </div>
75
+ </div>
76
+ </details>
77
+ <details class="codex-set-group" data-set-group="privacy" open>
78
+ <summary class="codex-set-group-head">
79
+ <span class="codex-set-group-title">${codexSetIcon('lock')}<span data-i18n="privacyTitle">Privacy</span></span>
80
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
81
+ </summary>
82
+ <div class="codex-set-card">
83
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
84
+ <span class="codex-project-settings-row-label" data-i18n="sensitiveCheckEnabled">Check for sensitive content before Codex runs</span>
85
+ <input type="checkbox" class="codex-switch" data-sensitive-check-enabled>
86
+ </label>
87
+ <p class="codex-set-row-help" data-i18n="sensitiveCheckHelp">Scan project files for secrets and PII before each Codex run.</p>
88
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
89
+ <span class="codex-project-settings-row-label" data-i18n="sensitiveConfirmAllowed">Allow explicit confirmation when sensitive findings exist</span>
90
+ <input type="checkbox" class="codex-switch" data-sensitive-confirm-allowed>
91
+ </label>
92
+ <p class="codex-set-row-help" data-i18n="sensitiveConfirmHelp">Let you review and proceed when the scan flags content.</p>
93
+ </div>
94
+ </details>
95
+ <details class="codex-set-group" data-set-group="experimental">
96
+ <summary class="codex-set-group-head">
97
+ <span class="codex-set-group-title">${codexSetIcon('flask')}<span data-i18n="experimentalTitle">Experimental</span></span>
98
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
99
+ </summary>
100
+ <div class="codex-set-card">
101
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
102
+ <span class="codex-project-settings-row-label">
103
+ <span data-i18n="experimentalOtMenuTitle">Experimental OT Mirror</span>
104
+ <small class="codex-set-row-sub" data-i18n="experimentalOtMenuSubtitle">Experimental — speeds up reading the file you are editing; Codex falls back to normal reading whenever unsure.</small>
105
+ </span>
106
+ <input type="checkbox" class="codex-switch" data-experimental-ot>
107
+ </label>
108
+ <p class="codex-set-row-help" data-experimental-ot-menu-status></p>
109
+ </div>
110
+ </details>
111
+ </div>
112
+ <div class="codex-project-settings-scope codex-project-settings-scope--global">
113
+ <details class="codex-set-group" data-set-group="history" data-history-card>
114
+ <summary class="codex-set-group-head">
115
+ <span class="codex-set-group-title">${codexSetIcon('history')}<span data-i18n="historyTitle">Change history</span></span>
116
+ </summary>
117
+ <div class="codex-set-card">
118
+ <p class="codex-set-row-help" data-i18n="historyHelp">What Codex wrote to this project, per run. Filter by file name or task text.</p>
119
+ <input type="text" class="codex-set-select codex-history-filter" data-history-filter>
120
+ <div class="codex-history-list" data-history-list></div>
121
+ </div>
122
+ </details>
123
+ <div class="codex-project-settings-scope-title codex-set-eyebrow" data-i18n="settingsScopeGlobalTitle">All projects</div>
124
+ <details class="codex-set-group" data-set-group="appearance" open>
125
+ <summary class="codex-set-group-head">
126
+ <span class="codex-set-group-title">${codexSetIcon('appearance')}<span data-i18n="appearanceTitle">Appearance</span></span>
127
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
128
+ </summary>
129
+ <div class="codex-set-card">
130
+ <label class="codex-project-settings-row codex-project-settings-row--select">
131
+ <span class="codex-project-settings-row-label" data-i18n="themeLabel">Theme</span>
132
+ <select class="codex-set-select" data-theme-select>
133
+ <option value="dark" data-i18n="themeDark">Dark</option>
134
+ <option value="light" data-i18n="themeLight">Light</option>
135
+ <option value="auto" data-i18n="themeAuto">Follow system</option>
136
+ </select>
137
+ </label>
138
+ <p class="codex-set-row-help" data-i18n="themeHelp">Switch the Codex panel between dark, light, or following your operating system.</p>
139
+ <label class="codex-project-settings-row codex-project-settings-row--select">
140
+ <span class="codex-project-settings-row-label" data-i18n="languageLabel">Language</span>
141
+ <select class="codex-set-select" data-language-select>
142
+ <option value="en" data-i18n="languageEnglish">English</option>
143
+ <option value="zh" data-i18n="languageChinese">中文</option>
144
+ </select>
145
+ </label>
146
+ <p class="codex-set-row-help" data-i18n="languageHelp">Panel display language.</p>
147
+ </div>
148
+ </details>
149
+ <details class="codex-set-group" data-set-group="storage" data-storage-card>
150
+ <summary class="codex-set-group-head">
151
+ <span class="codex-set-group-title">${codexSetIcon('database')}<span data-i18n="storageTitle">History &amp; storage</span></span>
152
+ </summary>
153
+ <div class="codex-set-card">
154
+ <p class="codex-set-row-help" data-storage-usage data-i18n="storageUsageLoading">Calculating usage…</p>
155
+ <button type="button" class="codex-set-btn codex-set-btn--danger" data-clear-all-history data-i18n="storageClearAll">Clear all history…</button>
156
+ <p class="codex-set-row-help" data-i18n="storageClearAllHelp">Removes every stored session, run and audit record for all projects. Project settings and rules are kept.</p>
157
+ </div>
158
+ </details>
159
+ <details class="codex-set-group" data-set-group="skills" open>
160
+ <summary class="codex-set-group-head">
161
+ <span class="codex-set-group-title">${codexSetIcon('bolt')}<span data-i18n="localSkillsTitle">Skills</span></span>
162
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
163
+ </summary>
164
+ <div class="codex-set-card">
165
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
166
+ <span class="codex-project-settings-row-label" data-i18n="loadCodexLocalSkills">Load local Codex skills</span>
167
+ <input type="checkbox" class="codex-switch" data-load-codex-local-skills>
168
+ </label>
169
+ <p class="codex-set-row-help" data-i18n="loadCodexLocalSkillsHelp">Pull skills from your local Codex installation.</p>
170
+ <button type="button" class="codex-skills-entry" data-skills-entry>
171
+ <span class="codex-skills-entry-label" data-i18n="codexOverleafSkillsEntry">Codex Overleaf skills</span>
172
+ <span class="codex-skills-entry-summary" data-skills-entry-summary></span>
173
+ <span class="codex-skills-entry-chevron" aria-hidden="true">›</span>
174
+ </button>
175
+ </div>
176
+ </details>
177
+ </div>
178
+ </section>
179
+ <section class="codex-custom-instructions-panel codex-skills-panel" data-skills-screen>
180
+ <div class="codex-custom-instructions-head">
181
+ <button type="button" data-skills-back title="Back" aria-label="Back">‹</button>
182
+ <div>
183
+ <div class="codex-custom-instructions-title" data-i18n="codexOverleafSkillsTitle">Codex Overleaf skills</div>
184
+ </div>
185
+ <span class="codex-set-saved" data-set-saved hidden>✓ <span data-i18n="settingsSaved">Saved</span></span>
186
+ </div>
187
+ <div class="codex-set-card">
188
+ <label class="codex-project-settings-row codex-project-settings-row--switch">
189
+ <span class="codex-project-settings-row-label" data-i18n="loadCodexOverleafSkills">Load Codex Overleaf skills</span>
190
+ <input type="checkbox" class="codex-switch" data-load-codex-overleaf-skills>
191
+ </label>
192
+ </div>
193
+ <div class="codex-skills-divider"></div>
194
+ <div class="codex-local-skill-list" data-local-skill-list></div>
195
+ </section>
196
+ `;
197
+
198
+ container.querySelector('[data-settings-back]')?.addEventListener('click', () => instance.callbacks.onBack?.());
199
+ container.querySelector('[data-skills-back]')?.addEventListener('click', () => instance.callbacks.onSkillsBack?.());
200
+ container.querySelector('[data-skills-entry]')?.addEventListener('click', () => instance.callbacks.onSkillsOpen?.());
201
+ container.querySelector('[data-clear-all-history]')?.addEventListener('click', () => instance.callbacks.onClearAllHistory?.());
202
+ container.querySelector('[data-history-card]')?.addEventListener('toggle', event => {
203
+ if (event.target.open) {
204
+ instance.callbacks.onHistoryOpen?.();
205
+ }
206
+ });
207
+ container.querySelector('[data-history-filter]')?.addEventListener('input', () => instance.callbacks.onHistoryFilter?.());
208
+ // Experimental OT mirror: a single visible switch. The click is
209
+ // intercepted (the runtime's handler preventDefaults, runs the enable
210
+ // confirmation, then sets checked + drives the change flow itself) so the
211
+ // confirm-before-enable contract survives the control unification.
212
+ container.querySelector('[data-experimental-ot]')?.addEventListener('click', event => instance.callbacks.onOtToggleClick?.(event));
213
+ // Personalization textarea: auto-save on change (the change event fires on blur — avoids per-keystroke saves).
214
+ const customInstructionsInput = container.querySelector('[data-custom-instructions-input]');
215
+ customInstructionsInput?.addEventListener('change', event => {
216
+ instance.callbacks.onInputChange?.(event);
217
+ flashSaved(instance, event);
218
+ });
219
+ // Governance and skill fields: auto-save on change and input for immediate response.
220
+ for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]', '[data-sensitive-check-enabled]', '[data-sensitive-confirm-allowed]', '[data-load-codex-local-skills]', '[data-load-codex-overleaf-skills]', '[data-theme-select]', '[data-language-select]']) {
221
+ const element = container.querySelector(selector);
222
+ element?.addEventListener?.('change', event => {
223
+ instance.callbacks.onInputChange?.(event);
224
+ flashSaved(instance, event);
225
+ });
226
+ element?.addEventListener?.('input', event => instance.callbacks.onInputChange?.(event));
227
+ }
228
+ // Guardrails: live, non-blocking analysis of the protection globs.
229
+ for (const selector of ['[data-governance-readonly-patterns]', '[data-governance-writable-patterns]']) {
230
+ const element = container.querySelector(selector);
231
+ element?.addEventListener?.('input', () => updateGovernanceNotes(instance));
232
+ element?.addEventListener?.('change', () => updateGovernanceNotes(instance));
233
+ }
234
+ setupGroupPersistence(instance);
235
+
236
+ return {
237
+ show: () => show(instance),
238
+ hide: () => hide(instance),
239
+ loadState: state => loadState(instance, state),
240
+ readState: () => readState(instance),
241
+ setStatus: (text, status) => setStatus(instance, text, status),
242
+ clearStatus: () => clearStatus(instance),
243
+ setSkillsSummary: text => setSkillsSummary(instance, text),
244
+ refreshNotes: () => updateGovernanceNotes(instance),
245
+ destroy: () => destroy(instance),
246
+ _instance: instance
247
+ };
248
+ }
249
+
250
+ function show(target) {
251
+ const instance = target?._instance || target;
252
+ const root = getRoot(instance);
253
+ if (!root) {
254
+ return;
255
+ }
256
+ const button = getButton(instance);
257
+ if (button) {
258
+ button.dataset.active = 'true';
259
+ button.setAttribute('aria-expanded', 'true');
260
+ }
261
+ root.querySelector('[data-custom-instructions-input]')?.focus?.();
262
+ }
263
+
264
+ function hide(target) {
265
+ const instance = target?._instance || target;
266
+ const root = getRoot(instance);
267
+ if (!root) {
268
+ return;
269
+ }
270
+ const button = getButton(instance);
271
+ if (button) {
272
+ button.dataset.active = 'false';
273
+ button.setAttribute('aria-expanded', 'false');
274
+ }
275
+ clearStatus(instance);
276
+ }
277
+
278
+ function isVisible(target) {
279
+ const instance = target?._instance || target;
280
+ // Visibility is governed by the panel root's data-view attribute. The
281
+ // settings surface counts as visible for both the settings screen and the
282
+ // skills sub-page, so the gear toggle can close either back to the session.
283
+ const panelRoot = instance?.container?.closest?.('[data-view]') ||
284
+ instance?.container?.ownerDocument?.querySelector?.('[data-view]');
285
+ if (panelRoot) {
286
+ return panelRoot.dataset.view === 'settings' || panelRoot.dataset.view === 'skills';
287
+ }
288
+ // Fallback: treat as not visible if we cannot read the view state.
289
+ return false;
290
+ }
291
+
292
+ function loadState(target, state = {}) {
293
+ const instance = target?._instance || target;
294
+ const root = getRoot(instance);
295
+ if (!root) {
296
+ return;
297
+ }
298
+ if (Object.prototype.hasOwnProperty.call(state, 'customInstructions')) {
299
+ const input = root.querySelector('[data-custom-instructions-input]');
300
+ if (input) {
301
+ input.value = state.customInstructions || '';
302
+ }
303
+ }
304
+ if (Object.prototype.hasOwnProperty.call(state, 'governanceRules')) {
305
+ const rules = state.governanceRules || {};
306
+ setValue(root, '[data-governance-readonly-patterns]', (rules.readonlyPatterns || []).join('\n'));
307
+ setValue(root, '[data-governance-writable-patterns]', (rules.writablePatterns || []).join('\n'));
308
+ setChecked(root, '[data-sensitive-check-enabled]', rules.sensitiveCheckEnabled !== false);
309
+ setChecked(root, '[data-sensitive-confirm-allowed]', rules.sensitiveConfirmAllowed === true);
310
+ updateGovernanceNotes(instance);
311
+ }
312
+ if (Object.prototype.hasOwnProperty.call(state, 'skillToggles')) {
313
+ // The local-skills switch lives on the settings screen; the Codex Overleaf
314
+ // master switch lives on the skills screen. Both are reachable from the
315
+ // shared container, so query that instead of a single screen root.
316
+ const scope = instance?.container || root;
317
+ const toggles = state.skillToggles || {};
318
+ setChecked(scope, '[data-load-codex-local-skills]', toggles.loadCodexLocalSkills !== false);
319
+ setChecked(scope, '[data-load-codex-overleaf-skills]', toggles.loadCodexOverleafSkills !== false);
320
+ setValue(scope, '[data-theme-select]', state.theme || 'dark');
321
+ setValue(scope, '[data-language-select]', state.language || 'en');
322
+ }
323
+ }
324
+
325
+ function readState(target) {
326
+ const instance = target?._instance || target;
327
+ const root = getRoot(instance);
328
+ // Skill toggles span two screens, so read them from the shared container.
329
+ const scope = instance?.container || root;
330
+ return {
331
+ customInstructions: root?.querySelector('[data-custom-instructions-input]')?.value || '',
332
+ governanceRules: {
333
+ readonlyPatterns: readPatternList(root?.querySelector('[data-governance-readonly-patterns]')?.value),
334
+ writablePatterns: readPatternList(root?.querySelector('[data-governance-writable-patterns]')?.value),
335
+ sensitiveCheckEnabled: root?.querySelector('[data-sensitive-check-enabled]')?.checked !== false,
336
+ sensitiveConfirmAllowed: root?.querySelector('[data-sensitive-confirm-allowed]')?.checked === true
337
+ },
338
+ skillToggles: {
339
+ loadCodexLocalSkills: scope?.querySelector('[data-load-codex-local-skills]')?.checked !== false,
340
+ loadCodexOverleafSkills: scope?.querySelector('[data-load-codex-overleaf-skills]')?.checked !== false
341
+ },
342
+ theme: scope?.querySelector('[data-theme-select]')?.value || 'dark',
343
+ language: scope?.querySelector('[data-language-select]')?.value || 'en'
344
+ };
345
+ }
346
+
347
+ function setStatus(target, text, status = 'info') {
348
+ const instance = target?._instance || target;
349
+ const element = getRoot(instance)?.querySelector('[data-project-settings-status]');
350
+ if (!element) {
351
+ return;
352
+ }
353
+ element.textContent = text || '';
354
+ element.dataset.status = status;
355
+ }
356
+
357
+ function clearStatus(target) {
358
+ const instance = target?._instance || target;
359
+ const element = getRoot(instance)?.querySelector('[data-project-settings-status]');
360
+ if (!element) {
361
+ return;
362
+ }
363
+ element.textContent = '';
364
+ delete element.dataset.status;
365
+ }
366
+
367
+ function setSkillsSummary(target, text) {
368
+ const instance = target?._instance || target;
369
+ const element = instance?.container?.querySelector('[data-skills-entry-summary]');
370
+ if (!element) {
371
+ return;
372
+ }
373
+ element.textContent = text || '';
374
+ }
375
+
376
+ // Flash the "✓ Saved" indicator after a field auto-saves, then fade it out.
377
+ // The fields persist immediately on change, so this is purely a confirmation
378
+ // cue — it never gates the save itself.
379
+ function flashSaved(instance, event) {
380
+ // Card-level feedback: the ✓ appears on the card that was actually
381
+ // changed (falls back to the screen's first badge, e.g. the skills page).
382
+ const origin = event?.target?.closest?.('details.codex-set-group, section');
383
+ const badge = origin?.querySelector?.('[data-set-saved]')
384
+ || instance?.container?.querySelector('[data-set-saved]');
385
+ if (!badge) {
386
+ return;
387
+ }
388
+ badge.hidden = false;
389
+ if (badge._savedFlashTimer) {
390
+ clearTimeout(badge._savedFlashTimer);
391
+ }
392
+ badge._savedFlashTimer = setTimeout(() => {
393
+ badge.hidden = true;
394
+ }, 1600);
395
+ }
396
+
397
+ function t(instance, key, params) {
398
+ if (typeof instance?.i18n?.tr === 'function') {
399
+ return instance.i18n.tr(key, params);
400
+ }
401
+ return key;
402
+ }
403
+
404
+ // Guardrail analysis for the protection globs: warn (never block) when a
405
+ // read-only rule matches everything. Broadness is probed against the REAL
406
+ // enforcement matcher (governanceRules.matchesGovernancePattern) so the
407
+ // warning can neither cry wolf on inert patterns like './**' (which the
408
+ // engine never matches) nor miss genuinely-total ones like '***'. Falls
409
+ // back to a literal check when the module is absent (test harnesses).
410
+ const BROAD_PROBE_PATHS = ['main.tex', 'sections/deep/chapter.tex'];
411
+
412
+ function patternMatchesEverything(pattern) {
413
+ const rules = (typeof window !== 'undefined' ? window : globalThis)?.CodexOverleafGovernanceRules;
414
+ if (typeof rules?.matchesGovernancePattern === 'function') {
415
+ return BROAD_PROBE_PATHS.every(path => rules.matchesGovernancePattern(path, pattern));
416
+ }
417
+ return pattern === '**' || pattern === '*';
418
+ }
419
+
420
+ function analyzePatternList(value) {
421
+ const lines = String(value || '').split('\n').map(line => line.trim()).filter(Boolean);
422
+ return {
423
+ count: lines.length,
424
+ broad: lines.find(line => patternMatchesEverything(line)) || ''
425
+ };
426
+ }
427
+
428
+ function updateGovernanceNotes(instance) {
429
+ const root = instance?.container;
430
+ if (!root) {
431
+ return;
432
+ }
433
+ const readonlyNote = root.querySelector('[data-governance-readonly-note]');
434
+ const writableNote = root.querySelector('[data-governance-writable-note]');
435
+ const readonly = analyzePatternList(root.querySelector('[data-governance-readonly-patterns]')?.value);
436
+ const writable = analyzePatternList(root.querySelector('[data-governance-writable-patterns]')?.value);
437
+ if (readonlyNote) {
438
+ if (readonly.broad) {
439
+ readonlyNote.dataset.tone = 'warn';
440
+ readonlyNote.textContent = t(instance, 'governanceBroadReadonlyWarning', { pattern: readonly.broad });
441
+ readonlyNote.hidden = false;
442
+ } else {
443
+ readonlyNote.hidden = true;
444
+ }
445
+ }
446
+ if (writableNote) {
447
+ if (writable.count > 0) {
448
+ writableNote.dataset.tone = 'info';
449
+ writableNote.textContent = t(instance, 'governanceWritableAllowlistNote');
450
+ writableNote.hidden = false;
451
+ } else {
452
+ writableNote.hidden = true;
453
+ }
454
+ }
455
+ }
456
+
457
+ // Collapse memory: card open/closed state persists per browser profile.
458
+ const GROUP_STORE_KEY = 'codexOverleafSettingsGroups';
459
+
460
+ function setupGroupPersistence(instance) {
461
+ let stored = {};
462
+ try {
463
+ stored = JSON.parse(globalThis.localStorage?.getItem(GROUP_STORE_KEY) || '{}') || {};
464
+ } catch (_error) {
465
+ stored = {};
466
+ }
467
+ for (const details of instance.container.querySelectorAll('details[data-set-group]')) {
468
+ const key = details.dataset.setGroup;
469
+ if (key in stored) {
470
+ details.open = stored[key] !== false;
471
+ }
472
+ details.addEventListener('toggle', () => {
473
+ try {
474
+ const current = JSON.parse(globalThis.localStorage?.getItem(GROUP_STORE_KEY) || '{}') || {};
475
+ current[key] = details.open;
476
+ globalThis.localStorage?.setItem(GROUP_STORE_KEY, JSON.stringify(current));
477
+ } catch (_error) { /* private mode — collapse state just doesn't persist */ }
478
+ });
479
+ }
480
+ }
481
+
482
+ function getRoot(instance) {
483
+ return instance?.container?.querySelector('[data-project-settings-panel]') || null;
484
+ }
485
+
486
+ function getButton(instance) {
487
+ return instance?.button || document.querySelector('[data-custom-instructions-settings]');
488
+ }
489
+
490
+ function setValue(root, selector, value) {
491
+ const element = root.querySelector(selector);
492
+ if (element) {
493
+ element.value = value;
494
+ }
495
+ }
496
+
497
+ function setChecked(root, selector, checked) {
498
+ const element = root.querySelector(selector);
499
+ if (element) {
500
+ element.checked = checked;
501
+ }
502
+ }
503
+
504
+ function readPatternList(value) {
505
+ return String(value || '').split(/\r?\n/).map(item => item.trim()).filter(Boolean);
506
+ }
507
+
508
+ function destroy(instance) {
509
+ if (instance._savedFlashTimer) {
510
+ clearTimeout(instance._savedFlashTimer);
511
+ instance._savedFlashTimer = null;
512
+ }
513
+ instance.container.textContent = '';
514
+ }
515
+
516
+ window.CodexOverleafSettingsPanel = {
517
+ create,
518
+ loadState,
519
+ readState,
520
+ show,
521
+ hide,
522
+ isVisible,
523
+ setStatus,
524
+ clearStatus,
525
+ setSkillsSummary
526
+ };
527
+ })();
@@ -0,0 +1,91 @@
1
+ (function initCodexOverleafTheme(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafTheme = factory();
6
+ }
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function themeFactory() {
8
+ 'use strict';
9
+
10
+ // The user-selectable theme preferences. 'dark' is the historical default and
11
+ // the CSS baseline (no [data-theme="light"] override applies to it); 'auto'
12
+ // follows the operating system via prefers-color-scheme.
13
+ const THEMES = ['dark', 'light', 'auto'];
14
+ const DEFAULT_THEME = 'dark';
15
+ const RESOLVED_DARK = 'dark';
16
+ const RESOLVED_LIGHT = 'light';
17
+
18
+ // Coerce an arbitrary stored value (possibly undefined or stale) to a valid
19
+ // preference, falling back to the dark default.
20
+ function normalizeThemePreference(value) {
21
+ return THEMES.indexOf(value) !== -1 ? value : DEFAULT_THEME;
22
+ }
23
+
24
+ // The matchMedia list for "OS prefers light", or null when matchMedia is
25
+ // unavailable (e.g. the Node test environment).
26
+ function prefersLightMedia() {
27
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
28
+ return null;
29
+ }
30
+ return window.matchMedia('(prefers-color-scheme: light)');
31
+ }
32
+
33
+ // Resolve a preference to a concrete applied theme ('dark' | 'light'). 'auto'
34
+ // consults the OS; the optional mediaQueryList lets tests inject a result
35
+ // without a real matchMedia. When the OS preference can't be read, auto falls
36
+ // back to dark (the safe default that needs no override block).
37
+ function resolveTheme(preference, mediaQueryList) {
38
+ const pref = normalizeThemePreference(preference);
39
+ if (pref !== 'auto') {
40
+ return pref;
41
+ }
42
+ const mql = mediaQueryList !== undefined ? mediaQueryList : prefersLightMedia();
43
+ return mql && mql.matches ? RESOLVED_LIGHT : RESOLVED_DARK;
44
+ }
45
+
46
+ // Write the resolved theme onto the panel root's data-theme attribute. Dark is
47
+ // the CSS baseline, but we always set the resolved value explicitly so the
48
+ // attribute is a faithful mirror of what is showing (and so auto can flip it
49
+ // live). Returns the resolved theme.
50
+ function applyTheme(preference, panelRoot, mediaQueryList) {
51
+ const resolved = resolveTheme(preference, mediaQueryList);
52
+ if (panelRoot && typeof panelRoot.setAttribute === 'function') {
53
+ panelRoot.setAttribute('data-theme', resolved);
54
+ }
55
+ return resolved;
56
+ }
57
+
58
+ // While the preference is 'auto', re-apply whenever the OS light/dark setting
59
+ // changes. Returns a disposer that detaches the listener. For non-auto
60
+ // preferences (or when matchMedia is unavailable) it attaches nothing and
61
+ // returns a no-op disposer, so callers can unconditionally dispose+rewatch on
62
+ // every preference change.
63
+ function watchAuto(preference, panelRoot, onChange) {
64
+ if (normalizeThemePreference(preference) !== 'auto') {
65
+ return function noopDisposer() {};
66
+ }
67
+ const mql = prefersLightMedia();
68
+ if (!mql || typeof mql.addEventListener !== 'function') {
69
+ return function noopDisposer() {};
70
+ }
71
+ const handler = function handleSchemeChange() {
72
+ const resolved = applyTheme('auto', panelRoot, mql);
73
+ if (typeof onChange === 'function') {
74
+ onChange(resolved);
75
+ }
76
+ };
77
+ mql.addEventListener('change', handler);
78
+ return function disposeWatcher() {
79
+ mql.removeEventListener('change', handler);
80
+ };
81
+ }
82
+
83
+ return {
84
+ THEMES,
85
+ DEFAULT_THEME,
86
+ normalizeThemePreference,
87
+ resolveTheme,
88
+ applyTheme,
89
+ watchAuto
90
+ };
91
+ });