codex-overleaf-link 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +78 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "entry": "extension/entries/content-entry.mjs",
4
4
  "output": "extension/src/content/generated/content.bundle.js",
5
- "sourceDigest": "548acfc19dd7be907e0c5494f2da2e11fa68dfc501e0552846ad63d4e5a35dba",
6
- "outputDigest": "404e0523fc534b56e87ea96e62cd112514c06ce034209ec7a05feef83d7a64a9",
7
- "byteLength": 3162346,
5
+ "sourceDigest": "0cf851b033afb0885464fd86506f423f35fe82d6da8c1f1644012350cab0ddc4",
6
+ "outputDigest": "0abf629ca414d00be2ab6bbde553c66a7f66ad2d0a4551bb272f3a5948a7d461",
7
+ "byteLength": 3387559,
8
8
  "inputs": [
9
9
  "extension/entries/content-entry.mjs",
10
10
  "extension/src/content/activeTurnControl.js",
@@ -15,6 +15,7 @@
15
15
  "extension/src/content/composerPanel.js",
16
16
  "extension/src/content/contentRuntime.js",
17
17
  "extension/src/content/contextTray.js",
18
+ "extension/src/content/contextUsageView.js",
18
19
  "extension/src/content/diagnosticsController.js",
19
20
  "extension/src/content/diagnosticsExport.js",
20
21
  "extension/src/content/diagnosticsPanel.js",
@@ -89,6 +90,7 @@
89
90
  "extension/src/shared/compatibility.js",
90
91
  "extension/src/shared/compileAdapter.js",
91
92
  "extension/src/shared/composerAttachmentDraft.js",
93
+ "extension/src/shared/contextUsage.js",
92
94
  "extension/src/shared/directoryCleanupProof.js",
93
95
  "extension/src/shared/failureReasons.js",
94
96
  "extension/src/shared/globalPreferences.js",
@@ -99,6 +101,7 @@
99
101
  "extension/src/shared/models.js",
100
102
  "extension/src/shared/otText.js",
101
103
  "extension/src/shared/pageRpcContract.js",
104
+ "extension/src/shared/panelIcons.js",
102
105
  "extension/src/shared/pathRedaction.js",
103
106
  "extension/src/shared/projectFiles.js",
104
107
  "extension/src/shared/providerProfiles.js",
@@ -29,6 +29,8 @@
29
29
  'ComposerPanel',
30
30
  'ContentRuntime',
31
31
  'ContextTray',
32
+ 'ContextUsage',
33
+ 'ContextUsageView',
32
34
  'DiagnosticsController',
33
35
  'DiagnosticsExport',
34
36
  'DiagnosticsPanel',
@@ -55,10 +57,12 @@
55
57
  'PostNavigationSettlementPersistence',
56
58
  'ProjectFiles',
57
59
  'ProjectProviderSelection',
60
+ 'PanelIcons',
58
61
  'ProjectSettingsCoordinator',
59
62
  'ProviderSettingsCoordinator',
60
63
  'RecentProjects',
61
64
  'ReviewHunks',
65
+ 'Reviewing',
62
66
  'RunActivitySummary',
63
67
  'RunPresence',
64
68
  'RunActivityModel',
@@ -320,7 +320,7 @@
320
320
  toggle.disabled = true;
321
321
  confirmBtn.textContent = tr('localSkillRemoving');
322
322
  removeCodexOverleafSkill(id).catch(error => {
323
- setProjectSettingsStatus(tx(`Failed to remove Codex Overleaf skill: ${error.message}`, `删除 Codex Overleaf 技能失败:${error.message}`), 'failed');
323
+ setProjectSettingsStatus(tx(`Failed to remove Codex Overleaf skill: ${error.message}`, `删除 Codex Overleaf 技能失败:${error.message}`), 'failed', { page: 'context' });
324
324
  // Restore row on failure so the user can retry.
325
325
  confirmBtn.disabled = false;
326
326
  cancelBtn.disabled = false;
@@ -381,7 +381,7 @@
381
381
  if (activeSkill?.scope === 'codex-overleaf' && activeSkill.id === id) {
382
382
  clearComposerSkillInvocation();
383
383
  }
384
- setProjectSettingsStatus(tr('codexOverleafSkillRemoveDone'), 'completed');
384
+ setProjectSettingsStatus(tr('codexOverleafSkillRemoveDone'), 'completed', { page: 'context' });
385
385
  clearSlashCodexOverleafSkills();
386
386
  await refreshLocalSkills();
387
387
  await saveState?.();
@@ -468,10 +468,10 @@
468
468
  textNode.textContent = `${match[1]}${text.slice(match[0].length)}`;
469
469
  textNode.parentNode.insertBefore(label, textNode);
470
470
  }
471
- decorateChangeReceipts(container, document, tx);
471
+ decorateChangeReceipts(container, document, tx, options.formatOperationType);
472
472
  }
473
473
 
474
- function decorateChangeReceipts(container, document, tx) {
474
+ function decorateChangeReceipts(container, document, tx, formatOperationType = type => type) {
475
475
  const label = container.querySelector?.('.run-system-label[data-system-label="changes"]');
476
476
  const list = label?.closest?.('p')?.nextElementSibling;
477
477
  if (!list || !['UL', 'OL'].includes(list.tagName)) return;
@@ -508,7 +508,9 @@
508
508
  file.textContent = receipt.file;
509
509
  const operation = document.createElement('span');
510
510
  operation.className = 'run-change-receipt__operation';
511
- operation.textContent = receipt.operation;
511
+ const type = ({ 编辑: 'edit', 新建: 'create', 重命名: 'rename', 移动: 'move', 删除: 'delete' })[receipt.operation]
512
+ || receipt.operation.toLowerCase();
513
+ operation.textContent = formatOperationType(type);
512
514
  receipt.item.classList.add('run-change-receipt');
513
515
  receipt.item.setAttribute('aria-label', receipt.original);
514
516
  receipt.item.title = receipt.original;
@@ -99,7 +99,7 @@
99
99
  index = next.end;
100
100
  }
101
101
 
102
- return nodes;
102
+ return groupLineReferenceNodes(nodes);
103
103
  }
104
104
 
105
105
  function buildInlineCodeNodes(value) {
@@ -152,7 +152,7 @@
152
152
  cursor = start + rawText.length;
153
153
  }
154
154
  appendSanitizedTextNode(nodes, source.slice(cursor));
155
- return nodes;
155
+ return groupLineReferenceNodes(nodes);
156
156
  }
