codex-overleaf-link 2.1.5 → 2.2.1

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 (67) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +10 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +14 -5
  9. package/extension/src/content/contentRuntime.js +330 -309
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/panelRenderer.js +112 -12
  17. package/extension/src/content/pendingInputView.js +106 -0
  18. package/extension/src/content/projectProviderSelection.js +68 -0
  19. package/extension/src/content/projectSessionCleanup.js +4 -0
  20. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  21. package/extension/src/content/providerSettingsDialog.js +2 -2
  22. package/extension/src/content/recentProjects.js +7 -2
  23. package/extension/src/content/runGuidanceController.js +70 -0
  24. package/extension/src/content/runGuidanceView.js +48 -0
  25. package/extension/src/content/runQueueScheduler.js +260 -0
  26. package/extension/src/content/runTimelineView.js +33 -80
  27. package/extension/src/content/sessionManager.js +2 -2
  28. package/extension/src/content/sessionPersistence.js +62 -0
  29. package/extension/src/content/settingsPanel.js +1 -1
  30. package/extension/src/content/updateNotice.js +80 -1
  31. package/extension/src/page/writebackRouter.js +3 -9
  32. package/extension/src/shared/compatibility.js +2 -1
  33. package/extension/src/shared/i18n.js +38 -0
  34. package/extension/src/shared/providerProfiles.js +3 -2
  35. package/extension/src/shared/runInputQueue.js +253 -0
  36. package/extension/src/shared/sessionState.js +45 -22
  37. package/extension/src/shared/storageDb.js +39 -44
  38. package/extension/src/shared/storageMigration.js +123 -3
  39. package/extension/src/shared/storageRunActions.js +34 -1
  40. package/extension/src/shared/updateConsent.js +15 -4
  41. package/extension/styles/panel.css +319 -20
  42. package/extension/vendor/katex/katex.min.css +1 -1
  43. package/extension/vendor/markdown-it/LICENSE +22 -0
  44. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  45. package/extension/vendor/markdown-it/VERSION +1 -0
  46. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  47. package/native-host/src/anthropicBridgeRequest.js +3 -3
  48. package/native-host/src/anthropicBridgeResponse.js +58 -17
  49. package/native-host/src/anthropicMessagesBridge.js +156 -23
  50. package/native-host/src/chatBridgeRequest.js +1 -1
  51. package/native-host/src/chatBridgeResponse.js +12 -8
  52. package/native-host/src/chatCompletionsBridge.js +22 -40
  53. package/native-host/src/codexPromptAssembly.js +3 -1
  54. package/native-host/src/codexProviderLaunch.js +6 -3
  55. package/native-host/src/codexSessionRunner.js +65 -65
  56. package/native-host/src/codexSessionTiming.js +45 -0
  57. package/native-host/src/index.js +58 -3
  58. package/native-host/src/managedInstall.js +45 -7
  59. package/native-host/src/mirrorWorkspace.js +15 -8
  60. package/native-host/src/providerProfile.js +2 -2
  61. package/native-host/src/providerStreamLifecycle.js +100 -0
  62. package/native-host/src/responsesPassthroughBridge.js +636 -0
  63. package/native-host/src/taskRunnerRuntime.js +81 -2
  64. package/native-host/src/updateArchive.js +4 -3
  65. package/native-host/src/updateManager.js +306 -19
  66. package/native-host/src/updateTrust.js +1 -1
  67. package/package.json +3 -1
@@ -463,13 +463,56 @@
463
463
  return '#';
464
464
  }
465
465
 
