codex-overleaf-link 1.9.10 → 1.9.12

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.
@@ -0,0 +1,218 @@
1
+ (function initCodexOverleafSessionMenuView() {
2
+ 'use strict';
3
+
4
+ function create(deps = {}) {
5
+ const {
6
+ commitSessionRename,
7
+ deleteSessionWithConfirm,
8
+ document,
9
+ formatSessionTime,
10
+ getActiveSession,
11
+ getPanel,
12
+ getSessionDisplayTitle,
13
+ getState,
14
+ isDisplayableSession,
15
+ isSessionRunning,
16
+ startNewSession,
17
+ switchSession,
18
+ tr
19
+ } = deps;
20
+
21
+ function applySessionLabel() {
22
+ 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
+ const textNode = getPanel().querySelector('[data-session-label-text]');
28
+ const deleteButton = getPanel().querySelector('[data-session-delete]');
29
+ const renameButton = getPanel().querySelector('[data-session-rename]');
30
+ const renameInput = getPanel().querySelector('[data-session-rename-input]');
31
+ const active = getActiveSession(getState());
32
+ if (textNode) {
33
+ textNode.textContent = active && isDisplayableSession(active)
34
+ ? getSessionDisplayTitle(active)
35
+ : tr('newSessionFallback');
36
+ }
37
+ if (deleteButton) {
38
+ const running = active ? isSessionRunning(active) : false;
39
+ const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (getState()?.sessions || []).length <= 1;
40
+ deleteButton.disabled = !active || running || soleEmpty;
41
+ deleteButton.title = running ? tr('deleteRunningSession') : tr('deleteSession');
42
+ deleteButton.setAttribute('aria-label', running ? tr('deleteRunningSession') : tr('deleteSession'));
43
+ }
44
+ if (renameButton) {
45
+ renameButton.title = tr('renameSession');
46
+ renameButton.setAttribute('aria-label', tr('renameSession'));
47
+ }
48
+ renameInput?.setAttribute('aria-label', tr('renameSession'));
49
+ }
50
+
51
+ function bindActiveSessionHeader() {
52
+ getPanel().querySelector('[data-session-delete]')?.addEventListener('click', event => {
53
+ event.stopPropagation();
54
+ if (getState()?.activeSessionId) {
55
+ deleteSessionWithConfirm(getState().activeSessionId);
56
+ }
57
+ });
58
+ getPanel().querySelector('[data-session-rename]')?.addEventListener('click', event => {
59
+ event.stopPropagation();
60
+ beginActiveSessionRename();
61
+ });
62
+ getPanel().querySelector('[data-session-menu-trigger]')?.addEventListener('click', event => {
63
+ event.stopPropagation();
64
+ toggleSessionMenu();
65
+ });
66
+ document.addEventListener('click', event => {
67
+ const menu = getPanel()?.querySelector('[data-session-menu]');
68
+ if (!menu || menu.hidden) {
69
+ return;
70
+ }
71
+ const trigger = getPanel()?.querySelector('[data-session-menu-trigger]');
72
+ if (menu.contains(event.target) || trigger?.contains(event.target)) {
73
+ return;
74
+ }
75
+ closeSessionMenu();
76
+ }, true);
77
+ document.addEventListener('keydown', event => {
78
+ if (event.key === 'Escape') {
79
+ closeSessionMenu();
80
+ }
81
+ }, true);
82
+ }
83
+
84
+ function toggleSessionMenu() {
85
+ const menu = getPanel()?.querySelector('[data-session-menu]');
86
+ if (!menu) {
87
+ return;
88
+ }
89
+ if (menu.hidden) {
90
+ renderSessionMenu(menu);
91
+ }
92
+ menu.hidden = !menu.hidden;
93
+ getPanel()?.querySelector('[data-session-menu-trigger]')
94
+ ?.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
95
+ }
96
+
97
+ function closeSessionMenu() {
98
+ const menu = getPanel()?.querySelector('[data-session-menu]');
99
+ if (!menu || menu.hidden) {
100
+ return;
101
+ }
102
+ menu.hidden = true;
103
+ getPanel()?.querySelector('[data-session-menu-trigger]')?.setAttribute('aria-expanded', 'false');
104
+ }
105
+
106
+ function renderSessionMenu(menu) {
107
+ menu.replaceChildren();
108
+ const state = getState();
109
+ const sessions = (state?.sessions || [])
110
+ .filter(isDisplayableSession)
111
+ .slice()
112
+ .sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
113
+ for (const session of sessions) {
114
+ const row = document.createElement('button');
115
+ row.type = 'button';
116
+ row.className = 'codex-session-menu-item';
117
+ row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
118
+ row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
119
+ const title = document.createElement('span');
120
+ title.className = 'codex-session-menu-item-title';
121
+ title.textContent = getSessionDisplayTitle(session);
122
+ title.title = getSessionDisplayTitle(session);
123
+ const time = document.createElement('time');
124
+ time.className = 'codex-session-menu-item-time';
125
+ time.textContent = formatSessionTime(session.updatedAt || session.createdAt);
126
+ const absoluteTs = session.updatedAt || session.createdAt;
127
+ if (absoluteTs) {
128
+ time.title = new Date(absoluteTs).toLocaleString();
129
+ }
130
+ row.append(title, time);
131
+ row.addEventListener('click', event => {
132
+ event.stopPropagation();
133
+ closeSessionMenu();
134
+ if (session.id !== getState()?.activeSessionId) {
135
+ switchSession(session.id);
136
+ }
137
+ });
138
+ menu.append(row);
139
+ }
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 => {
145
+ event.stopPropagation();
146
+ closeSessionMenu();
147
+ startNewSession();
148
+ });
149
+ menu.append(createButton);
150
+ }
151
+
152
+ function beginActiveSessionRename() {
153
+ const active = getActiveSession(getState());
154
+ const wrap = getPanel().querySelector('[data-session-label]');
155
+ const trigger = getPanel().querySelector('[data-session-menu-trigger]');
156
+ const textNode = getPanel().querySelector('[data-session-label-text]');
157
+ const input = getPanel().querySelector('[data-session-rename-input]');
158
+ if (!active || !wrap || !textNode || !input) {
159
+ return;
160
+ }
161
+ closeSessionMenu();
162
+ let settled = false;
163
+ wrap.dataset.editing = 'true';
164
+ const seed = isDisplayableSession(active) ? getSessionDisplayTitle(active) : '';
165
+ input.value = seed;
166
+ input.hidden = false;
167
+ textNode.hidden = true;
168
+ if (trigger) {
169
+ trigger.hidden = true;
170
+ }
171
+ input.focus();
172
+ input.select();
173
+ const cleanup = () => {
174
+ input.removeEventListener('keydown', onKeydown);
175
+ input.removeEventListener('blur', onBlur);
176
+ };
177
+ const finish = commit => {
178
+ if (settled) {
179
+ return;
180
+ }
181
+ settled = true;
182
+ cleanup();
183
+ input.hidden = true;
184
+ textNode.hidden = false;
185
+ if (trigger) {
186
+ trigger.hidden = false;
187
+ }
188
+ delete wrap.dataset.editing;
189
+ if (commit && input.value.trim() !== seed.trim()) {
190
+ commitSessionRename(active.id, input.value);
191
+ }
192
+ };
193
+ const onKeydown = event => {
194
+ if (event.key === 'Enter') {
195
+ event.preventDefault();
196
+ finish(true);
197
+ } else if (event.key === 'Escape') {
198
+ event.preventDefault();
199
+ finish(false);
200
+ }
201
+ };
202
+ const onBlur = () => finish(true);
203
+ input.addEventListener('keydown', onKeydown);
204
+ input.addEventListener('blur', onBlur);
205
+ }
206
+
207
+ return {
208
+ applySessionLabel,
209
+ beginActiveSessionRename,
210
+ bindActiveSessionHeader,
211
+ closeSessionMenu,
212
+ renderSessionMenu,
213
+ toggleSessionMenu
214
+ };
215
+ }
216
+
217
+ window.CodexOverleafSessionMenuView = { create };
218
+ })();
@@ -41,7 +41,7 @@
41
41
  } catch (_error) {
42
42
  saveResult = { ok: false, state: 'unavailable' };
43
43
  }