157
157
 
158
158
  function buildMarkdownLinkNodes(text, href) {
@@ -187,6 +187,24 @@
187
187
  }
188
188
  }
189
189
 
190
+ function groupLineReferenceNodes(nodes) {
191
+ const grouped = [];
192
+ for (const node of nodes) {
193
+ const previous = grouped[grouped.length - 1];
194
+ if (node.classList?.contains('codex-line-reference') && previous?.nodeType === 3 &&
195
+ previous.textContent.endsWith('(')) {
196
+ previous.textContent = previous.textContent.slice(0, -1);
197
+ const group = document.createElement('span');
198
+ group.className = 'codex-line-reference-group';
199
+ group.append(document.createTextNode('('), node);
200
+ grouped.push(group);
201
+ } else {
202
+ grouped.push(node);
203
+ }
204
+ }
205
+ return grouped;
206
+ }
207
+
190
208
  function resolveMarkdownLineReference(text, href) {
191
209
  const candidates = [
192
210
  { value: href, mode: 'markdown-link-target' },
@@ -230,12 +248,16 @@
230
248
  return null;
231
249
  }
232
250
  const line = Number(ref?.line);
251
+ const endLine = ref?.endLine === undefined ? null : Number(ref.endLine);
233
252
  const column = ref?.column === null || ref?.column === undefined || ref?.column === ''
234
253
  ? null
235
254
  : Number(ref.column);
236
255
  if (!Number.isSafeInteger(line) || line < 1 || (column !== null && (!Number.isSafeInteger(column) || column < 1))) {
237
256
  return null;
238
257
  }
258
+ if (endLine !== null && (!Number.isSafeInteger(endLine) || endLine < line)) {
259
+ return null;
260
+ }
239
261
  let resolved;
240
262
  try {
241
263
  resolved = LineReferences.resolveProjectReference({
@@ -251,6 +273,7 @@
251
273
  return {
252
274
  path: resolved.path,
253
275
  line,
276
+ ...(endLine > line ? { endLine } : {}),
254
277
  column
255
278
  };
256
279
  }
@@ -262,10 +285,15 @@
262
285
  button.textContent = formatLineReferenceText(reference);
263
286
  button.dataset.path = reference.path;
264
287
  button.dataset.line = String(reference.line);
288
+ if (reference.endLine) {
289
+ button.dataset.endLine = String(reference.endLine);
290
+ }
265
291
  if (reference.column) {
266
292
  button.dataset.column = String(reference.column);
267
293
  }
268
- const ariaLabel = reference.column
294
+ const ariaLabel = reference.endLine
295
+ ? `Open ${reference.path} lines ${reference.line} to ${reference.endLine}`
296
+ : reference.column
269
297
  ? `Open ${reference.path} line ${reference.line} column ${reference.column}`
270
298
  : `Open ${reference.path} line ${reference.line}`;
271
299
  button.setAttribute('aria-label', ariaLabel);
@@ -278,7 +306,10 @@
278
306
  line: reference.line,
279
307
  selectLine: !reference.column
280
308
  };
281
- if (reference.column) {
309
+ if (reference.endLine) {
310
+ params.endLine = reference.endLine;
311
+ params.selectLine = true;
312
+ } else if (reference.column) {
282
313
  params.column = reference.column;
283
314
  params.selectLine = false;
284
315
  }
@@ -310,7 +341,7 @@
310
341
  }
311
342
 
312
343
  function formatLineReferenceText(reference) {
313
- return `${reference.path}:${reference.line}${reference.column ? `:${reference.column}` : ''}`;
344
+ return `${reference.path}:${reference.line}${reference.endLine ? `–${reference.endLine}` : reference.column ? `:${reference.column}` : ''}`;
314
345
  }
315
346
 
316
347
  function normalizeReferencePathForRuntime(value) {
@@ -499,6 +530,12 @@
499
530
  renderText: text => buildMarkdownInlineNodes(text) }),
500
531
  recordDiagnostic: recordRenderingDiagnostic
501
532
  });