466
- function renderMarkdownBlockText(target, value) {
467
- const source = normalizeInlineOrderedLists(String(value || '').trim());
468
- target.replaceChildren();
469
- if (!source) {
466
+ const renderingDiagnosticKeys = new Set();
467
+
468
+ function renderMarkdownBlockText(target, value, renderOptions = {}) {
469
+ const rawSource = String(value || '').trim();
470
+ if (!rawSource) {
471
+ target.replaceChildren();
470
472
  return;
471
473
  }
472
474
 
475
+ if (!renderOptions.streaming) {
476
+ const runtimeRoot = typeof window !== 'undefined' ? window : globalThis;
477
+ const domRenderer = runtimeRoot.CodexOverleafMarkdownDomRenderer;
478
+ try {
479
+ if (!domRenderer?.renderMarkdown || typeof runtimeRoot.markdownit !== 'function') {
480
+ const dependencyError = new Error('The vendored Markdown DOM renderer is unavailable.');
481
+ dependencyError.code = 'markdown_dom_dependency_missing';
482
+ throw dependencyError;
483
+ }
484
+ domRenderer.renderMarkdown(target, rawSource, {
485
+ document,
486
+ markdownIt: runtimeRoot.markdownit,
487
+ mathText: runtimeRoot.CodexOverleafMathText,
488
+ renderTextNodes: text => buildMarkdownInlineNodes(text),
489
+ renderLinkNodes: (label, href) => {
490
+ const safeLabel = String(label || '');
491
+ const target = String(href || '').trim();
492
+ if (!/^https?:\/\//i.test(target) && !resolveMarkdownLineReference(safeLabel, target)) return buildMarkdownInlineNodes(safeLabel);
493
+ return buildMarkdownInlineNodes(
494
+ `[${safeLabel.replace(/]/g, '\\]')}](${target.replace(/\)/g, '\\)')})`
495
+ );
496
+ },
497
+ renderCodeNodes: text => buildMarkdownInlineNodes(`\`${String(text || '').replace(/`/g, '\\`')}\``),
498
+ sanitizeText: sanitizeAssistantVisibleText,
499
+ createMathNode: segment => runtimeRoot.CodexOverleafMathText.createMathNode(segment, {
500
+ document, katex: runtimeRoot.katex,
501
+ renderText: text => buildMarkdownInlineNodes(text)
502
+ }),
503
+ recordDiagnostic: recordRenderingDiagnostic
504
+ });
505
+ return;
506
+ } catch (error) {
507
+ recordRenderingDiagnostic({
508
+ code: error?.code || 'markdown_dom_fallback',
509
+ detail: String(error?.message || 'Markdown DOM rendering failed.')
510
+ });
511
+ }
512
+ }
513
+
514
+ const source = normalizeInlineOrderedLists(rawSource);
515
+ target.replaceChildren();
473
516
  const lines = source.split(/\r?\n/);
474
517
  let index = 0;
475
518
 
@@ -533,10 +576,46 @@
533
576
  index++;
534
577
  }
535
578
 
536
- const paragraph = document.createElement('p');
537
- paragraph.append(...buildMarkdownInlineNodes(paragraphLines.join(' ')));
538
- target.append(paragraph);
579
+ appendMarkdownParagraphFlow(target, paragraphLines.join(' '));
580
+ }
581
+ }
582
+
583
+ function recordRenderingDiagnostic(diagnostic = {}) {
584
+ const code = String(diagnostic.code || 'markdown_rendering_diagnostic');
585
+ const detail = String(diagnostic.detail || '').slice(0, 240);
586
+ const key = `${code}:${detail}`;
587
+ if (renderingDiagnosticKeys.has(key)) {
588
+ return;
589
+ }
590
+ renderingDiagnosticKeys.add(key);
591
+ if (typeof options.recordRenderingDiagnostic === 'function') options.recordRenderingDiagnostic({ code, detail });
592
+ }
593
+
594
+ function appendMarkdownParagraphFlow(target, value) {
595
+ const nodes = buildMarkdownInlineNodes(value);
596
+ let paragraph = null;
597
+ let paragraphHasContent = false;
598
+ const flushParagraph = () => {
599
+ if (paragraph && paragraphHasContent) {
600
+ target.append(paragraph);
601
+ }
602
+ paragraph = null;
603
+ paragraphHasContent = false;
604
+ };
605
+
606
+ for (const node of nodes) {
607
+ if (node?.dataset?.mathDisplay === 'block') {
608
+ flushParagraph();
609
+ target.append(node);
610
+ continue;
611
+ }
612
+ if (!paragraph) {
613
+ paragraph = document.createElement('p');
614
+ }
615
+ paragraph.append(node);
616
+ paragraphHasContent = true;
539
617
  }
618
+ flushParagraph();
540
619
  }
541
620
 
542
621
  function isMarkdownFenceLine(line) {
@@ -7,6 +7,8 @@
7
7
  Object.freeze({ open: '\\(', close: '\\)', display: false, singleDollar: false }),
8
8
  Object.freeze({ open: '$', close: '$', display: false, singleDollar: true })
9
9
  ]);
