codex-overleaf-link 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  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 +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  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 +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -1,17 +1,29 @@
1
1
  (function initCodexOverleafRunController(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('../shared/projectFiles'));
4
4
  } else {
5
- root.CodexOverleafRunController = factory();
5
+ root.CodexOverleafRunController = factory(root.CodexOverleafProjectFiles);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function runControllerFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function runControllerFactory(ProjectFiles) {
8
8
  'use strict';
9
9
 
10
10
  const DEFAULT_EXPECTED_MIRROR_FRESHNESS_MS = 5 * 60 * 1000;
11
11
 
12
+ function captureSessionContext(state = {}) {
13
+ // This is the prompt projection, not the mutable visible-session alias.
14
+ const session = state.session;
15
+ return session ? JSON.parse(JSON.stringify(session)) : null;
16
+ }
17
+
18
+ function isSubmittedComposerUnchanged(submitted, current) {
19
+ return ['task', 'attachments', 'skillInvocation', 'selectionContext'].every(key =>
20
+ JSON.stringify(submitted[key] ?? null) === JSON.stringify(current[key] ?? null));
21
+ }
22
+
12
23
  function buildCodexRunParams({
13
24
  currentProjectId,
14
25
  state = {},
26
+ locale = state.locale,
15
27
  task,
16
28
  project,
17
29
  useExistingMirror,
@@ -22,6 +34,7 @@
22
34
  customInstructions,
23
35
  skillLoadingSettings,
24
36
  enabledCodexOverleafSkillIds,
37
+ enabledCodexLocalSkillIds,
25
38
  attachments,
26
39
  skillInvocation,
27
40
  compileLogContext,
@@ -45,17 +58,21 @@
45
58
  otWarmStart: useExistingMirror && otWarmStart ? true : undefined,
46
59
  warmStartStrategy: useExistingMirror && otWarmStart ? 'ot-warm-mirror' : undefined,
47
60
  focusFiles,
61
+ focusFilesAreLiteral: true,
48
62
  restrictToFocusFiles: restrictToFocusFiles || undefined,
49
63
  model: state.model,
50
64
  reasoningEffort: state.reasoningEffort,
51
65
  speedTier: state.speedTier,
52
- locale: state.locale === 'zh' ? 'zh' : 'en',
66
+ locale: locale === 'zh' ? 'zh' : 'en',
53
67
  session: state.session,
54
68
  threadId: codexThreadId || undefined,
55
69
  customInstructions: normalizedCustomInstructions || undefined,
56
70
  loadCodexLocalSkills: skillLoadingSettings?.loadCodexLocalSkills !== false,
57
71
  loadCodexOverleafSkills: skillLoadingSettings?.loadCodexOverleafSkills !== false,
58
72
  enabledCodexOverleafSkillIds: normalizedEnabledSkillIds,
73
+ enabledCodexLocalSkillIds: Array.isArray(enabledCodexLocalSkillIds)
74
+ ? [...new Set(enabledCodexLocalSkillIds.filter(id => typeof id === 'string' && isSafeSkillId(id)))]
75
+ : undefined,
59
76
  skillInvocation: normalizedSkillInvocation || undefined,
60
77
  attachments: normalizedAttachments.length ? normalizedAttachments : undefined
61
78
  };
@@ -106,13 +123,14 @@
106
123
  project = {},
107
124
  snapshotWarnings = {},
108
125
  focusFiles = [],
126
+ focusFilesAreLiteral = false,
109
127
  isUsableProjectFileContent = defaultIsUsableProjectFileContent
110
128
  } = {}) {
111
129
  const blocking = Array.isArray(snapshotWarnings.blocking) ? snapshotWarnings.blocking : [];
112
130
  if (!blocking.length || !blocking.some(isFullSnapshotWarning)) {
113
131
  return false;
114
132
  }
115
- const normalizedFocusFiles = normalizeFocusFiles(focusFiles);
133
+ const normalizedFocusFiles = normalizeFocusFiles(focusFiles, focusFilesAreLiteral === true);
116
134
  if (!normalizedFocusFiles.length) {
117
135
  return false;
118
136
  }
@@ -122,10 +140,10 @@
122
140
 
123
141
  const textFilesByPath = new Map((project.files || [])
124
142
  .filter(file => file && typeof file.path === 'string' && typeof file.content === 'string')
125
- .map(file => [normalizeFocusPath(file.path), file]));
143
+ .map(file => [normalizeFocusPath(file.path, true), file]));
126
144
 
127
145
  return normalizedFocusFiles.every(filePath => {
128
- const file = textFilesByPath.get(normalizeFocusPath(filePath));
146
+ const file = textFilesByPath.get(filePath);
129
147
  return file && isUsableProjectFileContent(file.content);
130
148
  });
131
149
  }
