codex-overleaf-link 2.1.4 → 2.2.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 (69) 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 +11 -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 +19 -6
  9. package/extension/src/content/contentRuntime.js +335 -317
  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/otWarmMirror.js +1 -1
  17. package/extension/src/content/panelRenderer.js +112 -12
  18. package/extension/src/content/pendingInputView.js +106 -0
  19. package/extension/src/content/projectProviderSelection.js +68 -0
  20. package/extension/src/content/projectSessionCleanup.js +104 -0
  21. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  22. package/extension/src/content/providerSettingsDialog.js +2 -2
  23. package/extension/src/content/recentProjects.js +24 -13
  24. package/extension/src/content/runController.js +2 -2
  25. package/extension/src/content/runGuidanceController.js +70 -0
  26. package/extension/src/content/runGuidanceView.js +48 -0
  27. package/extension/src/content/runQueueScheduler.js +260 -0
  28. package/extension/src/content/runTimelineView.js +33 -80
  29. package/extension/src/content/sessionManager.js +2 -2
  30. package/extension/src/content/sessionPersistence.js +62 -0
  31. package/extension/src/content/settingsPanel.js +1 -1
  32. package/extension/src/content/updateNotice.js +80 -1
  33. package/extension/src/page/writebackRouter.js +3 -9
  34. package/extension/src/shared/compatibility.js +2 -1
  35. package/extension/src/shared/i18n.js +54 -0
  36. package/extension/src/shared/providerProfiles.js +3 -2
  37. package/extension/src/shared/runInputQueue.js +253 -0
  38. package/extension/src/shared/sessionState.js +45 -22
  39. package/extension/src/shared/storageDb.js +39 -44
  40. package/extension/src/shared/storageMigration.js +123 -3
  41. package/extension/src/shared/storageRunActions.js +34 -1
  42. package/extension/src/shared/updateConsent.js +15 -4
  43. package/extension/styles/panel.css +370 -23
  44. package/extension/vendor/katex/katex.min.css +1 -1
  45. package/extension/vendor/markdown-it/LICENSE +22 -0
  46. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  47. package/extension/vendor/markdown-it/VERSION +1 -0
  48. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  49. package/native-host/src/anthropicBridgeRequest.js +3 -3
  50. package/native-host/src/anthropicBridgeResponse.js +58 -17
  51. package/native-host/src/anthropicMessagesBridge.js +156 -23
  52. package/native-host/src/chatBridgeRequest.js +1 -1
  53. package/native-host/src/chatBridgeResponse.js +12 -8
  54. package/native-host/src/chatCompletionsBridge.js +22 -40
  55. package/native-host/src/codexPromptAssembly.js +3 -1
  56. package/native-host/src/codexProviderLaunch.js +6 -3
  57. package/native-host/src/codexSessionRunner.js +66 -66
  58. package/native-host/src/codexSessionTiming.js +45 -0
  59. package/native-host/src/diffEngine.js +10 -5
  60. package/native-host/src/index.js +58 -3
  61. package/native-host/src/managedInstall.js +45 -7
  62. package/native-host/src/providerProfile.js +2 -2
  63. package/native-host/src/providerStreamLifecycle.js +100 -0
  64. package/native-host/src/responsesPassthroughBridge.js +636 -0
  65. package/native-host/src/taskRunnerRuntime.js +81 -2
  66. package/native-host/src/updateArchive.js +4 -3
  67. package/native-host/src/updateManager.js +306 -19
  68. package/native-host/src/updateTrust.js +1 -1
  69. package/package.json +3 -1