10
+ const LONG_INLINE_MATH_CHARS = 120;
11
+ const COMPLEX_INLINE_MATH_CHARS = 80;
10
12
 
11
13
  function parseMathSegments(value) {
12
14
  const source = String(value || '');
@@ -186,14 +188,19 @@
186
188
  function createMathNode(segment, options = {}) {
187
189
  const documentRef = options.document || root.document;
188
190
  const node = documentRef.createElement('span');
189
- node.className = `run-math ${segment.display ? 'run-math--display' : 'run-math--inline'}`;
190
- node.dataset.mathDisplay = segment.display ? 'block' : 'inline';
191
+ const source = normalizeMathForRendering(segment.value);
192
+ const display = segment.display || shouldPromoteInlineMath(source);
193
+ node.className = `run-math ${display ? 'run-math--display' : 'run-math--inline'}`;
194
+ node.dataset.mathDisplay = display ? 'block' : 'inline';
195
+ if (display && !segment.display) {
196
+ node.classList.add('run-math--promoted');
197
+ }
191
198
  try {
192
199
  if (typeof options.katex?.render !== 'function') {
193
200
  throw new Error('KaTeX is unavailable');
194
201
  }
195
- options.katex.render(segment.value, node, {
196
- displayMode: segment.display,
202
+ options.katex.render(source, node, {
203
+ displayMode: display,
197
204
  throwOnError: true,
198
205
  trust: false,
199
206
  strict: 'warn',
@@ -211,6 +218,23 @@
211
218
  }
212
219
  }
213
220
 
221
+ function normalizeMathForRendering(value) {
222
+ return String(value || '')
223
+ .replace(/\r\n?/g, '\n')
224
+ .replace(/(\S)\s+\|\s+(\S)/g, '$1 \\mid $2');
225
+ }
226
+
227
+ function shouldPromoteInlineMath(value) {
228
+ const source = String(value || '').trim();
229
+ if (source.length >= LONG_INLINE_MATH_CHARS) {
230
+ return true;
231
+ }
232
+ if (source.length < COMPLEX_INLINE_MATH_CHARS) {
233
+ return false;
234
+ }
235
+ return /(?:=|\\(?:le|ge|approx|sim|to|Rightarrow|Longrightarrow)\b|\\begin\{(?:aligned|gathered|split|array)\}|\\\\)/.test(source);
236
+ }
237
+
214
238
  function toNodeArray(value) {
215
239
  if (Array.isArray(value)) {
216
240
  return value;
@@ -221,9 +245,30 @@
221
245
  return [value];
222
246
  }
223
247
 
248
+ function matchMathAt(value, index = 0) {
249
+ const source = String(value || '');
250
+ const start = Number.isSafeInteger(index) ? index : 0;
251
+ if (start < 0 || start >= source.length || (source[start] !== '$' && source[start] !== '\\')) {
252
+ return null;
253
+ }
254
+ const segment = parseMathSegments(source.slice(start))[0];
255
+ if (!segment || segment.type !== 'math' || !segment.raw) {
256
+ return null;
257
+ }
258
+ return {
259
+ start,
260
+ end: start + segment.raw.length,
261
+ raw: segment.raw,
262
+ value: segment.value,
263
+ display: Boolean(segment.display)
264
+ };
265
+ }
266
+
224
267
  const api = {
225
268
  buildMathNodes,
226
269
  createMathNode,
270
+ matchMathAt,
271
+ normalizeMathForRendering,
227
272
  parseMathSegments
228
273
  };
229
274
  root.CodexOverleafMathText = api;
@@ -102,7 +102,7 @@
102
102
  updateModelDisplay();
103
103
  await persistPanelInputs(changedInput ? { type: 'change', target: changedInput } : undefined);
104
104
  }
105
- async function loadModelOptions(providerSelection) {
105
+ async function loadModelOptions(providerSelection, options = {}) {
106
106
  const generation = ++modelLoadGeneration;
107
107
  const selection = providerSelection || getProviderSelection?.() || null;
108
108
  const selectedModel = resolveSelectedModel();
@@ -131,14 +131,17 @@
131
131
  && response.result.models.length > 0;
132
132
  if (!hasDiscoveredModels && !Support.shouldUseBuiltInFallback(response)) {
133
133
  applyUnavailableModelOptions(response?.error);
134
- return;
134
+ return { stale: false, error: response?.error || { code: 'provider_models_unavailable' } };
135
135
  }
136
136
  const sourceModels = hasDiscoveredModels ? response.result.models : fallbackModels;
137
137
  const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel);
138
138
  const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
139
139
  if (normalized.usedFallback && response.result?.providerId && response.result.providerId !== 'builtin') {
140
140
  applyUnavailableModelOptions({ code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' });
141
- return;
141
+ return {
142
+ stale: false,
143
+ error: { code: 'provider_model_catalog_invalid', message: 'The active provider returned no usable models.' }
144
+ };
142
145
  }
143
146
  renderModelOptions(normalized.models, retainedSelectedModel, { allowUnknownSelected: false });
144
147
  modelDiscovery = {
@@ -151,8 +154,14 @@
151
154
  providerRevision: Number(response?.result?.providerRevision || selection?.providerRevision || 0)
152
155
  };
153
156
  updateModelDisplay();
154
- await persistPanelInputs();
155
- return { stale: false, providerId: modelDiscovery.providerId };
157
+ if (options.persist !== false) {
158
+ await persistPanelInputs();
159
+ }
160
+ return {
161
+ stale: false,
162
+ providerId: modelDiscovery.providerId,
163
+ selectedModel: resolveSelectedModel()
164
+ };
156
165
  } catch (error) {
157
166
  if (generation !== modelLoadGeneration) {
158
167
  return { stale: true };
@@ -14,15 +14,20 @@
14
14
  const backgroundEventHandlers = new Map();
15
15
 
16
16
  function sendNative(payload) {
17
+ return sendNativeTracked(payload).promise;
18
+ }
19
+
20
+ function sendNativeTracked(payload) {
17
21
  const id = cryptoImpl.randomUUID();
18
22
  activeRequestId = id;
19
- return runtime.sendMessage({
23
+ const promise = runtime.sendMessage({
20
24
  type: 'codex-overleaf/native-request',
21
25
  payload: {
22
26
  id,
23
27
  ...payload
24
28
  }
25
29
  });
30
+ return { id, promise };
26
31
  }
27
32
 
28
33
  function sendBackgroundNative(payload, onEvent) {
@@ -71,6 +76,7 @@
71
76
  handleBackgroundNativeEvent,
72
77
  sendBackgroundNative,
73
78
  sendNative,
79
+ sendNativeTracked,
74
80
  shouldHandleNativeEvent
75
81
  };
76
82
  }
@@ -51,18 +51,6 @@
51
51
  <div data-session-slot></div>
52
52
  <div class="codex-toast-region" data-toast-region aria-live="polite" aria-atomic="false"></div>
53
53
  <section class="codex-thread-section">
54
- <div class="codex-thread-title" data-session-label>
55
- <button type="button" class="codex-thread-title-trigger" data-session-menu-trigger aria-haspopup="menu" aria-expanded="false">
56
- <span class="codex-thread-title-text" data-session-label-text></span>
57
- <span class="codex-thread-title-chevron" aria-hidden="true">▾</span>
58
- </button>
59
- <input type="text" class="codex-thread-title-input" data-session-rename-input maxlength="80" hidden>
60
- <div class="codex-thread-title-actions">
61
- <button type="button" class="codex-thread-title-action" data-session-rename title="Rename session" aria-label="Rename session">✎</button>
62
- <button type="button" class="codex-thread-title-action codex-thread-title-action--delete" data-session-delete title="Delete session" aria-label="Delete session">×</button>
63
- </div>
64
- <div class="codex-session-menu" data-session-menu hidden></div>
65
- </div>
66
54
  <div class="col-log" data-log></div>
67
55
  </section>
68
56
  </div>
@@ -236,6 +224,117 @@
236
224
  actions.prepend(button);
237
225
  }
238
226
 
227
+ function createConfirmController(options = {}) {
228
+ let activeResolve = null;
229
+
230
+ function isOpen() {
231
+ return Boolean(activeResolve);
232
+ }
233
+
234
+ function dismiss(value = false) {
235
+ activeResolve?.(value);
236
+ }
237
+
238
+ function show(config = {}) {
239
+ if (!options.getPanel?.()) {
240
+ options.ensurePanelOpen?.();
241
+ }
242
+ const panel = options.getPanel?.();
243
+ if (!panel) {
244
+ return Promise.resolve(false);
245
+ }
246
+ dismiss(false);
247
+ const tr = options.tr || (key => key);
248
+ const title = config.title || tr('confirmDefaultTitle');
249
+ const message = config.message || '';
250
+ const confirmLabel = config.confirmLabel || tr('confirmDefaultConfirm');
251
+ const cancelLabel = config.cancelLabel || tr('confirmDefaultCancel');
252
+ const destructive = config.destructive === true;
253
+ const doc = panel.ownerDocument || document;
254
+
255
+ return new Promise(resolve => {
256
+ let settled = false;
257
+ const overlay = doc.createElement('div');
258
+ overlay.className = 'codex-plugin-confirm';
259
+ overlay.setAttribute('data-plugin-confirm', 'true');
260
+ overlay.setAttribute('role', 'dialog');
261
+ overlay.setAttribute('aria-modal', 'true');
262
+ overlay.setAttribute('aria-label', title);
263
+
264
+ const card = doc.createElement('section');
265
+ card.className = 'codex-plugin-confirm-card';
266
+ const head = doc.createElement('div');
267
+ head.className = 'codex-plugin-confirm-head';
268
+ const icon = doc.createElement('img');
269
+ icon.className = 'codex-plugin-confirm-icon';
270
+ icon.alt = '';
271
+ icon.setAttribute('aria-hidden', 'true');
272
+ icon.src = options.getIconUrl?.() || '';
273
+ const titleWrap = doc.createElement('div');
274
+ const brand = doc.createElement('div');
275
+ brand.className = 'codex-plugin-confirm-brand';
276
+ brand.textContent = tr('confirmBrand');
277
+ const titleEl = doc.createElement('div');
278
+ titleEl.className = 'codex-plugin-confirm-title';
279
+ titleEl.textContent = title;
280
+ titleWrap.append(brand, titleEl);
281
+ head.append(icon, titleWrap);
282
+
283
+ const body = doc.createElement('div');
284
+ body.className = 'codex-plugin-confirm-body';
285
+ body.textContent = String(message);
286
+ const actions = doc.createElement('div');
287
+ actions.className = 'codex-plugin-confirm-actions';
288
+ const cancel = doc.createElement('button');
289
+ cancel.type = 'button';
290
+ cancel.className = 'codex-plugin-confirm-cancel';
291
+ cancel.textContent = cancelLabel;
292
+ const confirm = doc.createElement('button');
293
+ confirm.type = 'button';
294
+ confirm.className = 'codex-plugin-confirm-confirm';
295
+ if (destructive) confirm.dataset.destructive = 'true';
296
+ confirm.textContent = confirmLabel;
297
+ actions.append(cancel, confirm);
298
+ card.append(head, body, actions);
299
+ overlay.append(card);
300
+ panel.append(overlay);
301
+
302
+ const cleanup = value => {
303
+ if (settled) return;
304
+ settled = true;
305
+ activeResolve = null;
306
+ doc.removeEventListener('keydown', onKeydown, true);
307
+ overlay.remove();
308
+ resolve(value);
309
+ };
310
+ activeResolve = cleanup;
311
+ const onKeydown = event => {
312
+ if (event.key === 'Escape') {
313
+ event.preventDefault();
314
+ cleanup(false);
315
+ } else if (event.key === 'Enter') {
316
+ event.preventDefault();
317
+ if (!destructive && event.target !== cancel) cleanup(true);
318
+ else if (event.target === cancel) cleanup(false);
319
+ else if (event.target === confirm) cleanup(true);
320
+ } else if (event.key === 'Tab') {
321
+ event.preventDefault();
322
+ (event.target === cancel ? confirm : cancel).focus();
323
+ }
324
+ };
325
+ overlay.addEventListener('click', event => {
326
+ if (event.target === overlay) cleanup(false);
327
+ });
328
+ cancel.addEventListener('click', () => cleanup(false));
329
+ confirm.addEventListener('click', () => cleanup(true));
330
+ doc.addEventListener('keydown', onKeydown, true);
331
+ (destructive ? cancel : confirm).focus();
332
+ });
333
+ }
334
+
335
+ return { dismiss, isOpen, show };
336
+ }
337
+
239
338
  function destroy(instance) {
240
339
  for (const { target, type, listener, options } of instance.listeners.splice(0)) {
241
340
  target.removeEventListener?.(type, listener, options);
@@ -246,6 +345,7 @@
246
345
 
247
346
  window.CodexOverleafPanelRenderer = {
248
347
  create,
348
+ createConfirmController,
249
349
  setVisible,
250
350
  setBadge,
251
351
  setWidth
@@ -0,0 +1,106 @@
1
+ (function initCodexOverleafPendingInputView(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafPendingInputView = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function pendingInputViewFactory() {
8
+ 'use strict';
9
+
10
+ function create(options = {}) {
11
+ const container = options.container;
12
+
13
+ function render(items = [], state = {}) {
14
+ if (!container) {
15
+ return;
16
+ }
17
+ container.replaceChildren();
18
+ container.hidden = !items.length;
19
+ for (const item of items) {
20
+ const row = document.createElement('article');
21
+ row.className = 'codex-pending-input';
22
+ row.dataset.status = item.status || 'queued';
23
+ const copy = document.createElement('div');
24
+ copy.className = 'codex-pending-input-copy';
25
+ const status = document.createElement('span');
26
+ status.className = 'codex-pending-input-status';
27
+ status.textContent = item.status === 'paused'
28
+ ? options.tr?.('queuedInputPaused')
29
+ : item.status === 'steering'
30
+ ? options.tr?.('queuedInputGuiding')
31
+ : item.status === 'executing'
32
+ ? options.tr?.('queuedInputExecuting')
33
+ : options.tr?.('queuedInputQueued');
34
+ const text = document.createElement('span');
35
+ text.className = 'codex-pending-input-text';
36
+ text.textContent = item.text;
37
+ copy.append(status, text);
38
+ const actions = document.createElement('div');
39
+ actions.className = 'codex-pending-input-actions';
40
+ if (item.status === 'paused') {
41
+ actions.append(createButton(options.tr?.('queuedInputResume'), () => options.onResume?.()));
42
+ } else if (state.running && item.status === 'queued') {
43
+ const guide = createIconButton(
44
+ options.tr?.('queuedInputGuide'),
45
+ 'guide',
46
+ () => options.onGuide?.(item.id)
47
+ );
48
+ guide.disabled = !state.canGuide;
49
+ guide.title = state.canGuide
50
+ ? options.tr?.('queuedInputGuideTitle')
51
+ : options.tr?.('queuedInputGuideUnavailable');
52
+ actions.append(guide);
53
+ }
54
+ const remove = createIconButton(
55
+ options.tr?.('queuedInputRemove'),
56
+ 'remove',
57
+ () => options.onRemove?.(item.id)
58
+ );
59
+ remove.classList.add('codex-pending-input-remove');
60
+ remove.disabled = item.status === 'steering' || item.status === 'executing';
61
+ text.title = item.text;
62
+ actions.append(remove);
63
+ row.append(copy, actions);
64
+ container.append(row);
65
+ }
66
+ }
67
+
68
+ return { render };
69
+ }
70
+
71
+ function createButton(label, onClick) {
72
+ const button = document.createElement('button');
73
+ button.type = 'button';
74
+ button.textContent = label || '';
75
+ button.addEventListener('click', onClick);
76
+ return button;
77
+ }
78
+
79
+ function createIconButton(label, icon, onClick) {
80
+ const button = createButton('', onClick);
81
+ button.className = 'codex-pending-input-icon';
82
+ button.dataset.action = icon;
83
+ button.title = label || '';
84
+ button.setAttribute('aria-label', label || '');
85
+ button.append(createIcon(icon));
86
+ return button;
87
+ }
88
+
89
+ function createIcon(icon) {
90
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
91
+ svg.setAttribute('viewBox', '0 0 20 20');
92
+ svg.setAttribute('aria-hidden', 'true');
93
+ svg.setAttribute('focusable', 'false');
94
+ const paths = icon === 'guide'
95
+ ? ['M4 4.5v4a4 4 0 0 0 4 4h7', 'm12 9 3.5 3.5L12 16']
96
+ : ['M6 6h8', 'M8 6V4.5h4V6', 'm7.2 8 0.6 7.5h4.4l.6-7.5'];
97
+ for (const value of paths) {
98
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
99
+ path.setAttribute('d', value);
100
+ svg.append(path);
101
+ }
102
+ return svg;
103
+ }
104
+
105
+ return { create };
106
+ });
@@ -0,0 +1,68 @@
1
+ (function initCodexOverleafProjectProviderSelection() {
2
+ 'use strict';
3
+
4
+ function create(deps = {}) {
5
+ async function confirmSwitch({ providerName } = {}) {
6
+ if ((deps.getRunningSessionIds?.() || []).length) {
7
+ deps.showToast?.(deps.tx(
8
+ 'Wait for the running task to finish before switching providers.',
9
+ '请等待正在运行的任务结束后再切换模型服务。'
10
+ ), { status: 'warning' });
11
+ return false;
12
+ }
13
+ return deps.showConfirm?.({
14
+ title: deps.tx('Switch provider for this project?', '切换当前项目的模型服务?'),
15
+ message: deps.tx(
16
+ `All sessions in this project will use ${providerName || 'the selected provider'}. The model, reasoning, and speed choices may change; existing run history stays unchanged, while future turns start fresh provider threads and may behave differently.`,
17
+ `当前项目的所有会话都将使用 ${providerName || '所选模型服务'}。模型、推理强度和速度选项可能变化;已有运行历史保持不变,后续轮次会创建新的服务线程,输出效果也可能变化。`
18
+ ),
19
+ confirmLabel: deps.tx('Switch provider', '切换模型服务'),
20
+ cancelLabel: deps.tr('confirmDefaultCancel')
21
+ });
22
+ }
23
+
24
+ function commit(providerId, model) {
25
+ const state = deps.getState?.() || {};
26
+ const nextProviderId = providerId || 'builtin';
27
+ const providerChanged = nextProviderId !== (state.providerId || 'builtin');
28
+ const nextModel = model || deps.readSelectedModel?.() || state.model || '';
29
+ const nextReasoningEffort = deps.getPanel?.()?.querySelector('[data-reasoning]')?.value || state.reasoningEffort;
30
+ const nextSpeedTier = deps.readSelectedSpeed?.() || state.speedTier;
31
+ const runSelection = deps.getRunSelection?.(nextProviderId);
32
+ const nextProviderRevision = Number(runSelection?.providerRevision || 0);
33
+ const sessions = (state.sessions || []).map(session => ({
34
+ ...session,
35
+ providerId: nextProviderId,
36
+ model: nextModel,
37
+ reasoningEffort: nextReasoningEffort,
38
+ speedTier: nextSpeedTier,
39
+ codexThreadId: providerChanged ? '' : session.codexThreadId,
40
+ pendingInputs: (session.pendingInputs || []).map(item => ({
41
+ ...item,
42
+ payload: {
43
+ ...(item.payload || {}),
44
+ providerId: nextProviderId,
45
+ providerRevision: nextProviderRevision,
46
+ model: nextModel,
47
+ reasoningEffort: nextReasoningEffort,
48
+ speedTier: nextSpeedTier
49
+ }
50
+ }))
51
+ }));
52
+ deps.setState?.(deps.normalizeState({
53
+ ...state,
54
+ providerId: nextProviderId,
55
+ providerRevision: nextProviderRevision,
56
+ model: nextModel,
57
+ reasoningEffort: nextReasoningEffort,
58
+ speedTier: nextSpeedTier,
59
+ sessions
60
+ }));
61
+ deps.applyStateToPanel?.();
62
+ }
63
+
64
+ return { confirmSwitch, commit };
65
+ }
66
+
67
+ window.CodexOverleafProjectProviderSelection = { create };
68
+ })();
@@ -49,6 +49,10 @@
49
49
  if (!approved) {
50
50
  return;
51
51
  }
52
+ const Migration = window.CodexOverleafStorageMigration;
53
+ if (Migration?.addSessionTombstones) {
54
+ await Migration.addSessionTombstones(projectId, records.map(record => record.id));
55
+ }
52
56
  try {
53
57
  await mutateProjectPanelState(projectId, state => records.reduce(
54
58
  (nextState, record) => SessionState.deleteSession(nextState, record.id),