@@ -138,11 +156,11 @@
138
156
  return /Full project snapshot was not captured/i.test(String(warning || ''));
139
157
  }
140
158
 
141
- function normalizeFocusFiles(value) {
159
+ function normalizeFocusFiles(value, literal = false) {
142
160
  const seen = new Set();
143
161
  const files = [];
144
162
  for (const item of Array.isArray(value) ? value : []) {
145
- const filePath = normalizeFocusPath(item);
163
+ const filePath = normalizeFocusPath(item, literal);
146
164
  if (!filePath || seen.has(filePath)) {
147
165
  continue;
148
166
  }
@@ -152,7 +170,8 @@
152
170
  return files;
153
171
  }
154
172
 
155
- function normalizeFocusPath(value) {
173
+ function normalizeFocusPath(value, literal = false) {
174
+ if (literal) return ProjectFiles.normalizeSafeProjectPath(value);
156
175
  return String(value || '')
157
176
  .replace(/^@file:/i, '')
158
177
  .replace(/\\/g, '/')
@@ -187,19 +206,22 @@
187
206
  if (!isSafeSkillId(id)) {
188
207
  return null;
189
208
  }
190
- const title = String(value?.title || 'Skill Installer').trim().slice(0, 80) || 'Skill Installer';
191
- if (id === 'skill-installer') {
209
+ const installer = id === 'skill-installer' && (value?.scope == null || value.scope === 'builtin');
210
+ const fallbackTitle = installer ? 'Skill Installer' : id;
211
+ const title = String(value?.title || fallbackTitle).trim().slice(0, 80) || fallbackTitle;
212
+ if (installer) {
192
213
  return { id, title };
193
214
  }
194
- if (value?.scope !== 'codex-overleaf') {
215
+ if (!['codex-overleaf', 'codex-local'].includes(value?.scope)) {
195
216
  return null;
196
217
  }
197
- return { id, title, scope: 'codex-overleaf' };
218
+ return { id, title, scope: value.scope };
198
219
  }
199
220
 
200
221
  function isSafeSkillId(id) {
201
222
  return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(String(id || ''))
202
- && !String(id || '').includes('..');
223
+ && !String(id || '').includes('..')
224
+ && !['__proto__', 'prototype', 'constructor'].includes(id);
203
225
  }
204
226
 
205
227
  function normalizeAttachmentName(value) {
@@ -266,6 +288,8 @@
266
288
  }
267
289
 
268
290
  return {
291
+ captureSessionContext,
292
+ isSubmittedComposerUnchanged,
269
293
  buildCodexRunParams,
270
294
  canUseFocusedPartialSnapshot,
271
295
  shouldRestrictWritebackToFocus,
@@ -1,15 +1,14 @@
1
1
  (function initRunFailureNotice(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory();
3
+ module.exports = factory(require('../shared/failureReasons'));
4
4
  } else {
5
- root.CodexOverleafModuleRegistry.define('RunFailureNotice', [], factory);
5
+ root.CodexOverleafModuleRegistry.define('RunFailureNotice', ['FailureReasons'], factory);
6
6
  }
7
- })(typeof window !== 'undefined' ? window : globalThis, function runFailureNoticeFactory() {
7
+ })(typeof window !== 'undefined' ? window : globalThis, function runFailureNoticeFactory(FailureReasons) {
8
8
  'use strict';
9
9
 
10
10
  const REVIEW_CODES = new Set(['write_timeout', 'partial_write_needs_review']);
11
11
  const CANCEL_CODES = new Set(['cancelled', 'user_cancelled', 'codex_cancelled']);
12
-
13
12
  // Old attempts remain in the stored audit trail; a resolved undo must not
14
13
  // advertise them as a current failure in the transcript or its summary.
15
14
  function projectResolvedUndo(run) {
@@ -27,18 +26,25 @@
27
26
  document: documentRef = globalThis.document } = {}) {
28
27
  const notices = new WeakMap();
29
28
 
30
- function append(report, event = {}, run = {}) {
29
+ function append(report, event = {}, run = {}, outcomeTarget = null) {
31
30
  const projection = typeof projectRunSettlement === 'function' && run
32
31
  ? projectRunSettlement(run) : null;
33
32
  const failure = projection?.primaryFailure || event.failure;
34
33
  if (report.dataset.status === 'cancelled' || CANCEL_CODES.has(failure?.code)) return;
35
34
  const facts = run?.settlement || run?.settlementFacts || {};
36
- const unconfirmed = REVIEW_CODES.has(failure?.code)
35
+ const unconfirmed = projection?.requiresWriteReview === true || REVIEW_CODES.has(failure?.code)
37
36
  || facts.documentEffect === 'possibly-changed'
38
37
  || (facts.evidence?.applied === 'unknown' && facts.evidence?.settled === 'needs-review');
39
38
  const partial = facts.evidence?.applied === 'partial' || run?.partialWriteback === true;
40
39
  if (!failure && !unconfirmed && !partial && report.dataset.status !== 'failed') return;
41
40
 
41
+ // A quiet outcome replaces the notice's presentation, not its recovery
42
+ // selection or precedence. Keep the same context for prepareRecovery.
43
+ if (outcomeTarget) {
44
+ notices.set(report, { notice: outcomeTarget, unconfirmed, failure, file: safeProjectPath(failure?.file) });
45
+ return;
46
+ }
47
+
42
48
  const notice = documentRef.createElement('aside');
43
49
  notice.className = 'run-feedback';
44
50
  notice.dataset.tone = unconfirmed || partial || failure?.terminalState === 'degraded'
@@ -49,9 +55,14 @@
49
55
  heading.textContent = tr(unconfirmed ? 'runFeedbackUnconfirmedTitle'
50
56
  : partial ? 'runFeedbackPartialTitle' : 'runFeedbackFailedTitle');
51
57
  const message = documentRef.createElement('p');
52
- const detail = typeof failure?.userMessage === 'string' ? failure.userMessage : '';
53
58
  const file = safeProjectPath(failure?.file);
54
- message.textContent = sanitizeText([file, detail].filter(Boolean).join(': ').slice(0, 1000))
59
+ // Localization is a display projection. Recovery keeps the original
60
+ // failure and its trusted file, even when historical text yields a path.
61
+ const display = FailureReasons?.formatFailureReasonForDisplay?.(failure, undefined, key => {
62
+ const value = tr(key);
63
+ return value && value !== key ? value : undefined;
64
+ });
65
+ message.textContent = sanitizeText((display?.message || [file, failure?.userMessage].filter(Boolean).join(': ')).slice(0, 1000))
55
66
  || tr('runFeedbackGeneric');
56
67
  notice.append(heading, message);
57
68
  if (unconfirmed) {
@@ -74,10 +85,15 @@
74
85
  return row.value;
75
86
  }
76
87
 
77
- function prepareRecovery(report, { openFile } = {}) {
88
+ function getRecoveryContext(report) {
78
89
  const info = notices.get(report);
79
- const target = info?.notice || report;
80
- if (!info?.unconfirmed) return { target, handled: false };
90
+ return { target: info?.notice || report, failure: info?.failure, unconfirmed: info?.unconfirmed === true, file: info?.file || '' };
91
+ }
92
+
93
+ function prepareRecovery(report, { openFile } = {}) {
94
+ const info = getRecoveryContext(report);
95
+ const target = info.target;
96
+ if (!info.unconfirmed) return { target, handled: false, failure: info.failure };
81
97
  const canOpen = Boolean(info.file && typeof openFile === 'function');
82
98
  const button = documentRef.createElement('button');
83
99
  button.type = 'button';
@@ -100,10 +116,10 @@
100
116
  }
101
117
  });
102
118
  target.append(button);
103
- return { target, handled: true };
119
+ return { target, handled: true, failure: info.failure };
104
120
  }
105
121
 
106
- return Object.freeze({ append, formatMetaValue, prepareRecovery });
122
+ return Object.freeze({ append, formatMetaValue, getRecoveryContext, prepareRecovery });
107
123
  }
108
124
 
109
125
  function safeProjectPath(value) {
@@ -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
  })();
@@ -39,6 +39,10 @@
39
39
  const MARK_STYLE = '--mark-leaf-origin:70.6% 41.6%;--mark-cloud-origin:44.3% 55%';
40
40
  const ICONS = {
41
41
  alert: '<circle cx="8" cy="8" r="6"/><path d="M8 4.5v4 M8 11h.01"/>',
42
+ minus: '<circle cx="8" cy="8" r="6"/><path d="M5 8h6"/>',
43
+ half: '<circle cx="6" cy="6" r="5"/><path d="M6 1a5 5 0 0 0 0 10z" fill="currentColor" stroke="none"/>',
44
+ written: '<path d="m2.4 6.3 2.4 2.4 4.8-5.2" stroke-width="1.5"/>',
45
+ skipped: '<path d="m3 3 6 6M9 3 3 9" stroke-width="1.5"/>',
42
46
  check: '<path d="m3.5 8 3 3 6-6"/>',
43
47
  file: '<path d="M9 2H4v12h8V5L9 2Z M9 2v4h3 M6 9h4 M6 11h3"/>',
44
48
  refresh: '<path d="M4.2 5.3A5 5 0 1 1 3.6 10"/><path d="M4.2 2.2v3.1H1.1"/>'
@@ -49,7 +53,8 @@
49
53
  })[char]);
50
54
 
51
55
  function icon(name, className = 'codex-glyph') {
52
- return '<svg class="' + className + '" viewBox="0 0 16 16" aria-hidden="true" focusable="false">'
56
+ const size = ['half', 'written', 'skipped'].includes(name) ? 12 : 16;
57
+ return '<svg class="' + className + '" viewBox="0 0 ' + size + ' ' + size + '" aria-hidden="true" focusable="false">'
53
58
  + (ICONS[name] || ICONS.check) + '</svg>';
54
59
  }
55
60
 
@@ -103,14 +108,16 @@
103
108
  const files = tx(facts.files + (facts.files === 1 ? ' file' : ' files'), facts.files + ' 个文件');
104
109
  // A clipped history only proves the files it recorded.
105
110
  parts.push('<span>' + escapeHtml(facts.filesClipped ? tx(files + ' recorded', '已记录 ' + files) : files) + '</span>');
106
- if (facts.added || facts.removed) {
107
- parts.push('<span><span class="run-summary-add">+' + Number(facts.added || 0) + '</span> <span class="run-summary-del">−'
108
- + Number(facts.removed || 0) + '</span></span>');
109
- }
111
+ // Show only nonzero sides; a red "−0" reads as a problem that isn't there.
112
+ const counts = [];
113
+ if (Number(facts.added) > 0) counts.push('<span class="run-summary-add">+' + Number(facts.added) + '</span>');
114
+ if (Number(facts.removed) > 0) counts.push('<span class="run-summary-del">−' + Number(facts.removed) + '</span>');
115
+ if (counts.length) parts.push('<span>' + counts.join(' ') + '</span>');
110
116
  } else if (facts.steps > 0) {
111
- parts.push('<span>' + escapeHtml(tx(facts.steps + ' steps', facts.steps + ' 步')) + '</span>');
117
+ parts.push('<span>' + escapeHtml(tx(facts.steps + (facts.steps === 1 ? ' step' : ' steps'), facts.steps + ' 步')) + '</span>');
112
118
  }
113
119
  if (facts.compiled) parts.push('<span>' + escapeHtml(tx('compiled', '已编译')) + '</span>');
120
+ if (facts.compileChip) parts.push('<span class="run-summary-compile-chip">' + escapeHtml(facts.compileChip) + '</span>');
114
121
  if (facts.firstCompile) {
115
122
  parts.push('<span class="run-summary-qed">' + escapeHtml(tx('Q.E.D. — first clean compile', 'Q.E.D. · 首次编译通过')) + '</span>');
116
123
  }
@@ -253,8 +260,11 @@
253
260
  el.dataset.presence = facts.kind || 'plain';
254
261
  if (facts.kind === 'done' || facts.kind === 'stopped') {
255
262
  el.innerHTML = settledHtml({ ...facts, firstCompile }, tx, formatElapsed);
256
- } else if (facts.kind === 'failed') {
257
- el.innerHTML = icon('alert') + '<span>' + escapeHtml(facts.text) + '</span>';
263
+ } else if (['failed', 'not-started', 'not-started-cancelled'].includes(facts.kind)) {
264
+ el.innerHTML = icon(facts.kind === 'not-started-cancelled' ? 'minus' : 'alert') + '<span>' + escapeHtml(facts.text) + '</span>';
265
+ } else if (['didnt-finish', 'partly-written', 'not-written', 'check-overleaf'].includes(facts.kind)) {
266
+ el.innerHTML = icon(facts.kind === 'check-overleaf' ? 'alert' : facts.kind === 'partly-written' ? 'half' : 'minus') + '<span>' + escapeHtml(facts.text) + '</span>'
267
+ + (Number.isFinite(facts.elapsedMs) ? '<span class="run-summary-sep">·</span><span>' + escapeHtml(formatElapsed(facts.elapsedMs)) + '</span>' : '');
258
268
  } else {
259
269
  el.textContent = facts.text || '';
260
270
  }