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.
- package/README.md +45 -21
- package/bin/codex-overleaf-link.mjs +58 -0
- package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
- package/extension/assets/icons/codex-overleaf-icon.png +0 -0
- package/extension/assets/icons/icon128.png +0 -0
- package/extension/assets/icons/icon16.png +0 -0
- package/extension/assets/icons/icon32.png +0 -0
- package/extension/assets/icons/icon48.png +0 -0
- package/extension/bootstrap/background.js +404 -0
- package/extension/bootstrap/manifest.template.json +51 -0
- package/extension/bootstrap/popup.css +26 -0
- package/extension/bootstrap/popup.html +38 -0
- package/extension/bootstrap/popup.js +44 -0
- package/extension/bootstrap/runtimeContext.js +2 -0
- package/extension/runtime-manifest.json +60 -0
- package/extension/src/background.js +532 -0
- package/extension/src/content/applyResultFormatters.js +387 -0
- package/extension/src/content/composerAttachments.js +522 -0
- package/extension/src/content/composerPanel.js +254 -0
- package/extension/src/content/contentRuntime.js +8895 -0
- package/extension/src/content/contextTray.js +686 -0
- package/extension/src/content/diagnosticsController.js +659 -0
- package/extension/src/content/diagnosticsPanel.js +342 -0
- package/extension/src/content/diffReviewPanel.js +797 -0
- package/extension/src/content/localSkillsPanel.js +280 -0
- package/extension/src/content/markdownText.js +606 -0
- package/extension/src/content/mirrorHealth.js +92 -0
- package/extension/src/content/modelPicker.js +489 -0
- package/extension/src/content/nativeChannel.js +62 -0
- package/extension/src/content/otWarmMirror.js +964 -0
- package/extension/src/content/otWarmMirrorController.js +174 -0
- package/extension/src/content/panelMaintenance.js +367 -0
- package/extension/src/content/panelRenderer.js +253 -0
- package/extension/src/content/recentProjects.js +1063 -0
- package/extension/src/content/reviewHunks.js +247 -0
- package/extension/src/content/runController.js +242 -0
- package/extension/src/content/runTimelineView.js +1126 -0
- package/extension/src/content/sessionManager.js +487 -0
- package/extension/src/content/sessionPanel.js +263 -0
- package/extension/src/content/settingsPanel.js +527 -0
- package/extension/src/content/themeController.js +91 -0
- package/extension/src/content/updateIdle.js +67 -0
- package/extension/src/content/writebackController.js +300 -0
- package/extension/src/content/writebackOrchestrator.js +850 -0
- package/extension/src/contentScript.js +8 -0
- package/extension/src/page/compileBridge.js +460 -0
- package/extension/src/page/overleafCapabilities.js +100 -0
- package/extension/src/page/overleafEditor.js +282 -0
- package/extension/src/page/overleafProjectSnapshot.js +234 -0
- package/extension/src/page/overleafRealtimeObserver.js +491 -0
- package/extension/src/page/pageBridgeCapability.js +71 -0
- package/extension/src/page/snapshotRouter.js +948 -0
- package/extension/src/page/trackedChangesLifecycle.js +1506 -0
- package/extension/src/page/treeOperations.js +1584 -0
- package/extension/src/page/writeGuard.js +142 -0
- package/extension/src/page/writebackRouter.js +1934 -0
- package/extension/src/pageBridge.js +2224 -0
- package/extension/src/popup.js +247 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/styles/panel.css +4048 -0
- package/native-host/src/index.js +9 -2
- package/native-host/src/managedInstall.js +393 -0
- package/native-host/src/managedLauncherRuntime.js +71 -0
- package/native-host/src/nativeHostPlatform.js +16 -0
- package/native-host/src/runtimeInstaller.js +6 -1
- package/native-host/src/taskRunnerRuntime.js +8 -0
- package/native-host/src/updateArchive.js +253 -0
- package/native-host/src/updateManager.js +548 -0
- package/native-host/src/updateTrust.js +156 -0
- package/package.json +11 -2
- package/scripts/install-managed.mjs +64 -0
- package/scripts/uninstall-managed.mjs +48 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
(function initCodexOverleafSessionManager() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const SessionPanel = window.CodexOverleafSessionPanel;
|
|
5
|
+
const {
|
|
6
|
+
createSession,
|
|
7
|
+
deleteSession,
|
|
8
|
+
setActiveSession,
|
|
9
|
+
getActiveSession,
|
|
10
|
+
isDisplayableSession,
|
|
11
|
+
normalizePanelState,
|
|
12
|
+
deriveSessionTitle
|
|
13
|
+
} = window.CodexOverleafSessionState;
|
|
14
|
+
|
|
15
|
+
// Session manager — the session lifecycle + list/rename/delete surface
|
|
16
|
+
// carved out of contentRuntime.js (v1.4.7 structural-debt phase 3). Code
|
|
17
|
+
// moved verbatim except that mutable runtime state is read through
|
|
18
|
+
// getState()/getPanel()/getSessionPanelInstance() and written back through
|
|
19
|
+
// setState() (sessions are immutable-update: every mutation rebuilds state
|
|
20
|
+
// via the shared sessionState helpers and hands it back to the runtime).
|
|
21
|
+
function create(deps = {}) {
|
|
22
|
+
const {
|
|
23
|
+
tr,
|
|
24
|
+
showPluginConfirm,
|
|
25
|
+
showPluginToast,
|
|
26
|
+
sendBackgroundNative,
|
|
27
|
+
saveState,
|
|
28
|
+
saveStateSoon,
|
|
29
|
+
readPanelInputs,
|
|
30
|
+
applyStateToPanel,
|
|
31
|
+
getPanel,
|
|
32
|
+
getState,
|
|
33
|
+
setState,
|
|
34
|
+
getSessionPanelInstance,
|
|
35
|
+
formatSessionTime
|
|
36
|
+
} = deps;
|
|
37
|
+
|
|
38
|
+
function applySessionLabel() {
|
|
39
|
+
const trigger = getPanel().querySelector('[data-session-menu-trigger]');
|
|
40
|
+
if (trigger) {
|
|
41
|
+
trigger.title = tr('sessionMenuOpen');
|
|
42
|
+
trigger.setAttribute('aria-label', tr('sessionMenuOpen'));
|
|
43
|
+
}
|
|
44
|
+
const textNode = getPanel().querySelector('[data-session-label-text]');
|
|
45
|
+
const deleteButton = getPanel().querySelector('[data-session-delete]');
|
|
46
|
+
const renameButton = getPanel().querySelector('[data-session-rename]');
|
|
47
|
+
const renameInput = getPanel().querySelector('[data-session-rename-input]');
|
|
48
|
+
const active = getActiveSession(getState());
|
|
49
|
+
if (textNode) {
|
|
50
|
+
// Always show a title — fall back to the New Session placeholder for an
|
|
51
|
+
// empty active session (which has no list row), so the session you are in
|
|
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();
|
|
178
|
+
});
|
|
179
|
+
menu.append(create);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Inline-rename the active session from the header bar (mirrors the per-row
|
|
183
|
+
// rename: Enter / blur commits, Escape cancels).
|
|
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
|
+
}
|
|
242
|
+
|
|
243
|
+
async function startNewSession() {
|
|
244
|
+
// Leaving a still-running session is allowed but never silent: the run
|
|
245
|
+
// keeps going in the background and lands in its own session.
|
|
246
|
+
const runningActive = getActiveSession(getState());
|
|
247
|
+
if (runningActive && isSessionRunning(runningActive)) {
|
|
248
|
+
const approved = await showPluginConfirm({
|
|
249
|
+
title: tr('sessionSwitchRunningTitle'),
|
|
250
|
+
message: tr('sessionSwitchRunningMessage'),
|
|
251
|
+
confirmLabel: tr('sessionSwitchRunningConfirm')
|
|
252
|
+
});
|
|
253
|
+
if (!approved) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
readPanelInputs();
|
|
258
|
+
// Reuse an already-empty, idle active session instead of minting a ghost.
|
|
259
|
+
// Empty sessions are invisible in the list yet still consume the storage cap
|
|
260
|
+
// (slice(-20) / maxSessions), so spamming "New Session" could silently evict
|
|
261
|
+
// real history. Only mint when the current session holds real work.
|
|
262
|
+
const active = getActiveSession(getState());
|
|
263
|
+
if (active && !isDisplayableSession(active) && !isSessionRunning(active)) {
|
|
264
|
+
getPanel().querySelector('[data-task]')?.focus();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// The 21st session silently and PERMANENTLY evicts the oldest one
|
|
268
|
+
// (slice(-20) + the storage retention diff physically deletes its
|
|
269
|
+
// record). Never let that happen without consent.
|
|
270
|
+
const currentSessions = getState().sessions || [];
|
|
271
|
+
if (currentSessions.length >= 20) {
|
|
272
|
+
const oldest = currentSessions[0];
|
|
273
|
+
const evictionApproved = await showPluginConfirm({
|
|
274
|
+
title: tr('sessionEvictTitle'),
|
|
275
|
+
message: tr('sessionEvictMessage', { title: String(oldest?.title || '').slice(0, 40) || '…' }),
|
|
276
|
+
confirmLabel: tr('sessionEvictConfirm'),
|
|
277
|
+
destructive: true
|
|
278
|
+
});
|
|
279
|
+
if (!evictionApproved) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const session = createSession({
|
|
284
|
+
mode: getState().mode,
|
|
285
|
+
model: getState().model,
|
|
286
|
+
reasoningEffort: getState().reasoningEffort,
|
|
287
|
+
speedTier: getState().speedTier,
|
|
288
|
+
requireReviewing: getState().requireReviewing
|
|
289
|
+
});
|
|
290
|
+
setState(normalizePanelState({
|
|
291
|
+
...getState(),
|
|
292
|
+
sessions: [...(getState().sessions || []), session].slice(-20),
|
|
293
|
+
activeSessionId: session.id
|
|
294
|
+
}));
|
|
295
|
+
await saveState();
|
|
296
|
+
applyStateToPanel();
|
|
297
|
+
getPanel().querySelector('[data-task]')?.focus();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function switchSession(sessionId) {
|
|
301
|
+
if (sessionId === getState().activeSessionId) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const runningActive = getActiveSession(getState());
|
|
305
|
+
if (runningActive && isSessionRunning(runningActive)) {
|
|
306
|
+
const approved = await showPluginConfirm({
|
|
307
|
+
title: tr('sessionSwitchRunningTitle'),
|
|
308
|
+
message: tr('sessionSwitchRunningMessage'),
|
|
309
|
+
confirmLabel: tr('sessionSwitchRunningConfirm')
|
|
310
|
+
});
|
|
311
|
+
if (!approved) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
readPanelInputs();
|
|
316
|
+
setState(setActiveSession(getState(), sessionId));
|
|
317
|
+
await saveState();
|
|
318
|
+
applyStateToPanel();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function deleteSessionWithConfirm(sessionId) {
|
|
322
|
+
const target = (getState().sessions || []).find(session => session.id === sessionId);
|
|
323
|
+
if (!target) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (isSessionRunning(target)) {
|
|
327
|
+
showPluginToast(tr('deleteSessionRunningToast'), { status: 'warning' });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const remaining = (getState().sessions || []).filter(session => session.id !== sessionId);
|
|
332
|
+
|
|
333
|
+
// An empty (non-displayable) session has no task/runs/history and no native
|
|
334
|
+
// thread, so deleting it is a lossless local action — skip the destructive
|
|
335
|
+
// modal. Deleting the sole empty session would just mint another empty, so
|
|
336
|
+
// it is a no-op.
|
|
337
|
+
if (!isDisplayableSession(target)) {
|
|
338
|
+
if (!remaining.length) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
setState(deleteSession(getState(), sessionId));
|
|
342
|
+
await saveState();
|
|
343
|
+
applyStateToPanel();
|
|
344
|
+
showPluginToast(tr('deleteSessionDeletedToast'), { status: 'info' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Deleting the only session is really a reset (deleteSession mints a fresh
|
|
349
|
+
// empty replacement), so use honest copy for that case.
|
|
350
|
+
const lastSession = remaining.length === 0;
|
|
351
|
+
const approved = await showPluginConfirm({
|
|
352
|
+
title: lastSession ? tr('resetSessionTitle') : tr('deleteSessionTitle'),
|
|
353
|
+
message: lastSession
|
|
354
|
+
? tr('resetSessionMessage')
|
|
355
|
+
: [
|
|
356
|
+
getSessionDisplayTitle(target),
|
|
357
|
+
'',
|
|
358
|
+
tr('deleteSessionMessage')
|
|
359
|
+
].join('\n'),
|
|
360
|
+
confirmLabel: lastSession ? tr('resetSessionConfirm') : tr('deleteSessionConfirm'),
|
|
361
|
+
cancelLabel: tr('confirmDefaultCancel'),
|
|
362
|
+
destructive: true
|
|
363
|
+
});
|
|
364
|
+
if (!approved) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
setState(deleteSession(getState(), sessionId));
|
|
369
|
+
await saveState();
|
|
370
|
+
applyStateToPanel();
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const response = await sendBackgroundNative({
|
|
374
|
+
method: 'codex.history.clearPlugin',
|
|
375
|
+
params: {
|
|
376
|
+
sessionId,
|
|
377
|
+
threadId: target.codexThreadId || ''
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
if (!response?.ok) {
|
|
381
|
+
showPluginToast(tr('deleteSessionHistoryFailedToast', { message: response?.error?.message || 'native host did not return success' }), { status: 'warning', sticky: true });
|
|
382
|
+
} else if (response.result?.skipped) {
|
|
383
|
+
showPluginToast(tr('deleteSessionNoThreadToast'), { status: 'info' });
|
|
384
|
+
} else {
|
|
385
|
+
showPluginToast(tr('deleteSessionDoneToast'), { status: 'completed' });
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
showPluginToast(tr('deleteSessionHistoryFailedToast', { message: error.message }), { status: 'warning', sticky: true });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function renderSessionList(options = {}) {
|
|
392
|
+
// Omit showAll unless a caller explicitly sets it, so the getPanel() keeps its own
|
|
393
|
+
// expanded/collapsed getState() across re-renders (see sessionPanel.update).
|
|
394
|
+
const updateOptions = { pinnedSessionIds: getRunningSessionIds() };
|
|
395
|
+
if (options.showAll !== undefined) {
|
|
396
|
+
updateOptions.showAll = options.showAll;
|
|
397
|
+
}
|
|
398
|
+
SessionPanel.update(getSessionPanelInstance(), getState()?.sessions || [], getState()?.activeSessionId || null, updateOptions);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getSessionDisplayTitle(session) {
|
|
402
|
+
return SessionPanel.getDisplayTitle(getSessionPanelInstance(), session);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
async function commitSessionRename(sessionId, title) {
|
|
408
|
+
const session = findSessionById(sessionId);
|
|
409
|
+
if (!session) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const cleanTitle = String(title || '').replace(/\s+/g, ' ').trim();
|
|
413
|
+
const derived = deriveSessionTitle(session.runs, session.task);
|
|
414
|
+
// Only a value that is genuinely a custom title becomes a pinned manual
|
|
415
|
+
// title. An empty value, the localized New Session placeholder, or the
|
|
416
|
+
// auto-derived title itself all stay 'auto' — otherwise renaming an empty
|
|
417
|
+
// session would promote the placeholder into a real title (resurrecting the
|
|
418
|
+
// ghost the prune/reuse-guard eliminate) and a no-op blur would freeze an
|
|
419
|
+
// auto title.
|
|
420
|
+
const isCustom = Boolean(cleanTitle) && cleanTitle !== tr('newSessionFallback') && cleanTitle !== derived;
|
|
421
|
+
replaceSessionInState({
|
|
422
|
+
...session,
|
|
423
|
+
title: isCustom ? cleanTitle : derived,
|
|
424
|
+
titleSource: isCustom ? 'manual' : 'auto',
|
|
425
|
+
updatedAt: new Date().toISOString()
|
|
426
|
+
});
|
|
427
|
+
await saveState();
|
|
428
|
+
applySessionLabel();
|
|
429
|
+
renderSessionList();
|
|
430
|
+
}
|
|
431
|
+
function getRunningSessionIds() {
|
|
432
|
+
return (getState().sessions || [])
|
|
433
|
+
.filter(isSessionRunning)
|
|
434
|
+
.map(session => session.id);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isSessionRunning(session) {
|
|
438
|
+
return Boolean((session?.runs || []).some(run => run.status === 'running'));
|
|
439
|
+
}
|
|
440
|
+
function findSessionById(sessionId) {
|
|
441
|
+
if (!sessionId) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
return (getState().sessions || []).find(session => session.id === sessionId)
|
|
445
|
+
|| (getState().session?.id === sessionId ? getState().session : null);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function replaceSessionInState(session) {
|
|
449
|
+
if (!session?.id) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
setState(normalizePanelState({
|
|
453
|
+
...getState(),
|
|
454
|
+
sessions: (getState().sessions || []).map(item => item.id === session.id ? session : item)
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function updateSessionById(sessionId, patch = {}) {
|
|
459
|
+
const session = findSessionById(sessionId);
|
|
460
|
+
if (!session) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
replaceSessionInState({
|
|
464
|
+
...session,
|
|
465
|
+
...patch
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
closeSessionMenu,
|
|
471
|
+
applySessionLabel,
|
|
472
|
+
bindActiveSessionHeader,
|
|
473
|
+
startNewSession,
|
|
474
|
+
switchSession,
|
|
475
|
+
deleteSessionWithConfirm,
|
|
476
|
+
renderSessionList,
|
|
477
|
+
commitSessionRename,
|
|
478
|
+
getRunningSessionIds,
|
|
479
|
+
isSessionRunning,
|
|
480
|
+
findSessionById,
|
|
481
|
+
replaceSessionInState,
|
|
482
|
+
updateSessionById
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
window.CodexOverleafSessionManager = { create };
|
|
487
|
+
})();
|