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.
- package/README.md +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/runtime-manifest.json +5 -3
- package/extension/src/content/contentRuntime.js +61 -326
- package/extension/src/content/projectSettingsCoordinator.js +365 -0
- package/extension/src/content/sessionManager.js +20 -204
- package/extension/src/content/sessionMenuView.js +218 -0
- package/extension/src/content/updateIdle.js +1 -1
- package/extension/src/page/saveState.js +292 -0
- package/extension/src/pageBridge.js +10 -252
- package/extension/src/shared/compatibility.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
(function initCodexOverleafProjectSettingsCoordinator() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function create(deps = {}) {
|
|
5
|
+
const {
|
|
6
|
+
CodexOverleafTheme,
|
|
7
|
+
GovernanceRules,
|
|
8
|
+
SettingsPanel,
|
|
9
|
+
closeContextTray,
|
|
10
|
+
closeDiagnosticsMenu,
|
|
11
|
+
closeDiagnosticsResult,
|
|
12
|
+
closeModelConfigPopover,
|
|
13
|
+
closeSlashMenu,
|
|
14
|
+
getCurrentProjectId,
|
|
15
|
+
getLocalSkillsPanel,
|
|
16
|
+
getLocale,
|
|
17
|
+
getPanel,
|
|
18
|
+
getPanelRendererInstance,
|
|
19
|
+
getSettingsPanelInstance,
|
|
20
|
+
getState,
|
|
21
|
+
isProjectEditorRoute,
|
|
22
|
+
refreshStorageUsageSummary,
|
|
23
|
+
renderAuditHistoryPanel,
|
|
24
|
+
renderRecentProjectsVariant,
|
|
25
|
+
saveStateSoon,
|
|
26
|
+
setState,
|
|
27
|
+
tr,
|
|
28
|
+
tx,
|
|
29
|
+
window
|
|
30
|
+
} = deps;
|
|
31
|
+
let customInstructionsEditorProjectId = '';
|
|
32
|
+
let customInstructionsEditorValue = '';
|
|
33
|
+
let themeAutoDisposer = null;
|
|
34
|
+
|
|
35
|
+
function normalizeCustomInstructionsByProject(value) {
|
|
36
|
+
const result = {};
|
|
37
|
+
const textMaxChars = 12000;
|
|
38
|
+
const keyMaxChars = 160;
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
for (const rawKey of Object.keys(value)) {
|
|
43
|
+
const key = typeof rawKey === 'string' ? rawKey.trim() : '';
|
|
44
|
+
if (!key) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const normalizedKey = key.length <= keyMaxChars ? key : key.slice(0, keyMaxChars - 1) + '…';
|
|
48
|
+
const rawText = typeof value[rawKey] === 'string' ? value[rawKey] : '';
|
|
49
|
+
result[normalizedKey] = rawText.length <= textMaxChars
|
|
50
|
+
? rawText
|
|
51
|
+
: rawText.slice(0, textMaxChars - 1) + '…';
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getCustomInstructionsForCurrentProject() {
|
|
57
|
+
const projectId = getCurrentProjectId();
|
|
58
|
+
const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: '' });
|
|
59
|
+
const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
|
|
60
|
+
if (!normalizedProjectId) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
return normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject)[normalizedProjectId] || '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setCustomInstructionsForProject(projectId, value) {
|
|
67
|
+
const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: value });
|
|
68
|
+
const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
|
|
69
|
+
if (!normalizedProjectId) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setState({
|
|
73
|
+
...getState(),
|
|
74
|
+
customInstructionsByProject: {
|
|
75
|
+
...normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject),
|
|
76
|
+
[normalizedProjectId]: normalizedProject[normalizedProjectId]
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openCustomInstructionsSettings() {
|
|
82
|
+
const settingsPanelInstance = getSettingsPanelInstance();
|
|
83
|
+
if (!settingsPanelInstance) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
closeDiagnosticsMenu();
|
|
87
|
+
closeDiagnosticsResult();
|
|
88
|
+
closeModelConfigPopover();
|
|
89
|
+
closeContextTray();
|
|
90
|
+
closeSlashMenu();
|
|
91
|
+
clearProjectSettingsStatus();
|
|
92
|
+
syncCustomInstructionsEditorForProject(getCurrentProjectId(), { force: true });
|
|
93
|
+
syncProjectSettingsEditorForProject();
|
|
94
|
+
getPanelRendererInstance()?.setView?.('settings');
|
|
95
|
+
SettingsPanel.show(settingsPanelInstance);
|
|
96
|
+
refreshStorageUsageSummary();
|
|
97
|
+
if (settingsPanelInstance.container?.querySelector('[data-history-card]')?.open) {
|
|
98
|
+
renderAuditHistoryPanel();
|
|
99
|
+
}
|
|
100
|
+
refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
|
|
101
|
+
`Could not list local skills: ${error.message}`,
|
|
102
|
+
`无法列出本地技能:${error.message}`
|
|
103
|
+
), 'failed'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toggleCustomInstructionsSettings() {
|
|
107
|
+
if (SettingsPanel.isVisible(getSettingsPanelInstance())) {
|
|
108
|
+
closeCustomInstructionsSettings();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
openCustomInstructionsSettings();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function closeCustomInstructionsSettings() {
|
|
115
|
+
SettingsPanel.hide(getSettingsPanelInstance());
|
|
116
|
+
if (isProjectEditorRoute(window.location)) {
|
|
117
|
+
getPanelRendererInstance()?.setView?.('session');
|
|
118
|
+
} else {
|
|
119
|
+
renderRecentProjectsVariant().catch(() => { /* swallow */ });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function openSkillsView() {
|
|
124
|
+
if (!getSettingsPanelInstance()) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
getPanelRendererInstance()?.setView?.('skills');
|
|
128
|
+
refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
|
|
129
|
+
`Could not list local skills: ${error.message}`,
|
|
130
|
+
`无法列出本地技能:${error.message}`
|
|
131
|
+
), 'failed'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function closeSkillsView() {
|
|
135
|
+
getPanelRendererInstance()?.setView?.('settings');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function updateSkillsEntrySummary() {
|
|
139
|
+
const settingsPanelInstance = getSettingsPanelInstance();
|
|
140
|
+
if (!settingsPanelInstance) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const summary = getSkillLoadingSettings().loadCodexOverleafSkills === false
|
|
144
|
+
? tr('codexOverleafSkillsSummaryOff')
|
|
145
|
+
: tr('codexOverleafSkillsSummaryCount', { count: countEnabledCodexOverleafSkills() });
|
|
146
|
+
SettingsPanel.setSkillsSummary(settingsPanelInstance, summary);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function countEnabledCodexOverleafSkills() {
|
|
150
|
+
const skills = Array.isArray(getState()?.codexOverleafSkills) ? getState().codexOverleafSkills : [];
|
|
151
|
+
return skills.reduce((total, skill) => {
|
|
152
|
+
const id = String(skill?.id || '').trim();
|
|
153
|
+
return id && isCodexOverleafSkillEnabled(id) ? total + 1 : total;
|
|
154
|
+
}, 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function syncCustomInstructionsEditorForProject(projectId = getCurrentProjectId(), options) {
|
|
158
|
+
const syncOptions = options || {};
|
|
159
|
+
const panel = getPanel();
|
|
160
|
+
const input = panel?.querySelector('[data-custom-instructions-input]');
|
|
161
|
+
if (!input) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const normalizedProject = normalizeCustomInstructionsByProject({ [projectId]: '' });
|
|
165
|
+
const normalizedProjectId = Object.keys(normalizedProject)[0] || '';
|
|
166
|
+
const storedValue = normalizedProjectId
|
|
167
|
+
? normalizeCustomInstructionsByProject(getState()?.customInstructionsByProject)[normalizedProjectId] || ''
|
|
168
|
+
: '';
|
|
169
|
+
input.placeholder = tr('customInstructionsPlaceholder');
|
|
170
|
+
const editorIsOpen = panel?.dataset?.view === 'settings' || panel?.dataset?.view === 'skills';
|
|
171
|
+
const editorIsDirty = normalizedProjectId
|
|
172
|
+
&& customInstructionsEditorProjectId === normalizedProjectId
|
|
173
|
+
&& input.value !== customInstructionsEditorValue;
|
|
174
|
+
if (!syncOptions.force && editorIsOpen && editorIsDirty) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
input.value = storedValue;
|
|
178
|
+
customInstructionsEditorProjectId = normalizedProjectId;
|
|
179
|
+
customInstructionsEditorValue = storedValue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeGovernanceRulesByProject(value) {
|
|
183
|
+
const result = {};
|
|
184
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
for (const rawKey of Object.keys(value)) {
|
|
188
|
+
const key = normalizeProjectPreferenceKey(rawKey);
|
|
189
|
+
if (!key) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
result[key] = normalizeGovernanceRules(value[rawKey]);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeProjectPreferenceKey(value) {
|
|
198
|
+
const text = String(value || '').trim();
|
|
199
|
+
return text.length <= 160 ? text : text.slice(0, 159) + '…';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeGovernanceRules(value = {}) {
|
|
203
|
+
if (GovernanceRules?.normalizeGovernanceRules) {
|
|
204
|
+
return GovernanceRules.normalizeGovernanceRules(value);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
readonlyPatterns: normalizePatternTextList(value.readonlyPatterns),
|
|
208
|
+
writablePatterns: normalizePatternTextList(value.writablePatterns),
|
|
209
|
+
sensitiveCheckEnabled: value.sensitiveCheckEnabled !== false,
|
|
210
|
+
sensitiveConfirmAllowed: value.sensitiveConfirmAllowed === true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizePatternTextList(value) {
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
return value.map(item => String(item || '').trim()).filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
return String(value || '').split(/\r?\n/).map(item => item.trim()).filter(Boolean);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getGovernanceRulesForCurrentProject() {
|
|
222
|
+
const projectId = normalizeProjectPreferenceKey(getCurrentProjectId());
|
|
223
|
+
return normalizeGovernanceRulesByProject(getState()?.governanceRulesByProject)[projectId]
|
|
224
|
+
|| normalizeGovernanceRules({});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setGovernanceRulesForCurrentProject(rules) {
|
|
228
|
+
const projectId = normalizeProjectPreferenceKey(getCurrentProjectId());
|
|
229
|
+
if (!projectId) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setState({
|
|
233
|
+
...getState(),
|
|
234
|
+
governanceRulesByProject: {
|
|
235
|
+
...normalizeGovernanceRulesByProject(getState()?.governanceRulesByProject),
|
|
236
|
+
[projectId]: normalizeGovernanceRules(rules)
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getThemePreference() {
|
|
242
|
+
return CodexOverleafTheme.normalizeThemePreference(getState()?.theme);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function applyPanelTheme(preference) {
|
|
246
|
+
const normalized = CodexOverleafTheme.normalizeThemePreference(preference);
|
|
247
|
+
CodexOverleafTheme.applyTheme(normalized, getPanel());
|
|
248
|
+
if (themeAutoDisposer) {
|
|
249
|
+
themeAutoDisposer();
|
|
250
|
+
themeAutoDisposer = null;
|
|
251
|
+
}
|
|
252
|
+
themeAutoDisposer = CodexOverleafTheme.watchAuto(normalized, getPanel());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getSkillLoadingSettings() {
|
|
256
|
+
return {
|
|
257
|
+
loadCodexLocalSkills: getState()?.loadCodexLocalSkills !== false,
|
|
258
|
+
loadCodexOverleafSkills: getState()?.loadCodexOverleafSkills !== false
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function setSkillLoadingSettings(settings = {}) {
|
|
263
|
+
setState({
|
|
264
|
+
...getState(),
|
|
265
|
+
loadCodexLocalSkills: settings.loadCodexLocalSkills !== false,
|
|
266
|
+
loadCodexOverleafSkills: settings.loadCodexOverleafSkills !== false
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getCodexOverleafSkillEnabled() {
|
|
271
|
+
const map = getState()?.codexOverleafSkillEnabled;
|
|
272
|
+
return map && typeof map === 'object' && !Array.isArray(map) ? map : {};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isCodexOverleafSkillEnabled(skillId) {
|
|
276
|
+
const map = getCodexOverleafSkillEnabled();
|
|
277
|
+
if (!Object.prototype.hasOwnProperty.call(map, skillId)) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return map[skillId] !== false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function setCodexOverleafSkillEnabled(skillId, enabled) {
|
|
284
|
+
const map = getCodexOverleafSkillEnabled();
|
|
285
|
+
setState({
|
|
286
|
+
...getState(),
|
|
287
|
+
codexOverleafSkillEnabled: {
|
|
288
|
+
...map,
|
|
289
|
+
[skillId]: Boolean(enabled)
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
saveStateSoon();
|
|
293
|
+
renderLocalSkillList();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readSkillLoadingSettingsFromSettings() {
|
|
297
|
+
return SettingsPanel.readState(getSettingsPanelInstance()).skillToggles;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readGovernanceRulesFromSettings() {
|
|
301
|
+
return normalizeGovernanceRules(SettingsPanel.readState(getSettingsPanelInstance()).governanceRules);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function syncProjectSettingsEditorForProject() {
|
|
305
|
+
SettingsPanel.loadState(getSettingsPanelInstance(), {
|
|
306
|
+
governanceRules: getGovernanceRulesForCurrentProject(),
|
|
307
|
+
skillToggles: getSkillLoadingSettings(),
|
|
308
|
+
theme: getThemePreference(),
|
|
309
|
+
language: getLocale()
|
|
310
|
+
});
|
|
311
|
+
renderLocalSkillList();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setProjectSettingsStatus(text, status = 'info') {
|
|
315
|
+
SettingsPanel.setStatus(getSettingsPanelInstance(), text, status);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function clearProjectSettingsStatus() {
|
|
319
|
+
SettingsPanel.clearStatus(getSettingsPanelInstance());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function refreshLocalSkills() {
|
|
323
|
+
return getLocalSkillsPanel().refreshLocalSkills();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderLocalSkillList() {
|
|
327
|
+
getLocalSkillsPanel().renderLocalSkillList();
|
|
328
|
+
updateSkillsEntrySummary();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
applyPanelTheme,
|
|
333
|
+
clearProjectSettingsStatus,
|
|
334
|
+
closeCustomInstructionsSettings,
|
|
335
|
+
closeSkillsView,
|
|
336
|
+
getCodexOverleafSkillEnabled,
|
|
337
|
+
getCustomInstructionsForCurrentProject,
|
|
338
|
+
getGovernanceRulesForCurrentProject,
|
|
339
|
+
getSkillLoadingSettings,
|
|
340
|
+
getThemePreference,
|
|
341
|
+
isCodexOverleafSkillEnabled,
|
|
342
|
+
normalizeCustomInstructionsByProject,
|
|
343
|
+
normalizeGovernanceRules,
|
|
344
|
+
normalizeGovernanceRulesByProject,
|
|
345
|
+
normalizeProjectPreferenceKey,
|
|
346
|
+
openCustomInstructionsSettings,
|
|
347
|
+
openSkillsView,
|
|
348
|
+
readGovernanceRulesFromSettings,
|
|
349
|
+
readSkillLoadingSettingsFromSettings,
|
|
350
|
+
refreshLocalSkills,
|
|
351
|
+
renderLocalSkillList,
|
|
352
|
+
setCodexOverleafSkillEnabled,
|
|
353
|
+
setCustomInstructionsForProject,
|
|
354
|
+
setGovernanceRulesForCurrentProject,
|
|
355
|
+
setProjectSettingsStatus,
|
|
356
|
+
setSkillLoadingSettings,
|
|
357
|
+
syncCustomInstructionsEditorForProject,
|
|
358
|
+
syncProjectSettingsEditorForProject,
|
|
359
|
+
toggleCustomInstructionsSettings,
|
|
360
|
+
updateSkillsEntrySummary
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
window.CodexOverleafProjectSettingsCoordinator = { create };
|
|
365
|
+
})();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
deriveSessionTitle
|
|
13
13
|
} = window.CodexOverleafSessionState;
|
|
14
14
|
|
|
15
|
-
// Session manager — the session lifecycle + list/
|
|
15
|
+
// Session manager — the session lifecycle + list/delete surface
|
|
16
16
|
// carved out of contentRuntime.js (v1.4.7 structural-debt phase 3). Code
|
|
17
17
|
// moved verbatim except that mutable runtime state is read through
|
|
18
18
|
// getState()/getPanel()/getSessionPanelInstance() and written back through
|
|
@@ -35,210 +35,26 @@
|
|
|
35
35
|
formatSessionTime
|
|
36
36
|
} = deps;
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// is never blank and unmanageable from the header.
|
|
53
|
-
textNode.textContent = active && isDisplayableSession(active)
|
|
54
|
-
? getSessionDisplayTitle(active)
|
|
55
|
-
: tr('newSessionFallback');
|
|
56
|
-
}
|
|
57
|
-
if (deleteButton) {
|
|
58
|
-
const running = active ? isSessionRunning(active) : false;
|
|
59
|
-
// Deleting the sole empty session is a documented no-op (it would just
|
|
60
|
-
// mint another empty), so disable it rather than leave a dead click.
|
|
61
|
-
const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (getState()?.sessions || []).length <= 1;
|
|
62
|
-
deleteButton.disabled = !active || running || soleEmpty;
|
|
63
|
-
deleteButton.title = running ? tr('deleteRunningSession') : tr('deleteSession');
|
|
64
|
-
deleteButton.setAttribute('aria-label', running ? tr('deleteRunningSession') : tr('deleteSession'));
|
|
65
|
-
}
|
|
66
|
-
if (renameButton) {
|
|
67
|
-
renameButton.title = tr('renameSession');
|
|
68
|
-
renameButton.setAttribute('aria-label', tr('renameSession'));
|
|
69
|
-
}
|
|
70
|
-
renameInput?.setAttribute('aria-label', tr('renameSession'));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Wire the active-session header bar once (rename + delete act on the active
|
|
74
|
-
// session, reusing the same paths as the per-row controls).
|
|
75
|
-
function bindActiveSessionHeader() {
|
|
76
|
-
getPanel().querySelector('[data-session-delete]')?.addEventListener('click', event => {
|
|
77
|
-
event.stopPropagation();
|
|
78
|
-
if (getState()?.activeSessionId) {
|
|
79
|
-
deleteSessionWithConfirm(getState().activeSessionId);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
getPanel().querySelector('[data-session-rename]')?.addEventListener('click', event => {
|
|
83
|
-
event.stopPropagation();
|
|
84
|
-
beginActiveSessionRename();
|
|
85
|
-
});
|
|
86
|
-
getPanel().querySelector('[data-session-menu-trigger]')?.addEventListener('click', event => {
|
|
87
|
-
event.stopPropagation();
|
|
88
|
-
toggleSessionMenu();
|
|
89
|
-
});
|
|
90
|
-
// Close the dropdown on any click outside it (capture phase, so the
|
|
91
|
-
// panel's own click-stoppers don't swallow the dismiss — same pattern as
|
|
92
|
-
// the diagnostics menu) and on Escape.
|
|
93
|
-
document.addEventListener('click', event => {
|
|
94
|
-
const menu = getPanel()?.querySelector('[data-session-menu]');
|
|
95
|
-
if (!menu || menu.hidden) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const trigger = getPanel()?.querySelector('[data-session-menu-trigger]');
|
|
99
|
-
if (menu.contains(event.target) || trigger?.contains(event.target)) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
closeSessionMenu();
|
|
103
|
-
}, true);
|
|
104
|
-
document.addEventListener('keydown', event => {
|
|
105
|
-
if (event.key === 'Escape') {
|
|
106
|
-
closeSessionMenu();
|
|
107
|
-
}
|
|
108
|
-
}, true);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Header session dropdown: lists every saved session where the user works,
|
|
112
|
-
// switches on click, and offers New session — the embedded list below stays
|
|
113
|
-
// as the always-visible overview.
|
|
114
|
-
function toggleSessionMenu() {
|
|
115
|
-
const menu = getPanel()?.querySelector('[data-session-menu]');
|
|
116
|
-
if (!menu) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (menu.hidden) {
|
|
120
|
-
renderSessionMenu(menu);
|
|
121
|
-
}
|
|
122
|
-
menu.hidden = !menu.hidden;
|
|
123
|
-
getPanel()?.querySelector('[data-session-menu-trigger]')
|
|
124
|
-
?.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function closeSessionMenu() {
|
|
128
|
-
const menu = getPanel()?.querySelector('[data-session-menu]');
|
|
129
|
-
if (!menu || menu.hidden) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
menu.hidden = true;
|
|
133
|
-
getPanel()?.querySelector('[data-session-menu-trigger]')?.setAttribute('aria-expanded', 'false');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function renderSessionMenu(menu) {
|
|
137
|
-
menu.replaceChildren();
|
|
138
|
-
const state = getState();
|
|
139
|
-
const sessions = (state?.sessions || [])
|
|
140
|
-
.filter(isDisplayableSession)
|
|
141
|
-
.slice()
|
|
142
|
-
.sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
|
|
143
|
-
for (const session of sessions) {
|
|
144
|
-
const row = document.createElement('button');
|
|
145
|
-
row.type = 'button';
|
|
146
|
-
row.className = 'codex-session-menu-item';
|
|
147
|
-
row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
|
|
148
|
-
row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
|
|
149
|
-
const title = document.createElement('span');
|
|
150
|
-
title.className = 'codex-session-menu-item-title';
|
|
151
|
-
title.textContent = getSessionDisplayTitle(session);
|
|
152
|
-
title.title = getSessionDisplayTitle(session);
|
|
153
|
-
const time = document.createElement('time');
|
|
154
|
-
time.className = 'codex-session-menu-item-time';
|
|
155
|
-
time.textContent = formatSessionTime(session.updatedAt || session.createdAt);
|
|
156
|
-
const absoluteTs = session.updatedAt || session.createdAt;
|
|
157
|
-
if (absoluteTs) {
|
|
158
|
-
time.title = new Date(absoluteTs).toLocaleString();
|
|
159
|
-
}
|
|
160
|
-
row.append(title, time);
|
|
161
|
-
row.addEventListener('click', event => {
|
|
162
|
-
event.stopPropagation();
|
|
163
|
-
closeSessionMenu();
|
|
164
|
-
if (session.id !== getState()?.activeSessionId) {
|
|
165
|
-
switchSession(session.id);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
menu.append(row);
|
|
169
|
-
}
|
|
170
|
-
const create = document.createElement('button');
|
|
171
|
-
create.type = 'button';
|
|
172
|
-
create.className = 'codex-session-menu-new';
|
|
173
|
-
create.textContent = `+ ${tr('newSession')}`;
|
|
174
|
-
create.addEventListener('click', event => {
|
|
175
|
-
event.stopPropagation();
|
|
176
|
-
closeSessionMenu();
|
|
177
|
-
startNewSession();
|
|
38
|
+
const sessionMenuView = window.CodexOverleafSessionMenuView.create({
|
|
39
|
+
commitSessionRename,
|
|
40
|
+
deleteSessionWithConfirm,
|
|
41
|
+
document,
|
|
42
|
+
formatSessionTime,
|
|
43
|
+
getActiveSession,
|
|
44
|
+
getPanel,
|
|
45
|
+
getSessionDisplayTitle,
|
|
46
|
+
getState,
|
|
47
|
+
isDisplayableSession,
|
|
48
|
+
isSessionRunning,
|
|
49
|
+
startNewSession,
|
|
50
|
+
switchSession,
|
|
51
|
+
tr
|
|
178
52
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
function beginActiveSessionRename() {
|
|
185
|
-
const active = getActiveSession(getState());
|
|
186
|
-
const wrap = getPanel().querySelector('[data-session-label]');
|
|
187
|
-
const trigger = getPanel().querySelector('[data-session-menu-trigger]');
|
|
188
|
-
const textNode = getPanel().querySelector('[data-session-label-text]');
|
|
189
|
-
const input = getPanel().querySelector('[data-session-rename-input]');
|
|
190
|
-
if (!active || !wrap || !textNode || !input) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
closeSessionMenu();
|
|
194
|
-
let settled = false;
|
|
195
|
-
wrap.dataset.editing = 'true';
|
|
196
|
-
// Seed the box from a real title only; an empty/auto session opens blank so
|
|
197
|
-
// accepting the default does not commit the placeholder as a manual title.
|
|
198
|
-
const seed = isDisplayableSession(active) ? getSessionDisplayTitle(active) : '';
|
|
199
|
-
input.value = seed;
|
|
200
|
-
input.hidden = false;
|
|
201
|
-
textNode.hidden = true;
|
|
202
|
-
if (trigger) {
|
|
203
|
-
trigger.hidden = true;
|
|
204
|
-
}
|
|
205
|
-
input.focus();
|
|
206
|
-
input.select();
|
|
207
|
-
const cleanup = () => {
|
|
208
|
-
input.removeEventListener('keydown', onKeydown);
|
|
209
|
-
input.removeEventListener('blur', onBlur);
|
|
210
|
-
};
|
|
211
|
-
const finish = commit => {
|
|
212
|
-
if (settled) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
settled = true;
|
|
216
|
-
cleanup();
|
|
217
|
-
input.hidden = true;
|
|
218
|
-
textNode.hidden = false;
|
|
219
|
-
if (trigger) {
|
|
220
|
-
trigger.hidden = false;
|
|
221
|
-
}
|
|
222
|
-
delete wrap.dataset.editing;
|
|
223
|
-
// Treat an unchanged value (incl. a no-op blur) as a cancel so it never
|
|
224
|
-
// rewrites the title source.
|
|
225
|
-
if (commit && input.value.trim() !== seed.trim()) {
|
|
226
|
-
commitSessionRename(active.id, input.value);
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
const onKeydown = event => {
|
|
230
|
-
if (event.key === 'Enter') {
|
|
231
|
-
event.preventDefault();
|
|
232
|
-
finish(true);
|
|
233
|
-
} else if (event.key === 'Escape') {
|
|
234
|
-
event.preventDefault();
|
|
235
|
-
finish(false);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
const onBlur = () => finish(true);
|
|
239
|
-
input.addEventListener('keydown', onKeydown);
|
|
240
|
-
input.addEventListener('blur', onBlur);
|
|
241
|
-
}
|
|
53
|
+
const {
|
|
54
|
+
applySessionLabel,
|
|
55
|
+
bindActiveSessionHeader,
|
|
56
|
+
closeSessionMenu
|
|
57
|
+
} = sessionMenuView;
|
|
242
58
|
|
|
243
59
|
async function startNewSession() {
|
|
244
60
|
// Leaving a still-running session is allowed but never silent: the run
|