533
+ // The DOM renderer emits text and links from separate Markdown tokens.
534
+ const parents = new Set(Array.from(target.querySelectorAll('.codex-line-reference'), button => button.parentNode)
535
+ .filter(parent => !parent.classList.contains('codex-line-reference-group')));
536
+ for (const parent of parents) {
537
+ parent.replaceChildren(...groupLineReferenceNodes(Array.from(parent.childNodes)));
538
+ }
502
539
  return;
503
540
  } catch (error) {
504
541
  recordRenderingDiagnostic({
@@ -525,6 +525,7 @@
525
525
  readSelectedSpeedInput() === 'fast' ? tx('Fast mode', '快速模式') : tx('Standard speed', '标准速度')
526
526
  ].filter(Boolean).join(' · ');
527
527
  }
528
+ deps.onSelectionRendered?.();
528
529
  }
529
530
 
530
531
  return {
@@ -88,7 +88,7 @@
88
88
  requiredVersion: compatibility.requiredVersion || Compatibility?.BUILD_TARGET_VERSION || '',
89
89
  releaseUrl: compatibility.releaseUrl || ''
90
90
  };
91
- notifyBlocked(error, compatibility);
91
+ notifyBlocked(error, compatibility, method);
92
92
  return { ok: false, compatibility, response: { ok: false, error } };
93
93
  }
94
94
 
@@ -103,7 +103,7 @@
103
103
  };
104
104
  }
105
105
 