44
- const saved = saveResult?.ok === true && saveResult?.state === 'verified_saved';
44
+ const saved = saveResult?.ok === true && ['verified_saved', 'verified_quiet'].includes(saveResult?.state);
45
45
  if (saved) {
46
46
  stableSavedSince = stableSavedSince || Date.now();
47
47
  } else {
@@ -0,0 +1,292 @@
1
+ (function initCodexOverleafSaveState(root) {
2
+ 'use strict';
3
+
4
+ function create(deps = {}) {
5
+ const {
6
+ delay,
7
+ detectEditor,
8
+ document,
9
+ getActiveFilePath,
10
+ readActiveEditorText
11
+ } = deps;
12
+ const pageWindow = deps.window || root;
13
+
14
+ async function waitForSaveState(params = {}) {
15
+ const deadlineMs = Number.isFinite(Number(params.deadlineMs)) ? Number(params.deadlineMs) : 5000;
16
+ const pollInterval = Number.isFinite(Number(params.pollIntervalMs)) ? Number(params.pollIntervalMs) : 100;
17
+ const quietFallbackMs = Number.isFinite(Number(params.quietFallbackMs)) ? Number(params.quietFallbackMs) : 1000;
18
+ const deadline = Date.now() + Math.max(0, deadlineMs);
19
+ let lastState = { state: 'unknown', reason: 'Overleaf save state has not been checked yet.' };
20
+ let quietSignature = '';
21
+ let quietSince = 0;
22
+
23
+ do {
24
+ let current;
25
+ try {
26
+ current = getOverleafSaveState();
27
+ } catch (error) {
28
+ return {
29
+ ok: false,
30
+ state: 'unavailable',
31
+ reason: `Could not check Overleaf save state: ${error.message}`
32
+ };
33
+ }
34
+ if (current.state === 'verified_saved') {
35
+ return { ok: true, state: 'verified_saved' };
36
+ }
37
+ if (current.state === 'unavailable') {
38
+ return {
39
+ ok: false,
40
+ state: 'unavailable',
41
+ reason: current.reason || 'Overleaf save state is unavailable.'
42
+ };
43
+ }
44
+ const quietEligible = params.allowQuietEditorFallback === true
45
+ && current.state === 'unknown'
46
+ && /save indicator was not found/i.test(current.reason || '')
47
+ && pageWindow.navigator?.onLine !== false
48
+ && detectEditor()?.ok === true;
49
+ if (quietEligible) {
50
+ const text = String(readActiveEditorText() || '');
51
+ const signature = `${getActiveFilePath()}:${text.length}:${text.slice(0, 80)}:${text.slice(-80)}`;
52
+ if (signature === quietSignature && Date.now() - quietSince >= Math.max(0, quietFallbackMs)) {
53
+ return { ok: true, state: 'verified_quiet', evidence: 'stable_editor_snapshot_without_save_indicator' };
54
+ }
55
+ if (signature !== quietSignature) {
56
+ quietSignature = signature;
57
+ quietSince = Date.now();
58
+ }
59
+ } else {
60
+ quietSignature = '';
61
+ quietSince = 0;
62
+ }
63
+ lastState = current;
64
+ if (Date.now() >= deadline) {
65
+ break;
66
+ }
67
+ await delay(Math.min(Math.max(1, pollInterval), Math.max(1, deadline - Date.now())));
68
+ } while (Date.now() < deadline);
69
+
70
+ return {
71
+ ok: false,
72
+ state: 'unknown_timeout',
73
+ reason: buildSaveStateTimeoutReason(lastState)
74
+ };
75
+ }
76
+
77
+ function getOverleafSaveState() {
78
+ if (!document || typeof document.querySelectorAll !== 'function') {
79
+ return {
80
+ state: 'unavailable',
81
+ reason: 'Overleaf document is unavailable for save-state detection.'
82
+ };
83
+ }
84
+ const selectors = [
85
+ '.toolbar-header [class*="save" i]',
86
+ '[class*="saving-status" i]',
87
+ '[class*="save-status" i]',
88
+ '[data-testid*="save" i]',
89
+ '[aria-label*="save" i]'
90
+ ];
91
+ let sawSaveCandidate = false;
92
+ let sawVerifiedSaved = false;
93
+ let sawNegativeState = false;
94
+ let sawSavingState = false;
95
+ for (const selector of selectors) {
96
+ const elements = document.querySelectorAll(selector);
97
+ for (const el of elements) {
98
+ if (isHiddenSaveIndicatorNode(el)) {
99
+ continue;
100
+ }
101
+ const texts = readSaveIndicatorTexts(el);
102
+ if (!texts.length) {
103
+ continue;
104
+ }
105
+ const combinedText = texts.join(' ');
106
+ if (isAnySaveIndicatorText(texts, combinedText, isNegativeSaveStateText)) {
107
+ sawNegativeState = true;
108
+ sawSaveCandidate = true;
109
+ continue;
110
+ }
111
+ if (isAnySaveIndicatorText(texts, combinedText, isSavingStateText)) {
112
+ sawSavingState = true;
113
+ sawSaveCandidate = true;
114
+ continue;
115
+ }
116
+ if (texts.some(isVerifiedSavedText)) {
117
+ sawVerifiedSaved = true;
118
+ continue;
119
+ }
120
+ if (isAnySaveIndicatorText(texts, combinedText, isSaveIndicatorCandidateText)) {
121
+ sawSaveCandidate = true;
122
+ }
123
+ }
124
+ }
125
+ if (sawNegativeState) {
126
+ return {
127
+ state: 'unknown',
128
+ reason: 'Overleaf save indicator reports changes are not saved.'
129
+ };
130
+ }
131
+ if (sawSavingState) {
132
+ return {
133
+ state: 'saving',
134
+ reason: 'Overleaf is still saving or syncing changes.'
135
+ };
136
+ }
137
+ if (sawVerifiedSaved) {
138
+ return { state: 'verified_saved' };
139
+ }
140
+ return {
141
+ state: 'unknown',
142
+ reason: sawSaveCandidate
143
+ ? 'Overleaf save indicator was present, but did not verify that all changes are saved.'
144
+ : 'Overleaf save indicator was not found.'
145
+ };
146
+ }
147
+
148
+ function isHiddenSaveIndicatorNode(node) {
149
+ if (!node) {
150
+ return true;
151
+ }
152
+ if (node.hidden === true || node.inert === true) {
153
+ return true;
154
+ }
155
+ if (hasSaveIndicatorAttribute(node, 'hidden') || hasSaveIndicatorAttribute(node, 'inert')) {
156
+ return true;
157
+ }
158
+ if (/^true$/i.test(String(node.getAttribute?.('aria-hidden') || ''))) {
159
+ return true;
160
+ }
161
+ if (hasHiddenSaveIndicatorAncestor(node)) {
162
+ return true;
163
+ }
164
+ if (isHiddenStyle(node.style)) {
165
+ return true;
166
+ }
167
+ if (isHiddenStyle(getComputedStyleForSaveIndicator(node))) {
168
+ return true;
169
+ }
170
+ return hasZeroSaveIndicatorLayout(node);
171
+ }
172
+
173
+ function hasHiddenSaveIndicatorAncestor(node) {
174
+ if (typeof node.closest === 'function') {
175
+ try {
176
+ if (node.closest('[hidden],[inert],[aria-hidden="true"]')) {
177
+ return true;
178
+ }
179
+ } catch (_error) {
180
+ // Fall back to walking parentElement below.
181
+ }
182
+ }
183
+ let current = node.parentElement;
184
+ while (current) {
185
+ if (current.hidden === true || current.inert === true) {
186
+ return true;
187
+ }
188
+ if (hasSaveIndicatorAttribute(current, 'hidden') || hasSaveIndicatorAttribute(current, 'inert')) {
189
+ return true;
190
+ }
191
+ if (/^true$/i.test(String(current.getAttribute?.('aria-hidden') || ''))) {
192
+ return true;
193
+ }
194
+ current = current.parentElement;
195
+ }
196
+ return false;
197
+ }
198
+
199
+ function hasSaveIndicatorAttribute(node, attribute) {
200
+ if (typeof node.hasAttribute === 'function') {
201
+ try {
202
+ return node.hasAttribute(attribute);
203
+ } catch (_error) {
204
+ return false;
205
+ }
206
+ }
207
+ const value = node.getAttribute?.(attribute);
208
+ return value != null && value !== '';
209
+ }
210
+
211
+ function isHiddenStyle(style) {
212
+ if (!style) {
213
+ return false;
214
+ }
215
+ return /^none$/i.test(String(style.display || ''))
216
+ || /^(hidden|collapse)$/i.test(String(style.visibility || ''));
217
+ }
218
+
219
+ function getComputedStyleForSaveIndicator(node) {
220
+ try {
221
+ const ownerWindow = node.ownerDocument?.defaultView || null;
222
+ return (ownerWindow || pageWindow)?.getComputedStyle?.(node) || null;
223
+ } catch (_error) {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ function hasZeroSaveIndicatorLayout(node) {
229
+ if (typeof node.getClientRects !== 'function') {
230
+ return false;
231
+ }
232
+ try {
233
+ const rects = node.getClientRects();
234
+ if (rects && rects.length > 0) {
235
+ return false;
236
+ }
237
+ return node.offsetWidth === 0 && node.offsetHeight === 0;
238
+ } catch (_error) {
239
+ return false;
240
+ }
241
+ }
242
+
243
+ function readSaveIndicatorTexts(node) {
244
+ const parts = [
245
+ node.textContent,
246
+ node.innerText,
247
+ node.getAttribute?.('aria-label'),
248
+ node.getAttribute?.('title'),
249
+ node.getAttribute?.('value')
250
+ ].map(value => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
251
+ return Array.from(new Set(parts));
252
+ }
253
+
254
+ function isAnySaveIndicatorText(texts, combinedText, predicate) {
255
+ return predicate(combinedText) || texts.some(predicate);
256
+ }
257
+
258
+ function isNegativeSaveStateText(text) {
259
+ return /\bnot\b[\s\S]{0,40}\bsaved\b|\b(?:unsaved|save failed|failed to save|could not save|unable to save)\b|未保存|保存失败/i.test(text);
260
+ }
261
+
262
+ function isSavingStateText(text) {
263
+ return /\b(saving|syncing|compiling)\b|保存中|正在保存|同步中/i.test(text);
264
+ }
265
+
266
+ function isVerifiedSavedText(text) {
267
+ if (isNegativeSaveStateText(text)) {
268
+ return false;
269
+ }
270
+ return /^\s*(?:all changes saved|changes saved|saved)[\s.!]*$/i.test(text)
271
+ || /已保存/i.test(text);
272
+ }
273
+
274
+ function isSaveIndicatorCandidateText(text) {
275
+ return /\bsave\b|\bsaved\b|\bsaving\b|保存/i.test(text);
276
+ }
277
+
278
+ function buildSaveStateTimeoutReason(lastState = {}) {
279
+ if (lastState.state === 'saving') {
280
+ return `Timed out waiting for Overleaf to finish saving changes. ${lastState.reason || ''}`.trim();
281
+ }
282
+ return `Timed out waiting for a verified Overleaf save state. ${lastState.reason || ''}`.trim();
283
+ }
284
+
285
+ return {
286
+ getOverleafSaveState,
287
+ waitForSaveState
288
+ };
289
+ }
290
+
291
+ root.CodexOverleafSaveState = { create };
292
+ })(typeof window !== 'undefined' ? window : globalThis);