@@ -0,0 +1,104 @@
1
+ (function initCodexOverleafProjectSessionCleanup() {
2
+ 'use strict';
3
+
4
+ const FRESH_RUNNING_WINDOW_MS = 30 * 60 * 1000;
5
+
6
+ function create(deps = {}) {
7
+ const {
8
+ tr,
9
+ getAccountScopeId,
10
+ showPluginConfirm,
11
+ showPluginToast,
12
+ sendBackgroundNative,
13
+ mutateProjectPanelState,
14
+ renderRecentProjectsVariant
15
+ } = deps;
16
+
17
+ function isFreshRunningRecord(StorageDb, record) {
18
+ if (StorageDb.derivePrimaryStatusBadge(record) !== 'running') {
19
+ return false;
20
+ }
21
+ const touched = Date.parse(record.updatedAt || record.lastActivityAt || record.createdAt || '');
22
+ return !Number.isFinite(touched) || (Date.now() - touched) <= FRESH_RUNNING_WINDOW_MS;
23
+ }
24
+
25
+ async function clearProjectSessions(projectId, projectName) {
26
+ const StorageDb = window.CodexOverleafStorageDb;
27
+ const SessionState = window.CodexOverleafSessionState;
28
+ if (!StorageDb || !SessionState) {
29
+ return;
30
+ }
31
+ const scope = getAccountScopeId();
32
+ const records = (await StorageDb.getAllByIndex('sessions', 'projectId', projectId))
33
+ .filter(record => record && (!scope || record.accountScopeId === scope));
34
+ if (!records.length) {
35
+ await renderRecentProjectsVariant();
36
+ return;
37
+ }
38
+ if (records.some(record => isFreshRunningRecord(StorageDb, record))) {
39
+ showPluginToast(tr('recentProjects_clearProject_running'), { status: 'warning', sticky: true });
40
+ return;
41
+ }
42
+ const approved = await showPluginConfirm({
43
+ title: tr('recentProjects_clearProject_title', { project: projectName }),
44
+ message: tr('recentProjects_clearProject_message', { count: String(records.length), project: projectName }),
45
+ confirmLabel: tr('recentProjects_clearProject_confirm'),
46
+ cancelLabel: tr('confirmDefaultCancel'),
47
+ destructive: true
48
+ });
49
+ if (!approved) {
50
+ return;
51
+ }
52
+ const Migration = window.CodexOverleafStorageMigration;
53
+ if (Migration?.addSessionTombstones) {
54
+ await Migration.addSessionTombstones(projectId, records.map(record => record.id));
55
+ }
56
+ try {
57
+ await mutateProjectPanelState(projectId, state => records.reduce(
58
+ (nextState, record) => SessionState.deleteSession(nextState, record.id),
59
+ state
60
+ ));
61
+ } catch (_error) { /* IndexedDB deletion remains authoritative for the dashboard. */ }
62
+
63
+ let removed = 0;
64
+ let historyFailures = 0;
65
+ for (const record of records) {
66
+ try {
67
+ await StorageDb.deleteRecord('sessions', record.id);
68
+ removed++;
69
+ } catch (_error) { /* The remaining record stays visible and can be retried. */ }
70
+ try {
71
+ const response = await sendBackgroundNative({
72
+ method: 'codex.history.clearPlugin',
73
+ params: { sessionId: record.id, threadId: record.codexThreadId || '' }
74
+ });
75
+ if (!response || !response.ok) {
76
+ historyFailures++;
77
+ }
78
+ } catch (_error) {
79
+ historyFailures++;
80
+ }
81
+ }
82
+ if (removed < records.length) {
83
+ showPluginToast(tr('recentProjects_clearProject_partial', {
84
+ removed: String(removed),
85
+ count: String(records.length)
86
+ }), { status: 'warning', sticky: true });
87
+ } else if (historyFailures) {
88
+ showPluginToast(tr('recentProjects_clearProject_historyPartial', {
89
+ count: String(historyFailures)
90
+ }), { status: 'warning', sticky: true });
91
+ } else {
92
+ showPluginToast(tr('recentProjects_clearProject_done', {
93
+ count: String(removed),
94
+ project: projectName
95
+ }), { status: 'completed' });
96
+ }
97
+ await renderRecentProjectsVariant();
98
+ }
99
+
100
+ return { clearProjectSessions };
101
+ }
102
+
103
+ window.CodexOverleafProjectSessionCleanup = { create };
104
+ })();
@@ -10,6 +10,7 @@
10
10
  getSelectedModel: options.getSelectedModel || (() => ''),
11
11
  getSelectedProviderId: options.getSelectedProviderId || (() => 'builtin'),
12
12
  setSelectedProviderId: options.setSelectedProviderId || (() => {}),
13
+ confirmProviderSwitch: options.confirmProviderSwitch || (() => true),
13
14
  clearSelectedModel: options.clearSelectedModel || (() => {}),
14
15
  refreshModelOptions: options.refreshModelOptions || (() => {}),
15
16
  persistInputs: options.persistInputs || (() => {}),
@@ -38,7 +39,7 @@
38
39
  setupCrossTabRefresh(instance, options.window || window);
