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
@@ -2,40 +2,85 @@
2
2
  'use strict';
3
3
 
4
4
  function create(deps = {}) {
5
+ // Unacknowledged input is a page-local projection. Only confirmed delivery
6
+ // joins record.events; the durable queue owns refresh/recovery before that.
7
+ const pending = new Map();
8
+ let lastTimestamp = 0;
9
+ const keyOf = target => JSON.stringify([target.sessionId, target.recordId, target.guidanceId]);
10
+ const sameScope = scope => !scope || ['accountScopeId', 'projectId', 'generation'].every(key => scope[key] === deps.getScope?.()?.[key]);
11
+ function ownedRecord(target) {
12
+ if (!target.sessionId || !target.recordId) return null;
13
+ if (target.sessionCreatedAt !== undefined && deps.getSessionCreatedAt
14
+ && deps.getSessionCreatedAt(target.sessionId) !== target.sessionCreatedAt) return null;
15
+ return deps.findRunRecord?.(target.recordId, target.sessionId) || null;
16
+ }
17
+ function nextTimestamp() {
18
+ lastTimestamp = Math.max(Date.now(), lastTimestamp + 1);
19
+ return new Date(lastTimestamp).toISOString();
20
+ }
21
+ function getPending(run) {
22
+ return [...pending.values()].filter(token => token.target.recordId === run?.id
23
+ && (!deps.getActiveSessionId || token.target.sessionId === deps.getActiveSessionId())
24
+ && sameScope(token.scope) && ownedRecord(token.target)).map(token => token.event);
25
+ }
26
+ function refresh(target) {
27
+ const record = ownedRecord(target);
28
+ if (!record) return null;
29
+ const view = deps.getCurrentRunView?.();
30
+ const root = deps.getRunRoot?.(target) || (view?.recordId === target.recordId
31
+ && view?.sessionId === target.sessionId ? view.root : null);
32
+ const container = root?.querySelector('[data-run-guidance]');
33
+ deps.RunGuidanceView?.reconcileGuidance(container,
34
+ (record.events || []).concat(getPending(record)), deps.tx, { active: record.status === 'running' });
35
+ return container;
36
+ }
37
+ function refreshSettled(target) {
38
+ try { refresh(target); } catch (error) { deps.onPresentationError?.(error); }
39
+ }
40
+ function preview(text, target = {}) {
41
+ const title = String(text || '').trim();
42
+ if (!title || !target.guidanceId || !ownedRecord(target)) return null;
43
+ const token = { target: { ...target }, scope: deps.getScope?.(), event: { title, status: 'steering', kind: 'guidance',
44
+ guidanceId: target.guidanceId, timestamp: nextTimestamp(), guidancePending: true } };
45
+ pending.set(keyOf(target), token);
46
+ try {
47
+ const container = refresh(target);
48
+ const row = container?.querySelector?.(`[data-guidance-id="${deps.cssEscape?.(target.guidanceId) || target.guidanceId}"]`);
49
+ if (row) deps.revealGuidance?.(row);
50
+ deps.bumpUnreadIfDetached?.();
51
+ } catch (error) {
52
+ if (pending.get(keyOf(target)) === token) pending.delete(keyOf(target));
53
+ try { refresh(target); } catch { /* preserve the original presentation error */ }
54
+ throw error;
55
+ }
56
+ return token;
57
+ }
58
+ function clearPreview(token) {
59
+ if (!token || pending.get(keyOf(token.target)) !== token) return;
60
+ pending.delete(keyOf(token.target));
61
+ if (sameScope(token.scope)) refreshSettled(token.target);
62
+ }
5
63
  function upsert(text, target = {}) {
6
- const record = deps.findRunRecord?.(target.recordId, target.sessionId);
64
+ const record = ownedRecord(target);
7
65
  const guidanceId = String(target.guidanceId || '').trim();
8
66
  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;
67
+ const existing = (record.events || []).find(entry => entry.kind === 'guidance' && entry.guidanceId === guidanceId);
68
+ const event = { title: String(text || '').trim(), status: target.status || 'completed', kind: 'guidance', guidanceId,
69
+ timestamp: existing?.timestamp || target.timestamp || pending.get(keyOf(target))?.event.timestamp || nextTimestamp() };
70
+ if (existing) Object.assign(existing, event);
71
+ else record.events = [...(record.events || []), event].slice(-(deps.maxRunEvents || 300));
72
+ // A late ACK still belongs to the captured run, even if another run is
73
+ // now active. The coordinator saves it together with queue removal.
74
+ deps.onRecordChanged?.(target);
75
+ refreshSettled(target);
76
+ return existing || event;
28
77
  }
29
-
30
78
  function remove(guidanceId, target = {}) {
31
- const record = deps.findRunRecord?.(target.recordId, target.sessionId);
79
+ const record = ownedRecord(target);
32
80
  if (!record || !guidanceId) return false;
33
81
  const before = (record.events || []).length;
34
82
  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
- }
83
+ refreshSettled(target);
39
84
  return record.events.length !== before;
40
85
  }
