codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,140 @@
1
+ (function initCodexOverleafSensitivePreflight(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafSensitivePreflight = factory();
6
+ }
7
+ })(typeof window !== 'undefined' ? window : globalThis, function sensitivePreflightFactory() {
8
+ 'use strict';
9
+
10
+ function create({
11
+ SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
12
+ getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
13
+ }) {
14
+ async function runSensitivePreflight(options) {
15
+ options = options || {};
16
+ const { task, project, rules, useExistingMirror = false, guidance = false } = options;
17
+ const normalizedRules = normalizeGovernanceRules(rules);
18
+ if (normalizedRules.sensitiveCheckEnabled === false) {
19
+ return { blocked: false, findings: [] };
20
+ }
21
+ if (!SensitiveScan?.scanSensitiveInputs && !useExistingMirror) {
22
+ return { blocked: false, findings: [] };
23
+ }
24
+ let findings = SensitiveScan?.scanSensitiveInputs
25
+ ? SensitiveScan.scanSensitiveInputs({
26
+ task,
27
+ files: guidance ? [] : (project?.files || []).filter(isTextSnapshotFile)
28
+ })
29
+ : [];
30
+ if (useExistingMirror && !guidance) {
31
+ findings = dedupeSensitiveFindings([
32
+ ...findings,
33
+ ...await scanNativeMirrorSensitiveFindings()
34
+ ]);
35
+ }
36
+ if (!findings.length) {
37
+ return { blocked: false, findings: [] };
38
+ }
39
+ options.assertCurrent?.();
40
+ appendRunEvent({
41
+ title: tx(
42
+ `Sensitive content check found ${findings.length} item(s).`,
43
+ `敏感内容检查发现 ${findings.length} 项。`
44
+ ),
45
+ status: 'failed',
46
+ detail: findings.slice(0, 8).map(finding => ({
47
+ [tr('detailFile')]: finding.path || finding.source || 'task',
48
+ [tr('detailReason')]: finding.detectorId,
49
+ [tx('Preview', '预览')]: finding.preview || '[REDACTED]'
50
+ }))
51
+ });
52
+ if (normalizedRules.sensitiveConfirmAllowed === true) {
53
+ const approved = await showPluginConfirm({
54
+ title: tr('sensitiveConfirmTitle'),
55
+ message: formatSensitiveConfirmMessage(findings),
56
+ confirmLabel: guidance ? tx('Send guidance', '发送指导') : tr('sensitiveConfirmRun'),
57
+ cancelLabel: tr('confirmDefaultCancel'),
58
+ destructive: true
59
+ });
60
+ options.assertCurrent?.();
61
+ return { blocked: !approved, findings };
62
+ }
63
+ // Blocking an extra message must not finish the task already in progress.
64
+ if (guidance) return { blocked: true, findings };
65
+ appendCompletionReport({
66
+ conclusion: tx(
67
+ 'This run was blocked before Codex received project context because sensitive content was detected.',
68
+ '检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
69
+ ),
70
+ status: 'blocked',
71
+ operations: [],
72
+ applyResults: [],
73
+ nextStep: tx(
74
+ 'Remove the sensitive content, narrow @context, or enable explicit sensitive confirmation in Project Settings.',
75
+ '请移除敏感内容、缩小 @context,或在项目设置里允许敏感内容显式确认。'
76
+ )
77
+ });
78
+ return { blocked: true, findings };
79
+ }
80
+
81
+ function formatSensitiveConfirmMessage(findings = []) {
82
+ const visible = findings.slice(0, 8).map(finding => {
83
+ const path = finding.path || finding.source || 'task';
84
+ return `${path}: ${finding.detectorId || 'sensitive'} (${finding.preview || '[REDACTED]'})`;
85
+ });
86
+ return [
87
+ tr('sensitiveConfirmMessage'),
88
+ '',
89
+ ...visible,
90
+ findings.length > visible.length ? tx(`${findings.length - visible.length} more finding(s).`, `另有 ${findings.length - visible.length} 项。`) : ''
91
+ ].filter(Boolean).join('\n');
92
+ }
93
+
94
+ async function scanNativeMirrorSensitiveFindings() {
95
+ try {
96
+ const response = await sendBackgroundNative({
97
+ method: 'mirror.scanSensitive',
98
+ params: { projectId: getCurrentProjectId() }
99
+ });
100
+ if (response?.ok) {
101
+ return Array.isArray(response.result?.findings) ? response.result.findings : [];
102
+ }
103
+ return [{
104
+ detectorId: 'mirror-sensitive-scan-unavailable',
105
+ source: 'local-mirror',
106
+ preview: response?.error?.message || 'Could not scan the local mirror before Codex runs.'
107
+ }];
108
+ } catch (error) {
109
+ return [{
110
+ detectorId: 'mirror-sensitive-scan-unavailable',
111
+ source: 'local-mirror',
112
+ preview: error?.message || 'Could not scan the local mirror before Codex runs.'
113
+ }];
114
+ }
115
+ }
116
+
117
+ function dedupeSensitiveFindings(findings = []) {
118
+ const seen = new Set();
119
+ const deduped = [];
120
+ for (const finding of findings || []) {
121
+ const key = [
122
+ finding?.detectorId || '',
123
+ finding?.source || '',
124
+ finding?.path || '',
125
+ finding?.preview || ''
126
+ ].join('\u0000');
127
+ if (seen.has(key)) {
128
+ continue;
129
+ }
130
+ seen.add(key);
131
+ deduped.push(finding);
132
+ }
133
+ return deduped;
134
+ }
135
+
136
+ return { run: runSensitivePreflight };
137
+ }
138
+
139
+ return { create };
140
+ });
@@ -7,15 +7,32 @@
7
7
 