39
40
  return {
40
41
  open: () => open(instance),
41
- refreshSummary: () => refresh(instance),
42
+ refreshSummary: () => refreshSummary(instance),
42
43
  ensureLoaded: () => ensureLoaded(instance),
43
44
  getRunSelection: providerId => instance.loaded
44
45
  ? Profiles.buildRunSelection(instance.catalog, providerId || instance.getSelectedProviderId())
@@ -93,9 +94,45 @@
93
94
  return instance.catalog;
94
95
  }
95
96
 
97
+ async function ensureLoadedWithRetry(instance) {
98
+ try {
99
+ return await ensureLoaded(instance);
100
+ } catch (_firstError) {
101
+ // Extension reloads can race the first Native Messaging connection.
102
+ // Keep the persisted project selection visible, then retry once after
103
+ // the service worker and Native Host have had a chance to reconnect.
104
+ updateSettingsSummary(instance);
105
+ await new Promise(resolve => setTimeout(resolve, 800));
106
+ try {
107
+ const result = await refresh(instance);
108
+ return result?.catalog || instance.catalog;
109
+ } catch (error) {
110
+ updateSettingsSummary(instance);
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+
116
+ async function refreshSummary(instance) {
117
+ // Project state hydrates before the Native Host catalog is guaranteed to
118
+ // be ready. Project the persisted selection immediately so Settings never
119
+ // claims Built-in Codex (or stays on a loading placeholder) while the
120
+ // authoritative catalog request is still pending.
121
+ updateSettingsSummary(instance);
122
+ try {
123
+ await ensureLoadedWithRetry(instance);
124
+ } catch (_error) {
125
+ // This card describes the project's saved selection. Native connection
126
+ // failures are surfaced when Configure is opened, without replacing the
127
+ // selection with a misleading Built-in/unavailable state.
128
+ }
129
+ updateSettingsSummary(instance);
130
+ return instance.catalog;
131
+ }
132
+
96
133
  async function syncSessionProvider(instance) {
97
134
  try {
98
- await ensureLoaded(instance);
135
+ await ensureLoadedWithRetry(instance);
99
136
  } catch (_error) {
100
137
  return instance.refreshModelOptions(
101
138
  window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, instance.getSelectedProviderId())
@@ -109,22 +146,78 @@
109
146
  const requestedProviderId = typeof change.sessionProviderId === 'string' && change.sessionProviderId
110
147
  ? change.sessionProviderId
111
148
  : previousProviderId;
112
- const sessionProviderChanged = requestedProviderId !== previousProviderId;
113
- if (sessionProviderChanged) {
149
+ const projectProviderChanged = requestedProviderId !== previousProviderId;
150
+ if (projectProviderChanged && change.providerSwitchApproved !== true) {
114
151
  const requestedProvider = window.CodexOverleafProviderProfiles.getProviderById(catalog, requestedProviderId);
115
- const preferredModelId = requestedProvider?.kind === 'custom'
116
- ? requestedProvider.defaultModelId || requestedProvider.models?.[0]?.id || ''
117
- : '';
118
- instance.setSelectedProviderId(requestedProviderId, preferredModelId);
152
+ const approved = await instance.confirmProviderSwitch({
153
+ providerId: requestedProviderId,
154
+ providerName: requestedProvider?.name || requestedProviderId
155
+ });
156
+ if (!approved) {
157
+ return { cancelled: true };
158
+ }
119
159
  }
120
160
  const changedProviderIds = Array.isArray(change.changedProviderIds) ? change.changedProviderIds : [];
121
- if (!change.forceSessionRefresh && !sessionProviderChanged && !changedProviderIds.includes(requestedProviderId)) {
161
+ if (!change.forceSessionRefresh && !projectProviderChanged && !changedProviderIds.includes(requestedProviderId)) {
122
162
  return;
123
163
  }
124
- await instance.refreshModelOptions(
125
- window.CodexOverleafProviderProfiles.buildRunSelection(catalog, requestedProviderId)
126
- );
164
+ const loadResult = change.modelsPreloaded === true
165
+ ? { stale: false, selectedModel: change.preloadedModelId || '' }
166
+ : await instance.refreshModelOptions(
167
+ window.CodexOverleafProviderProfiles.buildRunSelection(catalog, requestedProviderId),
168
+ { persist: false }
169
+ );
170
+ if (loadResult?.stale || loadResult?.error) {
171
+ const error = createClientError(
172
+ loadResult?.error?.code || 'provider_model_catalog_unavailable',
173
+ loadResult?.error?.message || 'The provider model catalog could not be loaded.'
174
+ );
175
+ error.details = loadResult?.error || {};
176
+ throw error;
177
+ }
178
+ if (projectProviderChanged) {
179
+ await instance.setSelectedProviderId(requestedProviderId, loadResult?.selectedModel || '');
180
+ }
127
181
  await instance.persistInputs();
182
+ updateSettingsSummary(instance);
183
+ return { providerId: requestedProviderId, selectedModel: loadResult?.selectedModel || '' };
184
+ }
185
+
186
+ async function prepareProviderActivation(instance, providerId) {
187
+ const previousProviderId = instance.getSelectedProviderId() || 'builtin';
188
+ const provider = window.CodexOverleafProviderProfiles.getProviderById(instance.catalog, providerId);
189
+ if (providerId !== previousProviderId) {
190
+ const approved = await instance.confirmProviderSwitch({
191
+ providerId,
192
+ providerName: provider?.name || providerId
193
+ });
194
+ if (!approved) {
195
+ return { cancelled: true, previousProviderId };
196
+ }
197
+ }
198
+ const result = await instance.refreshModelOptions(
199
+ window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, providerId),
200
+ { persist: false }
201
+ );
202
+ if (result?.stale || result?.error) {
203
+ if (providerId !== previousProviderId) {
204
+ await instance.refreshModelOptions(
205
+ window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, previousProviderId),
206
+ { persist: false }
207
+ ).catch(() => {});
208
+ }
209
+ const error = createClientError(
210
+ result?.error?.code || 'provider_model_catalog_unavailable',
211
+ result?.error?.message || 'The provider model catalog could not be loaded.'
212
+ );
213
+ error.details = result?.error || {};
214
+ throw error;
215
+ }
216
+ return {
217
+ cancelled: false,
218
+ previousProviderId,
219
+ selectedModel: result?.selectedModel || ''
220
+ };
128
221
  }
129
222
 
130
223
  async function testProvider(instance, context) {
@@ -264,8 +357,13 @@
264
357
  }
265
358
 
266
359
  async function activateProvider(instance, context) {
267
- instance.dialog.setBusy('saving', instance.tx('Activating provider…', '正在启用模型服务…'));
360
+ let preparation;
268
361
  try {
362
+ preparation = await prepareProviderActivation(instance, context.profileId);
363
+ if (preparation.cancelled) {
364
+ return;
365
+ }
366
+ instance.dialog.setBusy('saving', instance.tx('Activating provider…', '正在启用模型服务…'));
269
367
  const result = await request(instance, 'codex.providers.activate', {
270
368
  providerId: context.profileId,
271
369
  expectedRevision: context.expectedRevision,
@@ -276,30 +374,59 @@
276
374
  updateDialog: true,
277
375
  selectedId: context.profileId
278
376
  });
279
- await notifyChanged(instance, applied.change, { sessionProviderId: context.profileId });
377
+ await notifyChanged(instance, applied.change, {
378
+ sessionProviderId: context.profileId,
379
+ providerSwitchApproved: true,
380
+ modelsPreloaded: true,
381
+ preloadedModelId: preparation.selectedModel,
382
+ requireProjection: true
383
+ });
280
384
  instance.dialog.setBusy('', '');
281
385
  instance.dialog.setStatus({
282
386
  tone: 'success',
283
387
  title: instance.tx('Provider activated.', '模型服务已启用。')
284
388
  });
285
389
  } catch (error) {
390
+ if (preparation?.previousProviderId && preparation.previousProviderId !== context.profileId) {
391
+ await instance.refreshModelOptions(
392
+ window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
393
+ { persist: false }
394
+ ).catch(() => {});
395
+ }
286
396
  instance.dialog.setBusy('failed', formatError(instance, error));
287
397
  instance.dialog.setStatus({ tone: 'failed', title: formatError(instance, error) });
288
398
  }
289
399
  }
290
400
 
291
401
  async function activateBuiltin(instance) {
292
- instance.dialog.setBusy('saving', instance.tx('Activating built-in Codex…', '正在启用内置 Codex…'));
402
+ let preparation;
293
403
  try {
404
+ preparation = await prepareProviderActivation(instance, 'builtin');
405
+ if (preparation.cancelled) {
406
+ return;
407
+ }
408
+ instance.dialog.setBusy('saving', instance.tx('Activating built-in Codex…', '正在启用内置 Codex…'));
294
409
  const result = await request(instance, 'codex.providers.activate', {
295
410
  providerId: 'builtin',
296
411
  expectedRevision: 0
297
412
  });
298
413
  const applied = applyCatalog(instance, result, { updateDialog: true, selectedId: 'builtin' });
299
- await notifyChanged(instance, applied.change, { sessionProviderId: 'builtin' });
414
+ await notifyChanged(instance, applied.change, {
415
+ sessionProviderId: 'builtin',
416
+ providerSwitchApproved: true,
417
+ modelsPreloaded: true,
418
+ preloadedModelId: preparation.selectedModel,
419
+ requireProjection: true
420
+ });
300
421
  instance.dialog.setBusy('', '');
301
422
  instance.dialog.setStatus({ tone: 'success', title: instance.tx('Built-in Codex is active.', '已启用内置 Codex。') });
302
423
  } catch (error) {
424
+ if (preparation?.previousProviderId && preparation.previousProviderId !== 'builtin') {
425
+ await instance.refreshModelOptions(
426
+ window.CodexOverleafProviderProfiles.buildRunSelection(instance.catalog, preparation.previousProviderId),
427
+ { persist: false }
428
+ ).catch(() => {});
429
+ }
303
430
  instance.dialog.setBusy('failed', formatError(instance, error));
304
431
  }
305
432
  }
@@ -340,13 +467,31 @@
340
467
  }
341
468
 
342
469
  function updateSettingsSummary(instance, override = {}) {
343
- const active = window.CodexOverleafProviderProfiles.getActiveProvider(instance.catalog);
344
- const summary = override.summary || (active.kind === 'builtin'
345
- ? instance.tx('Built-in Codex · default for new sessions', '内置 Codex · 新会话默认')
346
- : `${active.name} · ${active.defaultModelId || instance.tx('No model', '未配置模型')} · ${instance.tx('Default for new sessions', '新会话默认')}`);
470
+ const selectedProviderId = instance.getSelectedProviderId() || 'builtin';
471
+ const catalogProvider = window.CodexOverleafProviderProfiles.getProviderById(
472
+ instance.catalog,
473
+ selectedProviderId
474
+ );
475
+ const selectedProvider = catalogProvider?.id === selectedProviderId
476
+ ? catalogProvider
477
+ : (selectedProviderId === 'builtin'
478
+ ? window.CodexOverleafProviderProfiles.getActiveProvider(instance.catalog)
479
+ : {
480
+ id: selectedProviderId,
481
+ name: selectedProviderId,
482
+ kind: 'custom',
483
+ defaultModelId: instance.getSelectedModel() || '',
484
+ wireApiPreference: '',
485
+ resolvedWireApi: ''
486
+ });
487
+ const summary = override.summary || (selectedProvider.kind === 'builtin'
488
+ ? instance.tx('Built-in Codex · current project', '内置 Codex · 当前项目')
489
+ : `${selectedProvider.name} · ${instance.getSelectedModel() || selectedProvider.defaultModelId || instance.tx('No model', '未配置模型')} · ${instance.tx('Current project', '当前项目')}`);
347
490
  instance.getSettingsPanelInstance()?.setProviderSummary?.({
348
491
  summary,
349
- tone: override.tone || (active.kind === 'custom' && active.wireApiPreference === 'auto' && !active.resolvedWireApi ? 'warning' : 'ok')
492
+ tone: override.tone || (selectedProvider.kind === 'custom'
493
+ && selectedProvider.wireApiPreference === 'auto'
494
+ && !selectedProvider.resolvedWireApi ? 'warning' : 'ok')
350
495
  });
351
496
  }
352
497
 
@@ -358,7 +503,10 @@
358
503
  }
359
504
  try {
360
505
  await instance.onProviderChanged(instance.catalog, { ...change, ...localChange });
361
- } catch (_error) {
506
+ } catch (error) {
507
+ if (localChange.requireProjection === true) {
508
+ throw error;
509
+ }
362
510
  // The authoritative catalog is already saved. A later refresh can retry
363
511
  // the local model projection without rolling back the provider change.
364
512
  }
@@ -381,7 +381,7 @@
381
381
  </label>
382
382
  <label class="codex-provider-field">
383
383
  <span>${escapeHtml(tx('Maximum output tokens', '最大输出 Token'))}</span>
384
- <input type="number" data-provider-field="maxOutputTokens" min="256" max="200000" step="256" value="${Number(draft.maxOutputTokens) || 32768}">
384
+ <input type="number" data-provider-field="maxOutputTokens" min="256" max="65536" step="256" value="${Number(draft.maxOutputTokens) || 65536}">
385
385
  </label>
386
386
  <div class="codex-provider-field codex-provider-field--wide">
387
387
  <span>${escapeHtml(tx('Anthropic compatibility', 'Anthropic 兼容能力'))}</span>
@@ -736,7 +736,7 @@
736
736
  ,anthropicThinkingMode: get('anthropicThinkingMode')?.value || 'budget'
737
737
  ,anthropicPromptCaching: Boolean(get('anthropicPromptCaching')?.checked)
738
738
  ,impersonateClaudeCode: Boolean(get('impersonateClaudeCode')?.checked)
739
- ,maxOutputTokens: Number(get('maxOutputTokens')?.value || 32768)
739
+ ,maxOutputTokens: Number(get('maxOutputTokens')?.value || 65536)
740
740
  };
741
741
  const apiKey = String(get('apiKey')?.value || '');
742
742
  const secretMutation = instance.secretAction === 'replace' && apiKey
@@ -1,12 +1,7 @@
1
1
  (function initCodexOverleafRecentProjects() {
2
2
  'use strict';
3
3
 
4
- // Recent-projects dashboard — the cross-project welcome variant carved out
5
- // of contentRuntime.js (v1.4.8 structural-debt phase 4): the account-scoped
6
- // project-name cache + DOM enrichment, the welcome/empty/degraded states,
7
- // row rendering, and the per-project/variant switchers. Code moved verbatim;
8
- // runtime collaborators are factory-injected (the account scope id stays in
9
- // the runtime and is read through a getter).
4
+ // Cross-project dashboard: account-scoped names, project rows and sessions.
10
5
  function create(deps = {}) {
11
6
  const {
12
7
  tr,
@@ -24,6 +19,10 @@
24
19
  PROJECT_EDITOR_RESERVED_IDS,
25
20
  STATUS_BADGE_CLASS
26
21
  } = deps;
22
+ const projectSessionCleanup = window.CodexOverleafProjectSessionCleanup.create({
23
+ tr, getAccountScopeId: getCachedAccountScopeId, showPluginConfirm, showPluginToast,
24
+ sendBackgroundNative, mutateProjectPanelState, renderRecentProjectsVariant
25
+ });
27
26
 
28
27
  // chrome.storage.local cache key (spec §5.6.3). The cache is keyed by
29
28
  // accountScopeId so a second account on the same Chrome profile cannot
@@ -416,9 +415,6 @@
416
415
  el.appendChild(textNode(tr('recentProjects_row_projectLinkUnavailable'), 'recent-projects-row-warning'));
417
416
  }
418
417
  if (!valid) {
419
- // Dead entry: the session records behind it carry an empty/garbage
420
- // projectId, so it can never be opened or expanded. Offer a cleanup
421
- // action that removes those records (and with them, this row).
422
418
  var deadWrap = document.createElement('div');
423
419
  deadWrap.className = 'recent-projects-row-wrap';
424
420
  var deadHead = document.createElement('div');
@@ -438,8 +434,6 @@
438
434
  deadWrap.appendChild(deadHead);
439
435
  return deadWrap;
440
436
  }
441
- // Wrap the row with an expand toggle + a lazily-populated session list so
442
- // sessions can be managed (delete/rename) without entering the project.
443
437
  var wrap = document.createElement('div');
444
438
  wrap.className = 'recent-projects-row-wrap';
445
439
  wrap.setAttribute('data-project-row-wrap', projectId);
@@ -456,8 +450,20 @@
456
450
  expand.textContent = tr('recentProjects_sessions_action');
457
451
  var head = document.createElement('div');
458
452
  head.className = 'recent-projects-row-head';
453
+ var clear = document.createElement('button');
454
+ clear.type = 'button';
455
+ clear.className = 'recent-projects-row-clear';
456
+ clear.setAttribute('data-project-clear', '');
457
+ clear.title = tr('recentProjects_clearProject');
458
+ clear.setAttribute('aria-label', tr('recentProjects_clearProject'));
459
+ clear.textContent = '×';
460
+ clear.addEventListener('click', function (event) {
461
+ event.stopPropagation();
462
+ projectSessionCleanup.clearProjectSessions(projectId, name).catch(function () { /* surfaced by the action */ });
463
+ });
459
464
  head.appendChild(el);
460
465
  head.appendChild(expand);
466
+ head.appendChild(clear);
461
467
  var sessionsEl = document.createElement('div');
462
468
  sessionsEl.className = 'recent-projects-sessions';
463
469
  sessionsEl.setAttribute('data-project-sessions', '');
@@ -504,9 +510,11 @@
504
510
  return [];
505
511
  }
506
512
  var records = await StorageDb.getAllByIndex('sessions', 'projectId', projectId);
513
+ var Persistence = window.CodexOverleafSessionPersistence;
514
+ var deletedIds = await Persistence.getDeletedSessionIds(projectId);
507
515
  var scope = getCachedAccountScopeId();
508
516
  return (records || [])
509
- .filter(function (record) { return record && (!scope || record.accountScopeId === scope); })
517
+ .filter(function (record) { return Persistence.isVisibleRecord(record, deletedIds, scope); })
510
518
  .sort(function (a, b) {
511
519
  return String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || ''));
512
520
  });
