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.
- package/README.md +155 -392
- package/README.zh-CN.md +154 -393
- package/extension/bootstrap/background.js +52 -9
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.html +1 -0
- package/extension/bootstrap/popupFallback.js +55 -0
- package/extension/bootstrap/updateStatus.js +1 -0
- package/extension/src/background.js +283 -55
- package/extension/src/backgroundBootstrapMigration.js +181 -0
- package/extension/src/backgroundSharedSessions.js +129 -16
- package/extension/src/backgroundUpdateCoordinator.js +242 -29
- package/extension/src/backgroundUpdateSafety.js +173 -0
- package/extension/src/content/activeTurnControl.js +171 -8
- package/extension/src/content/applyResultFormatters.js +46 -73
- package/extension/src/content/assetTransferBroker.js +72 -14
- package/extension/src/content/composerAttachments.js +104 -47
- package/extension/src/content/composerPanel.js +110 -12
- package/extension/src/content/contentRuntime.js +1555 -926
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsController.js +122 -96
- package/extension/src/content/diagnosticsExport.js +19 -5
- package/extension/src/content/diagnosticsPanel.js +48 -0
- package/extension/src/content/directoryCleanupRecovery.js +54 -0
- package/extension/src/content/generated/content.bundle.js +11077 -3412
- package/extension/src/content/generated/content.bundle.meta.json +14 -3
- package/extension/src/content/globalPreferencesController.js +1 -1
- package/extension/src/content/legacyGlobalRegistry.js +10 -0
- package/extension/src/content/localSkillsPanel.js +104 -14
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +47 -11
- package/extension/src/content/modelPicker.js +8 -4
- package/extension/src/content/nativeCompatibilityController.js +74 -30
- package/extension/src/content/otWarmMirror.js +5 -6
- package/extension/src/content/otWarmMirrorController.js +17 -20
- package/extension/src/content/pageBridgeClient.js +17 -4
- package/extension/src/content/panelMaintenance.js +46 -14
- package/extension/src/content/panelRenderer.js +57 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectProviderSelection.js +13 -4
- package/extension/src/content/projectSessionCleanup.js +109 -62
- package/extension/src/content/projectSettingsCoordinator.js +67 -6
- package/extension/src/content/providerSettingsCoordinator.js +46 -8
- package/extension/src/content/providerSettingsDialog.js +73 -14
- package/extension/src/content/recentProjects.js +15 -8
- package/extension/src/content/runActivitySummary.js +160 -18
- package/extension/src/content/runCompletionMeta.js +532 -0
- package/extension/src/content/runController.js +39 -15
- package/extension/src/content/runFailureNotice.js +29 -13
- package/extension/src/content/runGuidanceController.js +72 -27
- package/extension/src/content/runGuidanceView.js +59 -10
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +157 -47
- package/extension/src/content/runRecoveryActions.js +147 -0
- package/extension/src/content/runResultActions.js +28 -143
- package/extension/src/content/runReviewController.js +633 -0
- package/extension/src/content/runScrollLayout.js +21 -2
- package/extension/src/content/runTimelineView.js +367 -175
- package/extension/src/content/scopedPersistencePanelState.js +134 -20
- package/extension/src/content/selectionContextView.js +1 -1
- package/extension/src/content/sensitivePreflight.js +153 -0
- package/extension/src/content/sessionForkController.js +80 -17
- package/extension/src/content/sessionManager.js +50 -4
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +22 -5
- package/extension/src/content/settingsWorkbench.js +132 -89
- package/extension/src/content/sharedSessionViewSync.js +49 -11
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateIdle.js +4 -1
- package/extension/src/content/updateNotice.js +118 -12
- package/extension/src/content/updateSafetyReceiver.js +58 -0
- package/extension/src/content/writebackController.js +53 -4
- package/extension/src/content/writebackOrchestrator.js +116 -94
- package/extension/src/content/writingStyleSettings.js +35 -11
- package/extension/src/page/binaryAssetUploader.js +136 -14
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/fileMover.js +256 -0
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/ownedDirectoryCleanup.js +94 -0
- package/extension/src/page/referenceProjects.js +1 -0
- package/extension/src/page/reviewContext.js +113 -0
- package/extension/src/page/saveState.js +6 -4
- package/extension/src/page/snapshotRouter.js +84 -39
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +114 -26
- package/extension/src/page/trackedChangeCapture.js +12 -2
- package/extension/src/page/trackedChangeOwnership.js +19 -1
- package/extension/src/page/trackedChangeReplay.js +41 -21
- package/extension/src/page/trackedChangesLifecycle.js +217 -199
- package/extension/src/page/treeOperations.js +7 -10
- package/extension/src/page/updateSafetyProbe.js +41 -0
- package/extension/src/page/writebackReceiptJournal.js +2 -0
- package/extension/src/page/writebackRouter.js +103 -155
- package/extension/src/pageBridge.js +126 -69
- package/extension/src/popup.js +62 -10
- package/extension/src/shared/agentTranscript.js +373 -17
- package/extension/src/shared/auditRecords.js +4 -0
- package/extension/src/shared/compatibility.js +22 -6
- package/extension/src/shared/compileAdapter.js +129 -66
- package/extension/src/shared/composerAttachmentDraft.js +156 -0
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/directoryCleanupProof.js +43 -0
- package/extension/src/shared/failureReasons.js +349 -4
- package/extension/src/shared/globalPreferences.js +30 -13
- package/extension/src/shared/governanceRules.js +4 -37
- package/extension/src/shared/i18n.js +590 -24
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/managedUpdateProjection.js +2 -2
- package/extension/src/shared/nativeRequestIdentity.js +1 -0
- package/extension/src/shared/pageRpcContract.js +5 -4
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/projectFiles.js +21 -44
- package/extension/src/shared/providerProfiles.js +5 -3
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/runInputQueue.js +48 -5
- package/extension/src/shared/selectionContext.js +12 -3
- package/extension/src/shared/sessionState.js +132 -28
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/settlementFacts.js +10 -2
- package/extension/src/shared/sharedSessionBridge.js +118 -27
- package/extension/src/shared/sharedSessionTransfer.js +140 -0
- package/extension/src/shared/staleGuard.js +166 -52
- package/extension/src/shared/storageDb.js +90 -26
- package/extension/src/shared/storageMigration.js +78 -112
- package/extension/src/shared/storageRunActions.js +267 -30
- package/extension/src/shared/subagentActivityModel.js +1 -1
- package/extension/src/shared/undoOperations.js +253 -32
- package/extension/src/shared/updateConsent.js +6 -4
- package/extension/src/shared/updateRuntimeIdentity.js +17 -3
- package/extension/src/shared/writebackIntent.js +16 -0
- package/extension/src/shared/writebackOperationEvidence.js +4 -15
- package/extension/src/shared/writebackSettlement.js +58 -40
- package/extension/styles/panel.css +1123 -493
- package/native-host/src/anthropicBridgeRequest.js +4 -2
- package/native-host/src/anthropicBridgeResponse.js +6 -2
- package/native-host/src/anthropicMessagesBridge.js +24 -16
- package/native-host/src/bootstrapMigration.js +296 -0
- package/native-host/src/bootstrapMigrationContract.js +101 -0
- package/native-host/src/bootstrapUpdate.js +269 -0
- package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +20 -1
- package/native-host/src/chatCompletionsBridge.js +48 -15
- package/native-host/src/codexConfigPaths.js +106 -0
- package/native-host/src/codexHome.js +66 -73
- package/native-host/src/codexHomeScope.js +124 -0
- package/native-host/src/codexInheritedAuth.js +90 -0
- package/native-host/src/codexLocalSkills.js +301 -0
- package/native-host/src/codexPromptAssembly.js +114 -127
- package/native-host/src/codexProviderTest.js +24 -26
- package/native-host/src/codexSessionRunner.js +234 -198
- package/native-host/src/codexSessionTiming.js +18 -8
- package/native-host/src/codexSkillIsolation.js +170 -0
- package/native-host/src/codexThreadFork.js +19 -3
- package/native-host/src/index.js +19 -7
- package/native-host/src/managedInstall.js +110 -35
- package/native-host/src/managedInstallRecovery.js +140 -0
- package/native-host/src/managedRuntimeEvidence.js +57 -0
- package/native-host/src/mirrorWorkspace.js +92 -22
- package/native-host/src/nativeDoctor.js +15 -0
- package/native-host/src/nativeHelperLifecycle.js +21 -0
- package/native-host/src/nativeHostRegistration.js +33 -0
- package/native-host/src/nativeRunLifecycle.js +71 -0
- package/native-host/src/processStopBarrier.js +42 -7
- package/native-host/src/projectPathPlatform.js +30 -0
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/providerHttp.js +80 -0
- package/native-host/src/providerProfile.js +4 -2
- package/native-host/src/providerReasoning.js +2 -7
- package/native-host/src/providerRuntime.js +1 -0
- package/native-host/src/providerStore.js +72 -14
- package/native-host/src/responsesPassthroughBridge.js +266 -52
- package/native-host/src/runtimeInstaller.js +21 -1
- package/native-host/src/selectionScope.js +36 -8
- package/native-host/src/subagentAdmission.js +26 -0
- package/native-host/src/subagentBroker.js +79 -66
- package/native-host/src/subagentTelemetry.js +19 -10
- package/native-host/src/subagentWorkspacePath.js +5 -2
- package/native-host/src/taskRunnerRuntime.js +92 -57
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/tomlStatements.js +140 -0
- package/native-host/src/updateManager.js +409 -46
- package/native-host/src/writingStyleRuntime.js +57 -29
- package/native-host/src/writingStyleStore.js +73 -0
- package/package.json +5 -8
- package/scripts/install-managed.mjs +1 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
235
|
-
await options.save?.({
|
|
236
|
-
queueMutation: { type: 'transition', sessionId: session.id, itemId }
|
|
237
|
-
});
|
|
302
|
+
guidanceAttempts.set(attemptKey, attempt);
|
|
238
303
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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(
|
|
257
|
-
guidanceId:
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
369
|
+
options.toast?.(formatGuideError(response?.error), 'warning');
|
|
267
370
|
}
|
|
371
|
+
clearPreview();
|
|
268
372
|
render();
|
|
269
|
-
|
|
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:
|
|
272
|
-
: { type: 'transition', sessionId:
|
|
377
|
+
? { type: 'remove', sessionId: identity.sessionId, itemId }
|
|
378
|
+
: { type: 'transition', sessionId: identity.sessionId, itemId }
|
|
273
379
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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,19 +431,29 @@
|
|
|
331
431
|
}
|
|
332
432
|
const session = options.getActiveSession?.();
|
|
333
433
|
const run = options.getCurrentRun?.();
|
|
334
|
-
|
|
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
|
});
|
|
338
444
|
}
|
|
339
445
|
|
|
340
446
|
async function startQueuedInput(item, sessionId) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
447
|
+
const assertCanStart = () => {
|
|
448
|
+
if (options.canStart?.(sessionId) === false || options.getCurrentRun?.()) {
|
|
449
|
+
const error = new Error('Queued input cannot start in the current session.');
|
|
450
|
+
error.code = 'queue_session_not_active';
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
assertCanStart();
|
|
346
455
|
await options.applyQueuedInput?.(item);
|
|
456
|
+
assertCanStart();
|
|
347
457
|
await options.run?.(item);
|
|
348
458
|
}
|
|
349
459
|
|
|
@@ -0,0 +1,147 @@
|
|
|
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', 'codex_context_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.dataset.recoveryAction = 'edit-resend';
|
|
60
|
+
retry.addEventListener('click', clickEvent => {
|
|
61
|
+
clickEvent.stopPropagation();
|
|
62
|
+
refillComposerForRetry(run || null);
|
|
63
|
+
});
|
|
64
|
+
report.append(retry);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function appendRecoveryActionForFailure(report, event, run) {
|
|
68
|
+
const recovery = failureNotice?.prepareRecovery(report, { openFile: openProjectFileForFailure });
|
|
69
|
+
const failure = recovery?.failure || event?.failure;
|
|
70
|
+
const failureCode = failure?.code || '';
|
|
71
|
+
if (recovery?.handled) return;
|
|
72
|
+
report = recovery?.target || report;
|
|
73
|
+
if (RETRYABLE_FAILURE_CODES.has(failureCode) && typeof refillComposerForRetry === 'function') {
|
|
74
|
+
appendEditAndResend(report, run, failureCode);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (NATIVE_SETUP_FAILURE_CODES.has(failureCode) && typeof showNativeSetupGuidance === 'function') {
|
|
78
|
+
const fix = buildRecoveryButton(failureCode,
|
|
79
|
+
tx('Fix the native host', '修复 native host'),
|
|
80
|
+
tx('Opens the install/update guidance with the copyable command.', '打开安装/更新指引(含可复制命令)。'));
|
|
81
|
+
fix.addEventListener('click', clickEvent => {
|
|
82
|
+
clickEvent.stopPropagation();
|
|
83
|
+
showNativeSetupGuidance();
|
|
84
|
+
});
|
|
85
|
+
report.append(fix);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (OPEN_FILE_FAILURE_CODES.has(failureCode) && failure?.file && typeof openProjectFileForFailure === 'function') {
|
|
89
|
+
const open = buildRecoveryButton(failureCode,
|
|
90
|
+
tx(`Open ${failure.file} in Overleaf`, `在 Overleaf 打开 ${failure.file}`), '');
|
|
91
|
+
open.addEventListener('click', clickEvent => {
|
|
92
|
+
clickEvent.stopPropagation();
|
|
93
|
+
openProjectFileForFailure(failure.file);
|
|
94
|
+
});
|
|
95
|
+
report.append(open);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (STORAGE_FAILURE_CODES.has(failureCode) && typeof openStorageSettings === 'function') {
|
|
99
|
+
const clean = buildRecoveryButton(failureCode,
|
|
100
|
+
tx('Open history & storage cleanup', '打开历史与存储清理'), '');
|
|
101
|
+
clean.addEventListener('click', clickEvent => {
|
|
102
|
+
clickEvent.stopPropagation();
|
|
103
|
+
openStorageSettings();
|
|
104
|
+
});
|
|
105
|
+
report.append(clean);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (CODEX_INSTALL_FAILURE_CODES.has(failureCode)) {
|
|
109
|
+
const guide = buildRecoveryButton(failureCode,
|
|
110
|
+
tx('Codex CLI troubleshooting guide', '查看 Codex CLI 排查指引'),
|
|
111
|
+
tx('Opens the README section on fixing a missing `codex` command (PATH / login).', '打开 README 中「找不到 codex 命令」的排查说明(PATH/登录)。'));
|
|
112
|
+
guide.addEventListener('click', clickEvent => {
|
|
113
|
+
clickEvent.stopPropagation();
|
|
114
|
+
window.open(CODEX_CLI_TROUBLESHOOTING_URL, '_blank', 'noopener');
|
|
115
|
+
});
|
|
116
|
+
report.append(guide);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (failureCode !== 'codex_project_locked') return;
|
|
120
|
+
const button = document.createElement('button');
|
|
121
|
+
button.type = 'button';
|
|
122
|
+
button.className = 'run-final-answer__recovery-action';
|
|
123
|
+
button.dataset.recoveryFor = failureCode;
|
|
124
|
+
button.textContent = tx('Force-release the stuck task', '强制释放卡住的任务');
|
|
125
|
+
button.title = tx(
|
|
126
|
+
'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.',
|
|
127
|
+
'请求取消当前项目的任务;请求已发送不代表任务已经停止。刷新页面仍未解除占用时使用。'
|
|
128
|
+
);
|
|
129
|
+
button.addEventListener('click', event => {
|
|
130
|
+
event.stopPropagation();
|
|
131
|
+
button.disabled = true;
|
|
132
|
+
button.textContent = tx('Releasing…', '正在释放…');
|
|
133
|
+
forceCancelStuckTaskForCurrentProject().then(result => {
|
|
134
|
+
button.textContent = formatForceCancelOutcome(result, tx);
|
|
135
|
+
button.disabled = result?.ok === true;
|
|
136
|
+
}).catch(() => {
|
|
137
|
+
button.disabled = false;
|
|
138
|
+
button.textContent = tx('Release failed — refresh the tab, then try again.', '释放失败,请刷新页面后重试。');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
report.append(button);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { buildRecoveryButton, appendRecoveryActionForFailure };
|
|
145
|
+
}
|
|
146
|
+
return { create, formatForceCancelOutcome };
|
|
147
|
+
});
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
(function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
})(typeof window !== 'undefined' ? window : globalThis, function runResultActionsFactory() {
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory(require('./runCompletionMeta'));
|
|
3
|
+
else root.CodexOverleafModuleRegistry.define('RunResultActions', ['RunCompletionMeta'], factory);
|
|
4
|
+
})(typeof window !== 'undefined' ? window : globalThis, function (RunCompletionMeta) {
|
|
6
5
|
'use strict';
|
|
7
6
|
|
|
7
|
+
const { splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta } = RunCompletionMeta;
|
|
8
|
+
|
|
9
|
+
|
|
8
10
|
function create(deps = {}) {
|
|
9
11
|
const {
|
|
10
12
|
tr,
|
|
11
|
-
getLocale,
|
|
12
13
|
forkRunFromNode,
|
|
13
14
|
document: documentRef = globalThis.document,
|
|
14
15
|
navigator: navigatorRef = globalThis.navigator
|
|
@@ -19,11 +20,27 @@
|
|
|
19
20
|
const reports = root.querySelectorAll('.run-completion-report');
|
|
20
21
|
const report = reports[reports.length - 1];
|
|
21
22
|
if (!actions || !report) return;
|
|
22
|
-
|
|
23
|
+
// Share one row with the facts summary when it exists; the expandable
|
|
24
|
+
// details list stays below both.
|
|
25
|
+
const factsToggle = report.querySelector(':scope > .run-final-answer__facts > .run-final-answer__facts-sum');
|
|
26
|
+
if (factsToggle) factsToggle.after(actions);
|
|
27
|
+
else {
|
|
28
|
+
// Without a facts summary, the report's own Details toggle shares the row.
|
|
29
|
+
const details = report.querySelector(':scope > .run-outcome-details, :scope > .run-outcome-footer > .run-outcome-details');
|
|
30
|
+
if (details) {
|
|
31
|
+
let footer = details.parentElement.matches('.run-outcome-footer') ? details.parentElement : null;
|
|
32
|
+
if (!footer) {
|
|
33
|
+
footer = documentRef.createElement('div');
|
|
34
|
+
footer.className = 'run-outcome-footer';
|
|
35
|
+
details.before(footer);
|
|
36
|
+
footer.append(details);
|
|
37
|
+
}
|
|
38
|
+
footer.append(actions);
|
|
39
|
+
} else report.append(actions);
|
|
40
|
+
}
|
|
23
41
|
actions.hidden = false;
|
|
24
42
|
configureCopyButton(root);
|
|
25
43
|
configureForkButton(root, run);
|
|
26
|
-
configureCompletedTime(actions, run);
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
function configureForkButton(root, run) {
|
|
@@ -38,7 +55,7 @@
|
|
|
38
55
|
button.disabled = !available;
|
|
39
56
|
button.title = tr(available ? 'forkRunTitle' : 'forkRunUnavailable');
|
|
40
57
|
button.setAttribute('aria-label', button.title);
|
|
41
|
-
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
|
|
58
|
+
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path transform="translate(.5 0)" d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
|
|
42
59
|
if (!available) return;
|
|
43
60
|
button.addEventListener('click', async event => {
|
|
44
61
|
event.stopPropagation();
|
|
@@ -53,19 +70,6 @@
|
|
|
53
70
|
});
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
function configureCompletedTime(actions, run) {
|
|
57
|
-
const target = actions.querySelector('[data-run-completed-time]');
|
|
58
|
-
const finishedAt = String(run.finishedAt || '').trim();
|
|
59
|
-
const date = finishedAt ? new Date(finishedAt) : null;
|
|
60
|
-
if (!target || !date || !Number.isFinite(date.getTime())) return;
|
|
61
|
-
target.hidden = false;
|
|
62
|
-
target.textContent = new Intl.DateTimeFormat(getLocale(), {
|
|
63
|
-
hour: '2-digit',
|
|
64
|
-
minute: '2-digit'
|
|
65
|
-
}).format(date);
|
|
66
|
-
target.title = date.toLocaleString(getLocale());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
73
|
function configureCopyButton(root) {
|
|
70
74
|
const existing = root.querySelector('[data-run-copy]');
|
|
71
75
|
const button = existing.cloneNode(true);
|
|
@@ -76,7 +80,7 @@
|
|
|
76
80
|
button.disabled = !copyText;
|
|
77
81
|
button.title = tr('copyConclusionTitle');
|
|
78
82
|
button.setAttribute('aria-label', button.title);
|
|
79
|
-
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></svg>';
|
|
83
|
+
button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><g transform="translate(.75 .75)"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></g></svg>';
|
|
80
84
|
if (!copyText) return;
|
|
81
85
|
button.addEventListener('click', async event => {
|
|
82
86
|
event.stopPropagation();
|
|
@@ -107,128 +111,9 @@
|
|
|
107
111
|
if (!copied) throw new Error('clipboard_copy_failed');
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectCompletionMeta, summarizeCompletionMeta });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const FLAT_REPORT_STATUS_SECTIONS = [
|
|
115
|
-
{ key: 'saveState', prefixes: ['Save:', '保存:'], en: 'Save', zh: '保存' },
|
|
116
|
-
{ key: 'unchangedReason', prefixes: ['Why nothing changed:', '未修改原因:'], en: 'Why nothing changed', zh: '未修改原因' },
|
|
117
|
-
{ key: 'writeResult', prefixes: ['Write result:', '写入结果:'], en: 'Write result', zh: '写入结果' },
|
|
118
|
-
{ key: 'undo', prefixes: ['Undo:', '可撤销:'], en: 'Undo', zh: '可撤销' },
|
|
119
|
-
{ key: 'nextStep', prefixes: ['Next:', '下一步:'], en: 'Next', zh: '下一步' }
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
function splitFlatCompletionReport(text, tx = value => value) {
|
|
123
|
-
const raw = typeof text === 'string' ? text : '';
|
|
124
|
-
if (!raw.trim()) return { body: raw, meta: [] };
|
|
125
|
-
const bodySections = [], meta = [];
|
|
126
|
-
for (const section of raw.split(/\n{2,}/)) {
|
|
127
|
-
const trimmed = section.trim();
|
|
128
|
-
if (!trimmed) continue;
|
|
129
|
-
const entry = trimmed.includes('\n') ? null
|
|
130
|
-
: FLAT_REPORT_STATUS_SECTIONS.find(item => item.prefixes.some(prefix => trimmed.startsWith(prefix)));
|
|
131
|
-
if (entry) {
|
|
132
|
-
const prefix = entry.prefixes.find(value => trimmed.startsWith(value));
|
|
133
|
-
const value = trimmed.slice(prefix.length).trim();
|
|
134
|
-
if (value) {
|
|
135
|
-
meta.push({ key: entry.key, label: tx(entry.en, entry.zh), value });
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
bodySections.push(trimmed);
|
|
140
|
-
}
|
|
141
|
-
return { body: bodySections.join('\n\n'), meta };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function projectCompletionMeta(meta, run, options) {
|
|
145
|
-
const { tx, trackedChangeInFlight, isTrackedChangeLifecycleRun, projectRunSettlement } = options;
|
|
146
|
-
if (!Array.isArray(meta) || !run) return meta;
|
|
147
|
-
const undone = run.undoStatus === 'applied' || run.trackedChangeStatus === 'rejected';
|
|
148
|
-
const rows = undone ? meta.filter(row => row?.key !== 'saveState') : meta;
|
|
149
|
-
const inFlight = trackedChangeInFlight?.get(run.id);
|
|
150
|
-
const tracked = isTrackedChangeLifecycleRun(run);
|
|
151
|
-
const pendingCreatedUndo = tracked && run.trackedChangeStatus === 'needs_review' && run.undoOperations?.some(operation => operation?.undoCreatedFile?.v === 1);
|
|
152
|
-
let undo;
|
|
153
|
-
if (run.undoStatus === 'running' || inFlight === 'reject') {
|
|
154
|
-
undo = tx('Undoing changes...', '正在撤销修改…');
|
|
155
|
-
} else if (run.undoStatus === 'applied' || (tracked && run.trackedChangeStatus === 'rejected')) {
|
|
156
|
-
undo = tx('This run\'s changes have been undone.', '本轮修改已撤销。');
|
|
157
|
-
} else if (tracked && run.trackedChangeStatus === 'accepted') {
|
|
158
|
-
undo = tx('Changes accepted; undo is no longer available.', '修改已接受,无法再撤销。');
|
|
159
|
-
} else if (run.undoStatus === 'partial' || pendingCreatedUndo) {
|
|
160
|
-
undo = hasUndoProgress(run)
|
|
161
|
-
? projectRunSettlement(run).canUndo
|
|
162
|
-
? tx(pendingCreatedUndo ? 'Some changes were undone; retry the remaining files.' : 'Some changes were undone; remaining changes can still be undone.', pendingCreatedUndo ? '已撤销部分修改,可重试剩余文件。' : '已撤销部分修改,剩余修改仍可撤销。')
|
|
163
|
-
: tx('Some changes were undone.', '已撤销部分修改。')
|
|
164
|
-
: projectRunSettlement(run).canUndo
|
|
165
|
-
? tx(pendingCreatedUndo ? 'Undo is incomplete; retry the remaining files.' : 'Undo is incomplete; remaining changes can still be undone.', pendingCreatedUndo ? '撤销未完成,可重试剩余文件。' : '撤销未完成,剩余修改仍可撤销。')
|
|
166
|
-
: tx('Undo is incomplete.', '撤销未完成。');
|
|
167
|
-
}
|
|
168
|
-
if (!undo && !run.saveCheck && !run.saveConfirmedAt) return rows;
|
|
169
|
-
return rows.map(row => row?.key === 'undo' && undo
|
|
170
|
-
? { ...row, label: tx('Undo', '撤销'), value: undo }
|
|
171
|
-
: row?.key === 'saveState' && (run.saveCheck || run.saveConfirmedAt)
|
|
172
|
-
? { ...row, label: tx('Save', '保存'), value: run.saveCheck
|
|
173
|
-
? tx('Pending confirmation', '待确认') : tx('Saved', '已确认保存') }
|
|
174
|
-
: row);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function hasUndoProgress(run) {
|
|
178
|
-
const failures = run?.settlement?.failures || run?.settlementFacts?.failures || [];
|
|
179
|
-
return (Array.isArray(run?.events) ? run.events : []).some(event => {
|
|
180
|
-
if (!['completed', 'failed'].includes(event?.status) || event.streamRole || ['report', 'guidance', 'stream'].includes(event.kind)) return false;
|
|
181
|
-
const count = String(event.title || '').match(/^(?:Undo result: (?:undone|rejected) (\d+) (?:item\(s\)|file\(s\)|tracked change\(s\)), skipped \d+(?: item\(s\))?|撤销结果:已(?:撤销|拒绝) (\d+) (?:项|条留痕|个文件),跳过 \d+ (?:项|条))$/);
|
|
182
|
-
if (!(Number(count?.[1] || count?.[2]) > 0)) return false;
|
|
183
|
-
const detail = event.detail || {}, applied = detail.Undone || detail.Rejected || detail['已撤销'] || detail['已拒绝'];
|
|
184
|
-
const skipped = detail.Skipped || detail['跳过'], path = item => item?.File || item?.['文件'];
|
|
185
|
-
return Array.isArray(applied) && Array.isArray(skipped) && applied.some(item =>
|
|
186
|
-
typeof path(item) === 'string' && path(item).trim() && !skipped.some(other => path(other) === path(item))
|
|
187
|
-
&& !(Array.isArray(failures) && failures.some(failure => failure?.code === 'undo_not_verified'
|
|
188
|
-
&& (!failure.file || failure.file === path(item)))));
|
|
189
|
-
});
|
|
114
|
+
return Object.freeze({ configureResultActions, projectUndoAvailability, splitFlatCompletionReport, projectNotStartedRun, projectFinishedRun, projectCompletionConclusion, projectCompletionBody, projectCompletionMeta, summarizeCompletionMeta });
|
|
190
115
|
}
|
|
191
116
|
|
|
192
|
-
// Generic reasons say nothing beyond "no files were written", so they stay in
|
|
193
|
-
// the details. Both locales match because a report keeps its original locale.
|
|
194
|
-
const GENERIC_UNCHANGED_REASONS = new Set([
|
|
195
|
-
'No file changes need to sync back to Overleaf.', '没有产生需要同步回 Overleaf 的文件改动。',
|
|
196
|
-
'No approved file changes remain to sync back to Overleaf.', '没有剩余已确认的文件改动需要同步回 Overleaf。',
|
|
197
|
-
'This run was Ask mode.', '这轮是只问不改。'
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
// One summary line for the meta rows: what was written, whether it can be
|
|
201
|
-
// undone, and a pending save. The rows themselves stay behind the disclosure,
|
|
202
|
-
// which opens by itself only when something needs the user's attention.
|
|
203
|
-
function summarizeCompletionMeta(rows, run, { tx, failed = false, unconfirmed = false } = {}) {
|
|
204
|
-
const find = key => String(rows.find(row => row?.key === key)?.value || '').trim();
|
|
205
|
-
const counts = find('writeResult').match(/wrote (\d+) items?, skipped (\d+)|已写入 (\d+) 项,跳过 (\d+) 项/);
|
|
206
|
-
const wrote = Number(counts?.[1] ?? counts?.[3] ?? 0), skipped = Number(counts?.[2] ?? counts?.[4] ?? 0);
|
|
207
|
-
const reason = find('unchangedReason');
|
|
208
|
-
const undone = run?.undoStatus === 'applied' || run?.trackedChangeStatus === 'rejected';
|
|
209
|
-
const savePending = !undone && (Boolean(run?.saveCheck) || (!run?.saveConfirmedAt && /^(?:Pending|待确认)/.test(find('saveState'))));
|
|
210
|
-
const pendingCreatedUndo = run?.trackedChangeStatus === 'needs_review'
|
|
211
|
-
&& run.undoOperations?.some(operation => operation?.undoCreatedFile?.v === 1);
|
|
212
|
-
const facts = [];
|
|
213
|
-
if (failed) facts.push({ key: 'write', tone: 'fail', text: unconfirmed ? tx('Write not confirmed', '写入结果未确认') : tx('Write incomplete', '写入未完成') });
|
|
214
|
-
else if (wrote) facts.push({ key: 'write', text: tx(`Wrote ${wrote} item${wrote === 1 ? '' : 's'}`, `已写入 ${wrote} 项`) });
|
|
215
|
-
else if (reason && !GENERIC_UNCHANGED_REASONS.has(reason)) facts.push({ key: 'write', text: tx('Not written: ', '未写入:') + reason.replace(/[.。]$/, '') });
|
|
216
|
-
else facts.push({ key: 'write', text: tx('No files written', '未写入文件') });
|
|
217
|
-
if (skipped) facts.push({ key: 'skip', tone: 'warn', text: tx(`${skipped} skipped`, `跳过 ${skipped} 项`) });
|
|
218
|
-
const undoable = Number(find('undo').match(/(\d+)/)?.[1] || 0);
|
|
219
|
-
const undo = run?.undoStatus === 'running' || /^(?:Undoing changes\.\.\.|正在撤销修改…)$/.test(find('undo')) ? tx('Undoing…', '正在撤销…')
|
|
220
|
-
: run?.undoStatus === 'applied' || run?.trackedChangeStatus === 'rejected' ? tx('Changes undone', '已撤销本轮修改')
|
|
221
|
-
: run?.trackedChangeStatus === 'accepted' ? tx('Changes accepted', '修改已接受')
|
|
222
|
-
: pendingCreatedUndo || run?.undoStatus === 'partial'
|
|
223
|
-
? (hasUndoProgress(run) ? tx('Partly undone', '已撤销部分修改') : tx('Undo incomplete', '撤销未完成'))
|
|
224
|
-
: undoable ? tx(`${undoable} undoable`, `可撤销 ${undoable} 项`)
|
|
225
|
-
: failed ? '' : tx('Nothing to undo', '无可撤销写入');
|
|
226
|
-
if (undo) facts.push({ key: 'undo', text: undo });
|
|
227
|
-
if (savePending) facts.push({ key: 'save', tone: 'warn', text: tx('Save pending', '保存待确认') });
|
|
228
|
-
const attention = failed || skipped > 0;
|
|
229
|
-
const warnKeys = new Set([...(attention ? ['writeResult', 'nextStep'] : []), ...(savePending ? ['saveState'] : [])]);
|
|
230
|
-
return { facts, open: attention, warnKeys };
|
|
231
|
-
}
|
|
232
117
|
|
|
233
118
|
function projectUndoAvailability(run, projectRunSettlement) {
|
|
234
119
|
const payload = run.recoveryPayload && typeof run.recoveryPayload === 'object'
|