41
86
 
@@ -64,7 +109,7 @@
64
109
  return true;
65
110
  }
66
111
 
67
- return { upsert, remove, settleView, appendToView };
112
+ return { preview, clearPreview, getPending, upsert, remove, settleView, appendToView };
68
113
  }
69
114
 
70
115
  window.CodexOverleafRunGuidanceController = { create };
@@ -26,23 +26,72 @@
26
26
  const row = document.createElement('div');
27
27
  row.className = 'run-guidance-message run-user-message';
28
28
  row.dataset.kind = 'guidance';
29
- row.dataset.status = event.status || 'completed';
30
- if (event.guidanceId) {
31
- row.dataset.guidanceId = event.guidanceId;
32
- }
33
29
  row.setAttribute('role', 'group');
34
- row.setAttribute('aria-label', tx('Follow-up guidance', '\u540e\u7eed\u5f15\u5bfc'));
35
-
36
30
  const text = document.createElement('div');
37
31
  text.className = 'run-guidance-text';
38
- text.textContent = guidanceText;
39
-
40
- row.append(text);
32
+ const label = document.createElement('div');
33
+ label.className = 'run-guidance-label';
34
+ row.append(text, label);
35
+ updateGuidanceMessage(row, event, guidanceText, tx);
41
36
  return row;
42
37
  }
43
38
 