106
- function notifyBlocked(error, compatibility = {}) {
106
+ function notifyBlocked(error, compatibility = {}, method) {
107
107
  const message = error?.message || String(error || '');
108
108
  if (options.getCurrentRunView?.()) {
109
109
  options.appendRunEvent?.({ title: message, status: 'failed' });
@@ -111,7 +111,17 @@
111
111
  return;
112
112
  }
113
113
  showUpdateGuidance(compatibility);
114
- options.showPluginToast?.(message, { status: 'failed', sticky: true });
114
+ const missing = compatibility.status === 'native_missing';
115
+ options.showPluginToast?.(missing ? tr('nativeCompatibilityMissingGuidance') : message, {
116
+ status: 'failed',
117
+ sticky: true,
118
+ ...(missing ? {
119
+ title: tr('nativeCompatibilityMissingTitle'),
120
+ groupKey: 'native-missing',
121
+ groupItem: method || 'native request',
122
+ groupSummary: count => tr('nativeCompatibilityUnavailableFeatures', { count })
123
+ } : {})
124
+ });
115
125
  }
116
126
 
117
127
  async function refreshBadge() {
@@ -197,11 +207,24 @@
197
207
  );
198
208
  const card = options.document.createElement('section');
199
209
  card.className = 'codex-plugin-confirm-card';
210
+ const head = options.document.createElement('div');
211
+ head.className = 'codex-plugin-confirm-head';
212
+ const icon = options.document.createElement('img');
213
+ icon.className = 'codex-plugin-confirm-icon';
214
+ icon.alt = '';
215
+ icon.setAttribute('aria-hidden', 'true');
216
+ icon.src = options.getIconUrl?.() || '';
217
+ const titleWrap = options.document.createElement('div');
218
+ const brand = options.document.createElement('div');
219
+ brand.className = 'codex-plugin-confirm-brand';
220
+ brand.textContent = 'Codex Overleaf Link';
200
221
  const title = options.document.createElement('div');
201
222
  title.className = 'codex-plugin-confirm-title';
202
223
  title.textContent = nativeMissing
203
224
  ? tx('One step left: install the native host', '还差一步:安装 Native host')
204
225
  : tx('Native host update available', 'Native host 可更新');
226
+ titleWrap.append(brand, title);
227
+ head.append(icon, titleWrap);
205
228
  const body = options.document.createElement('div');
206
229
  body.className = 'codex-plugin-confirm-body';
207
230
  body.textContent = [
@@ -222,20 +245,16 @@
222
245
  ? tx(' Use PowerShell.', ' 请使用 PowerShell。') : '')
223
246
  ].join('\n\n');
224
247
  const commandCode = options.document.createElement('code');
248
+ commandCode.className = 'codex-native-guidance-command';
225
249
  commandCode.textContent = command;
226
- Object.assign(commandCode.style, {
227
- display: 'block',
228
- whiteSpace: 'pre-wrap',
229
- marginTop: '0.75rem'
230
- });
231
250
  body.append(commandCode);
232
251
  if (releaseUrl) {
233
252
  const link = options.document.createElement('a');
253
+ link.className = 'codex-native-guidance-link';
234
254
  link.href = releaseUrl;
235
255
  link.target = '_blank';
236
256
  link.rel = 'noopener noreferrer';
237
257
  link.textContent = tx('Open GitHub Release', '打开 GitHub Release');
238
- Object.assign(link.style, { display: 'block', marginTop: '0.75rem' });
239
258
  body.append(link);
240
259
  }
241
260
  const actions = options.document.createElement('div');
@@ -256,7 +275,7 @@
256
275
  close.textContent = tx('Close', '关闭');
257
276
  close.addEventListener('click', () => overlay.remove());
258
277
  actions.append(close, copy);
259
- card.append(title, body, actions);
278
+ card.append(head, body, actions);
260
279
  overlay.append(card);