@@ -723,6 +731,7 @@
723
731
  if (!approved) {
724
732
  return;
725
733
  }
734
+ await window.CodexOverleafSessionPersistence.addTombstones(projectId, [record.id]);
726
735
  try {
727
736
  await mutateProjectPanelState(projectId, function (state) {
728
737
  return SessionState.deleteSession(state, record.id);
@@ -956,9 +965,11 @@
956
965
  try {
957
966
  var StorageDb = window.CodexOverleafStorageDb;
958
967
  if (StorageDb) {
968
+ var deletedSessionIdsByProject = await window.CodexOverleafSessionPersistence.loadTombstones();
959
969
  rows = await StorageDb.listRecentProjectsAcrossAccount({
960
970
  accountScopeId: accountScopeId,
961
- limit: showAll ? 500 : pageLimit + 1
971
+ limit: showAll ? 500 : pageLimit + 1,
972
+ deletedSessionIdsByProject: deletedSessionIdsByProject
962
973
  });
963
974
  }
964
975
  } catch (_error) {
@@ -130,8 +130,8 @@
130
130
  });
131
131
  }
132
132
 
133
- function shouldRestrictWritebackToFocus({ focusFiles = [] } = {}) {
134
- return normalizeFocusFiles(focusFiles).length > 0;
133
+ function shouldRestrictWritebackToFocus({ focusFiles = [], focusedPartialSnapshot = false, otWarmStart = false } = {}) {
134
+ return normalizeFocusFiles(focusFiles).length > 0 && (focusedPartialSnapshot === true || otWarmStart === true);
135
135
  }
136
136
 
137
137
  function isFullSnapshotWarning(warning) {
@@ -0,0 +1,70 @@
1
+ (function initCodexOverleafRunGuidanceController() {
2
+ 'use strict';
3
+
4
+ function create(deps = {}) {
5
+ function upsert(text, target = {}) {
6
+ const record = deps.findRunRecord?.(target.recordId, target.sessionId);
7
+ const guidanceId = String(target.guidanceId || '').trim();
8
+ if (!record || !guidanceId) return null;
9
+ const event = {
10
+ title: String(text || '').trim(),
11
+ status: target.status || 'queued',
12
+ kind: 'guidance',
13
+ guidanceId,
14
+ timestamp: new Date().toISOString()
15
+ };
16
+ const existing = (record.events || []).find(entry => entry.guidanceId === guidanceId);
17
+ if (!existing) {
18
+ deps.appendRunEvent?.(event, record);
19
+ return event;
20
+ }
21
+ Object.assign(existing, event, { timestamp: existing.timestamp || event.timestamp });
22
+ const view = deps.getCurrentRunView?.();
23
+ if (view?.recordId === target.recordId) {
24
+ view.root?.querySelector(`[data-guidance-id="${deps.cssEscape(guidanceId)}"]`)
25
+ ?.replaceWith(deps.renderRunEvent(existing));
26
+ }
27
+ return existing;
28
+ }
29
+
30
+ function remove(guidanceId, target = {}) {
31
+ const record = deps.findRunRecord?.(target.recordId, target.sessionId);
32
+ if (!record || !guidanceId) return false;
33
+ const before = (record.events || []).length;
34
+ record.events = (record.events || []).filter(event => event.guidanceId !== guidanceId);
35
+ const view = deps.getCurrentRunView?.();
36
+ if (view?.recordId === target.recordId) {
37
+ view.root?.querySelector(`[data-guidance-id="${deps.cssEscape(guidanceId)}"]`)?.remove();
38
+ }
39
+ return record.events.length !== before;
40
+ }
41
+
42
+ function settleView(view) {
43
+ const guidance = view?.guidance || view?.root?.querySelector('[data-run-guidance]');
44
+ if (!guidance || !view?.events) return;
45
+ view.guidance = guidance;
46
+ for (const row of view.events.querySelectorAll('[data-kind="guidance"]')) guidance.append(row);
47
+ }
48
+
49
+ function appendToView(event, view) {
50
+ const text = window.CodexOverleafRunGuidanceView?.getGuidanceText(event);
51
+ if (!text) return false;
52
+ const guidance = view.guidance || view.root?.querySelector('[data-run-guidance]');
53
+ const target = view.terminalStatus ? guidance : view.events;
54
+ if (!target) return false;
55
+ view.guidance = guidance;
56
+ const rendered = deps.renderRunEvent(event);
57
+ const existing = event.guidanceId
58
+ ? view.root?.querySelector(`[data-guidance-id="${deps.cssEscape(event.guidanceId)}"]`)
59
+ : null;
60
+ if (existing) existing.replaceWith(rendered);
61
+ else target.append(rendered);
62
+ deps.bumpUnreadIfDetached?.();
63
+ return true;
64
+ }
65
+
66
+ return { upsert, remove, settleView, appendToView };
67
+ }
68
+
69
+ window.CodexOverleafRunGuidanceController = { create };
70
+ })();
@@ -0,0 +1,48 @@
1
+ (function initCodexOverleafRunGuidanceView() {
2
+ 'use strict';
3
+
4
+ const LEGACY_GUIDANCE_TITLES = new Set([
5
+ 'Follow-up guidance was delivered to the active Codex turn.',
6
+ '\u540e\u7eed\u5f15\u5bfc\u5df2\u53d1\u9001\u5230\u5f53\u524d Codex turn\u3002'
7
+ ]);
8
+
9
+ const GUIDANCE_DELIVERY_PREFIX = /^(?:Guidance delivered|Follow-up guidance delivered|引导已发送|后续引导已发送)\s*[::]\s*/i;
10
+
11
+ function normalizeGuidanceText(value) {
12
+ return String(value || '').trim().replace(GUIDANCE_DELIVERY_PREFIX, '').trim();
13
+ }
14
+
15
+ function getGuidanceText(event = {}) {
16
+ const detailText = typeof event.detail === 'string' ? event.detail.trim() : '';
17
+ if (event.kind === 'guidance') {
18
+ return normalizeGuidanceText(detailText || event.title);
19
+ }
20
+ return detailText && LEGACY_GUIDANCE_TITLES.has(String(event.title || ''))
21
+ ? normalizeGuidanceText(detailText)
22
+ : '';
23
+ }
24
+
25
+ function renderGuidanceMessage(event, guidanceText, tx) {
26
+ const row = document.createElement('div');
27
+ row.className = 'run-guidance-message run-user-message';
28
+ row.dataset.kind = 'guidance';
29
+ row.dataset.status = event.status || 'completed';
30
+ if (event.guidanceId) {
31
+ row.dataset.guidanceId = event.guidanceId;
32
+ }
33
+ row.setAttribute('role', 'group');
34
+ row.setAttribute('aria-label', tx('Follow-up guidance', '\u540e\u7eed\u5f15\u5bfc'));
35
+
36
+ const text = document.createElement('div');
37
+ text.className = 'run-guidance-text';
38
+ text.textContent = guidanceText;
39
+
40
+ row.append(text);
41
+ return row;
42
+ }
43
+
44
+ window.CodexOverleafRunGuidanceView = {
45
+ getGuidanceText,
46
+ renderGuidanceMessage
47
+ };
48
+ })();