39
+ function updateGuidanceMessage(row, event, guidanceText, tx, { active = true } = {}) {
40
+ row.dataset.status = event.status || 'completed';
41
+ if (event.guidanceId) row.dataset.guidanceId = event.guidanceId;
42
+ row.dataset.guidanceTimestamp = event.timestamp || '';
43
+ const label = tx('Follow-up guidance', '补充指令');
44
+ const sending = (active || event.guidancePending === true) && ['queued', 'steering'].includes(event.status);
45
+ const text = row.querySelector('.run-guidance-text');
46
+ if (text.textContent !== guidanceText) text.textContent = guidanceText;
47
+ const status = row.querySelector('.run-guidance-label');
48
+ const statusText = label + (sending ? tx(' · Sending…', ' · 发送中…') : '');
49
+ if (status.textContent !== statusText) status.textContent = statusText;
50
+ row.setAttribute('aria-label', statusText);
51
+ }
52
+
53
+ // Stored events and the controller's in-flight projection share a container.
54
+ // Confirmation updates the same row; removed previews leave no stale bubble.
55
+ function reconcileGuidance(container, events, tx, options = {}) {
56
+ if (!container) return;
57
+ const completedLegacy = new Set(events.filter(event => !event.guidanceId && event.status === 'completed')
58
+ .map(getGuidanceText).filter(Boolean));
59
+ const entries = new Map();
60
+ for (const [index, event] of events.entries()) {
61
+ const text = getGuidanceText(event);
62
+ if (!text || (!event.guidanceId && event.status === 'queued' && completedLegacy.has(text))) continue;
63
+ const key = event.guidanceId ? 'id:' + event.guidanceId : 'legacy:' + index;
64
+ const previous = entries.get(key);
65
+ if (previous && event.guidancePending === true && previous.event.guidancePending !== true) continue;
66
+ entries.set(key, { key, event: previous ? { ...event, timestamp: previous.event.timestamp || event.timestamp } : event, text });
67
+ }
68
+ const existing = new Map();
69
+ for (const row of [...container.children]) {
70
+ const key = row.dataset.guidanceId ? 'id:' + row.dataset.guidanceId : row.dataset.guidanceKey;
71
+ if (!key || existing.has(key)) { row.remove(); continue; }
72
+ existing.set(key, row);
73
+ }
74
+ for (const [key, row] of existing) if (!entries.has(key)) row.remove();
75
+ const known = [...entries.values()].filter(entry => Number.isFinite(Date.parse(entry.event.timestamp)))
76
+ .sort((a, b) => Date.parse(a.event.timestamp) - Date.parse(b.event.timestamp));
77
+ let datedIndex = 0;
78
+ // Keep legacy rows without timestamps in their event-order slots.
79
+ const ordered = [...entries.values()].map(entry => Number.isFinite(Date.parse(entry.event.timestamp)) ? known[datedIndex++] : entry);
80
+ let cursor = container.firstChild;
81
+ for (const entry of ordered) {
82
+ const row = existing.get(entry.key) || renderGuidanceMessage(entry.event, entry.text, tx);
83
+ row.dataset.guidanceKey = entry.key;
84
+ if (entry.event.guidancePending === true) row.dataset.guidancePending = 'true';
85
+ else delete row.dataset.guidancePending;
86
+ updateGuidanceMessage(row, entry.event, entry.text, tx, options);
87
+ if (row !== cursor) container.insertBefore(row, cursor);
88
+ cursor = row.nextSibling;
89
+ }
90
+ }
91
+
44
92
  window.CodexOverleafRunGuidanceView = {
45
93
  getGuidanceText,
46
- renderGuidanceMessage
94
+ renderGuidanceMessage,
95
+ reconcileGuidance
47
96
  };
48
97
  })();
@@ -171,6 +171,25 @@
171
171
 