261
280
  overlay.addEventListener('click', event => {
262
281
  if (event.target === overlay) overlay.remove();
@@ -895,7 +895,7 @@
895
895
  appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
896
896
  }
897
897
  appendCompletionReport({
898
- conclusion: tr('warmMirrorStaleBlockedConclusion'),
898
+ conclusion: tr('warmMirrorStaleBlockedConclusion'), conclusionKey: 'warm_mirror_stale',
899
899
  status: 'blocked',
900
900
  operations: [],
901
901
  applyResults: [],
@@ -6,7 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  const PAGE_WORLD_SCRIPTS = Object.freeze([
9
- ['src/shared/reviewing.js', 'codex-overleaf-reviewing-script'],
9
+ ['src/shared/reviewing.js', 'codex-overleaf-reviewing-script', true],
10
10
  ['src/shared/projectFiles.js', 'codex-overleaf-project-files-script'],
11
11
  ['src/shared/compatibility.js', 'codex-overleaf-compatibility-page-script'],
12
12
  ['src/shared/pageRpcContract.js', 'codex-overleaf-page-rpc-contract-script', true],
@@ -15,7 +15,7 @@
15
15
  ['src/shared/sensitiveScan.js', 'codex-overleaf-sensitive-scan-script'],
16
16
  ['src/shared/auditRecords.js', 'codex-overleaf-audit-records-script'],
17
17
  ['src/page/saveState.js', 'codex-overleaf-save-state-script', true],
18
- ['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script'],
18
+ ['src/page/overleafCapabilities.js', 'codex-overleaf-capabilities-script', true],
19
19
  ['src/page/compileBridge.js', 'codex-overleaf-compile-bridge-script'],
20
20
  ['src/page/overleafEditor.js', 'codex-overleaf-editor-script'],
21
21
  ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
@@ -181,6 +181,7 @@
181
181
  // the failed task usually needs a tweak before it is worth resending.
182
182
  function refillComposerForRetry(run, textOverride) {
183
183
  const input = getPanel()?.querySelector('[data-task]');
184
+ if (input?.readOnly) return;
184
185
  const task = typeof textOverride === 'string' && textOverride.trim()
185
186
  ? textOverride
186
187
  : String(run?.task || '');
@@ -310,7 +311,9 @@
310
311
  // Settings history card, expanded and loaded.
311
312
  function openChangeHistory() {
312
313
  openCustomInstructionsSettings();
313
- const card = getSettingsPanelInstance()?.container?.querySelector('[data-history-card]');
314
+ // The workbench shows one page at a time; switch to the page holding the card.
315
+ const card = getSettingsPanelInstance()?.revealGroup?.('history')
316
+ || getSettingsPanelInstance()?.container?.querySelector('[data-history-card]');
314
317
  if (card) {
315
318
  if (!card.open) {
316
319
  card.open = true;
@@ -322,7 +325,8 @@
322
325
 
323
326
  function openStorageSettings() {
324
327
  openCustomInstructionsSettings();
325
- const card = getPanel()?.querySelector('[data-storage-card]');
328
+ const card = getSettingsPanelInstance()?.revealGroup?.('storage')
329
+ || getPanel()?.querySelector('[data-storage-card]');
326
330
  if (card) {
327
331
  card.open = true;
328
332
  card.scrollIntoView({ block: 'center' });
@@ -12,6 +12,7 @@
12
12
  plus: '<path d="M8 3.2v9.6"/><path d="M3.2 8h9.6"/>',
13
13
  panel: '<rect x="2.3" y="2.3" width="11.4" height="11.4" rx="2"/><path d="M9.2 2.5v11"/><path d="M5.1 6.1h1.5M5.1 8h1.5M5.1 9.9h1.5"/>',
14
14
  collapse: '<path d="m6.1 3.3 4.7 4.7-4.7 4.7"/>',
15
+ chevronDown: '<path d="m4.4 6.4 3.6 3.6 3.6-3.6"/>',
15
16
  settings: '<path d="M3.4 5.2h5.9"/><path d="M11.7 5.2h.9"/><path d="M10.1 3.6v3.2"/><path d="M3.4 10.8h1"/><path d="M6.8 10.8h5.8"/><path d="M5.9 9.2v3.2"/>'
16
17
  };
17
18
  const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : 'settings';
@@ -53,8 +54,15 @@
53
54
  <div class="codex-panel-resize-handle" data-panel-resize-handle title="Drag to resize the Codex panel. Double click to reset." aria-label="Drag to resize the Codex panel. Double click to reset." role="separator"></div>
54
55
  <div class="codex-vscode-head" data-panel-header>
55
56
  <div class="codex-vscode-title">CODEX</div>
57
+ <div class="codex-thread-title codex-head-session" data-session-label>
58
+ <button type="button" class="codex-thread-title-trigger" data-session-menu-trigger aria-haspopup="menu" aria-expanded="false">
59
+ <span class="codex-thread-title-text" data-session-label-text></span>
60
+ <span class="codex-thread-title-chevron">${codexIcon('chevronDown')}</span>
61
+ </button>
62
+ <input type="text" class="codex-thread-title-input" data-session-rename-input maxlength="80" hidden>
63
+ <div class="codex-session-menu" data-session-menu role="menu" hidden></div>
64
+ </div>
56
65
  <div class="codex-vscode-head-actions" aria-label="Codex actions">
57
- <button type="button" data-refresh title="Refresh current file status. This will not sync or modify files." aria-label="Refresh current file status. This will not sync or modify files.">${codexIcon('refresh')}</button>
58
66
  <div data-diagnostics-slot></div>
59
67
  <button type="button" data-new-session title="New Session" aria-label="New Session">${codexIcon('plus')}</button>
60
68
  <button type="button" data-custom-instructions-settings title="Project Settings" aria-label="Project Settings" aria-expanded="false">${codexIcon('settings')}</button>
@@ -86,7 +94,6 @@
86
94
 
87
95
  bind(instance, panelEl, 'click', event => event.stopPropagation());
88
96
  bind(instance, panelEl, 'mousedown', event => event.stopPropagation());
89
- bind(instance, panelEl.querySelector('[data-refresh]'), 'click', () => callbacks.onRefresh?.());
90
97
  bind(instance, panelEl.querySelector('[data-new-session]'), 'click', () => callbacks.onNewSession?.());
91
98
  bind(instance, panelEl.querySelector('[data-custom-instructions-settings]'), 'click', () => callbacks.onSettingsClick?.());
92
99
  bind(instance, panelEl.querySelector('[data-close-panel]'), 'click', () => callbacks.onClosePanel?.());
@@ -320,7 +327,7 @@
320
327
  }
321
328
  const panel = options.getPanel?.();
322
329
  if (!panel) {
323
- return Promise.resolve(false);
330
+ return Promise.resolve(config.choices?.length ? 'cancel' : false);
324
331
  }
325
332
  dismiss(false);
326
333
  const tr = options.tr || (key => key);
@@ -329,6 +336,8 @@
329
336
  const confirmLabel = config.confirmLabel || tr('confirmDefaultConfirm');
330
337
  const cancelLabel = config.cancelLabel || tr('confirmDefaultCancel');
331
338
  const destructive = config.destructive === true;
339
+ const choices = Array.isArray(config.choices) && config.choices.length ? config.choices : null;
340
+ const cancelled = choices ? 'cancel' : false;
332
341
  const doc = panel.ownerDocument || document;
333
342
 
334
343
  return new Promise(resolve => {
@@ -369,54 +378,69 @@
369
378
  body.textContent = String(message);
370
379
  const actions = doc.createElement('div');
371
380
  actions.className = 'codex-plugin-confirm-actions';
372
- const cancel = doc.createElement('button');
373
- cancel.type = 'button';
374
- cancel.className = 'codex-plugin-confirm-cancel';
375
- cancel.textContent = cancelLabel;
376
- const confirm = doc.createElement('button');
377
- confirm.type = 'button';
378
- confirm.className = 'codex-plugin-confirm-confirm';
379
- if (destructive) confirm.dataset.destructive = 'true';
380
- confirm.textContent = confirmLabel;
381
- actions.append(cancel, confirm);
381
+ const buttons = (choices || [
382
+ { value: false, label: cancelLabel }, { value: true, label: confirmLabel }
383
+ ]).map(choice => {
384
+ const button = doc.createElement('button');
385
+ button.type = 'button';
386
+ const cancel = choice.value === false || choice.value === 'cancel';
387
+ // Only one primary action: other affirmative choices can ask for the secondary style.
388
+ button.className = cancel || choice.secondary === true ? 'codex-plugin-confirm-cancel' : 'codex-plugin-confirm-confirm';
389
+ if (destructive && !cancel) button.dataset.destructive = 'true';
390
+ button.textContent = choice.label;
391
+ button.addEventListener('click', () => cleanup(choice.value));
392
+ actions.append(button);
393
+ return button;
394
+ });
382
395
  card.append(head, body, actions);
383
396
  overlay.append(card);
384
397
  panel.append(overlay);
385
398
  overlay.showModal?.();
386
399
 
400
+ let timeout, ownerCheck, removalObserver;
401
+ const onAbort = () => cleanup(cancelled);
387
402
  const cleanup = value => {
388
403
  if (settled) return;
389
404
  settled = true;
390
405
  activeResolve = null;
391
406
  doc.removeEventListener('keydown', onKeydown, true);
407
+ config.signal?.removeEventListener('abort', onAbort);
408
+ clearTimeout(timeout);
409
+ clearInterval(ownerCheck);
410
+ removalObserver?.disconnect();
392
411
  overlay.close?.();
393
412
  overlay.remove();
394
413
  returnFocus?.focus?.();
395
- resolve(value);
414
+ resolve(choices && !choices.some(choice => choice.value === value) ? cancelled : value);
396
415
  };
397
416
  activeResolve = cleanup;
398
417
  const onKeydown = event => {
399
418
  if (event.key === 'Escape') {
400
- event.preventDefault();
401
- cleanup(false);
419
+ event.preventDefault(); cleanup(cancelled);
402
420
  } else if (event.key === 'Enter') {
403
421
  event.preventDefault();
404
- if (!destructive && event.target !== cancel) cleanup(true);
405
- else if (event.target === cancel) cleanup(false);
406
- else if (event.target === confirm) cleanup(true);
422
+ if (buttons.includes(event.target)) event.target.click();
423
+ else if (!choices && !destructive) cleanup(true);
407
424
  } else if (event.key === 'Tab') {
408
425
  event.preventDefault();
409
- (event.target === cancel ? confirm : cancel).focus();
426
+ const index = buttons.indexOf(event.target);
427
+ buttons[(index + (event.shiftKey ? buttons.length - 1 : 1)) % buttons.length].focus();
410
428
  }
411
429
  };
412
- overlay.addEventListener('click', event => {
413
- if (event.target === overlay) cleanup(false);
414
- });
415
- overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(false); });
416
- cancel.addEventListener('click', () => cleanup(false));
417
- confirm.addEventListener('click', () => cleanup(true));
430
+ overlay.addEventListener('click', event => { if (event.target === overlay) cleanup(cancelled); });
431
+ overlay.addEventListener('cancel', event => { event.preventDefault(); cleanup(cancelled); });
432
+ overlay.addEventListener('close', () => cleanup(cancelled));
418
433
  doc.addEventListener('keydown', onKeydown, true);
419
- (destructive ? cancel : confirm).focus();
434
+ config.signal?.addEventListener('abort', onAbort, { once: true });
435
+ if (config.timeoutMs > 0) timeout = setTimeout(onAbort, config.timeoutMs);
436
+ if (config.isCurrent) ownerCheck = setInterval(() => { if (!config.isCurrent()) onAbort(); }, 100);
437
+ const Observer = doc.defaultView?.MutationObserver;
438
+ if (choices && Observer) {
439
+ removalObserver = new Observer(() => { if (!overlay.isConnected) onAbort(); });
440
+ removalObserver.observe(panel, { childList: true });
441
+ }
442
+ (choices ? buttons[0] : buttons[destructive ? 0 : 1]).focus();
443
+ if (config.signal?.aborted || config.isCurrent?.() === false) onAbort();
420
444
  });
421
445
  }
422
446
 
@@ -115,7 +115,7 @@
115
115
  // Pull a queued message back into the composer. A typed draft is never overwritten.
116
116
  function pullBack(item) {
117
117
  const task = container?.parentElement?.querySelector('[data-task]');
118
- if (!task || (item.status || 'queued') !== 'queued') return;
118
+ if (!task || task.readOnly || (item.status || 'queued') !== 'queued') return;
119
119
  if (String(task.value || '').trim()) { options.onToast?.(tr('queuedInputEditBlocked')); return; }
120
120
  options.onRemove?.(item.id);
121
121
  task.value = item.text;
@@ -1,6 +1,9 @@
1
1
  (function initCodexOverleafProjectSettingsCoordinator() {
2
2
  'use strict';
3
3
 
4
+ // Skill messages belong to the settings workbench page that holds the Skills group.
5
+ const SKILLS_STATUS = Object.freeze({ page: 'context' });
6
+
4
7
  function create(deps = {}) {
5
8
  const {
6
9
  CodexOverleafTheme,
@@ -100,7 +103,7 @@
100
103
  refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
101
104
  `Could not list local skills: ${error.message}`,
102
105
  `无法列出本地技能:${error.message}`
103
- ), 'failed'));
106
+ ), 'failed', SKILLS_STATUS));
104
107
  }
105
108
 
106
109
  function toggleCustomInstructionsSettings() {
@@ -128,7 +131,7 @@
128
131
  refreshLocalSkills().catch(error => setProjectSettingsStatus(tx(
129
132
  `Could not list local skills: ${error.message}`,
130
133
  `无法列出本地技能:${error.message}`
131
- ), 'failed'));
134
+ ), 'failed', SKILLS_STATUS));
132
135
  }
133
136
 
134
137
  function closeSkillsView() {
@@ -242,6 +245,25 @@
242
245
  });