8
8
  function create(deps = {}) {
9
9
  const {
10
- SessionState, getState, setState, sendBackgroundNative, saveState,
10
+ SessionState, getState, setState, getScope, sendBackgroundNative, saveState,
11
11
  applyStateToPanel, showPluginToast, tr
12
12
  } = deps;
13
13
 
14
14
  async function forkRunFromNode(runId) {
15
15
  const previousState = getState();
16
+ const skillPreferences = {
17
+ loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
18
+ loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false,
19
+ codexLocalSkillEnabled: { ...previousState.codexLocalSkillEnabled }
20
+ };
16
21
  const source = findSource(previousState, runId);
17
22
  if (!source) return failFork('session_fork_source_missing', tr('forkRunUnavailable'));
18
- const { session, run, runIndex } = source;
23
+ const scope = getScope ? { ...getScope() } : null;
24
+ const requestedActiveId = previousState.activeSessionId;
25
+ // The Native request and copied history must describe the same origin,
26
+ // even when the source session is edited while the request is pending.
27
+ const { session, run, runIndex } = JSON.parse(JSON.stringify(source));
28
+ const sameScope = () => {
29
+ const current = getScope?.();
30
+ return !scope || ['accountScopeId', 'projectId', 'generation'].every(key => current?.[key] === scope[key]);
31
+ };
32
+ const currentSource = () => {
33
+ const current = findSource(getState(), runId);
34
+ return current?.session.id === session.id && current.run.codexTurnId === run.codexTurnId;
35
+ };
19
36
  const draftThreadId = run.codexTurnId && run.interruptedDraft?.turnId === run.codexTurnId
20
37
  ? run.interruptedDraft.threadId : '';
21
38
  // Provider switches replace the session thread, not the selected node's origin.
@@ -25,25 +42,39 @@
25
42
  }
26
43
  let response;
27
44
  try {
45
+ const resolveLocalSkills = deps.captureLocalSkillIdsForFork || deps.getEnabledCodexLocalSkillIds;
46
+ const enabledCodexLocalSkillIds = skillPreferences.loadCodexLocalSkills
47
+ ? captureLocalSkillIds(await resolveLocalSkills?.(skillPreferences)) : [];
48
+ if (!sameScope() || !currentSource()) return null;
49
+ if (resolveLocalSkills && !Array.isArray(enabledCodexLocalSkillIds)) {
50
+ throw codedError('codex_local_skill_unavailable', tr('forkRunFailed'));
51
+ }
28
52
  response = await sendBackgroundNative({
29
53
  method: 'codex.thread.fork',
30
54
  params: {
31
55
  threadId: sourceThreadId,
32
56
  lastTurnId: run.codexTurnId,
33
57
  speedTier: session.speedTier,
34
- loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
35
- loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false
58
+ loadCodexLocalSkills: skillPreferences.loadCodexLocalSkills,
59
+ loadCodexOverleafSkills: skillPreferences.loadCodexOverleafSkills,
60
+ enabledCodexLocalSkillIds
36
61
  }
37
62
  });
38
63
  } catch (error) {
64
+ if (!sameScope() || !currentSource()) return null;
39
65
  return failFork(error?.code || 'session_fork_failed', error?.message || tr('forkRunFailed'));
40
66
  }
67
+ if (!sameScope() || !currentSource()) {
68
+ if (response?.ok && response.result?.threadId) await clearFork(response.result.threadId);
69
+ return null;
70
+ }
41
71
  if (!response?.ok || !response.result?.threadId) {
42
72
  return failFork(response?.error?.code || 'session_fork_failed', response?.error?.message || tr('forkRunFailed'));
43
73
  }
44
74
  const copiedRuns = session.runs.slice(0, runIndex + 1).map(copyRunSnapshot);
45
75
  const titleBase = String(session.title || copiedRuns[0]?.task || tr('newSessionFallback')).trim();
46
- const forkTitle = buildUniqueForkTitle(previousState.sessions, titleBase);
76
+ const mergeBase = getState();
77
+ const forkTitle = buildUniqueForkTitle(mergeBase.sessions, titleBase);
47
78
  const forked = SessionState.createSession({
48
79
  title: forkTitle,
49
80
  titleSource: 'manual',
@@ -62,27 +93,55 @@
62
93
  forkedFromSessionId: session.id,
63
94
  forkedFromRunId: run.id
64
95
  });
65
- setState(SessionState.normalizePanelState({
66
- ...previousState,
67
- sessions: [...(previousState.sessions || []), forked],
68
- activeSessionId: forked.id
69
- }));
96
+ const activeId = mergeBase.activeSessionId === requestedActiveId
97
+ ? forked.id : mergeBase.activeSessionId;
98
+ const installed = SessionState.setActiveSession({
99
+ ...mergeBase, sessions: [...(mergeBase.sessions || []), forked]
100
+ }, activeId);
101
+ const installedSnapshot = JSON.stringify(installed);
102
+ const forkSnapshot = JSON.stringify(forked);
103
+ setState(installed);
70
104
  try {
105
+ // Keep the visible composer and active-session owner aligned during save.
106
+ applyStateToPanel();
71
107
  await saveState();
72
108
  } catch (error) {
73
- setState(previousState);
74
- await sendBackgroundNative({
75
- method: 'codex.history.clearPlugin',
76
- params: { sessionId: forked.id, threadId: response.result.threadId }
77
- }).catch(() => null);
78
- showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
109
+ if (sameScope()) {
110
+ const current = getState();
111
+ const currentFork = current.sessions?.find(item => item.id === forked.id);
112
+ // Never erase work entered into the fork after its optimistic insert.
113
+ if (!currentFork || JSON.stringify(currentFork) === forkSnapshot) {
114
+ if (currentFork) {
115
+ if (current === installed && JSON.stringify(current) === installedSnapshot) {
116
+ setState(mergeBase);
117
+ } else {
118
+ const remaining = current.sessions.filter(item => item.id !== forked.id);
119
+ const fallbackId = remaining.some(item => item.id === mergeBase.activeSessionId)
120
+ ? mergeBase.activeSessionId : remaining[remaining.length - 1]?.id;
121
+ setState(SessionState.setActiveSession({ ...current, sessions: remaining },
122
+ current.activeSessionId === forked.id ? fallbackId : current.activeSessionId));
123
+ }
124
+ try { applyStateToPanel(); } catch (_renderError) { /* Preserve the original failure and finish cleanup. */ }
125
+ }
126
+ await clearFork(response.result.threadId, forked.id);
127
+ }
128
+ if (sameScope()) showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
129
+ }
79
130
  throw error;
80
131
  }
81
- applyStateToPanel();
132
+ // Persistence owns its captured scope. A late result cannot render a new
133
+ // view or delete a thread that may already have been saved in the old one.
134
+ if (!sameScope() || !getState().sessions?.some(item => item.id === forked.id)) return null;
82
135
  showPluginToast(tr('forkRunDone'), { status: 'completed' });
83
136
  return forked;
84
137
  }