172
172
  function createCoordinator(options = {}) {
173
173
  let view = null;
174
+ const guidanceAttempts = new Map();
175
+ const guidanceKey = (sessionId, itemId) => JSON.stringify([sessionId, itemId]);
176
+ const matchesGuidanceItem = (item, identity) => item?.id === identity.itemId
177
+ && item.createdAt === identity.itemCreatedAt && item.text === identity.text
178
+ && item.clientUserMessageId === identity.clientUserMessageId;
179
+ const matchesGuidanceScope = expected => {
180
+ const scope = options.getGuideScope?.();
181
+ return !expected || ['projectId', 'accountScopeId', 'generation'].every(key => scope?.[key] === expected[key]);
182
+ };
183
+ const guideErrorKeys = {
184
+ codex_turn_not_ready: 'queuedInputGuideNotReady',
185
+ codex_turn_mismatch: 'queuedInputGuideTurnChanged',
186
+ codex_steer_empty: 'queuedInputGuideEmpty',
187
+ codex_steer_too_large: 'queuedInputGuideTooLarge'
188
+ };
189
+ function formatGuideError(error) {
190
+ const key = guideErrorKeys[error?.code];
191
+ return (key && options.tr?.(key)) || error?.message || options.tr?.('queuedInputGuideFailed');
192
+ }
174
193
  const scheduler = create({
175
194
  getQueue: sessionId => options.getSession?.(sessionId)?.pendingInputs || [],
176
195
  setQueue: (sessionId, pendingInputs) => options.setQueue?.(sessionId, pendingInputs),
@@ -226,62 +245,143 @@
226
245
  const item = (session?.pendingInputs || []).find(entry => entry.id === itemId);
227
246
  const run = options.getCurrentRun?.();
228
247
  const binding = run?.activeTurn;
229
- if (!session || !item || !run || run.sessionId !== session.id || !binding?.threadId || !binding?.turnId) {
248
+ const initialScope = options.getGuideScope?.();
249
+ const guideScope = initialScope ? { ...initialScope } : null;
250
+ if (!session || !item || item.status !== 'queued' || !run || run.sessionId !== session.id || !binding?.threadId || !binding?.turnId) {
230
251
  options.toast?.(options.tr?.('queuedInputGuideUnavailable'), 'warning');
231
252
  return;
232
253
  }
254
+ const identity = { requestId: run.nativeRequestId, threadId: binding.threadId, turnId: binding.turnId,
255
+ projectKey: run.runProjectId, recordId: run.recordId, sessionId: session.id, sessionCreatedAt: session.createdAt,
256
+ itemId, itemCreatedAt: item.createdAt, text: item.text, clientUserMessageId: item.clientUserMessageId };
257
+ const attemptKey = guidanceKey(identity.sessionId, itemId);
258
+ const attempt = { identity, scope: guideScope, previewToken: null };
259
+ const sameScope = () => matchesGuidanceScope(guideScope);
260
+ const ownedSession = () => {
261
+ const latest = options.getSession?.(identity.sessionId);
262
+ return sameScope() && latest?.createdAt === identity.sessionCreatedAt ? latest : null;
263
+ };
264
+ const ownedItemSession = (allowPaused = false) => {
265
+ const latest = ownedSession();
266
+ const current = latest?.pendingInputs?.find(entry => entry.id === itemId);
267
+ return guidanceAttempts.get(attemptKey) === attempt
268
+ && (current?.status === 'steering' || (allowPaused && current?.status === 'paused'))
269
+ && matchesGuidanceItem(current, identity) ? latest : null;
270
+ };
271
+ function clearPreview() {
272
+ const token = attempt.previewToken;
273
+ attempt.previewToken = null;
274
+ if (token) options.clearGuidancePreview?.(token);
275
+ }
276
+ const assertCurrent = () => {
277
+ const currentRun = options.getCurrentRun?.();
278
+ if (options.isGuideCancelled?.() || currentRun !== run || options.getActiveSession?.()?.id !== identity.sessionId
279
+ || currentRun?.nativeRequestId !== identity.requestId
280
+ || currentRun?.recordId !== identity.recordId || currentRun?.runProjectId !== identity.projectKey
281
+ || currentRun?.activeTurn?.threadId !== identity.threadId || currentRun?.activeTurn?.turnId !== identity.turnId
282
+ || !ownedItemSession()) {
283
+ throw Object.assign(new Error(options.tr?.('queuedInputGuideUnavailable') || 'Guidance is no longer available for this turn.'), { code: 'codex_turn_mismatch' });
284
+ }
285
+ };
286
+ async function restoreUnsent(error) {
287
+ // No steer was dispatched. Restore only this attempt's still-owned item;
288
+ // a concurrent removal, replacement or scope switch must stay untouched.
289
+ clearPreview();
290
+ const latest = ownedItemSession();
291
+ if (latest) {
292
+ options.setQueue?.(identity.sessionId, Queue.returnToQueue(latest.pendingInputs, itemId));
293
+ render();
294
+ try {
295
+ await options.save?.({ queueMutation: { type: 'transition', sessionId: identity.sessionId, itemId } });
296
+ } catch (recoveryError) {
297
+ if (error && typeof error === 'object') error.persistenceRecoveryError = recoveryError?.message || String(recoveryError);
298
+ }
299
+ }
300
+ }
233
301
  options.setQueue?.(session.id, Queue.markSteering(session.pendingInputs, itemId));
234
- render();
235
- await options.save?.({
236
- queueMutation: { type: 'transition', sessionId: session.id, itemId }
237
- });
302
+ guidanceAttempts.set(attemptKey, attempt);
238
303
  try {
239
- const response = await options.sendSteer?.({
240
- requestId: run.nativeRequestId,
241
- projectKey: run.runProjectId,
242
- threadId: binding.threadId,
243
- expectedTurnId: binding.turnId,
244
- clientUserMessageId: item.clientUserMessageId,
245
- input: [{ type: 'text', text: item.text }]
246
- });
247
- const latest = options.getSession?.(session.id);
248
- options.setQueue?.(
249
- session.id,
250
- response?.ok
251
- ? Queue.remove(latest?.pendingInputs || [], itemId)
252
- : Queue.returnToQueue(latest?.pendingInputs || [], itemId, response?.error?.code || 'steer_failed')
304
+ try {
305
+ attempt.previewToken = options.previewGuidance?.(identity.text, {
306
+ guidanceId: itemId, sessionId: identity.sessionId, recordId: identity.recordId,
307
+ sessionCreatedAt: identity.sessionCreatedAt
308
+ });
309
+ render();
310
+ const preflight = await options.preflightGuide?.({ task: identity.text, assertCurrent });
311
+ assertCurrent();
312
+ if (preflight?.blocked) throw Object.assign(new Error('Guidance was blocked before dispatch.'), { guidanceBlocked: true });
313
+ await options.save?.({
314
+ queueMutation: { type: 'transition', sessionId: identity.sessionId, itemId }
315
+ });
316
+ assertCurrent();
317
+ } catch (error) {
318
+ await restoreUnsent(error);
319
+ if (error?.guidanceBlocked) return { ok: false, blocked: true };
320
+ throw error;
321
+ }
322
+ let response;
323
+ try {
324
+ response = await options.sendSteer?.({
325
+ requestId: identity.requestId,
326
+ projectKey: identity.projectKey,
327
+ threadId: identity.threadId,
328
+ expectedTurnId: identity.turnId,
329
+ clientUserMessageId: identity.clientUserMessageId,
330
+ input: [{ type: 'text', text: identity.text }]
331
+ }, { assertCurrent });
332
+ } catch (error) {
333
+ if (error?.requestDispatched === false) { await restoreUnsent(error); throw error; }
334
+ clearPreview();
335
+ const latest = ownedItemSession(true);
336
+ if (latest) {
337
+ const paused = latest.pendingInputs.map(entry => entry.id === itemId
338
+ ? { ...entry, status: 'paused', pauseReason: 'steer_delivery_unknown' }
339
+ : entry);
340
+ options.setQueue?.(identity.sessionId, Queue.normalizeQueue(paused));
341
+ }
342
+ options.toast?.(formatGuideError(error), 'warning');
343
+ render();
344
+ if (latest) await options.save?.({
345
+ queueMutation: { type: 'transition', sessionId: identity.sessionId, itemId }
346
+ });
347
+ return;
348
+ }
349
+ // Cancellation can pause the owned row while a dispatched steer is
350
+ // awaiting acknowledgement. A confirmed receipt also consumes that
351
+ // paused copy, so Resume cannot send an already accepted input again.
352
+ const latest = ownedItemSession(Boolean(response?.ok));
353
+ if (latest) options.setQueue?.(
354
+ identity.sessionId,
355
+ response?.ok ? Queue.remove(latest.pendingInputs, itemId)
356
+ : Queue.returnToQueue(latest.pendingInputs, itemId, response?.error?.code || 'steer_failed')
253
357
  );
254
358
  if (response?.ok) {
255
- if (typeof options.appendGuidance === 'function') {
256
- options.appendGuidance(item.text, {
257
- guidanceId: item.id,
258
- sessionId: session.id,
259
- recordId: run.recordId,
260
- status: 'completed'
359
+ if (ownedSession() && typeof options.appendGuidance === 'function') {
360
+ options.appendGuidance(identity.text, {
361
+ guidanceId: itemId, sessionId: identity.sessionId, recordId: identity.recordId,
362
+ sessionCreatedAt: identity.sessionCreatedAt, status: 'completed',
363
+ timestamp: attempt.previewToken?.event?.timestamp
261
364
  });
262
- } else {
365
+ } else if (ownedSession() && options.getCurrentRun?.() === run) {
263
366
  options.appendEvent?.(options.tr?.('queuedInputGuided'), 'completed');
264
367
  }
265
368
  } else {
266
- options.toast?.(response?.error?.message || options.tr?.('queuedInputGuideFailed'), 'warning');
369
+ options.toast?.(formatGuideError(response?.error), 'warning');
267
370
  }
371
+ clearPreview();
268
372
  render();
269
- await options.save?.({
373
+ // A Native acknowledgement is final delivery evidence. A later local
374
+ // save failure must not turn it into an unknown delivery or resend it.
375
+ if (latest) await options.save?.({
270
376
  queueMutation: response?.ok
271
- ? { type: 'remove', sessionId: session.id, itemId }
272
- : { type: 'transition', sessionId: session.id, itemId }
377
+ ? { type: 'remove', sessionId: identity.sessionId, itemId }
378
+ : { type: 'transition', sessionId: identity.sessionId, itemId }
273
379
  });
274
- } catch (error) {
275
- const latest = options.getSession?.(session.id);
276
- const paused = (latest?.pendingInputs || []).map(entry => entry.id === itemId
277
- ? { ...entry, status: 'paused', pauseReason: 'steer_delivery_unknown' }
278
- : entry);
279
- options.setQueue?.(session.id, Queue.normalizeQueue(paused));
280
- options.toast?.(error?.message || options.tr?.('queuedInputGuideFailed'), 'warning');
380
+ else if (response?.ok && ownedSession()) await options.save?.();
381
+ } finally {
382
+ clearPreview();
383
+ if (guidanceAttempts.get(attemptKey) === attempt) guidanceAttempts.delete(attemptKey);
281
384
  render();
282
- await options.save?.({
283
- queueMutation: { type: 'transition', sessionId: session.id, itemId }
284
- });
285
385
  }
286
386
  }
287
387
 
@@ -320,7 +420,7 @@
320
420
  view = options.createView?.({
321
421
  container,
322
422
  tr: options.tr,
323
- onGuide: itemId => guide(itemId).catch(error => options.toast?.(error.message, 'warning')),
423
+ onGuide: itemId => guide(itemId).catch(error => options.toast?.(formatGuideError(error), 'warning')),
324
424
  onRemove: remove,
325
425
  onResume: resume,
326
426
  onToast: message => options.toast?.(message, 'warning')
@@ -331,7 +431,13 @@
331
431
  }
332
432
  const session = options.getActiveSession?.();
333
433
  const run = options.getCurrentRun?.();
334
- view?.render?.(session?.pendingInputs || [], {
434
+ const visibleQueue = (session?.pendingInputs || []).filter(item => {
435
+ const attempt = guidanceAttempts.get(guidanceKey(session.id, item.id));
436
+ return !attempt?.previewToken || !['steering', 'paused'].includes(item.status)
437
+ || !matchesGuidanceScope(attempt.scope)
438
+ || session.createdAt !== attempt.identity.sessionCreatedAt || !matchesGuidanceItem(item, attempt.identity);
439
+ });
440
+ view?.render?.(visibleQueue, {
335
441
  running: Boolean(run),
336
442
  canGuide: Boolean(run?.activeTurn?.turnId)
337
443
  });
@@ -0,0 +1,146 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafModuleRegistry.define('RunRecoveryActions', [], factory);
4
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
5
+ 'use strict';
6
+
7
+ function formatForceCancelOutcome(response, tx) {
8
+ if (response?.ok !== true) return tx('Release failed — refresh the tab, then try again.', '释放失败,请刷新页面后重试。');
9
+ const result = response.result || response;
10
+ if (result.lockReleased === true) return tx('Force-released — you can retry the run.', '已释放,可以重试本轮任务。');
11
+ if (result.cancelled === true) return tx('Cancellation requested. Wait for the task to stop before retrying.', '已请求取消,请等待任务停止后再重试。');
12
+ if (result.cancelled === false) return tx('No active task matched the release request.', '没有发现与释放请求匹配的活动任务。');
13
+ if (result.sent === true) return tx('Cancellation request sent; release is not yet confirmed.', '取消请求已发送,释放状态尚未确认。');
14
+ return tx('Release is not yet confirmed. Check the current task before retrying.', '释放状态尚未确认,请检查当前任务后再重试。');
15
+ }
16
+
17
+ function create(deps = {}) {
18
+ const { tx, failureNotice, refillComposerForRetry, openProjectFileForFailure, showNativeSetupGuidance,
19
+ openStorageSettings, forceCancelStuckTaskForCurrentProject } = deps;
20
+ const document = deps.document || globalThis.document;
21
+ const window = deps.window || globalThis.window;
22
+
23
+ const RETRYABLE_FAILURE_CODES = new Set([
24
+ 'native_request_failed', 'codex_timeout', 'codex_no_usable_result',
25
+ 'codex_output_limit', 'stale_source_changed', 'patch_anchor_not_found',
26
+ 'target_editor_not_ready', 'write_timeout', 'partial_write_needs_review',
27
+ 'write_operation_failed'
28
+ ]);
29
+ const NATIVE_SETUP_FAILURE_CODES = new Set([
30
+ 'native_bridge_unavailable', 'native_protocol_incompatible'
31
+ ]);
32
+ const OPEN_FILE_FAILURE_CODES = new Set([
33
+ 'target_file_not_active', 'target_file_open_failed', 'target_file_not_found'
34
+ ]);
35
+ const STORAGE_FAILURE_CODES = new Set([
36
+ 'storage_quota_exceeded', 'run_state_persist_failed'
37
+ ]);
38
+ // A missing `codex` binary is fixed in the terminal, not the panel — the
39
+ // action opens the README troubleshooting section (PATH / login checks).
40
+ const CODEX_INSTALL_FAILURE_CODES = new Set(['codex_not_found']);
41
+ const CODEX_CLI_TROUBLESHOOTING_URL = 'https://github.com/Ghqqqq/codex-overleaf-link#faq-and-troubleshooting';
42
+
43
+ function buildRecoveryButton(failureCode, label, title) {
44
+ const button = document.createElement('button');
45
+ button.type = 'button';
46
+ button.className = 'run-final-answer__recovery-action';
47
+ button.dataset.recoveryFor = failureCode;
48
+ button.textContent = label;
49
+ if (title) {
50
+ button.title = title;
51
+ }
52
+ return button;
53
+ }
54
+
55
+ function appendEditAndResend(report, run, actionKey) {
56
+ const retry = buildRecoveryButton(actionKey,
57
+ tx('Edit & resend', '编辑后重发'),
58
+ tx('Put this run\u2019s task back into the composer so you can adjust and resend it.', '把本轮任务填回输入框,修改后可直接重发。'));
59
+ retry.addEventListener('click', clickEvent => {
60
+ clickEvent.stopPropagation();
61
+ refillComposerForRetry(run || null);
62
+ });
63
+ report.append(retry);
64
+ }
65
+
66
+ function appendRecoveryActionForFailure(report, event, run) {
67
+ const recovery = failureNotice?.prepareRecovery(report, { openFile: openProjectFileForFailure });
68
+ const failure = recovery?.failure || event?.failure;
69
+ const failureCode = failure?.code || '';
70
+ if (recovery?.handled) return;
71
+ report = recovery?.target || report;
72
+ if (RETRYABLE_FAILURE_CODES.has(failureCode) && typeof refillComposerForRetry === 'function') {
73
+ appendEditAndResend(report, run, failureCode);
74
+ return;
75
+ }
76
+ if (NATIVE_SETUP_FAILURE_CODES.has(failureCode) && typeof showNativeSetupGuidance === 'function') {
77
+ const fix = buildRecoveryButton(failureCode,
78
+ tx('Fix the native host', '修复 native host'),
79
+ tx('Opens the install/update guidance with the copyable command.', '打开安装/更新指引(含可复制命令)。'));
80
+ fix.addEventListener('click', clickEvent => {
81
+ clickEvent.stopPropagation();
82
+ showNativeSetupGuidance();
83
+ });
84
+ report.append(fix);
85
+ return;
86
+ }
87
+ if (OPEN_FILE_FAILURE_CODES.has(failureCode) && failure?.file && typeof openProjectFileForFailure === 'function') {
88
+ const open = buildRecoveryButton(failureCode,
89
+ tx(`Open ${failure.file} in Overleaf`, `在 Overleaf 打开 ${failure.file}`), '');
90
+ open.addEventListener('click', clickEvent => {
91
+ clickEvent.stopPropagation();
92
+ openProjectFileForFailure(failure.file);
93
+ });
94
+ report.append(open);
95
+ return;
96
+ }
97
+ if (STORAGE_FAILURE_CODES.has(failureCode) && typeof openStorageSettings === 'function') {
98
+ const clean = buildRecoveryButton(failureCode,
99
+ tx('Open history & storage cleanup', '打开历史与存储清理'), '');
100
+ clean.addEventListener('click', clickEvent => {
101
+ clickEvent.stopPropagation();
102
+ openStorageSettings();
103
+ });
104
+ report.append(clean);
105
+ return;
106
+ }
107
+ if (CODEX_INSTALL_FAILURE_CODES.has(failureCode)) {
108
+ const guide = buildRecoveryButton(failureCode,
109
+ tx('Codex CLI troubleshooting guide', '查看 Codex CLI 排查指引'),
110
+ tx('Opens the README section on fixing a missing `codex` command (PATH / login).', '打开 README 中「找不到 codex 命令」的排查说明(PATH/登录)。'));
111
+ guide.addEventListener('click', clickEvent => {
112
+ clickEvent.stopPropagation();
113
+ window.open(CODEX_CLI_TROUBLESHOOTING_URL, '_blank', 'noopener');
114
+ });
115
+ report.append(guide);
116
+ return;
117
+ }
118
+ if (failureCode !== 'codex_project_locked') return;
119
+ const button = document.createElement('button');
120
+ button.type = 'button';
121
+ button.className = 'run-final-answer__recovery-action';
122
+ button.dataset.recoveryFor = failureCode;
123
+ button.textContent = tx('Force-release the stuck task', '强制释放卡住的任务');
124
+ button.title = tx(
125
+ 'Requests cancellation for this project. A sent request does not confirm that the task has stopped. Use this when refreshing the tab did not free the project.',
126
+ '请求取消当前项目的任务;请求已发送不代表任务已经停止。刷新页面仍未解除占用时使用。'
127
+ );
128
+ button.addEventListener('click', event => {
129
+ event.stopPropagation();
130
+ button.disabled = true;
131
+ button.textContent = tx('Releasing…', '正在释放…');
132
+ forceCancelStuckTaskForCurrentProject().then(result => {
133
+ button.textContent = formatForceCancelOutcome(result, tx);
134
+ button.disabled = result?.ok === true;
135
+ }).catch(() => {
136
+ button.disabled = false;
137
+ button.textContent = tx('Release failed — refresh the tab, then try again.', '释放失败,请刷新页面后重试。');
138
+ });
139
+ });
140
+ report.append(button);
141
+ }
142
+
143
+ return { buildRecoveryButton, appendRecoveryActionForFailure };
144
+ }
145
+ return { create, formatForceCancelOutcome };
146
+ });