243
246
  }
244
247
 
248
+ function getReviewingAvailability() {
249
+ return getState()?.reviewingAvailabilityByProject?.[getCurrentProjectId()] || null;
250
+ }
251
+
252
+ function observeReviewingAvailability(status, evidence) {
253
+ const projectId = getCurrentProjectId();
254
+ if (!projectId || !['available', 'unavailable', 'unknown'].includes(status)) return;
255
+ const previous = getReviewingAvailability();
256
+ // A remembered failure must not renew its own expiry.
257
+ if (previous?.status === status && previous.evidence === evidence && status === 'available') return;
258
+ // Preserve ordering even when a retry and its previous observation share a millisecond.
259
+ const observedAt = Math.max(Date.now(), Number.isFinite(previous?.observedAt) ? previous.observedAt + 1 : 0);
260
+ setState({ ...getState(), reviewingAvailabilityByProject: {
261
+ ...getState()?.reviewingAvailabilityByProject,
262
+ [projectId]: { status, evidence, observedAt }
263
+ } });
264
+ saveStateSoon(0);
265
+ }
266
+
245
267
  function getThemePreference() {
246
268
  return CodexOverleafTheme.normalizeThemePreference(getState()?.theme);
247
269
  }
@@ -316,11 +338,11 @@
316
338
  const pending = deps.updateGlobalPreferences({ codexLocalSkillEnabled: { [skillId]: Boolean(enabled) } });