85
138
 
139
+ async function clearFork(threadId, sessionId) {
140
+ try {
141
+ await sendBackgroundNative({ method: 'codex.history.clearPlugin', params: { sessionId, threadId } });
142
+ } catch (_error) { /* Best-effort cleanup must not restore a stale view. */ }
143
+ }
144
+
86
145
  function failFork(code, message) {
87
146
  showPluginToast(message, { status: 'failed', sticky: true });
88
147
  throw codedError(code, message);
@@ -91,6 +150,10 @@
91
150
  return { forkRunFromNode };
92
151
  }
93
152
 
153
+ function captureLocalSkillIds(value) {
154
+ return Array.isArray(value) ? [...value] : undefined;
155
+ }
156
+
94
157
  function buildUniqueForkTitle(sessions = [], sourceTitle = '') {
95
158
  const fallback = String(sourceTitle || 'New Session').trim() || 'New Session';
96
159
  const baseTitle = fallback
@@ -201,7 +201,7 @@
201
201
  }
202
202
  });
203
203
  if (!response?.ok) {
204
- showPluginToast(tr('deleteSessionHistoryFailedToast', { message: response?.error?.message || 'native host did not return success' }), { status: 'warning', sticky: true });
204
+ showPluginToast(tr('deleteSessionHistoryFailedToast', { message: response?.error?.message || tr('deleteSessionNativeFailureDetail') }), { status: 'warning', sticky: true });
205
205
  } else if (response.result?.skipped) {
206
206
  showPluginToast(tr('deleteSessionNoThreadToast'), { status: 'info' });
207
207
  } else {
@@ -242,7 +242,7 @@
242
242
  </label>
243
243
  <p class="codex-set-row-help" data-i18n="loadCodexLocalSkillsHelp">Pull skills from your local Codex installation.</p>
244
244
  <button type="button" class="codex-skills-entry" data-skills-entry>
245
- <span class="codex-skills-entry-label" data-i18n="codexOverleafSkillsEntry">Codex Overleaf skills</span>
245
+ <span class="codex-skills-entry-label" data-i18n="skillsManageEntry">Manage skills</span>
246
246
  <span class="codex-skills-entry-summary" data-skills-entry-summary></span>
247
247
  <span class="codex-skills-entry-chevron" aria-hidden="true">›</span>
248
248
  </button>
@@ -252,9 +252,9 @@
252
252
  </section>
253
253
  <section class="codex-custom-instructions-panel codex-skills-panel" data-skills-screen>
254
254
  <div class="codex-custom-instructions-head">
255
- <button type="button" data-skills-back title="Back" aria-label="Back">‹</button>
255
+ <button type="button" data-skills-back data-i18n-title="settingsBack" data-i18n-aria-label="settingsBack" title="Back" aria-label="Back">‹</button>
256
256
  <div>
257
- <div class="codex-custom-instructions-title" data-i18n="codexOverleafSkillsTitle">Codex Overleaf skills</div>
257
+ <div class="codex-custom-instructions-title" data-i18n="localSkillsTitle">Skills</div>
258
258
  </div>
259
259
  <span class="codex-set-saved" data-set-saved hidden><svg class="codex-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m3.5 8 3 3 6-6"/></svg><span data-i18n="settingsSaved">Saved</span></span>
260
260
  </div>
@@ -266,6 +266,9 @@
266
266
  </div>
267
267
  <div class="codex-skills-divider"></div>
268
268
  <div class="codex-local-skill-list" data-local-skill-list></div>
269
+ <div class="codex-skills-divider"></div>
270
+ <div class="codex-wb-skills-list-title codex-wb-skills-caption" data-i18n="codexLocalSkillsTitle">Local Codex skills</div>
271
+ <div class="codex-local-skill-list" data-codex-local-skill-list></div>
269
272
  </section>
270
273
  `;
271
274
 
@@ -338,6 +341,7 @@
338
341
  setSkillsSummary: text => setSkillsSummary(instance, text),
339
342
  setProviderSummary: summary => setProviderSummary(instance, summary),
340
343
  refreshNotes: () => updateGovernanceNotes(instance),
344
+ refreshLocale: () => instance.workbench?.refresh(),
341
345
  destroy: () => destroy(instance),
342
346
  _instance: instance
343
347
  };
@@ -243,6 +243,11 @@
243
243
  projectFilter.setAttribute('aria-pressed', String(filter === 'project'));
244
244
  closeButton.title = t('settingsWorkbenchClose');
245
245
  closeButton.setAttribute('aria-label', t('settingsWorkbenchClose'));
246
+ const skillsBack = skills?.querySelector('[data-skills-back]');
247
+ if (skillsBack) {
248
+ skillsBack.title = t('settingsBack');
249
+ skillsBack.setAttribute('aria-label', t('settingsBack'));
250
+ }
246
251
  for (const [id, tab] of navButtons) {
247
252
  tab.hidden = !availablePages.has(id);
248
253
  tab.setAttribute('aria-selected', String(id === active));
@@ -4,11 +4,39 @@
4
4
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
5
5
  'use strict';
6
6
 
7
+ const sameScope = (a, b) => a.accountScopeId === b.accountScopeId
8
+ && a.projectId === b.projectId && a.generation === b.generation;
9
+
10
+ function applySnapshot(options, sessions) {
11
+ const previous = options.getState(), scope = options.getScope();
12
+ const next = options.normalize({ ...previous, sessions }, { restoreRunningRuns: false });
13
+ const unchangedSnapshot = JSON.stringify(next);
14
+ options.setState(next);
15
+ try {
16
+ options.render();
17
+ } catch (error) {
18
+ // A callback may replace state, edit it in place or navigate. Restore only
19
+ // the unchanged view owned by this failed passive application.
20
+ let unchanged = false;
21
+ try {
22
+ unchanged = options.getState() === next && sameScope(scope, options.getScope())
23
+ && JSON.stringify(options.getState()) === unchangedSnapshot;
24
+ } catch (_error) { /* Unknown state must not overwrite a newer edit. */ }
25
+ if (unchanged) {
26
+ options.setState(previous);
27
+ // A second render can also fail. The old merge base still protects
28
+ // subsequent saves, including values from partially restored controls.
29
+ try { options.render(); } catch (_error) { /* Report the first failure. */ }
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+
7
35
  function install(options) {
8
- const { Bridge, StorageDb, getScope, getState, isBusy, apply, notice } = options;
36
+ const { Bridge, StorageDb, getScope, getState, isBusy, notice } = options;
37
+ const apply = options.apply || (sessions => applySnapshot(options, sessions));
9
38
  let saving = 0, version = 0, pending = false, timer = null, refreshing = false, stopped = false;
10
- const sameScope = (a, b) => a.accountScopeId === b.accountScopeId
11
- && a.projectId === b.projectId && a.generation === b.generation;
39
+ let retryCount = 0;
12
40
  const signature = state => JSON.stringify([state.activeSessionId,
13
41
  (state.sessions || []).map(session => [session.id, session.updatedAt, session.task])]);
14
42
  function schedule(delay = 250) {
@@ -37,12 +65,21 @@
37
65
  const ids = new Set(records.map(record => record.id));
38
66
  const newLocal = (local.sessions || []).filter(session =>
39
67
  !ids.has(session.id) && !StorageDb.wasSharedSessionStored(session.id));
40
- // The data has now been adopted by this view; future writes may use it
41
- // as a merge baseline. Snapshot reads alone never advance the baseline.
42
- StorageDb.adoptSharedSessionBaseline(records);
68
+ // Rendering may fail after only part of the composer was updated.
69
+ // Keep the prior merge base until the complete view accepts the data.
43
70
  apply([...records, ...newLocal]);
71
+ if (stopped || !sameScope(scope, getScope())) return;
72
+ if (saving || version !== capturedVersion) { pending = true; return; }
73
+ StorageDb.adoptSharedSessionBaseline(records);
74
+ retryCount = 0;
44
75
  } catch (error) {
45
76
  notice?.(error);
77
+ // One retry can recover transient DOM failures. Local edits, navigation
78
+ // and persistent failures stop it; another notification starts afresh.
79
+ if (!stopped && sameScope(scope, getScope()) && version === capturedVersion
80
+ && signature(getState()) === capturedSignature && retryCount < 1) {
81
+ retryCount++; pending = true;
82
+ }
46
83
  } finally {
47
84
  refreshing = false;
48
85
  schedule(1000);
@@ -51,7 +88,7 @@
51
88
  const unsubscribe = Bridge.subscribe(change => {
52
89
  const scope = getScope();
53
90
  if (!change.projectId || change.projectId === scope.projectId) {
54
- pending = true; schedule();
91
+ retryCount = 0; pending = true; schedule();
55
92
  }
56
93
  });
57
94
  return {
@@ -68,5 +105,5 @@
68
105
  }
69
106
  };
70
107
  }
71
- return { install };
108
+ return { install, applySnapshot };
72
109
  });
@@ -5,9 +5,10 @@
5
5
  let lastUserActivityAt = Date.now();
6
6
  let stableSavedSince = 0;
7
7
  let applying = false;
8
+ let saveIdentity;
8
9
  const documentRef = options.document || root.document;
9
10
  const noteActivity = event => {
10
- if (event?.target?.closest?.('#codex-overleaf-panel')) return;
11
+ if (!options.includePanelActivity && event?.target?.closest?.('#codex-overleaf-panel')) return;
11
12
  lastUserActivityAt = Date.now();
12
13
  stableSavedSince = 0;
13
14
  };
@@ -42,6 +43,7 @@
42
43
  saveResult = { ok: false, state: 'unavailable' };
43
44
  }
44
45
  const saved = saveResult?.ok === true && ['verified_saved', 'verified_quiet'].includes(saveResult?.state);
46
+ if (saveResult?.identity !== saveIdentity) { stableSavedSince = 0; saveIdentity = saveResult?.identity; }
45
47
  if (saved) {
46
48
  stableSavedSince = stableSavedSince || Date.now();
47
49
  } else {
@@ -62,6 +64,7 @@
62
64
 
63
65
  return {
64
66
  buildIdleProbe,
67
+ noteActivity,
65
68
  handleMessage,
66
69
  isApplying: () => applying
67
70
  };
@@ -245,6 +245,7 @@
245
245
  if (!currentView) return;
246
246
  renderSettings();
247
247
  if (!notice) return;
248
+ notice.setAttribute('aria-label', tx('Stable update', '稳定版更新'));
248
249
  if (!currentView.showPanel) {
249
250
  notice.hidden = true;
250
251
  notice.replaceChildren();
@@ -355,7 +356,14 @@
355
356
  let statusText = '';
356
357
  let buttonText = tx('Check for updates', '检查更新');
357
358
 
358
- if (['reload_required', 'reload_tabs_required'].includes(stateName)) {
359
+ if (stateName === 'idle' && state.code === 'update_cooldown') {
360
+ const until = Number(state.cooldownUntil || 0);
361
+ const time = until > 0 && Number.isFinite(until) ? new Date(until).toLocaleString(getLocale() || undefined) : '';
362
+ statusText = tx(
363
+ `Update v${latestVersion} is paused after a failed installation. ${time ? `Check again after ${time}.` : 'Check again after its cooldown ends.'}`,
364
+ `v${latestVersion} 安装失败后已暂停更新。${time ? `请在 ${time} 后再次检查。` : '请在冷却期结束后再次检查。'}`
365
+ );
366
+ } else if (['reload_required', 'reload_tabs_required'].includes(stateName)) {
359
367
  summaryText = tx(`Installed v${latestVersion}; running extension v${currentVersion}.`, `已安装 v${latestVersion};扩展当前运行 v${currentVersion}。`);
360
368
  statusText = getCopy(state).detail;
361
369
  buttonText = stateName === 'reload_required' ? tx('Reload extension', '重新加载扩展') : tx('Refresh Overleaf tabs', '刷新 Overleaf 标签页');
@@ -373,7 +381,7 @@
373
381
  statusText = getCopy(state).detail;
374
382
  buttonText = tx('Update in progress', '更新进行中');
375
383
  } else if (stateName === 'failed' || stateName === 'rolled_back') {
376
- statusText = state.message || getCopy(state).detail;
384
+ statusText = formatUpdateMessage(state) || getCopy(state).detail;
377
385
  buttonText = tx('Retry', '重试');
378
386
  } else if (stateName === 'committed') {
379
387
  summaryText = tx(`Current v${currentVersion}. Update completed successfully.`, `当前版本 v${currentVersion}。更新已成功完成。`);
@@ -443,11 +451,11 @@
443
451
  const target = state.latestVersion ? 'v' + state.latestVersion : '';
444
452
  if (state.cancelRequested) return {
445
453
  eyebrow: tx('Cancelling update', '正在取消更新'), title: target,
446
- detail: state.message || tx('Stopping this update and clearing its authorization.', '正在停止本次更新并清理更新授权。')
454
+ detail: formatUpdateMessage(state) || tx('Stopping this update and clearing its authorization.', '正在停止本次更新并清理更新授权。')
447
455
  };
448
456
  if (state.recoveryPending) return {
449
457
  eyebrow: tx('Checking update recovery', '正在确认更新恢复状态'), title: target,
450
- detail: state.message || tx('Waiting for the installed version to be confirmed.', '正在确认实际安装的版本。')
458
+ detail: formatUpdateMessage(state) || tx('Waiting for the installed version to be confirmed.', '正在确认实际安装的版本。')
451
459
  };
452
460
  const blocker = blockersCopy(state.blockers?.length ? state.blockers : [state.blocker]);
453
461
  return {
@@ -455,11 +463,11 @@
455
463
  detail: tx('Reading the latest signed release.', '正在读取最新签名版本。') },
456
464
  reload_required: {
457
465
  eyebrow: tx('Installed, awaiting reload', '已安装,等待加载'), title: target,
458
- detail: state.message || tx('The new files are installed. Reload the extension when Overleaf is saved and idle.', '新版文件已安装,请在 Overleaf 保存并空闲后重新加载扩展。')
466
+ detail: formatUpdateMessage(state) || tx('The new files are installed. Reload the extension when Overleaf is saved and idle.', '新版文件已安装,请在 Overleaf 保存并空闲后重新加载扩展。')
459
467
  },
460
468
  reload_tabs_required: {
461
469
  eyebrow: tx('Updated runtime is ready', '新版运行组件已就绪'), title: target,
462
- detail: state.message || tx('Refresh the saved, idle Overleaf tabs to load the updated panel.', '刷新已保存且空闲的 Overleaf 标签页以加载新版面板。')
470
+ detail: formatUpdateMessage(state) || tx('Refresh the saved, idle Overleaf tabs to load the updated panel.', '刷新已保存且空闲的 Overleaf 标签页以加载新版面板。')
463
471
  },
464
472
  manual_install_required: {
465
473
  eyebrow: tx('Reinstall needed for this update', '这次更新需要重新安装'),
@@ -509,7 +517,7 @@
509
517
  failed: {
510
518
  eyebrow: tx('Update could not continue', '更新无法继续'),
511
519
  title: target,
512
- detail: state.message || tx('Review the update details and retry.', '请查看更新详情后重试。')
520
+ detail: formatUpdateMessage(state) || tx('Review the update details and retry.', '请查看更新详情后重试。')
513
521
  }
514
522
  }[state.state] || { eyebrow: '', title: '', detail: '' };
515
523
  }
@@ -521,6 +529,45 @@
521
529
  return `npm exec --yes codex-overleaf-link@${version} -- install-managed`;
522
530
  }
523
531
 
532
+ function formatUpdateMessage(state = {}) {
533
+ const code = state.code || '';
534
+ const messages = {
535
+ update_phase_timeout: ['The update timed out and its download was stopped. Retry when the connection is available.', '更新超时,下载已停止。连接恢复后请重试。'],
536
+ update_not_applied: ['The update was stopped before installation. Retry when ready.', '更新已在安装前停止,可准备就绪后重试。'],
537
+ update_recovery_pending: ['The update result is being checked. Recovery has not been confirmed.', '正在确认更新结果,恢复状态尚未确认。'],
538
+ update_rolled_back: ['The previous version has been restored.', '已恢复上一版本。'],
539
+ update_not_managed: ['Run install-managed once to enable coordinated automatic updates.', '请先运行一次 install-managed,以启用扩展与本机服务的协调更新。'],
540
+ update_native_busy: ['Native Host is still processing a Codex task.', 'Native Host 仍在处理 Codex 任务。'],
541
+ update_revoke_too_late: ['The update is already being installed. Wait for its result.', '更新已进入安装,请等待结果。'],
542
+ update_cancel_unconfirmed: ['Update cancellation is unconfirmed. Check recovery before retrying.', '尚未确认更新已取消,请先检查恢复状态。'],
543
+ update_apply_unconfirmed: ['Update installation is unconfirmed. Check recovery before retrying.', '尚未确认更新安装结果,请先检查恢复状态。'],
544
+ update_rollback_unconfirmed: ['Restoring the previous version is unconfirmed. Check recovery before retrying.', '尚未确认旧版本已恢复,请先检查恢复状态。'],
545
+ update_reload_unavailable: ['No verified installed update is awaiting reload.', '当前没有已验证且等待重载的更新。'],
546
+ update_network_cancelled: ['The update download was cancelled.', '更新下载已取消。'],
547
+ update_apply_failed: ['The update could not be installed. Check recovery and retry.', '无法安装更新,请检查恢复状态后重试。']
548
+ };
549
+ if (messages[code]) return tx(...messages[code]);
550
+ if (['update_health_failed', 'update_request_failed', 'update_failed'].includes(code)) return tx('The updated components could not be confirmed. Check recovery before retrying.', '尚未确认更新后的组件正常,请先检查恢复状态。');
551
+ if (['update_bootstrap_health_timeout', 'update_bootstrap_reload_failed'].includes(code)) return tx('The updated startup components did not finish restarting and confirming. Check recovery before retrying.', '更新后的启动组件未能完成重启和确认,请先检查恢复状态。');
552
+ if (['update_bootstrap_source_mismatch', 'update_bootstrap_source_version_mismatch'].includes(code)) return tx('The installed startup files do not match a supported update source. Check recovery before retrying.', '已安装的启动文件与支持的升级来源不一致,请先检查恢复状态。');
553
+ if (['update_bootstrap_record_missing', 'update_bootstrap_record_invalid', 'update_bootstrap_backup_mismatch', 'update_bootstrap_already_restoring'].includes(code)) return tx('The startup recovery material could not be confirmed. The update result remains unconfirmed.', '尚未确认启动组件的恢复材料有效,更新结果仍待确认。');
554
+ if (['update_bootstrap_confirmation_pending', 'update_bootstrap_native_restart_required'].includes(code)) return tx('Waiting for the updated startup components and Native Host to finish restarting and confirming.', '正在等待更新后的启动组件和本机服务完成重启及确认。');
555
+ if (code === 'update_bootstrap_monitor_unavailable') return tx('The update recovery monitor could not start. Check recovery before retrying.', '更新恢复监护程序未能启动,请先检查恢复状态。');
556
+ if (['update_bootstrap_transaction_mismatch', 'update_bootstrap_transaction_invalid'].includes(code)) return tx('The startup update transaction could not be matched. Check recovery before retrying.', '未能匹配启动组件的更新事务,请先检查恢复状态。');
557
+ if (code.startsWith('update_bootstrap_')) return tx('The startup update package or installed files could not be verified. Check recovery before retrying.', '无法验证启动组件更新包或已安装文件,请先检查恢复状态。');
558
+ if (['update_reload_required', 'update_reload_busy'].includes(code)) {
559
+ const match = /^Reload is waiting for Overleaf to be saved and idle \(([^)]+)\)\.$/.exec(state.message || '');
560
+ if (match) return blockersCopy(match[1].split(', '));
561
+ }
562
+ if (['update_check_timeout', 'update_activation_timeout', 'update_status_timeout'].includes(code)) return tx('The update operation timed out. Check recovery or retry when the connection is available.', '更新操作超时,请检查恢复状态,或在连接恢复后重试。');
563
+ if (['update_candidate_missing', 'update_candidate_stale', 'update_not_staged', 'update_staged_archive_invalid', 'update_recovery_superseded'].includes(code)) return tx('No current verified update is ready. Check for updates again.', '当前没有已验证且可安装的更新,请重新检查更新。');
564
+ if (['update_authorization_in_progress', 'update_transaction_in_progress', 'update_transaction_locked'].includes(code)) return tx('Another update operation is in progress. Wait for it to finish.', '另一项更新操作正在进行,请等待其完成。');
565
+ if (['update_authorization_invalid', 'update_consent_required', 'update_consent_mismatch', 'update_transaction_mismatch', 'update_transaction_state_changed'].includes(code)) return tx('The update authorization or transaction changed. Check for updates and authorize the current candidate again.', '更新授权或事务已变化,请重新检查更新并确认当前候选版本。');
566
+ if (['update_archive_checksum_invalid', 'update_archive_compressed_limit', 'update_archive_duplicate_path', 'update_archive_file_count_limit', 'update_archive_file_limit', 'update_archive_invalid', 'update_archive_path_forbidden', 'update_archive_path_too_long', 'update_archive_required_file_missing', 'update_archive_total_limit', 'update_archive_truncated', 'update_archive_type_forbidden', 'update_staging_not_empty', 'update_operation_invalid', 'update_bundle_hash_invalid', 'update_bundle_name_invalid', 'update_bundle_size_invalid', 'update_manifest_artifacts_invalid', 'update_manifest_commit_invalid', 'update_manifest_invalid', 'update_manifest_manual_install_invalid', 'update_manifest_origin_mismatch', 'update_manifest_protocol_invalid', 'update_manifest_schema_unsupported', 'update_manifest_timestamp_invalid', 'update_manifest_too_large', 'update_manifest_version_invalid', 'update_signature_algorithm_unsupported', 'update_signature_invalid', 'update_signature_key_invalid', 'update_signature_key_unknown', 'update_signature_too_large', 'update_version_invalid'].includes(code)) return tx('The signed update or archive could not be verified. Check the release details and retry.', '无法验证更新签名或压缩包,请检查发布详情后重试。');
567
+ if (['update_github_http_error', 'update_github_response_invalid', 'update_asset_missing', 'update_asset_url_forbidden', 'update_release_asset_invalid', 'update_release_manifest_mismatch', 'update_bundle_size_mismatch', 'update_bundle_hash_mismatch', 'update_native_version_mismatch', 'update_extension_version_mismatch', 'update_runtime_manifest_invalid', 'update_runtime_asset_missing', 'update_bootstrap_manifest_invalid', 'update_managed_layout_invalid', 'update_marker_invalid', 'update_current_version_invalid', 'update_health_version_mismatch', 'update_confirmation_unexpected', 'update_rollback_unavailable'].includes(code)) return tx('The update or installed files could not be verified. Check the update details and repair or retry.', '无法验证更新或已安装文件,请检查更新详情后修复或重试。');
568
+ return state.message || '';
569
+ }
570
+
524
571
  function blockerCopy(value) {
525
572
  return {
526
573
  unsaved: tx('Overleaf has not confirmed that this document is saved.', 'Overleaf 尚未确认当前文档已保存。'),
@@ -541,6 +588,7 @@
541
588
  tab_unavailable: tx('An Overleaf tab is unavailable for the safety check.', '某个 Overleaf 标签页当前无法完成安全检查。'),
542
589
  idle_probe_failed: tx('The Overleaf safety check failed. Reload the affected tab and retry.', 'Overleaf 安全检查失败;请刷新受影响的标签页后重试。'),
543
590
  native_project_locked: tx('Native Host still holds a project lock.', 'Native Host 仍持有项目锁。'),
591
+ native_process_stop_unconfirmed: tx('A previous Codex process has not yet confirmed its exit.', '前一次 Codex 进程尚未确认退出。'),
544
592
  native_run_active: tx('Native Host is still processing a task.', 'Native Host 仍在处理任务。'),
545
593
  busy: tx('A component is still busy.', '某个组件仍处于忙碌状态。')
546
594
  }[value] || tx('Waiting until every Overleaf tab is saved and idle.', '正在等待所有 Overleaf 标签页完成保存并进入空闲状态。');
@@ -555,6 +603,10 @@
555
603
  }
556
604
 
557
605
  function manualReasonText(state = {}) {
606
+ if (state.code === 'prerelease_promotion_requires_manual_update') {
607
+ return tx('The installed release candidate has the same version number as the stable release. Reinstall to use the stable release.',
608
+ '已安装的候选版与稳定版使用相同版本号。请重新安装以使用稳定版。');
609
+ }
558
610
  const reason = state.manualReason || {};
559
611
  return tx(reason.en || reason.zh || '', reason.zh || reason.en || '');
560
612
  }
@@ -564,5 +616,5 @@
564
616
  return /^zh\b/i.test(locale) ? chinese : english;
565
617
  }
566
618
 
567
- return Object.freeze({ mount });
619
+ return Object.freeze({ mount, refreshLocale: render });
568
620
  });