317
339
  Promise.resolve(pending).then(() => saveStateSoon()).catch(() => {
318
340
  renderLocalSkillList();
319
- setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
341
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
320
342
  });
321
343
  } catch (_error) {
322
344
  renderLocalSkillList();
323
- setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed');
345
+ setProjectSettingsStatus(tx('Could not save the local skill selection. Try again.', '无法保存本地技能选择,请重试。'), 'failed', SKILLS_STATUS);
324
346
  }
325
347
  } else {
326
348
  setState({ ...getState(), codexLocalSkillEnabled: { ...getCodexLocalSkillEnabled(), [skillId]: Boolean(enabled) } });
@@ -348,8 +370,8 @@
348
370
  renderLocalSkillList();
349
371
  }
350
372
 
351
- function setProjectSettingsStatus(text, status = 'info') {
352
- SettingsPanel.setStatus(getSettingsPanelInstance(), text, status);
373
+ function setProjectSettingsStatus(text, status = 'info', options) {
374
+ SettingsPanel.setStatus(getSettingsPanelInstance(), text, status, options);
353
375
  }
354
376
 
355
377
  function clearProjectSettingsStatus() {
@@ -378,6 +400,8 @@
378
400
  getGovernanceRulesForCurrentProject,
379
401
  getSkillLoadingSettings,
380
402
  getThemePreference,
403
+ getReviewingAvailability,
404
+ observeReviewingAvailability,
381
405
  isCodexOverleafSkillEnabled,
382
406
  normalizeCustomInstructionsByProject,
383
407
  normalizeGovernanceRules,
@@ -1296,7 +1296,8 @@
1296
1296
 
1297
1297
  function renderTemplatePicker(instance, detail) {
1298
1298
  const tx = instance.tx;
1299
- detail.innerHTML = '<div class="codex-provider-template-head"><button type="button" class="codex-provider-inline-button" data-provider-action="templates-back">'
1299
+ detail.innerHTML = '<div class="codex-provider-template-head"><button type="button" class="codex-provider-inline-button codex-provider-back-button" data-provider-action="templates-back">'
1300
+ + '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m9.8 3.6-4.4 4.4 4.4 4.4"/></svg>'
1300
1301
  + escapeHtml(tx('Back', '返回')) + '</button><h3>' + escapeHtml(tx('Add a connection', '添加连接')) + '</h3></div>'
1301
1302
  + '<p class="codex-provider-help">' + escapeHtml(tx(
1302
1303
  'Service shortcuts default to Anthropic Messages. Chat Completions remains available; preset addresses follow the selected protocol. Add the matching API key and model IDs separately.',
@@ -199,10 +199,11 @@
199
199
  } catch (_error) { /* swallow */ }
200
200
  try {
201
201
  var docTitle = (document.title || '').trim();
202
- // Overleaf typically formats as "<Project Name> - Overleaf" or
203
- // "<Project Name> - Online LaTeX Editor"; strip the " - " suffix.
202
+ // Overleaf formats the title as "<Project Name> - Overleaf" or
203
+ // "<Project Name> - Online LaTeX Editor Overleaf" (localized); drop the
204
+ // last " - " segment when it names Overleaf.
204
205
  if (docTitle) {
205
- var stripped = docTitle.replace(/\s*[-–]\s*Overleaf.*$/i, '').trim();
206
+ var stripped = docTitle.replace(/\s+[-–|]\s+[^-–|]*Overleaf[^-–|]*$/i, '').trim();
206
207
  if (stripped && stripped.toLowerCase() !== 'overleaf') {
207
208
  return stripped;
208
209
  }