codex-overleaf-link 2.6.0 → 2.6.5

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 (58) hide show
  1. package/README.md +9 -9
  2. package/README.zh-CN.md +9 -9
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +70 -0
  5. package/extension/src/content/composerAttachments.js +35 -8
  6. package/extension/src/content/contentRuntime.js +200 -51
  7. package/extension/src/content/diagnosticsController.js +77 -34
  8. package/extension/src/content/diagnosticsExport.js +1 -1
  9. package/extension/src/content/diagnosticsPanel.js +129 -42
  10. package/extension/src/content/generated/content.bundle.js +1283 -331
  11. package/extension/src/content/generated/content.bundle.meta.json +3 -3
  12. package/extension/src/content/localSkillsPanel.js +4 -2
  13. package/extension/src/content/markdownText.js +11 -9
  14. package/extension/src/content/modelPicker.js +28 -19
  15. package/extension/src/content/nativeCompatibilityController.js +23 -9
  16. package/extension/src/content/pageBridgeClient.js +5 -5
  17. package/extension/src/content/panelMaintenance.js +4 -0
  18. package/extension/src/content/panelRenderer.js +4 -1
  19. package/extension/src/content/providerSettingsDialog.js +26 -18
  20. package/extension/src/content/recentProjects.js +26 -16
  21. package/extension/src/content/runQueueScheduler.js +19 -9
  22. package/extension/src/content/runResultActions.js +51 -4
  23. package/extension/src/content/runReviewController.js +9 -3
  24. package/extension/src/content/scopedPersistencePanelState.js +34 -25
  25. package/extension/src/content/selectionContextView.js +30 -3
  26. package/extension/src/content/sharedSessionViewSync.js +9 -1
  27. package/extension/src/content/writebackOrchestrator.js +266 -50
  28. package/extension/src/content/writingStyleSettings.js +70 -17
  29. package/extension/src/page/overleafEditor.js +12 -3
  30. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  31. package/extension/src/page/snapshotRouter.js +75 -66
  32. package/extension/src/page/treeOperations.js +200 -19
  33. package/extension/src/page/writebackRouter.js +152 -40
  34. package/extension/src/pageBridge.js +17 -7
  35. package/extension/src/shared/agentTranscript.js +23 -2
  36. package/extension/src/shared/auditRecords.js +1 -0
  37. package/extension/src/shared/compatibility.js +1 -1
  38. package/extension/src/shared/composerAttachmentDraft.js +22 -7
  39. package/extension/src/shared/failureReasons.js +15 -0
  40. package/extension/src/shared/i18n.js +16 -0
  41. package/extension/src/shared/pageRpcContract.js +1 -1
  42. package/extension/src/shared/projectFiles.js +2 -1
  43. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  44. package/extension/src/shared/staleGuard.js +58 -0
  45. package/extension/src/shared/storageMigration.js +104 -1
  46. package/extension/styles/panel.css +191 -63
  47. package/native-host/src/codexSessionRunner.js +1 -0
  48. package/native-host/src/debugLog.js +27 -3
  49. package/native-host/src/index.js +42 -11
  50. package/native-host/src/localSkills.js +1 -1
  51. package/native-host/src/mirrorWorkspace.js +47 -3
  52. package/native-host/src/nativeEnvironment.js +27 -5
  53. package/native-host/src/providerRuntime.js +1 -0
  54. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  55. package/native-host/src/taskRunnerRuntime.js +11 -3
  56. package/native-host/src/updateManager.js +24 -6
  57. package/native-host/src/writingStyleRuntime.js +1 -1
  58. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- if (typeof document !== 'undefined' && document.documentElement) document.documentElement.setAttribute('data-codex-overleaf-source-digest', "0cf851b033afb0885464fd86506f423f35fe82d6da8c1f1644012350cab0ddc4");
1
+ if (typeof document !== 'undefined' && document.documentElement) document.documentElement.setAttribute('data-codex-overleaf-source-digest', "91739cf894fed56c2880986120922d3b54bb423ab2d116705385da1d0fe518be");
2
2
  (() => {
3
3
  // extension/entries/content-entry.mjs
4
4
  (function runClassicContentModule(module, exports, require2) {
@@ -913,6 +913,13 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
913
913
  fallbackUserMessage: "The file changed while Codex was working.",
914
914
  fallbackNextAction: "Review the current file, then rerun the task."
915
915
  },
916
+ document_identity_mismatch: {
917
+ stage: "preflight",
918
+ severity: "blocked",
919
+ defaultRetryable: true,
920
+ fallbackUserMessage: "Codex could not confirm that the editor content belongs to the target file, so it was not used.",
921
+ fallbackNextAction: "Wait for the target file to open and settle, then retry. Reload Overleaf if this continues."
922
+ },
916
923
  patch_anchor_not_found: {
917
924
  stage: "preflight",
918
925
  severity: "blocked",
@@ -1213,6 +1220,13 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
1213
1220
  fallbackUserMessage: "Native host request failed.",
1214
1221
  fallbackNextAction: "Open diagnostics and retry."
1215
1222
  },
1223
+ native_request_timeout: {
1224
+ stage: "native",
1225
+ severity: "error",
1226
+ defaultRetryable: true,
1227
+ fallbackUserMessage: "The local service did not respond.",
1228
+ fallbackNextAction: "Retry, or open Diagnostics to check the local connection."
1229
+ },
1216
1230
  native_protocol_incompatible: {
1217
1231
  stage: "native",
1218
1232
  severity: "blocked",
@@ -1311,6 +1325,13 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
1311
1325
  fallbackUserMessage: "The provider rate limit was reached.",
1312
1326
  fallbackNextAction: "Wait for the rate-limit window to recover and retry."
1313
1327
  },
1328
+ provider_quota_exceeded: {
1329
+ stage: "codex",
1330
+ severity: "error",
1331
+ defaultRetryable: true,
1332
+ fallbackUserMessage: "The model provider quota or billing limit was reached.",
1333
+ fallbackNextAction: "Check the provider account and plan, or switch model or provider before retrying."
1334
+ },
1314
1335
  provider_connection_timeout: {
1315
1336
  stage: "codex",
1316
1337
  severity: "error",
@@ -2038,6 +2059,9 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2038
2059
  "timeout": {
2039
2060
  "message": "timed out"
2040
2061
  },
2062
+ "native_request_timeout": {
2063
+ "code": "native_request_timeout"
2064
+ },
2041
2065
  "output_limit": {
2042
2066
  "message": "output limit exceeded"
2043
2067
  },
@@ -2054,7 +2078,7 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2054
2078
  "message": "unsupported_parameter reasoning.summary"
2055
2079
  },
2056
2080
  "storage_quota": {
2057
- "message": "quota"
2081
+ "message": "storage quota"
2058
2082
  },
2059
2083
  "checkpoint": {
2060
2084
  "message": "checkpoint"
@@ -2092,6 +2116,9 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2092
2116
  "provider_rate_limited": {
2093
2117
  "message": "provider_rate_limited"
2094
2118
  },
2119
+ "provider_quota_exceeded": {
2120
+ "message": "provider_quota_exceeded"
2121
+ },
2095
2122
  "provider_connection_timeout": {
2096
2123
  "message": "provider_connection_timeout"
2097
2124
  },
@@ -2660,6 +2687,14 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2660
2687
  nextEn: "Switch between Responses and Chat Completions, or test again with Auto detection.",
2661
2688
  code: "provider_protocol_incompatible"
2662
2689
  },
2690
+ {
2691
+ pattern: /provider_quota_exceeded|insufficient_quota|exceeded your current quota|(?:billing|usage|spend(?:ing)?|credit)[_ -]?(?:hard[_ -]?)?limit[_ -]?(?:reached|exceeded)|(?:billing|plan).{0,40}(?:quota|limit|exhausted)|(?:quota|credits?).{0,40}(?:billing|plan|exhausted|insufficient)/i,
2692
+ conclusionZh: "\u6A21\u578B\u670D\u52A1\u7684\u914D\u989D\u6216\u8D26\u5355\u9650\u989D\u5DF2\u8FBE\u5230\u3002",
2693
+ conclusionEn: "The model provider quota or billing limit was reached.",
2694
+ nextZh: "\u8BF7\u68C0\u67E5\u6A21\u578B\u670D\u52A1\u8D26\u6237\u548C\u5957\u9910\uFF0C\u6216\u5207\u6362\u6A21\u578B\u6216\u6A21\u578B\u670D\u52A1\u540E\u91CD\u8BD5\u3002",
2695
+ nextEn: "Check the provider account and plan, or switch model or provider before retrying.",
2696
+ code: "provider_quota_exceeded"
2697
+ },
2663
2698
  {
2664
2699
  pattern: /provider_rate_limited|rate limit was reached/i,
2665
2700
  conclusionZh: "\u8FD9\u8F6E\u6CA1\u6709\u5B8C\u6210\uFF1A\u7B2C\u4E09\u65B9\u6A21\u578B\u670D\u52A1\u89E6\u53D1\u4E86\u9650\u6D41\u3002",
@@ -2738,6 +2773,13 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2738
2773
  function translateRawErrorCopy(message, context = {}) {
2739
2774
  const locale = normalizeLocale(context);
2740
2775
  const text = String(message || "");
2776
+ if (context.code === "native_request_timeout" || /native_request_timeout/i.test(text)) {
2777
+ return {
2778
+ conclusion: textFor(locale, "\u672C\u673A\u670D\u52A1\u6CA1\u6709\u54CD\u5E94\u3002", "The local service did not respond."),
2779
+ nextStep: textFor(locale, "\u8BF7\u91CD\u8BD5\uFF0C\u6216\u6253\u5F00\u300C\u8BCA\u65AD\u300D\u68C0\u67E5\u672C\u673A\u8FDE\u63A5\u3002", "Retry, or open Diagnostics to check the local connection."),
2780
+ failureCode: "native_request_timeout"
2781
+ };
2782
+ }
2741
2783
  if (context.code === "codex_local_skill_unavailable" || /(?:The )?selected local Codex skill is unavailable/i.test(text)) {
2742
2784
  return {
2743
2785
  conclusion: textFor(locale, "\u6240\u9009 Codex \u672C\u5730\u6280\u80FD\u4E0D\u53EF\u7528\u6216\u5DF2\u5173\u95ED\uFF0C\u672C\u8F6E\u6CA1\u6709\u542F\u52A8\u3002", "The selected local Codex skill is unavailable or disabled. This run did not start."),
@@ -2835,7 +2877,7 @@ if (typeof document !== 'undefined' && document.documentElement) document.docume
2835
2877
  failureCode: "native_protocol_incompatible"
2836
2878
  };
2837
2879
  }
2838
- if (/quota|kQuotaBytes|QUOTA_BYTES/i.test(text)) {
2880
+ if (/kQuotaBytes|QUOTA_BYTES|QuotaExceededError|storage quota/i.test(text)) {
2839
2881
  return {
2840
2882
  conclusion: textFor(locale, "Codex \u7ED3\u679C\u5DF2\u7ECF\u751F\u6210\uFF0C\u4F46\u672C\u5730\u4F1A\u8BDD\u8BB0\u5F55\u8D85\u51FA Chrome \u5B58\u50A8\u914D\u989D\u3002", "Codex produced a result, but local session history exceeded Chrome storage quota."),
2841
2883
  nextStep: textFor(locale, "\u8BF7\u5220\u9664\u4E00\u4E9B\u65E7 session\uFF0C\u6216\u5237\u65B0\u6269\u5C55\u540E\u91CD\u8BD5\uFF1B\u8FD9\u4E0D\u662F\u8BBA\u6587\u5206\u6790\u672C\u8EAB\u5931\u8D25\u3002", "Delete older sessions or reload the extension and retry. The paper analysis itself did not fail."),
@@ -4228,10 +4270,11 @@ ${block}`;
4228
4270
  continue;
4229
4271
  }
4230
4272
  seen.add(normalized);
4231
- result.push(normalized);
4232
4273
  if (result.length >= limit) {
4274
+ result.truncated = true;
4233
4275
  break;
4234
4276
  }
4277
+ result.push(normalized);
4235
4278
  }
4236
4279
  return result;
4237
4280
  }
@@ -4835,6 +4878,46 @@ ${block}`;
4835
4878
  }
4836
4879
  })(typeof globalThis !== "undefined" ? globalThis : window, function staleGuardFactory() {
4837
4880
  "use strict";
4881
+ const DOCUMENT_PROVENANCE_OTHER_MIN = 0.9;
4882
+ const DOCUMENT_PROVENANCE_CLAIMED_MAX = 0.5;
4883
+ const DOCUMENT_PROVENANCE_EDGE_LIMIT = 64 * 1024;
4884
+ function assessDocumentProvenance(input = {}) {
4885
+ const { path, projectId, documentIdentity } = input || {};
4886
+ if (documentIdentity !== void 0) {
4887
+ const valid = documentIdentity && typeof documentIdentity === "object" && !Array.isArray(documentIdentity) && documentIdentity.version === 1 && documentIdentity.source === "cm-ranges-v1" && typeof projectId === "string" && projectId.length > 0 && documentIdentity.projectId === projectId && typeof path === "string" && path.length > 0 && documentIdentity.path === path && typeof documentIdentity.documentId === "string" && typeof documentIdentity.targetDocumentId === "string" && /^[a-f0-9]{24}$/.test(documentIdentity.documentId) && /^[a-f0-9]{24}$/.test(documentIdentity.targetDocumentId) && documentIdentity.documentId === documentIdentity.targetDocumentId;
4888
+ return valid ? { ok: true, identityState: "match" } : { ok: false, code: "document_identity_mismatch", identityState: "mismatch" };
4889
+ }
4890
+ try {
4891
+ const { path: path2, text, references } = input || {};
4892
+ if (typeof text !== "string" || !references) return { ok: true, identityState: "fallback" };
4893
+ const entries = references instanceof Map ? references : Object.entries(references);
4894
+ const claimed = references instanceof Map ? references.get(path2) : Object.prototype.hasOwnProperty.call(references, path2) ? references[path2] : void 0;
4895
+ if (text === claimed) return { ok: true, identityState: "fallback" };
4896
+ let matchedPath, bestScore = -1;
4897
+ for (const [otherPath, reference] of entries) {
4898
+ if (otherPath === path2 || typeof reference !== "string") continue;
4899
+ if (text === reference) return { ok: false, code: "document_identity_mismatch", identityState: "fallback", matchedPath: otherPath };
4900
+ const score = documentSimilarity(text, reference);
4901
+ if (score > bestScore) {
4902
+ bestScore = score;
4903
+ matchedPath = otherPath;
4904
+ }
4905
+ }
4906
+ if (bestScore >= DOCUMENT_PROVENANCE_OTHER_MIN && (typeof claimed !== "string" || documentSimilarity(text, claimed) < DOCUMENT_PROVENANCE_CLAIMED_MAX)) {
4907
+ return { ok: false, code: "document_identity_mismatch", identityState: "fallback", matchedPath };
4908
+ }
4909
+ } catch (_error) {
4910
+ }
4911
+ return { ok: true, identityState: "fallback" };
4912
+ }
4913
+ function documentSimilarity(left, right) {
4914
+ const length = Math.min(left.length, right.length);
4915
+ let prefix = 0, suffix = 0;
4916
+ while (prefix < Math.min(length, DOCUMENT_PROVENANCE_EDGE_LIMIT) && left[prefix] === right[prefix]) prefix++;
4917
+ while (suffix < Math.min(length - prefix, DOCUMENT_PROVENANCE_EDGE_LIMIT) && left[left.length - suffix - 1] === right[right.length - suffix - 1]) suffix++;
4918
+ const denominator = Math.min(Math.max(left.length, right.length), 2 * DOCUMENT_PROVENANCE_EDGE_LIMIT);
4919
+ return denominator ? (prefix + suffix) / denominator : 1;
4920
+ }
4838
4921
  function buildBaseFileLookup(files) {
4839
4922
  if (!Array.isArray(files) || files.length === 0) {
4840
4923
  return null;
@@ -5053,6 +5136,7 @@ ${block}`;
5053
5136
  return String(value || "").replace(/\s+/g, " ").replace(/\\/g, "/").trim().replace(/^\/+/, "");
5054
5137
  }
5055
5138
  return {
5139
+ assessDocumentProvenance,
5056
5140
  buildBaseFileLookup,
5057
5141
  checkOperationFreshness,
5058
5142
  prepareOperationPatches,
@@ -6446,6 +6530,8 @@ ${block}`;
6446
6530
  failureReason_provider_request_rejected_next: "Review the model ID, reasoning controls, tool support, and request overrides.",
6447
6531
  failureReason_provider_rate_limited_user: "The provider rate limit was reached.",
6448
6532
  failureReason_provider_rate_limited_next: "Wait for the rate-limit window to recover and retry.",
6533
+ failureReason_provider_quota_exceeded_user: "The model provider quota or billing limit was reached.",
6534
+ failureReason_provider_quota_exceeded_next: "Check the provider account and plan, or switch model or provider before retrying.",
6449
6535
  failureReason_provider_connection_timeout_user: "The provider connection timed out.",
6450
6536
  failureReason_provider_connection_timeout_next: "Check the endpoint and network, then retry.",
6451
6537
  failureReason_provider_dns_failed_user: "The provider hostname could not be resolved.",
@@ -6934,6 +7020,7 @@ ${block}`;
6934
7020
  queuedInputResume: "Resume",
6935
7021
  queuedInputRemove: "Remove",
6936
7022
  queuedInputAttachmentsUnsupported: "Queued follow-ups currently support text and @context. Remove binary attachments before queuing.",
7023
+ attachmentsLoading: "Attachments are still loading",
6937
7024
  queuedInputError_empty_input: "Enter a follow-up first.",
6938
7025
  queuedInputError_input_too_long: "This follow-up is too long to queue.",
6939
7026
  queuedInputError_queue_full: "The follow-up queue is full.",
@@ -7054,6 +7141,8 @@ ${block}`;
7054
7141
  failureReason_accept_replay_failed_next: "Inspect the file in Overleaf, then retry.",
7055
7142
  failureReason_native_request_failed_user: "The request to the local Codex bridge failed.",
7056
7143
  failureReason_native_request_failed_next: "Open Diagnostics to check the bridge, then retry.",
7144
+ failureReason_native_request_timeout_user: "The local service did not respond.",
7145
+ failureReason_native_request_timeout_next: "Retry, or open Diagnostics to check the local connection.",
7057
7146
  failureReason_panel_reload_interrupted_user: "This run was interrupted by a page refresh.",
7058
7147
  failureReason_panel_reload_interrupted_next: "Edit and resend the task to continue.",
7059
7148
  failureReason_sensitive_content_blocked_user: "Sensitive content was found before sending, so Codex did not receive this request.",
@@ -7080,6 +7169,8 @@ ${block}`;
7080
7169
  failureReason_target_editor_not_ready_next: "Wait for Overleaf to finish loading, then retry.",
7081
7170
  failureReason_stale_source_changed_user: "{file} changed while Codex was working, so Codex did not overwrite it.",
7082
7171
  failureReason_stale_source_changed_next: "Review the current file, then rerun the task.",
7172
+ failureReason_document_identity_mismatch_user: "Codex could not confirm that the editor content belongs to the target file, so it was not used.",
7173
+ failureReason_document_identity_mismatch_next: "Wait for the target file to open and settle, then retry. Reload Overleaf if this continues.",
7083
7174
  failureReason_patch_anchor_not_found_user: "The edit anchor for {file} no longer matches current Overleaf content.",
7084
7175
  failureReason_patch_anchor_not_found_next: "Rerun the task against the current document.",
7085
7176
  failureReason_write_observed_mismatch_user: "Codex attempted to write {file}, but the content read back from Overleaf did not match the approved change.",
@@ -7146,6 +7237,7 @@ ${block}`;
7146
7237
  recentProjects_previewUnavailableHint: "Saved conversation could not be loaded.",
7147
7238
  recentProjects_conversationGone: "This conversation is no longer available. Choose another conversation from the refreshed list.",
7148
7239
  recentProjects_continueFailed: "Could not open the conversation. Please try again.",
7240
+ recentProjects_renameFailed: "Could not rename the conversation. Your title is still available to edit or copy.",
7149
7241
  recentProjects_welcome_subtitle: "Conversations saved in this browser",
7150
7242
  recentProjects_empty: "No conversations saved in this browser yet. Open an Overleaf project and start a Codex conversation.",
7151
7243
  recentProjects_sessions_empty: "No saved conversations in this project",
@@ -7330,6 +7422,8 @@ ${block}`;
7330
7422
  failureReason_provider_request_rejected_next: "\u8BF7\u68C0\u67E5\u6A21\u578B\u6807\u8BC6\u3001\u63A8\u7406\u8BBE\u7F6E\u3001\u5DE5\u5177\u652F\u6301\u53CA\u81EA\u5B9A\u4E49\u8BF7\u6C42\u53C2\u6570\u3002",
7331
7423
  failureReason_provider_rate_limited_user: "\u6A21\u578B\u670D\u52A1\u5DF2\u8FBE\u5230\u8BF7\u6C42\u9891\u7387\u9650\u5236\u3002",
7332
7424
  failureReason_provider_rate_limited_next: "\u8BF7\u7B49\u5F85\u9650\u6D41\u7A97\u53E3\u6062\u590D\u540E\u91CD\u8BD5\u3002",
7425
+ failureReason_provider_quota_exceeded_user: "\u6A21\u578B\u670D\u52A1\u7684\u914D\u989D\u6216\u8D26\u5355\u9650\u989D\u5DF2\u8FBE\u5230\u3002",
7426
+ failureReason_provider_quota_exceeded_next: "\u8BF7\u68C0\u67E5\u6A21\u578B\u670D\u52A1\u8D26\u6237\u548C\u5957\u9910\uFF0C\u6216\u5207\u6362\u6A21\u578B\u6216\u6A21\u578B\u670D\u52A1\u540E\u91CD\u8BD5\u3002",
7333
7427
  failureReason_provider_connection_timeout_user: "\u6A21\u578B\u670D\u52A1\u8FDE\u63A5\u8D85\u65F6\u3002",
7334
7428
  failureReason_provider_connection_timeout_next: "\u8BF7\u68C0\u67E5\u670D\u52A1\u7AEF\u70B9\u548C\u7F51\u7EDC\uFF0C\u7136\u540E\u91CD\u8BD5\u3002",
7335
7429
  failureReason_provider_dns_failed_user: "\u65E0\u6CD5\u89E3\u6790\u6A21\u578B\u670D\u52A1\u7684\u4E3B\u673A\u540D\u3002",
@@ -7818,6 +7912,7 @@ ${block}`;
7818
7912
  queuedInputResume: "\u7EE7\u7EED",
7819
7913
  queuedInputRemove: "\u5220\u9664",
7820
7914
  queuedInputAttachmentsUnsupported: "\u5F53\u524D\u6392\u961F\u6D88\u606F\u652F\u6301\u6587\u672C\u548C @context\uFF1B\u8BF7\u5148\u79FB\u9664\u4E8C\u8FDB\u5236\u9644\u4EF6\u3002",
7915
+ attachmentsLoading: "\u9644\u4EF6\u8BFB\u53D6\u4E2D",
7821
7916
  queuedInputError_empty_input: "\u8BF7\u5148\u8F93\u5165\u540E\u7EED\u4EFB\u52A1\u3002",
7822
7917
  queuedInputError_input_too_long: "\u8FD9\u6761\u540E\u7EED\u4EFB\u52A1\u592A\u957F\uFF0C\u65E0\u6CD5\u6392\u961F\u3002",
7823
7918
  queuedInputError_queue_full: "\u540E\u7EED\u4EFB\u52A1\u961F\u5217\u5DF2\u6EE1\u3002",
@@ -7937,6 +8032,8 @@ ${block}`;
7937
8032
  failureReason_accept_replay_failed_next: "\u8BF7\u5728 Overleaf \u4E2D\u68C0\u67E5\u8BE5\u6587\u4EF6\u540E\u91CD\u8BD5\u3002",
7938
8033
  failureReason_native_request_failed_user: "\u5411\u672C\u5730 Codex bridge \u7684\u8BF7\u6C42\u5931\u8D25\u3002",
7939
8034
  failureReason_native_request_failed_next: "\u6253\u5F00\u300C\u8BCA\u65AD\u300D\u68C0\u67E5 bridge \u540E\u91CD\u8BD5\u3002",
8035
+ failureReason_native_request_timeout_user: "\u672C\u673A\u670D\u52A1\u6CA1\u6709\u54CD\u5E94\u3002",
8036
+ failureReason_native_request_timeout_next: "\u8BF7\u91CD\u8BD5\uFF0C\u6216\u6253\u5F00\u300C\u8BCA\u65AD\u300D\u68C0\u67E5\u672C\u673A\u8FDE\u63A5\u3002",
7940
8037
  failureReason_panel_reload_interrupted_user: "\u9875\u9762\u5237\u65B0\u5BFC\u81F4\u672C\u8F6E\u4E2D\u65AD\u3002",
7941
8038
  failureReason_panel_reload_interrupted_next: "\u53EF\u7F16\u8F91\u540E\u91CD\u53D1\u4EFB\u52A1\u4EE5\u7EE7\u7EED\u3002",
7942
8039
  failureReason_sensitive_content_blocked_user: "\u53D1\u9001\u524D\u53D1\u73B0\u654F\u611F\u5185\u5BB9\uFF0CCodex \u6CA1\u6709\u6536\u5230\u8FD9\u6B21\u8BF7\u6C42\u3002",
@@ -7963,6 +8060,8 @@ ${block}`;
7963
8060
  failureReason_target_editor_not_ready_next: "\u8BF7\u7B49 Overleaf \u52A0\u8F7D\u5B8C\u6210\u540E\u518D\u91CD\u8BD5\u3002",
7964
8061
  failureReason_stale_source_changed_user: "{file} \u5728\u4EFB\u52A1\u6267\u884C\u671F\u95F4\u88AB\u4FEE\u6539\u8FC7\uFF0CCodex \u6CA1\u6709\u8986\u76D6\u5B83\u3002",
7965
8062
  failureReason_stale_source_changed_next: "\u8BF7\u5148\u67E5\u770B\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF0C\u518D\u91CD\u65B0\u8FD0\u884C\u672C\u8F6E\u4EFB\u52A1\u3002",
8063
+ failureReason_document_identity_mismatch_user: "Codex \u65E0\u6CD5\u786E\u8BA4\u7F16\u8F91\u5668\u5185\u5BB9\u5C5E\u4E8E\u76EE\u6807\u6587\u4EF6\uFF0C\u56E0\u6B64\u6CA1\u6709\u4F7F\u7528\u8FD9\u4E9B\u5185\u5BB9\u3002",
8064
+ failureReason_document_identity_mismatch_next: "\u8BF7\u7B49\u76EE\u6807\u6587\u4EF6\u6253\u5F00\u5E76\u7A33\u5B9A\u540E\u91CD\u8BD5\uFF1B\u5982\u679C\u95EE\u9898\u6301\u7EED\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D Overleaf\u3002",
7966
8065
  failureReason_patch_anchor_not_found_user: "{file} \u7684\u4FEE\u6539\u951A\u70B9\u5DF2\u7ECF\u65E0\u6CD5\u548C\u5F53\u524D Overleaf \u5185\u5BB9\u5BF9\u9F50\u3002",
7967
8066
  failureReason_patch_anchor_not_found_next: "\u8BF7\u57FA\u4E8E\u5F53\u524D\u6587\u6863\u91CD\u65B0\u8FD0\u884C\u4EFB\u52A1\u3002",
7968
8067
  failureReason_write_observed_mismatch_user: "Codex \u5C1D\u8BD5\u5199\u5165 {file}\uFF0C\u4F46\u5199\u5165\u540E\u4ECE Overleaf \u8BFB\u56DE\u7684\u5185\u5BB9\u4E0E\u6279\u51C6\u7684\u6539\u52A8\u4E0D\u4E00\u81F4\u3002",
@@ -8029,6 +8128,7 @@ ${block}`;
8029
8128
  recentProjects_previewUnavailableHint: "\u5DF2\u4FDD\u5B58\u7684\u5BF9\u8BDD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u3002",
8030
8129
  recentProjects_conversationGone: "\u8BE5\u5BF9\u8BDD\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u4ECE\u66F4\u65B0\u540E\u7684\u5217\u8868\u4E2D\u9009\u62E9\u5176\u4ED6\u5BF9\u8BDD\u3002",
8031
8130
  recentProjects_continueFailed: "\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\u5BF9\u8BDD\uFF0C\u8BF7\u91CD\u8BD5\u3002",
8131
+ recentProjects_renameFailed: "\u6682\u65F6\u65E0\u6CD5\u91CD\u547D\u540D\u5BF9\u8BDD\uFF0C\u8F93\u5165\u7684\u6807\u9898\u4ECD\u4FDD\u7559\uFF0C\u53EF\u7EE7\u7EED\u7F16\u8F91\u6216\u590D\u5236\u3002",
8032
8132
  recentProjects_welcome_subtitle: "\u5F53\u524D\u6D4F\u89C8\u5668\u4FDD\u5B58\u7684\u5BF9\u8BDD",
8033
8133
  recentProjects_empty: "\u5F53\u524D\u6D4F\u89C8\u5668\u8FD8\u6CA1\u6709\u4FDD\u5B58\u7684\u5BF9\u8BDD\u3002\u6253\u5F00\u4E00\u4E2A Overleaf \u9879\u76EE\uFF0C\u4E0E Codex \u5F00\u59CB\u5BF9\u8BDD\u5373\u53EF\u3002",
8034
8134
  recentProjects_sessions_empty: "\u8BE5\u9879\u76EE\u8FD8\u6CA1\u6709\u4FDD\u5B58\u7684\u5BF9\u8BDD",
@@ -9943,7 +10043,18 @@ ${block}`;
9943
10043
  const sameScope = (a, b) => a.accountScopeId === b.accountScopeId && a.projectId === b.projectId && a.generation === b.generation;
9944
10044
  function applySnapshot(options, sessions) {
9945
10045
  const previous = options.getState(), scope = options.getScope();
9946
- const next = options.normalize({ ...previous, sessions }, { restoreRunningRuns: false });
10046
+ const input = sessions.length ? { ...previous, sessions } : {
10047
+ ...previous,
10048
+ session: null,
10049
+ sessions: [],
10050
+ runs: [],
10051
+ activeSessionId: "",
10052
+ task: "",
10053
+ focusFiles: [],
10054
+ selectionContext: null,
10055
+ composerAttachments: []
10056
+ };
10057
+ const next = options.normalize(input, { restoreRunningRuns: false });
9947
10058
  const unchangedSnapshot = JSON.stringify(next);
9948
10059
  options.setState(next);
9949
10060
  try {
@@ -10029,6 +10140,10 @@ ${block}`;
10029
10140
  }
10030
10141
  const unsubscribe = Bridge.subscribe((change) => {
10031
10142
  const scope = getScope();
10143
+ if (options.isDashboard?.()) {
10144
+ if (scope.accountScopeId && change.accountScopeId === scope.accountScopeId) options.refreshDashboard?.();
10145
+ return;
10146
+ }
10032
10147
  if (!change.projectId || change.projectId === scope.projectId) {
10033
10148
  retryCount = 0;
10034
10149
  pending = true;
@@ -10072,6 +10187,7 @@ ${block}`;
10072
10187
  })(typeof globalThis !== "undefined" ? globalThis : this, function(QueuePolicy, SharedSessions) {
10073
10188
  "use strict";
10074
10189
  var META_PREFIX = "codex-overleaf-scoped-persistence-v1:";
10190
+ var MIRROR_FENCE_PREFIX = "codex-overleaf-mirror-reuse-fence-v1:";
10075
10191
  var fallbackMeta = /* @__PURE__ */ new Map();
10076
10192
  var cleanPart = QueuePolicy.cleanPart;
10077
10193
  var normalizeMeta = QueuePolicy.normalizeMeta;
@@ -10127,7 +10243,35 @@ ${block}`;
10127
10243
  }
10128
10244
  };
10129
10245
  }
10246
+ async function getMirrorReuseFences(scope, chromeApi) {
10247
+ chromeApi = chromeApi || (typeof chrome !== "undefined" ? chrome : null);
10248
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local) throw new Error("Mirror fence storage is unavailable.");
10249
+ var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ":";
10250
+ var stored = await chromeApi.storage.local.get(null);
10251
+ return Object.keys(stored || {}).filter((key) => key.startsWith(prefix)).map((key) => {
10252
+ var generation = key.slice(prefix.length);
10253
+ if (!/^[a-zA-Z0-9_-]{1,160}$/.test(generation) || stored[key]?.v !== 1 || stored[key].generation !== generation) {
10254
+ throw new Error("Mirror fence storage contains an unreadable generation.");
10255
+ }
10256
+ return generation;
10257
+ }).sort();
10258
+ }
10259
+ async function setMirrorReuseFence(scope, generation, chromeApi) {
10260
+ chromeApi = chromeApi || (typeof chrome !== "undefined" ? chrome : null);
10261
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local || !/^[a-zA-Z0-9_-]{1,160}$/.test(generation)) throw new Error("Mirror fence storage is unavailable.");
10262
+ var key = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ":" + generation;
10263
+ await chromeApi.storage.local.set({ [key]: { v: 1, generation } });
10264
+ }
10265
+ async function clearMirrorReuseFences(scope, generations, chromeApi) {
10266
+ chromeApi = chromeApi || (typeof chrome !== "undefined" ? chrome : null);
10267
+ if (!normalizeScope(scope).projectId || !chromeApi?.storage?.local || !Array.isArray(generations) || generations.some((generation) => !/^[a-zA-Z0-9_-]{1,160}$/.test(generation))) throw new Error("Mirror fence storage is unavailable.");
10268
+ var prefix = MIRROR_FENCE_PREFIX + scopeKey(scope).slice(META_PREFIX.length) + ":";
10269
+ await chromeApi.storage.local.remove(generations.map((generation) => prefix + generation));
10270
+ }
10130
10271
  return Object.freeze({
10272
+ getMirrorReuseFences,
10273
+ setMirrorReuseFence,
10274
+ clearMirrorReuseFences,
10131
10275
  createBrowserAdapter,
10132
10276
  normalizeScope,
10133
10277
  scopeKey
@@ -14405,6 +14549,10 @@ ${block}`;
14405
14549
  "use strict";
14406
14550
  const EPOCH_KEY = "composer-attachment-draft:clear-epoch";
14407
14551
  const normalize = (value) => SessionState.normalizeComposerAttachmentsForPersistence(value);
14552
+ function normalizeOmitted(value) {
14553
+ const names = (Array.isArray(value?.names) ? value.names : []).filter((name) => typeof name === "string").map((name) => name.replace(/\0/g, "").trim().slice(0, 180)).filter(Boolean).slice(0, 8);
14554
+ return { count: names.length, names };
14555
+ }
14408
14556
  const scopeKey = (scope) => JSON.stringify([scope?.accountScopeId || "", scope?.projectId || ""]);
14409
14557
  function checkedScope(scope) {
14410
14558
  const accountScopeId = String(scope?.accountScopeId || "").trim();
@@ -14438,21 +14586,23 @@ ${block}`;
14438
14586
  throw failure("account_scope_unavailable");
14439
14587
  }
14440
14588
  const revision = Number(previous?.revision) || 0;
14441
- result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments) };
14589
+ result = { epoch, clearedAt, revision, attachments: normalize(previous?.attachments), omittedAttachments: normalizeOmitted(previous?.omittedAttachments) };
14442
14590
  if (!value) return;
14443
14591
  const attachments = normalize(value.attachments);
14592
+ const omittedAttachments = normalizeOmitted(value.omittedAttachments);
14593
+ const hasDraft = attachments.length || omittedAttachments.count;
14444
14594
  if (value.epoch !== epoch) {
14445
- if (!attachments.length) return;
14595
+ if (!hasDraft) return;
14446
14596
  throw failure("composer_draft_cleared");
14447
14597
  }
14448
14598
  if (value.revision !== revision) {
14449
- if (JSON.stringify(attachments) === JSON.stringify(result.attachments)) return;
14599
+ if (JSON.stringify(attachments) === JSON.stringify(result.attachments) && JSON.stringify(omittedAttachments) === JSON.stringify(result.omittedAttachments)) return;
14450
14600
  throw failure("composer_draft_conflict");
14451
14601
  }
14452
- if (attachments.length && value.unbased && clearedAt && value.changedAt <= clearedAt) {
14602
+ if (hasDraft && value.unbased && clearedAt && value.changedAt <= clearedAt) {
14453
14603
  throw failure("composer_draft_cleared");
14454
14604
  }
14455
- result = { epoch, clearedAt, revision: revision + 1, attachments };
14605
+ result = { epoch, clearedAt, revision: revision + 1, attachments, omittedAttachments };
14456
14606
  store.put({ id, type: "composer-attachment-draft", ...scope, ...result });
14457
14607
  } catch (caught) {
14458
14608
  error = caught;
@@ -14522,7 +14672,11 @@ ${block}`;
14522
14672
  function hydrate(attachments) {
14523
14673
  syncScope();
14524
14674
  if (hydrated) return;
14525
- if (!changedVersion && !options.getAttachments().length) options.restore(normalize(attachments));
14675
+ if (!changedVersion && !options.getAttachments().length) {
14676
+ options.restore(normalize(attachments));
14677
+ const omitted = normalizeOmitted(base?.omittedAttachments);
14678
+ if (omitted.count) options.onRestoreOmittedAttachments?.(omitted);
14679
+ }
14526
14680
  hydrated = true;
14527
14681
  publish();
14528
14682
  }
@@ -14547,10 +14701,11 @@ ${block}`;
14547
14701
  if (captured !== generation || scopeKey(options.getScope()) !== key || changedVersion === savedVersion) return;
14548
14702
  const version = changedVersion;
14549
14703
  const attachments = normalize(options.getAttachments());
14704
+ const omittedAttachments = normalizeOmitted({ names: options.getOmittedAttachmentNames?.() });
14550
14705
  const unbased = dirtyUnbased, changedAt = dirtySince;
14551
14706
  if (!base) await load(scope);
14552
14707
  if (captured !== generation || scopeKey(options.getScope()) !== key) return;
14553
- const written = await options.write(checkedScope(scope), { ...base, attachments, unbased, changedAt });
14708
+ const written = await options.write(checkedScope(scope), { ...base, attachments, omittedAttachments, unbased, changedAt });
14554
14709
  if (captured !== generation) return;
14555
14710
  base = written;
14556
14711
  savedVersion = version;
@@ -15851,6 +16006,95 @@ ${block}`;
15851
16006
  "selectedLocalSkillIdsByProject"
15852
16007
  ];
15853
16008
  var projectPreferenceLockTails = {};
16009
+ var projectPreferenceBaselines = /* @__PURE__ */ new Map();
16010
+ var projectPreferenceIntents = /* @__PURE__ */ new Map();
16011
+ var projectPreferenceBaselineVersions = /* @__PURE__ */ new WeakMap();
16012
+ function getProjectPreferenceBaseline(accountScopeId, projectId) {
16013
+ var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
16014
+ var baseline = copyPreferenceValue(projectPreferenceBaselines.get(key));
16015
+ var intents = projectPreferenceIntents.get(key);
16016
+ if (baseline && intents) projectPreferenceBaselineVersions.set(baseline, {
16017
+ owner: intents,
16018
+ versions: Object.assign({}, intents.acknowledged)
16019
+ });
16020
+ return baseline;
16021
+ }
16022
+ function adoptProjectPreferenceBaseline(prefs, accountScopeId, projectId) {
16023
+ var key = buildScopedProjectPreferenceKey(accountScopeId, projectId);
16024
+ if (key) {
16025
+ projectPreferenceBaselines.set(key, copyPreferenceValue(prefs));
16026
+ projectPreferenceIntents.set(key, { revision: 0, fields: {}, acknowledged: {} });
16027
+ }
16028
+ }
16029
+ function captureProjectPreferenceWrite(input) {
16030
+ var projectId = input.projectId;
16031
+ var key = buildScopedProjectPreferenceKey(input.accountScopeId, projectId);
16032
+ if (!key) return null;
16033
+ var intents = projectPreferenceIntents.get(key);
16034
+ if (!intents) {
16035
+ intents = { revision: 0, fields: {}, acknowledged: {} };
16036
+ projectPreferenceIntents.set(key, intents);
16037
+ }
16038
+ function values(prefs) {
16039
+ return {
16040
+ experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
16041
+ reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
16042
+ governanceRulesByProject: input.normalizeGovernanceRulesByProject({
16043
+ [projectId]: prefs.governanceRulesByProject?.[projectId] || {}
16044
+ })[projectId],
16045
+ customInstructionsByProject: input.normalizeCustomInstructionsByProject({
16046
+ [projectId]: prefs.customInstructionsByProject?.[projectId] || ""
16047
+ })[projectId]
16048
+ };
16049
+ }
16050
+ var baseline = input.baseline || getProjectPreferenceBaseline(input.accountScopeId, projectId);
16051
+ var captured = projectPreferenceBaselineVersions.get(baseline);
16052
+ var versions = captured?.owner === intents ? captured.versions : {};
16053
+ var previous = values(baseline || {});
16054
+ var current = values(input.state);
16055
+ var token = { key, owner: intents, projectId, fields: {}, versions: {}, written: [] };
16056
+ for (var name of Object.keys(current)) {
16057
+ var latest = intents.fields[name];
16058
+ var reference = latest && latest.revision > (versions[name] || 0) ? latest.value : previous[name];
16059
+ if (JSON.stringify(current[name]) !== JSON.stringify(reference)) {
16060
+ latest = { revision: ++intents.revision, value: copyPreferenceValue(current[name]), dirty: true };
16061
+ intents.fields[name] = latest;
16062
+ }
16063
+ token.fields[name] = latest && JSON.stringify(current[name]) === JSON.stringify(latest.value) ? latest : null;
16064
+ token.versions[name] = latest?.revision || 0;
16065
+ }
16066
+ return token;
16067
+ }
16068
+ function projectPreferencePatch(token, accountScopeId, projectId) {
16069
+ if (token.key !== buildScopedProjectPreferenceKey(accountScopeId, projectId)) {
16070
+ throw Object.assign(new Error("Scoped project preferences require account and project identity."), {
16071
+ code: "account_scope_unavailable"
16072
+ });
16073
+ }
16074
+ var current = projectPreferenceIntents.get(token.key) === token.owner;
16075
+ var patch = {};
16076
+ token.written = [];
16077
+ for (var name of Object.keys(token.fields)) {
16078
+ var intent = token.fields[name];
16079
+ var changed = current && intent?.dirty && token.owner.fields[name] === intent;
16080
+ patch[name] = changed ? { [token.projectId]: copyPreferenceValue(intent.value) } : {};
16081
+ if (changed) token.written.push(name);
16082
+ }
16083
+ return patch;
16084
+ }
16085
+ function completeProjectPreferenceWrite(token, prefs) {
16086
+ if (projectPreferenceIntents.get(token.key) !== token.owner) return [];
16087
+ for (var name of token.written) {
16088
+ var intent = token.fields[name];
16089
+ intent.dirty = false;
16090
+ token.owner.acknowledged[name] = Math.max(token.owner.acknowledged[name] || 0, intent.revision);
16091
+ }
16092
+ projectPreferenceBaselines.set(token.key, copyPreferenceValue(prefs));
16093
+ return Object.keys(token.fields).filter(function(name2) {
16094
+ var latest = token.owner.fields[name2];
16095
+ return (latest?.revision || 0) === token.versions[name2] && !latest?.dirty;
16096
+ });
16097
+ }
15854
16098
  function buildScopedProjectPreferenceKey(accountScopeId, projectId) {
15855
16099
  var account = compactScopePart(accountScopeId);
15856
16100
  var project = compactScopePart(projectId);
@@ -15984,6 +16228,7 @@ ${block}`;
15984
16228
  var visible = await StorageDb.claimSessionsForAccount(projectId, account, deleted);
15985
16229
  function result(currentPrefs, sessions2, migrated) {
15986
16230
  var wanted = currentPrefs.activeSessionByProject?.[projectId] || "";
16231
+ adoptProjectPreferenceBaseline(currentPrefs, account, projectId);
15987
16232
  return {
15988
16233
  prefs: currentPrefs,
15989
16234
  sessions: sessions2,
@@ -16071,7 +16316,9 @@ ${block}`;
16071
16316
  return chrome.storage.local.get([PREFS_KEY]).then(function(stored) {
16072
16317
  var claimed = claimLegacyProjectPreferences(stored[PREFS_KEY] || {}, account, project);
16073
16318
  var next = mergeScopedProjectPreferences(claimed.prefs, prefs, account, project);
16074
- return chrome.storage.local.set({ [PREFS_KEY]: next });
16319
+ return chrome.storage.local.set({ [PREFS_KEY]: next }).then(function() {
16320
+ return buildProjectPreferenceView(next, account, project);
16321
+ });
16075
16322
  });
16076
16323
  });
16077
16324
  }
@@ -16255,6 +16502,11 @@ ${block}`;
16255
16502
  runMigrationIfNeeded,
16256
16503
  savePrefs,
16257
16504
  loadPrefs,
16505
+ getProjectPreferenceBaseline,
16506
+ adoptProjectPreferenceBaseline,
16507
+ captureProjectPreferenceWrite,
16508
+ projectPreferencePatch,
16509
+ completeProjectPreferenceWrite,
16258
16510
  loadSessionTombstones,
16259
16511
  addSessionTombstones,
16260
16512
  getDeletedSessionIds,
@@ -16507,7 +16759,7 @@ ${block}`;
16507
16759
  const MIN_NATIVE_VERSION = "1.0.0";
16508
16760
  const MIN_COMPATIBLE_NATIVE_VERSION = "1.0.0";
16509
16761
  const MIN_COMPATIBLE_EXTENSION_VERSION = "1.0.0";
16510
- const BUILD_TARGET_VERSION = "2.6.0";
16762
+ const BUILD_TARGET_VERSION = "2.6.5";
16511
16763
  const DEFAULT_CHROME_EXTENSION_ID = "illdpneeeopfffmiepaejglgmhpmdhdc";
16512
16764
  const REQUIRED_CAPABILITIES = Object.freeze([
16513
16765
  "bridgePing",
@@ -16824,7 +17076,7 @@ ${block}`;
16824
17076
  if (root) root.CodexOverleafPageRpcContract = api;
16825
17077
  })(typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : this, function() {
16826
17078
  "use strict";
16827
- var REVISION = "2026-10-19-mode-menu-switch-v52";
17079
+ var REVISION = "2026-10-29-identity-v54";
16828
17080
  var METHODS = freezeCatalog({
16829
17081
  initializeCapability: entry("bridge.initialize", "control", "none", "default", false, "none"),
16830
17082
  probe: entry("bridge.probe", "read", "none", "default", false, "safe"),
@@ -17809,6 +18061,7 @@ ${block}`;
17809
18061
  return removeEmptySummaryFields(result);
17810
18062
  }
17811
18063
  function summarizeMirror(mirror = {}) {
18064
+ mirror = mirror ?? { status: "unavailable" };
17812
18065
  const files = Array.isArray(mirror.files) ? mirror.files : [];
17813
18066
  const skippedFiles = Array.isArray(mirror.skippedFiles) ? mirror.skippedFiles : [];
17814
18067
  return removeEmptySummaryFields({
@@ -18258,21 +18511,22 @@ ${block}`;
18258
18511
  ["src/shared/projectFiles.js", "codex-overleaf-project-files-script"],
18259
18512
  ["src/shared/compatibility.js", "codex-overleaf-compatibility-page-script"],
18260
18513
  ["src/shared/pageRpcContract.js", "codex-overleaf-page-rpc-contract-script", true],
18261
- ["src/shared/staleGuard.js", "codex-overleaf-stale-guard-script"],
18514
+ ["src/shared/staleGuard.js", "codex-overleaf-stale-guard-script", true],
18515
+ ["src/shared/otText.js", "codex-overleaf-ot-text-script"],
18262
18516
  ["src/shared/compileAdapter.js", "codex-overleaf-compile-adapter-script"],
18263
18517
  ["src/shared/sensitiveScan.js", "codex-overleaf-sensitive-scan-script"],
18264
18518
  ["src/shared/auditRecords.js", "codex-overleaf-audit-records-script"],
18265
18519
  ["src/page/saveState.js", "codex-overleaf-save-state-script", true],
18266
18520
  ["src/page/overleafCapabilities.js", "codex-overleaf-capabilities-script", true],
18267
18521
  ["src/page/compileBridge.js", "codex-overleaf-compile-bridge-script"],
18268
- ["src/page/overleafEditor.js", "codex-overleaf-editor-script"],
18269
- ["src/page/overleafProjectSnapshot.js", "codex-overleaf-project-snapshot-script"],
18522
+ ["src/page/overleafEditor.js", "codex-overleaf-editor-script", true],
18523
+ ["src/page/overleafProjectSnapshot.js", "codex-overleaf-project-snapshot-script", true],
18270
18524
  ["src/page/pageBridgeCapability.js", "codex-overleaf-page-bridge-capability-script"],
18271
18525
  ["src/page/treeOperations.js", "codex-overleaf-tree-operations-script", true],
18272
18526
  ["src/page/ownedDirectoryCleanup.js", "codex-overleaf-owned-directory-cleanup-script", true],
18273
18527
  ["src/page/textFileCreator.js", "codex-overleaf-text-file-creator-script", true],
18274
18528
  ["src/page/binaryAssetUploader.js", "codex-overleaf-binary-asset-uploader-script", true],
18275
- ["src/page/snapshotRouter.js", "codex-overleaf-snapshot-router-script"],
18529
+ ["src/page/snapshotRouter.js", "codex-overleaf-snapshot-router-script", true],
18276
18530
  ["src/page/referenceProjects.js", "codex-overleaf-reference-projects-script", true],
18277
18531
  ["src/page/fileMover.js", "codex-overleaf-file-mover-script", true],
18278
18532
  ["src/page/writeGuard.js", "codex-overleaf-write-guard-script", true],
@@ -18287,7 +18541,6 @@ ${block}`;
18287
18541
  ["src/pageBridge.js", "codex-overleaf-page-bridge-script", true]
18288
18542
  ]);
18289
18543
  const OPTIONAL_OT_SCRIPTS = Object.freeze([
18290
- ["src/shared/otText.js", "codex-overleaf-ot-text-script"],
18291
18544
  ["src/page/overleafRealtimeObserver.js", "codex-overleaf-realtime-observer-script"]
18292
18545
  ]);
18293
18546
  function create(options = {}) {
@@ -18525,7 +18778,8 @@ ${block}`;
18525
18778
  try {
18526
18779
  compatibilityGate = await ensureForMethod(payload?.method, context);
18527
18780
  if (!compatibilityGate.ok) return compatibilityGate.response;
18528
- if (gatedMethods.has(payload?.method) && payload.method !== "mirror.invalidate") {
18781
+ const independentCatalog = payload?.method === "codex.models" || payload?.method === "codex.providers.list";
18782
+ if (gatedMethods.has(payload?.method) && payload.method !== "mirror.invalidate" && !independentCatalog) {
18529
18783
  options.throwIfCancellationRequested?.();
18530
18784
  }
18531
18785
  context.assertCurrent?.();
@@ -18617,7 +18871,7 @@ ${block}`;
18617
18871
  options.setDiagnosticsHealth?.(classification === "update-available" ? "warn" : "fail");
18618
18872
  options.setBadge?.(renderer.headerEl, {
18619
18873
  type: "update",
18620
- tooltip: tx("Native host update available", "Native host \u53EF\u66F4\u65B0"),
18874
+ tooltip: compatibility.status === "native_unhealthy" ? tx("Codex Unavailable", "Codex \u4E0D\u53EF\u7528") : tx("Native host update available", "Native host \u53EF\u66F4\u65B0"),
18621
18875
  onClick: () => showUpdateGuidance(compatibility)
18622
18876
  });
18623
18877
  if (!compatibility?.native?.version) maybePromptFirstRunSetup();
@@ -18656,7 +18910,9 @@ ${block}`;
18656
18910
  const native = compatibility.native || {};
18657
18911
  const extensionVersion = compatibility.extensionVersion || Compatibility?.BUILD_TARGET_VERSION || "";
18658
18912
  const nativeVersion = compatibility.currentNativeVersion || compatibility.nativeVersion || native.version || tx("missing", "\u672A\u5B89\u88C5");
18659
- const command = resolveInstallCommand(compatibility);
18913
+ const codexUnavailable = compatibility.status === "native_unhealthy";
18914
+ const resolvedCommand = resolveInstallCommand(compatibility);
18915
+ const command = codexUnavailable ? "" : resolvedCommand;
18660
18916
  const releaseUrl = compatibility.releaseUrl || "";
18661
18917
  const overlay = options.document.createElement("div");
18662
18918
  overlay.className = "codex-plugin-confirm";
@@ -18666,7 +18922,7 @@ ${block}`;
18666
18922
  const nativeMissing = !native.version;
18667
18923
  overlay.setAttribute(
18668
18924
  "aria-label",
18669
- nativeMissing ? tx("Set up the native host", "\u5B89\u88C5 Native host") : tx("Native host update available", "Native host \u53EF\u66F4\u65B0")
18925
+ codexUnavailable ? tx("Codex Unavailable", "Codex \u4E0D\u53EF\u7528") : nativeMissing ? tx("Set up the native host", "\u5B89\u88C5 Native host") : tx("Native host update available", "Native host \u53EF\u66F4\u65B0")
18670
18926
  );
18671
18927
  const card = options.document.createElement("section");
18672
18928
  card.className = "codex-plugin-confirm-card";
@@ -18683,12 +18939,15 @@ ${block}`;
18683
18939
  brand.textContent = "Codex Overleaf Link";
18684
18940
  const title = options.document.createElement("div");
18685
18941
  title.className = "codex-plugin-confirm-title";
18686
- title.textContent = nativeMissing ? tx("One step left: install the native host", "\u8FD8\u5DEE\u4E00\u6B65\uFF1A\u5B89\u88C5 Native host") : tx("Native host update available", "Native host \u53EF\u66F4\u65B0");
18942
+ title.textContent = codexUnavailable ? tx("Codex Unavailable", "Codex \u4E0D\u53EF\u7528") : nativeMissing ? tx("One step left: install the native host", "\u8FD8\u5DEE\u4E00\u6B65\uFF1A\u5B89\u88C5 Native host") : tx("Native host update available", "Native host \u53EF\u66F4\u65B0");
18687
18943
  titleWrap.append(brand, title);
18688
18944
  head.append(icon, titleWrap);
18689
18945
  const body = options.document.createElement("div");
18690
18946
  body.className = "codex-plugin-confirm-body";
18691
- body.textContent = [
18947
+ body.textContent = codexUnavailable ? [
18948
+ tx("The extension reached the native host, but local Codex was not found, so tasks cannot start.", "\u63D2\u4EF6\u5DF2\u7ECF\u8FDE\u4E0A native host\uFF0C\u4F46\u6CA1\u6709\u627E\u5230\u672C\u673A Codex\uFF0C\u6240\u4EE5\u4E0D\u80FD\u542F\u52A8\u4EFB\u52A1\u3002"),
18949
+ tx("Make sure codex works in Terminal, then reload the extension.", "\u8BF7\u786E\u8BA4\u7EC8\u7AEF\u91CC\u53EF\u4EE5\u8FD0\u884C codex\uFF0C\u7136\u540E\u91CD\u65B0\u52A0\u8F7D\u6269\u5C55\u3002")
18950
+ ].join("\n\n") : [
18692
18951
  nativeMissing ? tx(
18693
18952
  `Extension v${extensionVersion} is ready; Codex still needs its local bridge to read and edit this project.`,
18694
18953
  `\u6269\u5C55 v${extensionVersion} \u5DF2\u5C31\u7EEA\uFF1BCodex \u8FD8\u9700\u8981\u672C\u5730\u6865\u63A5\u7A0B\u5E8F\u624D\u80FD\u8BFB\u5199\u8FD9\u4E2A\u9879\u76EE\u3002`
@@ -18704,8 +18963,8 @@ ${block}`;
18704
18963
  const commandCode = options.document.createElement("code");
18705
18964
  commandCode.className = "codex-native-guidance-command";
18706
18965
  commandCode.textContent = command;
18707
- body.append(commandCode);
18708
- if (releaseUrl) {
18966
+ if (!codexUnavailable) body.append(commandCode);
18967
+ if (!codexUnavailable && releaseUrl) {
18709
18968
  const link = options.document.createElement("a");
18710
18969
  link.className = "codex-native-guidance-link";
18711
18970
  link.href = releaseUrl;
@@ -18731,7 +18990,8 @@ ${block}`;
18731
18990
  close.className = "codex-plugin-confirm-cancel";
18732
18991
  close.textContent = tx("Close", "\u5173\u95ED");
18733
18992
  close.addEventListener("click", () => overlay.remove());
18734
- actions.append(close, copy);
18993
+ actions.append(close);
18994
+ if (!codexUnavailable) actions.append(copy);
18735
18995
  card.append(head, body, actions);
18736
18996
  overlay.append(card);
18737
18997
  overlay.addEventListener("click", (event) => {
@@ -21929,7 +22189,8 @@ ${block}`;
21929
22189
  copy.append(text);
21930
22190
  const descriptions = {
21931
22191
  "annotated-rewrite": tx("Guided editing for document revisions.", "\u7528\u4E8E\u6587\u7A3F\u4FEE\u8BA2\u7684\u7F16\u8F91\u6D41\u7A0B\u3002"),
21932
- "parallel-subagents": tx("Split independent subtasks across multiple agents.", "\u5C06\u76F8\u4E92\u72EC\u7ACB\u7684\u5B50\u4EFB\u52A1\u5206\u914D\u7ED9\u591A\u4E2A agent\u3002")
22192
+ "parallel-subagents": tx("Split independent subtasks across multiple agents.", "\u5C06\u76F8\u4E92\u72EC\u7ACB\u7684\u5B50\u4EFB\u52A1\u5206\u914D\u7ED9\u591A\u4E2A agent\u3002"),
22193
+ "reviewer-response": tx("Map reviewer comments to evidence, edits and reply drafts.", "\u628A\u5BA1\u7A3F\u610F\u89C1\u5BF9\u5E94\u5230\u8BBA\u6587\u8BC1\u636E\u3001\u4FEE\u6539\u548C\u56DE\u590D\u8349\u7A3F\u3002")
21933
22194
  };
21934
22195
  const description = !local && descriptions[id] || (typeof skill.description === "string" ? skill.description.replace(/\s+/g, " ").trim() : "");
21935
22196
  if (description) {
@@ -21967,7 +22228,7 @@ ${block}`;
21967
22228
  row._refreshLocale = () => {
21968
22229
  if (sourceLabel) sourceLabel.textContent = sourceText();
21969
22230
  toggle.setAttribute("aria-label", accessibleSkillName + ": " + tr("codexOverleafSkillEnableToggle"));
21970
- const label = !local && id === "annotated-rewrite" ? tx("Guided editing for document revisions.", "\u7528\u4E8E\u6587\u7A3F\u4FEE\u8BA2\u7684\u7F16\u8F91\u6D41\u7A0B\u3002") : !local && id === "parallel-subagents" ? tx("Split independent subtasks across multiple agents.", "\u5C06\u76F8\u4E92\u72EC\u7ACB\u7684\u5B50\u4EFB\u52A1\u5206\u914D\u7ED9\u591A\u4E2A agent\u3002") : "";
22231
+ const label = !local && id === "annotated-rewrite" ? tx("Guided editing for document revisions.", "\u7528\u4E8E\u6587\u7A3F\u4FEE\u8BA2\u7684\u7F16\u8F91\u6D41\u7A0B\u3002") : !local && id === "parallel-subagents" ? tx("Split independent subtasks across multiple agents.", "\u5C06\u76F8\u4E92\u72EC\u7ACB\u7684\u5B50\u4EFB\u52A1\u5206\u914D\u7ED9\u591A\u4E2A agent\u3002") : !local && id === "reviewer-response" ? tx("Map reviewer comments to evidence, edits and reply drafts.", "\u628A\u5BA1\u7A3F\u610F\u89C1\u5BF9\u5E94\u5230\u8BBA\u6587\u8BC1\u636E\u3001\u4FEE\u6539\u548C\u56DE\u590D\u8349\u7A3F\u3002") : "";
21971
22232
  const help = copy.querySelector(".codex-local-skill-description");
21972
22233
  if (label && help) {
21973
22234
  help.textContent = label;
@@ -22681,6 +22942,11 @@ ${block}`;
22681
22942
  async function executeUndo(runId) {
22682
22943
  let run = findRunRecord(runId);
22683
22944
  if (run) Object.assign(run, Modules.UndoOperations.prepareRunUndo(run));
22945
+ const mirrorOwner = {
22946
+ projectId: getRunProjectIdForWriteback(run),
22947
+ runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || "",
22948
+ sessionId: getState().activeSessionId
22949
+ };
22684
22950
  const assertCurrent = undoOwner(run);
22685
22951
  let assertInputs = checkpointGuard(run, assertCurrent);
22686
22952
  if (!getRunUndoCount(run) || run.undoStatus === "applied") {
@@ -22765,9 +23031,9 @@ ${block}`;
22765
23031
  // Undo remains bound to its original project, including after navigation.
22766
23032
  runProjectId: getRunProjectIdForWriteback(run)
22767
23033
  });
23034
+ writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
22768
23035
  run = assertInputs();
22769
23036
  const undoApplied = isUndoResultEffectivelyApplied(run, result);
22770
- writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
22771
23037
  appendUndoReviewingPolicyEvent(runId, result.reviewingPolicy);
22772
23038
  appendRunRecordEvent(runId, {
22773
23039
  title: tr("undoResult", { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
@@ -22793,6 +23059,11 @@ ${block}`;
22793
23059
  );
22794
23060
  }
22795
23061
  async function executeTrackedUndo(runId, run) {
23062
+ const mirrorOwner = {
23063
+ projectId: getRunProjectIdForWriteback(run),
23064
+ runAccountScopeId: run?.runAccountScopeId || getAccountScopeId?.() || "",
23065
+ sessionId: getState().activeSessionId
23066
+ };
22796
23067
  const assertCurrent = undoOwner(run);
22797
23068
  let assertInputs = checkpointGuard(run, assertCurrent);
22798
23069
  const fileUndoOperations = Modules.UndoOperations.getFileUndoOperations(run);
@@ -22876,8 +23147,8 @@ ${block}`;
22876
23147
  result: { ok: false, code: "undo_operation_failed", reason: error.message }
22877
23148
  })) };
22878
23149
  }
22879
- run = assertInputs();
22880
23150
  result = Modules.UndoOperations.mergeFileUndoResult(result, createdResult, fileUndoOperations);
23151
+ run = assertInputs();
22881
23152
  }
22882
23153
  appendRunRecordEvent(runId, {
22883
23154
  title: fileUndoOperations.length ? tr("undoResult", { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : trackedUndo ? tr("undoTrackedResult", { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }) : tr("undoNativeResult", { applied: result.applied?.length || 0, skipped: result.skipped?.length || 0 }),
@@ -22905,7 +23176,7 @@ ${block}`;
22905
23176
  }
22906
23177
  await applyLegacyUndoSettlement(runId, result.ok === true && !result.skipped?.length ? "applied" : "partial", result);
22907
23178
  } finally {
22908
- if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId, getRunProjectIdForWriteback(run), result);
23179
+ if (result) writebackOrchestrator.invalidateMirrorAfterUndo(runId, mirrorOwner.projectId, result, mirrorOwner);
22909
23180
  }
22910
23181
  }
22911
23182
  function reviewOwner(run) {
@@ -23593,7 +23864,7 @@ ${block}`;
23593
23864
  const maxAttachmentTotalBytes = Number(limits.maxAttachmentTotalBytes) || 32 * 1024 * 1024;
23594
23865
  const maxPreviewDataUrlChars = Number(limits.maxPreviewDataUrlChars) || 768 * 1024;
23595
23866
  let attachments = [];
23596
- const pendingAttachmentKeys = /* @__PURE__ */ new Set();
23867
+ const pendingAttachmentKeys = /* @__PURE__ */ new Map();
23597
23868
  let pendingAttachmentBytes = 0;
23598
23869
  let attachmentGeneration = 0;
23599
23870
  let attachmentRevision = 0;
@@ -23661,8 +23932,15 @@ ${block}`;
23661
23932
  continue;
23662
23933
  }
23663
23934
  attachmentRevision += 1;
23664
- pendingAttachmentKeys.add(dedupeKey);
23935
+ pendingAttachmentKeys.set(dedupeKey, {
23936
+ name: normalizeAttachmentName(file.name),
23937
+ mimeType: String(file.type || ""),
23938
+ size: fileSize,
23939
+ loading: true
23940
+ });
23665
23941
  pendingAttachmentBytes += fileSize;
23942
+ renderComposerAttachments();
23943
+ onChange();
23666
23944
  try {
23667
23945
  const dataUrl = await readFileAsDataUrl(file, tx);
23668
23946
  if (generation !== attachmentGeneration) return;
@@ -23690,6 +23968,8 @@ ${block}`;
23690
23968
  if (generation === attachmentGeneration) {
23691
23969
  pendingAttachmentBytes = Math.max(0, pendingAttachmentBytes - fileSize);
23692
23970
  pendingAttachmentKeys.delete(dedupeKey);
23971
+ renderComposerAttachments();
23972
+ onChange();
23693
23973
  }
23694
23974
  }
23695
23975
  }
@@ -23721,8 +24001,9 @@ ${block}`;
23721
24001
  if (!strip) {
23722
24002
  return;
23723
24003
  }
23724
- strip.hidden = attachments.length === 0;
23725
- renderAttachmentPreviewList(attachments, strip, {
24004
+ const visibleAttachments = [...attachments, ...pendingAttachmentKeys.values()];
24005
+ strip.hidden = visibleAttachments.length === 0;
24006
+ renderAttachmentPreviewList(visibleAttachments, strip, {
23726
24007
  root: getPanel() || document.body,
23727
24008
  tr,
23728
24009
  tx,
@@ -23840,6 +24121,14 @@ ${block}`;
23840
24121
  renderComposerAttachments();
23841
24122
  return attachments.length;
23842
24123
  }
24124
+ function getOmittedAttachmentNames() {
24125
+ let persistedCount = 0;
24126
+ return attachments.filter((attachment) => {
24127
+ if ((attachment.contentBase64 || "").length > PERSIST_MAX_CONTENT_CHARS || persistedCount >= PERSIST_MAX_COUNT) return true;
24128
+ persistedCount += 1;
24129
+ return false;
24130
+ }).map((attachment) => attachment.name);
24131
+ }
23843
24132
  function restoreSubmittedAttachments(value, expectedRevision) {
23844
24133
  if (expectedRevision !== attachmentRevision || attachments.length || pendingAttachmentBytes || pendingAttachmentKeys.size) return false;
23845
24134
  if (!Array.isArray(value) || value.length > maxAttachments) return false;
@@ -23858,6 +24147,8 @@ ${block}`;
23858
24147
  }
23859
24148
  return {
23860
24149
  serializePersistableAttachments,
24150
+ getOmittedAttachmentNames,
24151
+ hasPendingReads: () => pendingAttachmentKeys.size > 0,
23861
24152
  restorePersistedAttachments,
23862
24153
  restoreSubmittedAttachments,
23863
24154
  getRevision: () => attachmentRevision,
@@ -23956,6 +24247,10 @@ ${block}`;
23956
24247
  const card = document.createElement("div");
23957
24248
  card.className = "codex-attachment-preview-card";
23958
24249
  card.dataset.kind = isImage ? "image" : "file";
24250
+ if (attachment.loading) {
24251
+ card.dataset.loading = "true";
24252
+ card.setAttribute("aria-busy", "true");
24253
+ }
23959
24254
  card.title = label + " \xB7 " + formatFileSize(attachment.size);
23960
24255
  if (isImage) {
23961
24256
  const preview = document.createElement("button");
@@ -23984,11 +24279,11 @@ ${block}`;
23984
24279
  if (!isImage) {
23985
24280
  const size = document.createElement("span");
23986
24281
  size.className = "codex-attachment-preview-size";
23987
- size.textContent = formatFileSize(attachment.size);
24282
+ size.textContent = attachment.loading ? options.tx ? options.tx("Loading\u2026", "\u8BFB\u53D6\u4E2D\u2026") : "Loading\u2026" : formatFileSize(attachment.size);
23988
24283
  meta.append(size);
23989
24284
  }
23990
24285
  card.append(meta);
23991
- if (options.removable) {
24286
+ if (options.removable && !attachment.loading) {
23992
24287
  const remove = document.createElement("button");
23993
24288
  remove.type = "button";
23994
24289
  remove.className = "codex-attachment-preview-remove";
@@ -24308,7 +24603,10 @@ ${block}`;
24308
24603
  }
24309
24604
  return compact;
24310
24605
  }
24311
- function setVisible(panelEl, visible) {
24606
+ function setVisible(target, visible) {
24607
+ const instance = target?._instance;
24608
+ const panelEl = instance?.panelEl || target;
24609
+ if (visible && instance) updateCompactMode(instance);
24312
24610
  panelEl?.classList?.toggle("is-open", Boolean(visible));
24313
24611
  document.documentElement.classList.toggle(ROOT_CLASS, Boolean(visible));
24314
24612
  const launcherEl = panelEl?.ownerDocument?.getElementById?.("codex-overleaf-launcher");
@@ -26355,18 +26653,7 @@ ${block}`;
26355
26653
  actions.innerHTML = instance.embedded ? "" : `<button type="button" class="codex-provider-secondary-button" data-provider-action="close">${escapeHtml(tx("Close", "\u5173\u95ED"))}</button>`;
26356
26654
  return;
26357
26655
  }
26358
- const active = provider.id && provider.id === getCurrentProjectProviderId(instance);
26359
- const isNew = !provider.id;
26360
- const destinationChanged = active && getCurrentBaseUrl(instance) !== provider.endpointDisclosureBaseUrl;
26361
- const actionState = getFooterActionState({
26362
- isNew,
26363
- active,
26364
- dirty: instance.dirty,
26365
- destinationChanged,
26366
- canSave: canSaveCurrentDraft(instance),
26367
- hasProject: instance.callbacks.hasCurrentProject?.() !== false,
26368
- canActivate: canActivateCurrentProvider(instance)
26369
- });
26656
+ const actionState = getCurrentFooterActionState(instance);
26370
26657
  const saveDisabled = actionState.saveEnabled ? "" : ' disabled aria-disabled="true"';
26371
26658
  const useDisabled = actionState.useEnabled ? "" : ' disabled aria-disabled="true"';
26372
26659
  actions.innerHTML = `
@@ -26379,6 +26666,20 @@ ${block}`;
26379
26666
  ${actionState.showUse ? `<button type="button" class="codex-provider-primary-button" data-provider-action="use"${useDisabled}>${escapeHtml(tx("Use for this project", "\u7528\u4E8E\u5F53\u524D\u9879\u76EE"))}</button>` : ""}
26380
26667
  `;
26381
26668
  }
26669
+ function getCurrentFooterActionState(instance) {
26670
+ const provider = getSelectedProvider(instance);
26671
+ const active = Boolean(provider?.id && provider.id === getCurrentProjectProviderId(instance));
26672
+ return getFooterActionState({
26673
+ isNew: !provider?.id,
26674
+ active,
26675
+ dirty: instance.dirty,
26676
+ destinationChanged: active && getCurrentBaseUrl(instance) !== provider.endpointDisclosureBaseUrl,
26677
+ disclosureAccepted: instance.root.querySelector("[data-provider-disclosure]")?.checked === true,
26678
+ canSave: canSaveCurrentDraft(instance),
26679
+ hasProject: instance.callbacks.hasCurrentProject?.() !== false,
26680
+ canActivate: canActivateCurrentProvider(instance)
26681
+ });
26682
+ }
26382
26683
  function getFooterActionState(options = {}) {
26383
26684
  const isNew = options.isNew === true;
26384
26685
  const active = options.active === true;
@@ -26386,9 +26687,9 @@ ${block}`;
26386
26687
  return {
26387
26688
  showSave: isNew || dirty,
26388
26689
  showSaveAndUse: options.hasProject !== false && (isNew || dirty) && (!active || options.destinationChanged === true),
26389
- showUse: options.hasProject !== false && !isNew && !active && !dirty,
26690
+ showUse: options.hasProject !== false && !isNew && (!active || options.destinationChanged === true) && !dirty,
26390
26691
  saveEnabled: options.canSave === true,
26391
- useEnabled: options.canActivate === true
26692
+ useEnabled: options.canActivate === true && (!options.destinationChanged || options.disclosureAccepted === true)
26392
26693
  };
26393
26694
  }
26394
26695
  async function handleClick(instance, event) {
@@ -26574,6 +26875,11 @@ ${block}`;
26574
26875
  refreshTestState(instance);
26575
26876
  return;
26576
26877
  }
26878
+ if (event.target.matches("[data-provider-disclosure]")) {
26879
+ renderFooter(instance);
26880
+ applyBusyState(instance);
26881
+ return;
26882
+ }
26577
26883
  if (event.target.matches("[data-provider-field]")) {
26578
26884
  const field = event.target.dataset.providerField;
26579
26885
  if (event.target.dataset.providerField === "apiKey") {
@@ -26744,8 +27050,7 @@ ${block}`;
26744
27050
  }
26745
27051
  function applyBusyState(instance) {
26746
27052
  const busy = Boolean(instance.busy && instance.busy !== "failed");
26747
- const canSave = canSaveCurrentDraft(instance);
26748
- const canActivate = canActivateCurrentProvider(instance);
27053
+ const actionState = getCurrentFooterActionState(instance);
26749
27054
  instance.root.dataset.busy = instance.busy || "";
26750
27055
  for (const element of instance.root.querySelectorAll("input, select, textarea, button")) {
26751
27056
  if (element.matches("[data-provider-confirm-answer]")) {
@@ -26755,9 +27060,9 @@ ${block}`;
26755
27060
  } else if (element.matches('[data-provider-action="cancel-test"]')) {
26756
27061
  element.disabled = false;
26757
27062
  } else if (element.matches('[data-provider-action="save"], [data-provider-action="save-use"]')) {
26758
- element.disabled = busy || !canSave;
27063
+ element.disabled = busy || !actionState.saveEnabled;
26759
27064
  } else if (element.matches('[data-provider-action="use"]')) {
26760
- element.disabled = busy || !canActivate;
27065
+ element.disabled = busy || !actionState.useEnabled;
26761
27066
  } else {
26762
27067
  element.disabled = busy;
26763
27068
  }
@@ -28268,6 +28573,18 @@ ${block}`;
28268
28573
  });
28269
28574
  }
28270
28575
  }
28576
+ function projectPreferenceValues(input, prefs, projectId) {
28577
+ return {
28578
+ experimentalOtByProject: input.normalizeExperimentalOtByProject(prefs.experimentalOtByProject)[projectId] === true,
28579
+ reviewingAvailabilityByProject: prefs.reviewingAvailabilityByProject?.[projectId],
28580
+ governanceRulesByProject: input.normalizeGovernanceRulesByProject({
28581
+ [projectId]: prefs.governanceRulesByProject?.[projectId] || {}
28582
+ })[projectId],
28583
+ customInstructionsByProject: input.normalizeCustomInstructionsByProject({
28584
+ [projectId]: prefs.customInstructionsByProject?.[projectId] || ""
28585
+ })[projectId]
28586
+ };
28587
+ }
28271
28588
  async function persistPanelState(input) {
28272
28589
  var state = cloneSnapshot(input.state || {});
28273
28590
  var compactState = cloneSnapshot(input.compactState || {});
@@ -28293,6 +28610,14 @@ ${block}`;
28293
28610
  }
28294
28611
  var reviewRuns = (input.reviewRunIds || []).map((id) => (state.sessions || []).flatMap((session) => session.runs || []).find((run) => run.id === id));
28295
28612
  if (reviewRuns.some((run) => !run)) throw reviewPersistenceError();
28613
+ var preferenceBaseline = input.projectPreferenceBaseline || Migration.getProjectPreferenceBaseline?.(accountScopeId, projectId);
28614
+ var preferenceWrite = input.projectPreferenceWrite || Migration.captureProjectPreferenceWrite?.({
28615
+ ...input,
28616
+ state,
28617
+ baseline: preferenceBaseline,
28618
+ accountScopeId,
28619
+ projectId
28620
+ });
28296
28621
  var latestPrefs = typeof Migration.loadPrefs === "function" ? await Migration.loadPrefs(accountScopeId, projectId) : {};
28297
28622
  var prefs = Object.assign(
28298
28623
  {},
@@ -28304,32 +28629,21 @@ ${block}`;
28304
28629
  projectId,
28305
28630
  compactState.activeSessionId || state.activeSessionId || ""
28306
28631
  );
28307
- prefs.experimentalOtByProject = Object.assign(
28308
- {},
28309
- latestPrefs && latestPrefs.experimentalOtByProject || {},
28310
- { [projectId]: input.normalizeExperimentalOtByProject(state.experimentalOtByProject)[projectId] === true }
28311
- );
28312
- prefs.reviewingAvailabilityByProject = Object.assign(
28313
- {},
28314
- latestPrefs?.reviewingAvailabilityByProject || {},
28315
- state.reviewingAvailabilityByProject || {}
28316
- );
28317
- prefs.governanceRulesByProject = Object.assign(
28318
- {},
28319
- latestPrefs && latestPrefs.governanceRulesByProject || {},
28320
- input.normalizeGovernanceRulesByProject(state.governanceRulesByProject)
28321
- );
28322
- var normalizedCustomProject = input.normalizeCustomInstructionsByProject({ [projectId]: "" });
28323
- var customProjectId = Object.keys(normalizedCustomProject)[0] || "";
28324
- var currentCustom = input.normalizeCustomInstructionsByProject(state.customInstructionsByProject);
28325
- prefs.customInstructionsByProject = Object.assign(
28326
- {},
28327
- latestPrefs && latestPrefs.customInstructionsByProject || {}
28328
- );
28329
- if (customProjectId) {
28330
- prefs.customInstructionsByProject[customProjectId] = Object.prototype.hasOwnProperty.call(currentCustom, customProjectId) ? currentCustom[customProjectId] : "";
28632
+ var currentPreferences = projectPreferenceValues(input, state, projectId);
28633
+ if (preferenceWrite) {
28634
+ Object.assign(prefs, Migration.projectPreferencePatch(preferenceWrite, accountScopeId, projectId));
28635
+ } else {
28636
+ var baselinePreferences = projectPreferenceValues(input, preferenceBaseline || latestPrefs || {}, projectId);
28637
+ for (var name of Object.keys(currentPreferences)) {
28638
+ prefs[name] = JSON.stringify(currentPreferences[name]) === JSON.stringify(baselinePreferences[name]) ? {} : { [projectId]: currentPreferences[name] };
28639
+ }
28640
+ }
28641
+ var savedPrefs = await Migration.savePrefs(prefs, accountScopeId, projectId);
28642
+ if (savedPrefs) {
28643
+ var fields = preferenceWrite ? Migration.completeProjectPreferenceWrite(preferenceWrite, savedPrefs) : Object.keys(currentPreferences);
28644
+ var applied = input.applyProjectPreferences?.(savedPrefs, state, fields);
28645
+ if (!preferenceWrite && applied !== false) Migration.adoptProjectPreferenceBaseline?.(savedPrefs, accountScopeId, projectId);
28331
28646
  }
28332
- await Migration.savePrefs(prefs, accountScopeId, projectId);
28333
28647
  var persistenceMeta = input.persistenceContext?.nextMeta || {};
28334
28648
  var queueTombstones = persistenceMeta.queueTombstones || {};
28335
28649
  var sessionTombstones = persistenceMeta.sessionTombstones || {};
@@ -28702,11 +29016,18 @@ ${block}`;
28702
29016
  const details = root.querySelector("[data-diagnostics-result-details]");
28703
29017
  const scroll = root.querySelector(".codex-diagnostics-result-scroll");
28704
29018
  const open = details?.open, scrollTop = scroll?.scrollTop;
28705
- const checks = Array.from(root.querySelectorAll(".codex-diagnostics-check"), (node) => node.open);
29019
+ const checks = Array.from(root.querySelectorAll(".codex-diagnostics-check"), (node) => ({
29020
+ open: node.open,
29021
+ technicalOpen: node.querySelector(".codex-diagnostics-technical")?.open
29022
+ }));
28706
29023
  showResult(instance, result);
28707
29024
  if (details) details.open = open;
28708
29025
  root.querySelectorAll(".codex-diagnostics-check").forEach((node, index) => {
28709
- if (index < checks.length) node.open = checks[index];
29026
+ if (index < checks.length) {
29027
+ node.open = checks[index].open;
29028
+ const technical = node.querySelector(".codex-diagnostics-technical");
29029
+ if (technical) technical.open = checks[index].technicalOpen;
29030
+ }
28710
29031
  });
28711
29032
  if (scroll) scroll.scrollTop = scrollTop;
28712
29033
  }
@@ -28721,42 +29042,45 @@ ${block}`;
28721
29042
  root.hidden = false;
28722
29043
  root.dataset.status = result.status || "info";
28723
29044
  root.setAttribute("aria-label", result.title || t(instance, "diagnosticsResult"));
28724
- root.querySelector("[data-diagnostics-result-title]").textContent = result.title || t(instance, "diagnosticsResult");
29045
+ const titleNode = root.querySelector("[data-diagnostics-result-title]");
29046
+ const dot = document.createElement("span");
29047
+ dot.className = "codex-diagnostics-status-dot";
29048
+ dot.setAttribute("aria-hidden", "true");
29049
+ const titleText = document.createElement("span");
29050
+ titleText.className = "codex-diagnostics-result-title-text";
29051
+ titleText.textContent = result.title || t(instance, "diagnosticsResult");
29052
+ titleNode.replaceChildren(dot, titleText);
28725
29053
  root.querySelector("[data-diagnostics-result-subtitle]").textContent = result.subtitle || "";
28726
29054
  const body = root.querySelector("[data-diagnostics-result-body]");
28727
29055
  body.textContent = "";
28728
- if (result.summary) {
28729
- appendParagraph(body, result.summary);
29056
+ const facts = Array.isArray(result.facts) ? result.facts.filter((fact) => fact?.label) : [];
29057
+ const notes = [
29058
+ ...Array.isArray(result.notes) ? result.notes : [],
29059
+ ...Array.isArray(result.bullets) ? result.bullets : []
29060
+ ].filter(Boolean);
29061
+ const hasChecks = Array.isArray(result.checks) && result.checks.length > 0;
29062
+ if (result.summary && !(result.status === "completed" && facts.length)) {
29063
+ appendParagraph(body, result.summary, "codex-diagnostics-summary");
28730
29064
  }
28731
- if (Array.isArray(result.checks) && result.checks.length) {
28732
- renderCheckRows(body, result.checks);
28733
- } else if (!result.summary) {
29065
+ if (hasChecks) {
29066
+ renderCheckRows(instance, body, result.checks);
29067
+ } else if (!result.summary && !facts.length && !notes.length) {
28734
29068
  appendParagraph(body, t(instance, "diagnosticsNoResult"));
28735
29069
  }
28736
- if (Array.isArray(result.bullets) && result.bullets.length) {
28737
- const list = document.createElement("ul");
28738
- for (const item of result.bullets) {
28739
- const li = document.createElement("li");
28740
- li.textContent = item;
28741
- list.append(li);
28742
- }
28743
- body.append(list);
29070
+ if (facts.length) {
29071
+ renderFacts(body, facts);
29072
+ }
29073
+ if (notes.length) {
29074
+ renderNotes(body, notes);
28744
29075
  }
28745
29076
  if (result.nextStep) {
28746
- const next = document.createElement("p");
28747
- next.className = "codex-diagnostics-next-step";
28748
- next.textContent = `${t(instance, "nextStepPrefix")}${result.nextStep}`;
28749
- body.append(next);
29077
+ appendNextStep(instance, body, result.nextStep, "p", "codex-diagnostics-next-step");
28750
29078
  }
28751
29079
  if (result.installCommand) {
28752
29080
  renderInstallCommand(instance, body, result.installCommand);
28753
29081
  }
28754
29082
  const details = root.querySelector("[data-diagnostics-result-details]");
28755
- const technical = root.querySelector("[data-diagnostics-result-technical]");
28756
- const technicalText = String(result.technical || "").trim();
28757
- details.open = false;
28758
- details.hidden = !technicalText;
28759
- technical.textContent = technicalText;
29083
+ renderTechnicalDetails(details, result.technical);
28760
29084
  closeMenu(instance);
28761
29085
  openFloating(instance, root, 400);
28762
29086
  if (wasHidden) root.querySelector("[data-diagnostics-result-close]")?.focus({ preventScroll: true });
@@ -28829,13 +29153,13 @@ ${block}`;
28829
29153
  function renderInstallCommand(instance, container, command) {
28830
29154
  const wrap = document.createElement("div");
28831
29155
  wrap.className = "codex-install-command";
28832
- const label = document.createElement("div");
29156
+ const head = document.createElement("div");
29157
+ head.className = "codex-install-command-head";
29158
+ const label = document.createElement("span");
28833
29159
  label.className = "codex-install-command-label";
28834
29160
  label.textContent = t(instance, "runInTerminal");
28835
- wrap.append(label);
28836
29161
  const code = document.createElement("code");
28837
29162
  code.textContent = command;
28838
- wrap.append(code);
28839
29163
  const copyButton = document.createElement("button");
28840
29164
  copyButton.type = "button";
28841
29165
  copyButton.textContent = t(instance, "copyInstallCommand");
@@ -28846,21 +29170,82 @@ ${block}`;
28846
29170
  copyButton.textContent = t(instance, "copyInstallCommand");
28847
29171
  }, 1400);
28848
29172
  });
28849
- wrap.append(copyButton);
29173
+ head.append(label, copyButton);
29174
+ wrap.append(head, code);
28850
29175
  container.append(wrap);
28851
29176
  }
28852
- function appendParagraph(container, text) {
29177
+ function appendParagraph(container, text, className) {
28853
29178
  const paragraph = document.createElement("p");
29179
+ if (className) {
29180
+ paragraph.className = className;
29181
+ }
28854
29182
  paragraph.textContent = text;
28855
29183
  container.append(paragraph);
28856
29184
  }
29185
+ function appendNextStep(instance, container, text, tagName, className) {
29186
+ const next = document.createElement(tagName);
29187
+ next.className = className;
29188
+ const label = document.createElement("span");
29189
+ label.className = "codex-diagnostics-next-label";
29190
+ label.textContent = t(instance, "nextStepPrefix");
29191
+ next.append(label, document.createTextNode(text));
29192
+ container.append(next);
29193
+ }
29194
+ function renderFacts(container, facts) {
29195
+ const list = document.createElement("dl");
29196
+ list.className = "codex-diagnostics-facts";
29197
+ for (const fact of facts) {
29198
+ const term = document.createElement("dt");
29199
+ term.textContent = fact.label;
29200
+ const value = document.createElement("dd");
29201
+ const text = String(fact.value ?? "");
29202
+ if (fact.kind === "path") {
29203
+ value.textContent = shortenPath(text);
29204
+ value.title = text;
29205
+ } else {
29206
+ value.textContent = text;
29207
+ }
29208
+ if (fact.tone) {
29209
+ value.dataset.tone = fact.tone;
29210
+ }
29211
+ list.append(term, value);
29212
+ }
29213
+ container.append(list);
29214
+ }
29215
+ function shortenPath(path) {
29216
+ if (path.length <= 36) {
29217
+ return path;
29218
+ }
29219
+ const parts = path.split("/");
29220
+ if (parts.length < 5) {
29221
+ return path;
29222
+ }
29223
+ const short = `${parts.slice(0, 3).join("/")}/\u2026/${parts[parts.length - 1]}`;
29224
+ return short.length < path.length ? short : path;
29225
+ }
29226
+ function renderNotes(container, notes) {
29227
+ const list = document.createElement("ul");
29228
+ list.className = "codex-diagnostics-notes";
29229
+ for (const note of notes) {
29230
+ const item = document.createElement("li");
29231
+ item.textContent = note;
29232
+ list.append(item);
29233
+ }
29234
+ container.append(list);
29235
+ }
29236
+ function renderTechnicalDetails(details, value) {
29237
+ const text = String(value || "").trim();
29238
+ details.open = false;
29239
+ details.hidden = !text;
29240
+ details.querySelector("pre").textContent = text;
29241
+ }
28857
29242
  function healthBucket(status) {
28858
29243
  if (status === "completed" || status === "ok") return "ok";
28859
29244
  if (status === "warning" || status === "warn") return "warn";
28860
29245
  if (status === "failed" || status === "fail") return "fail";
28861
29246
  return "info";
28862
29247
  }
28863
- function renderCheckRows(container, checks) {
29248
+ function renderCheckRows(instance, container, checks) {
28864
29249
  const list = document.createElement("div");
28865
29250
  list.className = "codex-diagnostics-checks";
28866
29251
  for (const check of checks) {
@@ -28870,15 +29255,15 @@ ${block}`;
28870
29255
  row.dataset.status = healthBucket(check.status);
28871
29256
  row.open = row.dataset.status === "warn" || row.dataset.status === "fail";
28872
29257
  const heading = document.createElement("summary");
28873
- const glyph = document.createElement("span");
28874
- glyph.className = "codex-diagnostics-check-glyph";
28875
- glyph.setAttribute("aria-hidden", "true");
29258
+ const dot = document.createElement("span");
29259
+ dot.className = "codex-diagnostics-check-dot";
29260
+ dot.setAttribute("aria-hidden", "true");
28876
29261
  const textWrap = document.createElement("div");
28877
29262
  textWrap.className = "codex-diagnostics-check-text";
28878
29263
  const title = document.createElement("div");
28879
29264
  title.className = "codex-diagnostics-check-title";
28880
29265
  title.textContent = check.title || "";
28881
- heading.append(glyph, title);
29266
+ heading.append(dot, title);
28882
29267
  if (check.summary) {
28883
29268
  const summary = document.createElement("div");
28884
29269
  summary.className = "codex-diagnostics-check-summary";
@@ -28886,10 +29271,19 @@ ${block}`;
28886
29271
  textWrap.append(summary);
28887
29272
  }
28888
29273
  if (check.nextStep) {
28889
- const next = document.createElement("div");
28890
- next.className = "codex-diagnostics-check-next";
28891
- next.textContent = `\u2192 ${check.nextStep}`;
28892
- textWrap.append(next);
29274
+ appendNextStep(instance, textWrap, check.nextStep, "div", "codex-diagnostics-check-next");
29275
+ }
29276
+ if (check.installCommand) {
29277
+ renderInstallCommand(instance, textWrap, check.installCommand);
29278
+ }
29279
+ if (check.technical) {
29280
+ const details = document.createElement("details");
29281
+ details.className = "codex-diagnostics-technical";
29282
+ const summary = document.createElement("summary");
29283
+ summary.textContent = t(instance, "technicalDetails");
29284
+ details.append(summary, document.createElement("pre"));
29285
+ renderTechnicalDetails(details, check.technical);
29286
+ textWrap.append(details);
28893
29287
  }
28894
29288
  row.append(heading, textWrap);
28895
29289
  list.append(row);
@@ -29179,37 +29573,46 @@ ${block}`;
29179
29573
  claimToken: claimedItem?.claimToken
29180
29574
  }
29181
29575
  });
29182
- const latestQueue = options.getQueue?.(sessionId) || originalQueue;
29576
+ const latestQueue2 = options.getQueue?.(sessionId) || originalQueue;
29183
29577
  options.setQueue?.(
29184
29578
  sessionId,
29185
- dequeue(latestQueue, queueItemId, {
29579
+ dequeue(latestQueue2, queueItemId, {
29186
29580
  sessionId,
29187
29581
  reason: status === "completed" ? "completed" : "continue_queue"
29188
29582
  })
29189
29583
  );
29190
- } else if (!shouldContinue) {
29191
- const paused = originalQueue.length ? Queue.pauseAll(originalQueue, status || "run_did_not_complete") : originalQueue;
29584
+ }
29585
+ const canStart = shouldContinue ? options.canStart?.(sessionId) : true;
29586
+ const latestQueue = options.getQueue?.(sessionId) || originalQueue;
29587
+ const inactive = shouldContinue && canStart !== "session_transition" && (canStart === false || options.isRunning?.()) && latestQueue.some((item) => item?.status === "queued");
29588
+ if (!shouldContinue || inactive) {
29589
+ const reason = inactive ? "session_inactive" : status || "run_did_not_complete";
29590
+ const paused = latestQueue.length ? Queue.pauseAll(latestQueue, reason) : latestQueue;
29192
29591
  options.setQueue?.(sessionId, paused);
29193
29592
  try {
29194
29593
  await options.persist?.({
29195
29594
  queueMutation: { type: "transition", sessionId }
29196
29595
  });
29197
29596
  } catch (error) {
29198
- const latestQueue = options.getQueue?.(sessionId) || paused;
29597
+ const latestQueue2 = options.getQueue?.(sessionId) || paused;
29199
29598
  options.setQueue?.(
29200
29599
  sessionId,
29201
- Queue.pauseAll(latestQueue, status || "run_did_not_complete")
29600
+ Queue.pauseAll(latestQueue2, reason)
29202
29601
  );
29203
29602
  throw error;
29204
29603
  }
29205
29604
  }
29206
29605
  options.onChange?.(sessionId);
29207
- if (shouldContinue) {
29606
+ if (shouldContinue && !inactive) {
29208
29607
  scheduleOne(sessionId);
29209
29608
  }
29210
29609
  }
29211
29610
  function scheduleOne(sessionId) {
29212
- if (scheduling || options.isRunning?.() || options.canStart?.(sessionId) === false) {
29611
+ if (scheduling || options.isRunning?.()) {
29612
+ return false;
29613
+ }
29614
+ const canStart = options.canStart?.(sessionId);
29615
+ if (canStart === false || canStart === "session_transition") {
29213
29616
  return false;
29214
29617
  }
29215
29618
  const initialQueue = options.getQueue?.(sessionId) || [];
@@ -29326,7 +29729,7 @@ ${block}`;
29326
29729
  setQueue: (sessionId, pendingInputs) => options.setQueue?.(sessionId, pendingInputs),
29327
29730
  persist: (persistenceOptions) => options.save?.(persistenceOptions),
29328
29731
  isRunning: () => Boolean(options.getCurrentRun?.()),
29329
- canStart: (sessionId) => options.canStart?.(sessionId) !== false,
29732
+ canStart: (sessionId) => options.canStart?.(sessionId),
29330
29733
  start: (item, sessionId) => startQueuedInput(item, sessionId),
29331
29734
  onChange: () => render(),
29332
29735
  onDequeued: (item, context) => options.removeGuidance?.(item.id, {
@@ -29565,7 +29968,8 @@ ${block}`;
29565
29968
  }
29566
29969
  async function startQueuedInput(item, sessionId) {
29567
29970
  const assertCanStart = () => {
29568
- if (options.canStart?.(sessionId) === false || options.getCurrentRun?.()) {
29971
+ const canStart = options.canStart?.(sessionId);
29972
+ if (canStart === false || canStart === "session_transition" || options.getCurrentRun?.()) {
29569
29973
  const error = new Error("Queued input cannot start in the current session.");
29570
29974
  error.code = "queue_session_not_active";
29571
29975
  throw error;
@@ -30845,6 +31249,33 @@ ${block}`;
30845
31249
  return null;
30846
31250
  }
30847
31251
  }
31252
+ async function attachInstruction(action, selected) {
31253
+ const selection = await attach("edit", selected);
31254
+ if (!selection) return;
31255
+ const task = deps.getPanel()?.querySelector("[data-task]");
31256
+ if (!task) return;
31257
+ if (task.value.trim()) {
31258
+ deps.toast?.(tx("Kept your draft. The selection is attached for editing.", "\u5DF2\u4FDD\u7559\u4F60\u7684\u8349\u7A3F\uFF0C\u9009\u533A\u5DF2\u9644\u4E0A\uFF0C\u4EC5\u7F16\u8F91\u6B64\u5904\u3002"));
31259
+ return;
31260
+ }
31261
+ const instructions = {
31262
+ polish: tx(
31263
+ "Polish the selected passage for clarity and flow. Keep the meaning, citations, labels and math unchanged.",
31264
+ "\u6DA6\u8272\u9009\u4E2D\u6BB5\u843D\uFF0C\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u6D41\u7545\uFF0C\u4FDD\u6301\u539F\u610F\u4EE5\u53CA\u5F15\u7528\u3001\u6807\u7B7E\u3001\u516C\u5F0F\u4E0D\u53D8\u3002"
31265
+ ),
31266
+ shorten: tx(
31267
+ "Shorten the selected passage by about 20%. Keep citations, labels, math and the main claim unchanged.",
31268
+ "\u5C06\u9009\u4E2D\u6BB5\u843D\u7CBE\u7B80\u7EA6 20%\uFF0C\u4FDD\u7559\u5F15\u7528\u3001\u6807\u7B7E\u3001\u516C\u5F0F\u548C\u6838\u5FC3\u8BBA\u70B9\u4E0D\u53D8\u3002"
31269
+ ),
31270
+ translate: tx(
31271
+ "Translate the selected passage into English. Keep LaTeX commands, citations, labels and math unchanged.",
31272
+ "\u5C06\u9009\u4E2D\u6BB5\u843D\u7FFB\u8BD1\u6210\u82F1\u6587\uFF0C\u4FDD\u7559 LaTeX \u547D\u4EE4\u3001\u5F15\u7528\u3001\u6807\u7B7E\u548C\u516C\u5F0F\u4E0D\u53D8\u3002"
31273
+ )
31274
+ };
31275
+ task.value = instructions[action];
31276
+ task.setSelectionRange(task.value.length, task.value.length);
31277
+ task.dispatchEvent(new Event("input", { bubbles: true }));
31278
+ }
30848
31279
  function editorFor(node) {
30849
31280
  const element = node?.nodeType === 1 ? node : node?.parentElement;
30850
31281
  const editor = element?.closest?.(".cm-editor");
@@ -30871,6 +31302,9 @@ ${block}`;
30871
31302
  toolbar.setAttribute("aria-label", tx("Selected text actions", "\u9009\u533A\u64CD\u4F5C"));
30872
31303
  toolbar.querySelector('[data-selection-action="reference"]').textContent = tx("Add to Chat", "\u52A0\u5165\u5BF9\u8BDD");
30873
31304
  toolbar.querySelector('[data-selection-action="edit"]').textContent = tx("Edit Selection", "\u4EC5\u7F16\u8F91\u6B64\u5904");
31305
+ toolbar.querySelector('[data-selection-action="polish"]').textContent = tx("Polish", "\u6DA6\u8272");
31306
+ toolbar.querySelector('[data-selection-action="shorten"]').textContent = tx("Shorten", "\u7CBE\u7B80");
31307
+ toolbar.querySelector('[data-selection-action="translate"]').textContent = tx("Translate", "\u7FFB\u8BD1");
30874
31308
  }
30875
31309
  function ensureToolbar() {
30876
31310
  if (toolbar) return;
@@ -30880,8 +31314,8 @@ ${block}`;
30880
31314
  toolbar.setAttribute("role", "toolbar");
30881
31315
  toolbar.hidden = true;
30882
31316
  if (typeof toolbar.showPopover === "function") toolbar.setAttribute("popover", "manual");
30883
- for (const mode of ["reference", "edit"]) {
30884
- if (mode === "edit") {
31317
+ for (const mode of ["reference", "edit", "polish", "shorten", "translate"]) {
31318
+ if (mode === "edit" || mode === "polish") {
30885
31319
  const divider = document.createElement("span");
30886
31320
  divider.className = "codex-selection-action-divider";
30887
31321
  divider.setAttribute("aria-hidden", "true");
@@ -30892,7 +31326,9 @@ ${block}`;
30892
31326
  button.dataset.selectionAction = mode;
30893
31327
  button.addEventListener("click", () => {
30894
31328
  const selected = candidate;
30895
- if (selected) void attach(mode, selected);
31329
+ if (!selected) return;
31330
+ if (mode === "reference" || mode === "edit") void attach(mode, selected);
31331
+ else void attachInstruction(mode, selected);
30896
31332
  });
30897
31333
  toolbar.append(button);
30898
31334
  }
@@ -37519,6 +37955,18 @@ ${block}`;
37519
37955
  renderText: buildMarkdownInlineNodesWithoutMath
37520
37956
  });
37521
37957
  }
37958
+ function buildParsedTextNodes(value) {
37959
+ const source = String(value || "");
37960
+ if (!MathText?.buildMathNodes) {
37961
+ return buildLineReferenceInlineNodes(source);
37962
+ }
37963
+ return MathText.buildMathNodes(source, {
37964
+ document,
37965
+ katex: window.katex,
37966
+ macros: getMathMacros(),
37967
+ renderText: buildLineReferenceInlineNodes
37968
+ });
37969
+ }
37522
37970
  function buildMarkdownInlineNodesWithoutMath(value) {
37523
37971
  const source = String(value || "");
37524
37972
  const nodes = [];
@@ -37911,15 +38359,8 @@ ${block}`;
37911
38359
  document,
37912
38360
  markdownIt: runtimeRoot.markdownit,
37913
38361
  mathText: runtimeRoot.CodexOverleafMathText,
37914
- renderTextNodes: (text) => buildMarkdownInlineNodes(text),
37915
- renderLinkNodes: (label, href) => {
37916
- const safeLabel = String(label || "");
37917
- const target2 = String(href || "").trim();
37918
- if (!/^https?:\/\//i.test(target2) && !resolveMarkdownLineReference(safeLabel, target2)) return buildMarkdownInlineNodes(safeLabel);
37919
- return buildMarkdownInlineNodes(
37920
- `[${safeLabel.replace(/]/g, "\\]")}](${target2.replace(/\)/g, "\\)")})`
37921
- );
37922
- },
38362
+ renderTextNodes: buildParsedTextNodes,
38363
+ renderLinkNodes: buildMarkdownLinkNodes,
37923
38364
  renderCodeNodes: (text) => buildInlineCodeNodes(text),
37924
38365
  sanitizeText: sanitizeAssistantVisibleText,
37925
38366
  createMathNode: (segment) => runtimeRoot.CodexOverleafMathText.createMathNode(
@@ -38199,7 +38640,12 @@ ${block}`;
38199
38640
  try {
38200
38641
  result = await spec.run({ collectOnly: true });
38201
38642
  } catch (error) {
38202
- result = projectableResult(() => ({ status: "failed", summary: tr("diagnosticsCheckErrored"), technical: error?.message || String(error) }));
38643
+ result = projectableResult(() => ({
38644
+ status: "failed",
38645
+ summary: tr("diagnosticsCheckErrored"),
38646
+ nextStep: tr("runOutcomeNext"),
38647
+ technical: error?.message || String(error)
38648
+ }));
38203
38649
  }
38204
38650
  const bucket = diagnosticsHealthBucket(result?.status);
38205
38651
  if (bucket === "fail") {
@@ -38214,7 +38660,9 @@ ${block}`;
38214
38660
  status: result?.status || "info",
38215
38661
  title: tr(spec.label),
38216
38662
  summary: translated?.summary || translated?.subtitle || "",
38217
- nextStep: bucket === "ok" ? "" : translated?.nextStep || ""
38663
+ nextStep: bucket === "ok" ? "" : translated?.nextStep || "",
38664
+ installCommand: translated?.installCommand,
38665
+ technical: translated?.technical
38218
38666
  };
38219
38667
  });
38220
38668
  }
@@ -38296,9 +38744,9 @@ ${block}`;
38296
38744
  const codexOk = environment.codex?.ok === true;
38297
38745
  const latexTools = environment.latex?.available || [];
38298
38746
  const latexOk = latexTools.length > 0;
38299
- const bullets = [
38300
- codexOk ? tx(`Codex available: ${environment.codex.path || "found on PATH"}`, `Codex \u53EF\u7528\uFF1A${environment.codex.path || "\u5DF2\u5728 PATH \u4E2D\u627E\u5230"}`) : tx("Codex was not found: the extension cannot start local Codex.", "Codex \u6CA1\u6709\u627E\u5230\uFF1A\u63D2\u4EF6\u4E0D\u80FD\u542F\u52A8\u672C\u5730 Codex\u3002"),
38301
- latexOk ? tx(`LaTeX tools available: ${latexTools.join(", ")}`, `LaTeX \u5DE5\u5177\u53EF\u7528\uFF1A${latexTools.join(", ")}`) : tx("LaTeX tools were not found: text editing works, but local compile checks are limited.", "LaTeX \u5DE5\u5177\u6CA1\u6709\u627E\u5230\uFF1A\u53EF\u4EE5\u7F16\u8F91\u6587\u672C\uFF0C\u4F46\u4E0D\u80FD\u5728\u672C\u5730\u7F16\u8BD1\u9A8C\u8BC1\u3002")
38747
+ const facts = [
38748
+ codexOk ? environment.codex.path ? { label: "Codex", value: environment.codex.path, kind: "path" } : { label: "Codex", value: tx("Found on PATH", "\u5DF2\u5728 PATH \u4E2D\u627E\u5230") } : { label: "Codex", value: tx("Not found", "\u672A\u627E\u5230"), tone: "fail" },
38749
+ latexOk ? { label: "LaTeX", value: latexTools.join(", ") } : { label: "LaTeX", value: tx("Not found", "\u672A\u627E\u5230"), tone: "warn" }
38302
38750
  ];
38303
38751
  if (codexOk && latexOk) {
38304
38752
  return {
@@ -38306,7 +38754,7 @@ ${block}`;
38306
38754
  subtitle: tx("Codex and LaTeX are available.", "Codex \u548C LaTeX \u90FD\u53EF\u4EE5\u4F7F\u7528\u3002"),
38307
38755
  status: "completed",
38308
38756
  summary: tx("Codex is connected and local LaTeX tools are available. Codex can read, edit, and locally check this Overleaf project.", "Codex \u5DF2\u8FDE\u63A5\uFF0C\u672C\u673A LaTeX \u5DE5\u5177\u53EF\u7528\u3002\u4F60\u53EF\u4EE5\u8BA9 Codex \u8BFB\u53D6\u3001\u4FEE\u6539\u5E76\u672C\u5730\u68C0\u67E5 Overleaf \u9879\u76EE\u3002"),
38309
- bullets,
38757
+ facts,
38310
38758
  technical: JSON.stringify(environment, null, 2)
38311
38759
  };
38312
38760
  }
@@ -38315,7 +38763,7 @@ ${block}`;
38315
38763
  subtitle: codexOk ? tx("Codex is available, but compile tools are incomplete.", "Codex \u53EF\u7528\uFF0C\u4F46\u7F16\u8BD1\u5DE5\u5177\u4E0D\u5B8C\u6574\u3002") : tx("Local Codex was not found.", "\u6CA1\u6709\u627E\u5230\u672C\u673A Codex\u3002"),
38316
38764
  status: codexOk ? "warning" : "failed",
38317
38765
  summary: codexOk ? tx("Local Codex can run, but LaTeX compile tools were not found. File edits are unaffected, but compile verification is limited.", "\u672C\u673A Codex \u53EF\u4EE5\u8FD0\u884C\uFF0C\u4F46\u6CA1\u6709\u627E\u5230 LaTeX \u7F16\u8BD1\u5DE5\u5177\uFF1B\u4FEE\u6539\u6587\u4EF6\u4E0D\u53D7\u5F71\u54CD\uFF0C\u7F16\u8BD1\u9A8C\u8BC1\u4F1A\u53D7\u9650\u3002") : tx("The extension reached the native host, but local Codex was not found, so tasks cannot start.", "\u63D2\u4EF6\u5DF2\u7ECF\u8FDE\u4E0A native host\uFF0C\u4F46\u6CA1\u6709\u627E\u5230\u672C\u673A Codex\uFF0C\u6240\u4EE5\u4E0D\u80FD\u542F\u52A8\u4EFB\u52A1\u3002"),
38318
- bullets,
38766
+ facts,
38319
38767
  nextStep: codexOk ? tx("For compile verification, make sure latexmk or pdflatex is available on your Terminal PATH.", "\u5982\u9700\u7F16\u8BD1\u9A8C\u8BC1\uFF0C\u8BF7\u786E\u8BA4 latexmk \u6216 pdflatex \u5728\u7EC8\u7AEF PATH \u4E2D\u53EF\u7528\u3002") : tx("Make sure codex works in Terminal, then reload the extension.", "\u8BF7\u786E\u8BA4\u7EC8\u7AEF\u91CC\u53EF\u4EE5\u8FD0\u884C codex\uFF0C\u7136\u540E\u91CD\u65B0\u52A0\u8F7D\u6269\u5C55\u3002"),
38320
38768
  technical: JSON.stringify(environment, null, 2)
38321
38769
  };
@@ -38351,7 +38799,7 @@ ${block}`;
38351
38799
  subtitle: messages.subtitle,
38352
38800
  status: "failed",
38353
38801
  summary: messages.summary,
38354
- bullets: formatNativeCompatibilityBullets(compatibility),
38802
+ facts: formatNativeCompatibilityFacts(compatibility, statusValue),
38355
38803
  nextStep: messages.nextStep,
38356
38804
  installCommand: compatibility.installCommand,
38357
38805
  technical: formatNativeCompatibilityTechnicalDetails(compatibility, response)
@@ -38389,14 +38837,24 @@ ${block}`;
38389
38837
  };
38390
38838
  }
38391
38839
  }
38392
- function formatNativeCompatibilityBullets(compatibility = {}) {
38840
+ function formatNativeCompatibilityFacts(compatibility = {}, statusValue = "") {
38393
38841
  const native = compatibility.native || {};
38394
38842
  return [
38395
- tx(`Status: ${compatibility.status || "unknown"}`, `\u72B6\u6001\uFF1A${compatibility.status || "unknown"}`),
38396
- tx(`Classification: ${getNativeCompatibilityClassification(compatibility)}`, `\u517C\u5BB9\u5206\u7C7B\uFF1A${getNativeCompatibilityClassification(compatibility)}`),
38397
- tx(`Extension version: ${compatibility.extensionVersion || "unknown"}`, `\u6269\u5C55\u7248\u672C\uFF1A${compatibility.extensionVersion || "unknown"}`),
38398
- tx(`Native version: ${native.version || "unknown"}`, `Native \u7248\u672C\uFF1A${native.version || "unknown"}`),
38399
- tx(`Native protocol: ${formatNativeProtocolVersion(native)}`, `Native \u534F\u8BAE\uFF1A${formatNativeProtocolVersion(native)}`)
38843
+ {
38844
+ label: tx("Extension version", "\u6269\u5C55\u7248\u672C"),
38845
+ value: compatibility.extensionVersion || "unknown",
38846
+ tone: statusValue === "extension_too_old" ? "fail" : void 0
38847
+ },
38848
+ {
38849
+ label: tx("Native version", "Native \u7248\u672C"),
38850
+ value: native.version || "unknown",
38851
+ tone: statusValue === "native_too_old" ? "fail" : void 0
38852
+ },
38853
+ {
38854
+ label: tx("Native protocol", "Native \u534F\u8BAE"),
38855
+ value: formatNativeProtocolVersion(native),
38856
+ tone: statusValue === "protocol_unsupported" ? "fail" : void 0
38857
+ }
38400
38858
  ];
38401
38859
  }
38402
38860
  function formatNativeProtocolVersion(native = {}) {
@@ -38457,7 +38915,7 @@ ${block}`;
38457
38915
  subtitle: tr("diagnosticsOtSubtitle"),
38458
38916
  status: "completed",
38459
38917
  summary: tr("diagnosticsOtSummaryDisabled"),
38460
- bullets: [],
38918
+ facts: [],
38461
38919
  nextStep: "",
38462
38920
  technical: ""
38463
38921
  };
@@ -38552,10 +39010,14 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38552
39010
  subtitle: tr("diagnosticsOtSubtitle"),
38553
39011
  status: enabled && !fallback && !bridgeFailed ? "completed" : enabled || bridgeFailed ? "warning" : "completed",
38554
39012
  summary: tr(failureActive ? otWarmMirrorController.getFailureMessageKey(warmMirrorState.lastErrorCode) : enabled ? "diagnosticsOtSummaryEnabled" : "diagnosticsOtSummaryDisabled"),
38555
- bullets: [
38556
- `${tr("otStatus")}: ${formatOtStatusLabel(statusValue)}`,
38557
- `${tr("otFreshFiles")}: ${otFreshFileCount}`,
38558
- `${tr("otFallback")}: ${fallback ? tr("yes") : tr("no")}`
39013
+ facts: [
39014
+ { label: tr("otStatus"), value: formatOtStatusLabel(statusValue) },
39015
+ { label: tr("otFreshFiles"), value: String(otFreshFileCount) },
39016
+ {
39017
+ label: tr("otFallback"),
39018
+ value: fallback ? tr("yes") : tr("no"),
39019
+ tone: enabled && fallback ? "warn" : void 0
39020
+ }
38559
39021
  ],
38560
39022
  nextStep: fallback ? tr("diagnosticsOtNextStep") : "",
38561
39023
  technical: formatOtDiagnosticsTechnicalDetails({
@@ -38614,10 +39076,23 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38614
39076
  const reviewingOk = probe?.reviewing?.ok === true;
38615
39077
  const writable = probe?.capabilities?.editor?.write !== false;
38616
39078
  const readable = probe?.editor?.ok === true;
38617
- const bullets = [
38618
- readable ? tx("Current file was read.", "\u5F53\u524D\u6587\u4EF6\u5DF2\u7ECF\u8BFB\u5230\u3002") : tx("Current file content has not been verified yet.", "\u8FD8\u6CA1\u6709\u786E\u8BA4\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\u3002"),
38619
- writable ? tx("Editor write access will be verified again during writeback.", "\u5F53\u524D\u7F16\u8F91\u5668\u5199\u5165\u80FD\u529B\u4F1A\u5728\u5199\u5165\u65F6\u518D\u6B21\u9A8C\u8BC1\u3002") : tx("The current editor is not writable yet.", "\u5F53\u524D\u7F16\u8F91\u5668\u6682\u65F6\u4E0D\u53EF\u5199\u3002"),
38620
- reviewingOk ? tx("Reviewing/Track Changes status is verified.", "\u7559\u75D5/Reviewing \u72B6\u6001\u5DF2\u786E\u8BA4\u3002") : tx("Reviewing/Track Changes status is not verified yet.", "\u7559\u75D5/Reviewing \u72B6\u6001\u8FD8\u6CA1\u6709\u786E\u8BA4\u3002")
39079
+ const reviewingNeeded = mode !== "ask";
39080
+ const facts = [
39081
+ {
39082
+ label: tx("Current file", "\u5F53\u524D\u6587\u4EF6"),
39083
+ value: readable ? tx("Read", "\u5DF2\u8BFB\u53D6") : tx("Not verified", "\u672A\u786E\u8BA4"),
39084
+ tone: readable ? void 0 : "warn"
39085
+ },
39086
+ {
39087
+ label: tx("Write access", "\u5199\u5165"),
39088
+ value: writable ? tx("Checked again before writing", "\u5199\u5165\u524D\u4F1A\u518D\u6B21\u9A8C\u8BC1") : tx("Not writable yet", "\u6682\u4E0D\u53EF\u5199"),
39089
+ tone: writable ? void 0 : "warn"
39090
+ },
39091
+ {
39092
+ label: tx("Track changes", "\u7559\u75D5"),
39093
+ value: reviewingOk ? tx("Verified", "\u5DF2\u786E\u8BA4") : tx("Not verified", "\u672A\u786E\u8BA4"),
39094
+ tone: reviewingOk || !reviewingNeeded ? void 0 : "warn"
39095
+ }
38621
39096
  ];
38622
39097
  if (readable && writable) {
38623
39098
  return {
@@ -38625,7 +39100,7 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38625
39100
  subtitle: formatProbeStatusBar(probe),
38626
39101
  status: reviewingOk || mode === "ask" ? "completed" : "warning",
38627
39102
  summary: tx("The current Overleaf file can be read and written. The extension still verifies before writing to avoid overwriting new user or collaborator edits.", "\u5F53\u524D Overleaf \u6587\u4EF6\u53EF\u4EE5\u8BFB\u53D6\u548C\u5199\u5165\u3002\u5199\u5165\u524D\u63D2\u4EF6\u4ECD\u4F1A\u518D\u6B21\u9A8C\u8BC1\uFF0C\u907F\u514D\u8986\u76D6\u7528\u6237\u6216\u534F\u4F5C\u8005\u7684\u65B0\u6539\u52A8\u3002"),
38628
- bullets,
39103
+ facts,
38629
39104
  nextStep: reviewingOk || mode === "ask" ? "" : tx("If you need tracked writes, turn on Overleaf Reviewing/Track Changes first.", "\u5982\u679C\u9700\u8981\u7559\u75D5\u5199\u5165\uFF0C\u8BF7\u5148\u5728 Overleaf \u5F00\u542F Reviewing/Track Changes\u3002"),
38630
39105
  technical: formatPageDiagnosticsTechnicalDetails(probe)
38631
39106
  };
@@ -38635,7 +39110,7 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38635
39110
  subtitle: formatProbeStatusBar(probe),
38636
39111
  status: "warning",
38637
39112
  summary: readable ? tx("The extension read the current Overleaf file, but has not verified that the current editor can be written.", "\u63D2\u4EF6\u5DF2\u8BFB\u5230\u5F53\u524D Overleaf \u6587\u4EF6\uFF0C\u4F46\u8FD8\u6CA1\u6709\u786E\u8BA4\u5F53\u524D\u7F16\u8F91\u5668\u53EF\u4EE5\u5199\u5165\u3002") : tx("The extension has not verified that the current editor can be written. Usually this means no .tex file is open yet, or Overleaf is still loading.", "\u63D2\u4EF6\u6CA1\u6709\u786E\u8BA4\u5F53\u524D\u7F16\u8F91\u5668\u53EF\u4EE5\u5199\u5165\u3002\u901A\u5E38\u662F\u8FD8\u6CA1\u70B9\u5F00 .tex \u6587\u4EF6\uFF0C\u6216\u8005 Overleaf \u9875\u9762\u4ECD\u5728\u52A0\u8F7D\u3002"),
38638
- bullets,
39113
+ facts,
38639
39114
  nextStep: tx("Open the target .tex file from the left Overleaf file tree, wait for the editor to load, then check again.", "\u5728 Overleaf \u5DE6\u4FA7\u6587\u4EF6\u6811\u70B9\u5F00\u8981\u5904\u7406\u7684 .tex \u6587\u4EF6\uFF0C\u7B49\u7F16\u8F91\u5668\u52A0\u8F7D\u5B8C\u6210\u540E\u91CD\u65B0\u68C0\u6D4B\u3002"),
38640
39115
  technical: formatPageDiagnosticsTechnicalDetails(probe)
38641
39116
  };
@@ -38648,10 +39123,10 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38648
39123
  const warnings = getProjectSnapshotWarnings(project);
38649
39124
  const warningText = [...warnings.blocking, ...warnings.nonBlocking].map(formatProjectSnapshotWarning);
38650
39125
  const source = project?.capabilities?.method || "unknown";
38651
- const bullets = [
38652
- tx(`Text files: ${textCount}`, `\u6587\u672C\u6587\u4EF6\uFF1A${textCount} \u4E2A`),
38653
- tx(`Asset files: ${binaryCount}`, `\u8D44\u6E90\u6587\u4EF6\uFF1A${binaryCount} \u4E2A`),
38654
- project?.activePath ? tx(`Current file: ${project.activePath}`, `\u5F53\u524D\u6587\u4EF6\uFF1A${project.activePath}`) : tx("Current file: not detected", "\u5F53\u524D\u6587\u4EF6\uFF1A\u672A\u8BC6\u522B")
39126
+ const facts = [
39127
+ { label: tx("Text files", "\u6587\u672C\u6587\u4EF6"), value: tx(String(textCount), `${textCount} \u4E2A`) },
39128
+ { label: tx("Asset files", "\u8D44\u6E90\u6587\u4EF6"), value: tx(String(binaryCount), `${binaryCount} \u4E2A`) },
39129
+ { label: tx("Current file", "\u5F53\u524D\u6587\u4EF6"), value: project?.activePath || tx("Not detected", "\u672A\u8BC6\u522B") }
38655
39130
  ];
38656
39131
  if (fullProject && files.length) {
38657
39132
  return {
@@ -38662,7 +39137,8 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38662
39137
  `The extension read the full Overleaf project: ${textCount} text file(s)${binaryCount ? `, ${binaryCount} asset file(s)` : ""}.`,
38663
39138
  `\u63D2\u4EF6\u5DF2\u8BFB\u5230\u5B8C\u6574 Overleaf \u9879\u76EE\uFF1A${textCount} \u4E2A\u6587\u672C\u6587\u4EF6${binaryCount ? `\uFF0C${binaryCount} \u4E2A\u8D44\u6E90\u6587\u4EF6` : ""}\u3002`
38664
39139
  ),
38665
- bullets: warningText.length ? [...bullets, ...warningText] : bullets,
39140
+ facts,
39141
+ notes: warningText,
38666
39142
  technical: formatProjectSnapshotLog(project)
38667
39143
  };
38668
39144
  }
@@ -38674,7 +39150,8 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38674
39150
  `The full Overleaf project was not read. Currently read ${textCount} text file(s)${binaryCount ? `, ${binaryCount} asset file(s)` : ""}.`,
38675
39151
  `\u6CA1\u6709\u8BFB\u5230\u5B8C\u6574\u7684 Overleaf \u9879\u76EE\u3002\u5F53\u524D\u53EA\u8BFB\u5230 ${textCount} \u4E2A\u6587\u672C\u6587\u4EF6${binaryCount ? `\uFF0C${binaryCount} \u4E2A\u8D44\u6E90\u6587\u4EF6` : ""}\u3002`
38676
39152
  ) : tx("The full Overleaf project was not read, and no usable file content was available.", "\u6CA1\u6709\u8BFB\u5230\u5B8C\u6574\u7684 Overleaf \u9879\u76EE\uFF0C\u4E5F\u6CA1\u6709\u62FF\u5230\u53EF\u7528\u6587\u4EF6\u5185\u5BB9\u3002"),
38677
- bullets: warningText.length ? [...bullets, ...warningText] : bullets,
39153
+ facts,
39154
+ notes: warningText,
38678
39155
  nextStep: tx("Reload Overleaf and wait for the left file tree to load, then retry. If you only want one file, use + to add @file context.", "\u5237\u65B0 Overleaf \u9875\u9762\uFF0C\u7B49\u5DE6\u4FA7\u6587\u4EF6\u6811\u52A0\u8F7D\u5B8C\u6210\u540E\u91CD\u8BD5\uFF1B\u5982\u679C\u53EA\u60F3\u5904\u7406\u4E00\u4E2A\u6587\u4EF6\uFF0C\u53EF\u4EE5\u7528 + \u6DFB\u52A0 @file \u4E0A\u4E0B\u6587\u3002"),
38679
39156
  technical: formatProjectSnapshotLog(project)
38680
39157
  };
@@ -38886,7 +39363,7 @@ failedAt: ${new Date(failure.at).toISOString()}`;
38886
39363
  },
38887
39364
  platform: nativeDiagnostics.platform,
38888
39365
  nativeEnvironment: nativeDiagnostics.nativeEnvironment,
38889
- mirror,
39366
+ mirror: mirror ?? { status: "unavailable" },
38890
39367
  auditLogs,
38891
39368
  run: activeRecord?.runProjectId === projectId && matchesAccount(activeRecord, currentRunView?.runAccountScopeId) ? activeRecord : recentRuns[0] || {},
38892
39369
  recentRuns,
@@ -39737,17 +40214,18 @@ ${AgentTranscript.formatFailureBlock(failure, locale, indent)}`;
39737
40214
  const existing = root.querySelector("[data-run-copy]");
39738
40215
  const button = existing.cloneNode(true);
39739
40216
  existing.replaceWith(button);
39740
- const finalAnswer = root.querySelector(".run-completion-report .run-final-answer");
39741
- const copyText = String(finalAnswer?.innerText || finalAnswer?.textContent || "").trim();
40217
+ const available = Boolean(readConclusionText(root).trim());
39742
40218
  button.hidden = false;
39743
- button.disabled = !copyText;
40219
+ button.disabled = !available;
39744
40220
  button.title = tr("copyConclusionTitle");
39745
40221
  button.setAttribute("aria-label", button.title);
39746
40222
  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>';
39747
- if (!copyText) return;
40223
+ if (!available) return;
39748
40224
  button.addEventListener("click", async (event) => {
39749
40225
  event.stopPropagation();
39750
40226
  try {
40227
+ const copyText = readConclusionText(root);
40228
+ if (!copyText.trim()) return;
39751
40229
  await writeClipboardText(copyText);
39752
40230
  button.title = tr("copyConclusionDone");
39753
40231
  } catch (_error) {
@@ -39756,6 +40234,47 @@ ${AgentTranscript.formatFailureBlock(failure, locale, indent)}`;
39756
40234
  button.setAttribute("aria-label", button.title);
39757
40235
  });
39758
40236
  }
40237
+ function readConclusionText(root) {
40238
+ const answer = root.querySelector(".run-completion-report .run-final-answer");
40239
+ if (!answer) return "";
40240
+ return extractConclusionText(answer).replace(/\u200b/g, "").replace(/[ \t]+$/gm, "").trim();
40241
+ }
40242
+ function extractConclusionText(node) {
40243
+ if (node.nodeType === 3) return String(node.textContent || "").replace(/\s+/g, " ");
40244
+ const tag = node.tagName?.toLowerCase();
40245
+ if (tag === "br") return "\n";
40246
+ if (tag === "pre" || tag === "code") return node.textContent || "";
40247
+ if (node.classList?.contains("run-math")) {
40248
+ const source = node.dataset.mathRendered === "false" ? node.textContent || "" : node.querySelector('annotation[encoding="application/x-tex"]')?.textContent || "";
40249
+ const delimiter = node.dataset.mathDisplay === "block" ? "$$" : "$";
40250
+ return source ? `${delimiter}${source}${delimiter}` : "";
40251
+ }
40252
+ const children = Array.from(node.childNodes || []);
40253
+ if (tag === "ul" || tag === "ol") {
40254
+ const start = Number(node.getAttribute("start") || 1);
40255
+ return children.filter((child) => child.tagName?.toLowerCase() === "li").map((child, index) => `${tag === "ol" ? `${start + index}. ` : "- "}${extractConclusionText(child)}`).join("\n");
40256
+ }
40257
+ if (tag === "table") {
40258
+ return Array.from(node.querySelectorAll("tr"), (row) => Array.from(
40259
+ row.children,
40260
+ (cell) => extractConclusionText(cell)
40261
+ ).join(" ")).join("\n");
40262
+ }
40263
+ let text = "";
40264
+ let previousBlock = false;
40265
+ for (const child of children) {
40266
+ const value = extractConclusionText(child);
40267
+ if (!value) continue;
40268
+ const block = /^(p|h[1-6]|blockquote|ul|ol|pre|table|div)$/.test(child.tagName?.toLowerCase()) || child.dataset?.mathDisplay === "block";
40269
+ if (text && (previousBlock || block)) {
40270
+ const newlines = (text.match(/\n*$/)?.[0].length || 0) + (value.match(/^\n*/)?.[0].length || 0);
40271
+ text += "\n".repeat(Math.max(0, 2 - newlines));
40272
+ }
40273
+ text += value;
40274
+ previousBlock = block;
40275
+ }
40276
+ return text;
40277
+ }
39759
40278
  async function writeClipboardText(text) {
39760
40279
  if (navigatorRef?.clipboard?.writeText) {
39761
40280
  await navigatorRef.clipboard.writeText(text);
@@ -44577,7 +45096,7 @@ ${lines}`
44577
45096
  normalizeSpeedTiersForSelect,
44578
45097
  retainSelectedModel
44579
45098
  } = Support;
44580
- let modelDiscovery = { status: "fallback", source: "fallback", fetchedAt: "" };
45099
+ let modelDiscovery = { status: "fallback", source: "fallback", fetchedAt: "", provisional: true };
44581
45100
  let modelLoadGeneration = 0;
44582
45101
  function getModelDiscovery() {
44583
45102
  return modelDiscovery;
@@ -44621,22 +45140,18 @@ ${lines}`
44621
45140
  const modelSelect = getPanel()?.querySelector("[data-model]");
44622
45141
  const reasoningSelect = getPanel()?.querySelector("[data-reasoning]");
44623
45142
  const speedSelect = getPanel()?.querySelector("[data-speed]");
44624
- let changedInput = null;
44625
- if (choice.dataset.reasoningChoice && reasoningSelect) {
44626
- reasoningSelect.value = choice.dataset.reasoningChoice;
44627
- changedInput = reasoningSelect;
44628
- } else if (choice.dataset.modelChoice && modelSelect) {
44629
- modelSelect.value = choice.dataset.modelChoice;
44630
- changedInput = modelSelect;
45143
+ const changedInput = choice.dataset.reasoningChoice ? reasoningSelect : choice.dataset.modelChoice ? modelSelect : choice.dataset.speedChoice ? speedSelect : null;
45144
+ const value = choice.dataset.reasoningChoice || choice.dataset.modelChoice || choice.dataset.speedChoice;
45145
+ const option = Array.from(changedInput?.options || []).find((item) => item.value === value);
45146
+ if (!changedInput || changedInput.disabled || !option || option.disabled) return;
45147
+ changedInput.value = value;
45148
+ if (changedInput === modelSelect) {
44631
45149
  renderReasoningOptions(getRenderedModelEntries());
44632
45150
  renderSpeedOptions(getRenderedModelEntries());
44633
- } else if (choice.dataset.speedChoice && speedSelect) {
44634
- speedSelect.value = choice.dataset.speedChoice;
44635
- changedInput = speedSelect;
44636
45151
  }
44637
45152
  renderModelConfigChoices();
44638
45153
  updateModelDisplay();
44639
- await persistPanelInputs(changedInput ? { type: "change", target: changedInput } : void 0);
45154
+ await persistPanelInputs({ type: "change", target: changedInput });
44640
45155
  }
44641
45156
  async function loadModelOptions(providerSelection, options = {}) {
44642
45157
  const generation = ++modelLoadGeneration;
@@ -44668,7 +45183,7 @@ ${lines}`
44668
45183
  }
44669
45184
  const sourceModels = hasDiscoveredModels ? response.result.models : fallbackModels;
44670
45185
  const preserveBuiltin = (selection?.providerId || "builtin") === "builtin" && (getState()?.providerId || "builtin") === "builtin";
44671
- const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel, { preserveUnavailable: preserveBuiltin });
45186
+ const retainedSelectedModel = retainSelectedModel(sourceModels, currentSelectedModel, { preserveUnavailable: preserveBuiltin }) || (responseProviderId && responseProviderId !== "builtin" ? retainSelectedModel(sourceModels, response.result?.defaultModelId) : "");
44672
45187
  const normalized = modelCatalog.normalizeDiscoveredModels({ models: sourceModels, selectedModel: retainedSelectedModel });
44673
45188
  if (normalized.usedFallback && response.result?.providerId && response.result.providerId !== "builtin") {
44674
45189
  applyUnavailableModelOptions({ code: "provider_model_catalog_invalid", message: "The active provider returned no usable models." });
@@ -44766,6 +45281,13 @@ ${lines}`
44766
45281
  return;
44767
45282
  }
44768
45283
  const selectedId = normalizeModelOptionId(selectedModel);
45284
+ if (options.provisionalSelected === true) {
45285
+ models = [...Array.isArray(models) ? models : []];
45286
+ if (selectedId && !models.some((model) => normalizeModelOptionId(model?.id) === selectedId)) {
45287
+ models.push({ id: selectedId, label: selectedId, unverified: true });
45288
+ }
45289
+ models = Support.preserveSelectedCapabilities(models, selectedId, getState(), true);
45290
+ }
44769
45291
  modelSelect.textContent = "";
44770
45292
  modelSelect.disabled = false;
44771
45293
  let renderedSelected = false;
@@ -44887,7 +45409,8 @@ ${lines}`
44887
45409
  list.append(createModelConfigChoice({
44888
45410
  value: option.value,
44889
45411
  label: formatReasoningEffortLabel(option.value, reasoningSelect.dataset.presentation),
44890
- datasetName: "reasoningChoice"
45412
+ datasetName: "reasoningChoice",
45413
+ disabled: reasoningSelect.disabled || option.disabled
44891
45414
  }));
44892
45415
  }
44893
45416
  }
@@ -44903,7 +45426,8 @@ ${lines}`
44903
45426
  const choice = createModelConfigChoice({
44904
45427
  value: option.value,
44905
45428
  label: option.textContent || option.value,
44906
- datasetName: "modelChoice"
45429
+ datasetName: "modelChoice",
45430
+ disabled: modelSelect.disabled || option.disabled
44907
45431
  });
44908
45432
  choice.title = option.textContent || option.value;
44909
45433
  list.append(choice);
@@ -44920,15 +45444,16 @@ ${lines}`
44920
45444
  const choice = createModelConfigChoice({
44921
45445
  value: option.value,
44922
45446
  label: formatSpeedTierLabel(option.value),
44923
- datasetName: "speedChoice"
45447
+ datasetName: "speedChoice",
45448
+ disabled: speedSelect.disabled || option.disabled
44924
45449
  });
44925
- choice.disabled = speedSelect.disabled && speedSelect.options.length <= 1;
44926
45450
  list.append(choice);
44927
45451
  }
44928
45452
  }
44929
- function createModelConfigChoice({ value, label, datasetName }) {
45453
+ function createModelConfigChoice({ value, label, datasetName, disabled = false }) {
44930
45454
  const button = document.createElement("button");
44931
45455
  button.type = "button";
45456
+ button.disabled = disabled;
44932
45457
  button.className = "codex-model-config-choice";
44933
45458
  button.dataset[datasetName] = value;
44934
45459
  button.setAttribute("role", "menuitemradio");
@@ -45573,10 +46098,10 @@ ${lines}`
45573
46098
  restoreScrollTop: root ? root.scrollTop : 0
45574
46099
  };
45575
46100
  }
45576
- async function refreshDashboardView() {
46101
+ async function refreshDashboardView(options = {}) {
45577
46102
  var panelEl = getPanel();
45578
46103
  if (panelEl && panelEl.dataset.view === "recent-projects") {
45579
- await renderRecentProjectsVariant(dashboardViewOptions());
46104
+ await renderRecentProjectsVariant(Object.assign(dashboardViewOptions(), options));
45580
46105
  }
45581
46106
  }
45582
46107
  async function openDashboardConversation(projectId, record, row) {
@@ -45751,7 +46276,7 @@ ${lines}`
45751
46276
  var map = prefs.activeSessionByProject && typeof prefs.activeSessionByProject === "object" ? prefs.activeSessionByProject : {};
45752
46277
  map[projectId] = record.id;
45753
46278
  prefs.activeSessionByProject = map;
45754
- await Migration.savePrefs(prefs, accountScopeId, projectId);
46279
+ await Migration.savePrefs({ activeSessionByProject: map }, accountScopeId, projectId);
45755
46280
  }
45756
46281
  } catch (_error) {
45757
46282
  }
@@ -45886,18 +46411,30 @@ ${lines}`
45886
46411
  input.focus();
45887
46412
  input.select();
45888
46413
  var settled = false;
45889
- var finish = function(commit) {
46414
+ var finish = async function(commit) {
45890
46415
  if (settled) {
45891
46416
  return;
45892
46417
  }
45893
46418
  settled = true;
45894
46419
  var nextRaw = input.value;
45895
- input.remove();
45896
- titleEl.hidden = false;
45897
46420
  if (!commit || nextRaw.trim() === seed.trim()) {
46421
+ input.remove();
46422
+ titleEl.hidden = false;
46423
+ return;
46424
+ }
46425
+ input.readOnly = true;
46426
+ try {
46427
+ await commitDashboardSessionRename(sessionsEl, projectId, record, nextRaw);
46428
+ } catch (_error) {
46429
+ settled = false;
46430
+ input.readOnly = false;
46431
+ showPluginToast(tr("recentProjects_renameFailed"), { status: "failed", sticky: true });
46432
+ input.focus();
45898
46433
  return;
45899
46434
  }
45900
- commitDashboardSessionRename(sessionsEl, projectId, record, nextRaw).catch(function() {
46435
+ input.remove();
46436
+ titleEl.hidden = false;
46437
+ await refreshDashboardView().catch(function() {
45901
46438
  });
45902
46439
  };
45903
46440
  input.addEventListener("keydown", function(event) {
@@ -45935,16 +46472,12 @@ ${lines}`
45935
46472
  });
45936
46473
  } catch (_error) {
45937
46474
  }
45938
- try {
45939
- if (StorageDb) {
45940
- await StorageDb.putRecord("sessions", StorageDb.buildSessionRecord(Object.assign({}, record, {
45941
- title: next.title,
45942
- titleSource: next.titleSource
45943
- })));
45944
- }
45945
- } catch (_error) {
46475
+ if (StorageDb) {
46476
+ await StorageDb.putRecord("sessions", StorageDb.buildSessionRecord(Object.assign({}, record, {
46477
+ title: next.title,
46478
+ titleSource: next.titleSource
46479
+ }), { preserveRunActionPayload: true }));
45946
46480
  }
45947
- await refreshDashboardView();
45948
46481
  }
45949
46482
  function ensureRecentProjectsRoot() {
45950
46483
  if (!getPanel()) {
@@ -46166,6 +46699,7 @@ ${lines}`
46166
46699
  openProjectFromRow,
46167
46700
  renderRecentProjectRow,
46168
46701
  renderRecentProjectsVariant,
46702
+ refreshDashboardView,
46169
46703
  mountDashboardShell,
46170
46704
  resetProjectNameCache,
46171
46705
  renderPerProjectVariant
@@ -47298,6 +47832,12 @@ ${lines}`
47298
47832
  let appendRunEvent;
47299
47833
  let appendRunRecordEvent;
47300
47834
  let pendingMirrorRefresh = null;
47835
+ const mirrorSettlementOwners = /* @__PURE__ */ new Map();
47836
+ const mirrorReuseFences = /* @__PURE__ */ new Map();
47837
+ const mirrorFenceWrites = /* @__PURE__ */ new Map();
47838
+ const mirrorFenceRevisions = /* @__PURE__ */ new Map();
47839
+ const MIRROR_INVALIDATION_TIMEOUT_MS = 2e4;
47840
+ const MIRROR_FENCE_STORAGE_TIMEOUT_MS = 2e3;
47301
47841
  let appendChangeSummary;
47302
47842
  let appendCompletionReport;
47303
47843
  let appendOperationsPreview;
@@ -47308,6 +47848,8 @@ ${lines}`
47308
47848
  let callPageBridge;
47309
47849
  let sendBackgroundNative;
47310
47850
  let getCurrentProjectId;
47851
+ let getAccountScopeId;
47852
+ let mirrorFenceStore;
47311
47853
  let resetContextProject;
47312
47854
  let sanitizeRunProjectSnapshot;
47313
47855
  let getAssistantAnswerForCurrentRun;
@@ -47345,34 +47887,169 @@ ${lines}`
47345
47887
  function buildSettlement(input = {}) {
47346
47888
  return writebackSettlement?.settle instanceof Function ? writebackSettlement.settle(input) : null;
47347
47889
  }
47348
- function invalidateMirrorAfterUndo(runId, projectId, result = {}) {
47349
- if (!projectId || result?.changedDocument !== true && !result?.applied?.length) return Promise.resolve("not-attempted");
47350
- const pending = Promise.resolve(pendingMirrorRefresh).catch(() => {
47351
- }).then(async () => {
47352
- const response = await sendBackgroundNative({ method: "mirror.invalidate", params: { projectId } });
47353
- if (!response?.ok || response.result?.invalidated !== true) {
47354
- throw new Error(response?.error?.message || "Native host did not confirm mirror invalidation.");
47890
+ function mirrorFenceScope(projectId, accountScopeId = getAccountScopeId?.() || "") {
47891
+ return { projectId, accountScopeId };
47892
+ }
47893
+ function mirrorFenceKey(scope) {
47894
+ return JSON.stringify([scope.accountScopeId, scope.projectId]);
47895
+ }
47896
+ function getMirrorReuseFence(projectId, accountScopeId) {
47897
+ return mirrorReuseFences.get(mirrorFenceKey(mirrorFenceScope(projectId, accountScopeId))) || null;
47898
+ }
47899
+ function updateMirrorReuseFence(scope, generations, unknown = false) {
47900
+ const key = mirrorFenceKey(scope), current = mirrorReuseFences.get(key);
47901
+ const ids = [...new Set(generations)].sort();
47902
+ if (current && current.unknown === unknown && current.generations.join(":") === ids.join(":")) return current;
47903
+ const fence = ids.length || unknown ? { scope, generations: ids, unknown, syncAttempted: false } : null;
47904
+ if (fence) mirrorReuseFences.set(key, fence);
47905
+ else mirrorReuseFences.delete(key);
47906
+ mirrorFenceRevisions.set(key, (mirrorFenceRevisions.get(key) || 0) + 1);
47907
+ return fence;
47908
+ }
47909
+ async function awaitMirrorFenceStorage(work) {
47910
+ let timer;
47911
+ try {
47912
+ return await Promise.race([
47913
+ Promise.resolve().then(work),
47914
+ new Promise((_resolve, reject) => {
47915
+ timer = setTimeout(() => reject(new Error("Mirror fence storage timed out.")), MIRROR_FENCE_STORAGE_TIMEOUT_MS);
47916
+ })
47917
+ ]);
47918
+ } finally {
47919
+ clearTimeout(timer);
47920
+ }
47921
+ }
47922
+ async function refreshMirrorReuseFence(projectId, accountScopeId) {
47923
+ const scope = mirrorFenceScope(projectId, accountScopeId), key = mirrorFenceKey(scope);
47924
+ if (!mirrorFenceStore) return getMirrorReuseFence(projectId, scope.accountScopeId);
47925
+ const revision = mirrorFenceRevisions.get(key) || 0;
47926
+ try {
47927
+ const stored = await awaitMirrorFenceStorage(() => mirrorFenceStore.getMirrorReuseFences(scope));
47928
+ const writes = mirrorFenceWrites.get(key) || /* @__PURE__ */ new Map();
47929
+ const changed = revision !== (mirrorFenceRevisions.get(key) || 0);
47930
+ const generations = new Set(stored.filter((id) => !writes.get(id)?.retired));
47931
+ for (const [id, write] of writes) {
47932
+ if (!write.retired && (!write.persisted || changed)) generations.add(id);
47355
47933
  }
47356
- if (getCurrentProjectId() === projectId) {
47357
- resetContextProject();
47358
- await callPageBridge("invalidateProjectSnapshot", { invalidateFileList: true });
47934
+ if (changed) for (const id of mirrorReuseFences.get(key)?.generations || []) {
47935
+ if (!writes.get(id)?.retired) generations.add(id);
47359
47936
  }
47360
- return "invalidated";
47361
- }).catch((error) => {
47362
- appendRunRecordEvent?.(runId, {
47363
- status: "warning",
47364
- title: tx(
47365
- `Overleaf undo finished, but local mirror invalidation failed: ${error.message}. Refresh this project or reconnect the native host before running again.`,
47366
- `Overleaf \u64A4\u9500\u5DF2\u5B8C\u6210\uFF0C\u4F46\u672C\u5730\u955C\u50CF\u5931\u6548\u5904\u7406\u5931\u8D25\uFF1A${error.message}\u3002\u8BF7\u5237\u65B0\u9879\u76EE\u6216\u91CD\u65B0\u8FDE\u63A5 Native Host \u540E\u518D\u8FD0\u884C\u3002`
47367
- )
47368
- });
47369
- return "failed";
47937
+ return updateMirrorReuseFence(scope, [...generations]);
47938
+ } catch (_error) {
47939
+ return updateMirrorReuseFence(scope, mirrorReuseFences.get(key)?.generations || [], true);
47940
+ }
47941
+ }
47942
+ async function recordMirrorReuseFence(target) {
47943
+ const scope = mirrorFenceScope(target.runProjectId, target.runAccountScopeId);
47944
+ const key = mirrorFenceKey(scope);
47945
+ const generation = globalThis.crypto?.randomUUID?.() || Date.now().toString(36) + "-" + Math.random().toString(36).slice(2) + "-" + Math.random().toString(36).slice(2);
47946
+ const writes = mirrorFenceWrites.get(key) || /* @__PURE__ */ new Map();
47947
+ mirrorFenceWrites.set(key, writes);
47948
+ const write = { persisted: false, retired: false, clearAfterWrite: false };
47949
+ writes.set(generation, write);
47950
+ updateMirrorReuseFence(scope, [...mirrorReuseFences.get(key)?.generations || [], generation]);
47951
+ const token = { scope, generation };
47952
+ if (!mirrorFenceStore) return token;
47953
+ const pending = Promise.resolve().then(() => mirrorFenceStore.setMirrorReuseFence(scope, generation));
47954
+ pending.then(() => {
47955
+ write.persisted = true;
47956
+ mirrorFenceRevisions.set(key, (mirrorFenceRevisions.get(key) || 0) + 1);
47957
+ if (write.clearAfterWrite) {
47958
+ awaitMirrorFenceStorage(() => mirrorFenceStore.clearMirrorReuseFences(scope, [generation])).then(() => {
47959
+ write.retired = true;
47960
+ }).catch(() => {
47961
+ });
47962
+ }
47963
+ }, () => {
47964
+ });
47965
+ try {
47966
+ await awaitMirrorFenceStorage(() => pending);
47967
+ } catch (_error) {
47968
+ }
47969
+ return token;
47970
+ }
47971
+ async function clearMirrorReuseFenceGenerations(scope, generations) {
47972
+ const writes = mirrorFenceWrites.get(mirrorFenceKey(scope));
47973
+ for (const id of generations) if (writes?.has(id)) writes.get(id).clearAfterWrite = true;
47974
+ const clearing = Promise.resolve().then(() => mirrorFenceStore?.clearMirrorReuseFences(scope, generations));
47975
+ clearing.then(() => {
47976
+ for (const id of generations) if (writes?.has(id)) writes.get(id).retired = true;
47977
+ }, () => {
47978
+ });
47979
+ await awaitMirrorFenceStorage(() => clearing);
47980
+ }
47981
+ async function settleFullMirrorSync(projectId, project, fence, response, options = {}) {
47982
+ if (!fence || fence.unknown || getMirrorReuseFence(projectId, fence.scope.accountScopeId) !== fence || response?.ok !== true || project?.capabilities?.fullProjectSnapshot !== true || project.capabilities.contentComplete === false || project.capabilities.skipped?.length) return false;
47983
+ try {
47984
+ options.assertCurrent?.();
47985
+ const status = await (options.sendNative || sendBackgroundNative)({ method: "mirror.status", params: { projectId } });
47986
+ options.assertCurrent?.();
47987
+ const current = await refreshMirrorReuseFence(projectId, fence.scope.accountScopeId);
47988
+ options.assertCurrent?.();
47989
+ if (getCurrentProjectId() !== projectId || current !== fence || status?.ok !== true || status.result?.exists !== true || status.result.dirty === true) return false;
47990
+ await clearMirrorReuseFenceGenerations(fence.scope, fence.generations);
47991
+ options.assertCurrent?.();
47992
+ if (getMirrorReuseFence(projectId, fence.scope.accountScopeId) === fence) updateMirrorReuseFence(fence.scope, []);
47993
+ return !await refreshMirrorReuseFence(projectId, fence.scope.accountScopeId);
47994
+ } catch (_error) {
47995
+ options.assertCurrent?.();
47996
+ return false;
47997
+ }
47998
+ }
47999
+ async function syncFencedMirror(projectId, project, options = {}) {
48000
+ const fence = options.fence;
48001
+ if (!fence || fence.unknown || getMirrorReuseFence(projectId, fence.scope.accountScopeId) !== fence || fence.syncAttempted || project?.id !== projectId || project.capabilities?.fullProjectSnapshot !== true || project.capabilities.contentComplete === false || project.capabilities.skipped?.length) return false;
48002
+ options.assertCurrent();
48003
+ fence.syncAttempted = true;
48004
+ try {
48005
+ const response = await options.sendNative({ method: "mirror.sync", params: { projectId, project } });
48006
+ options.assertCurrent();
48007
+ return await settleFullMirrorSync(projectId, project, fence, response, options);
48008
+ } catch (_error) {
48009
+ options.assertCurrent();
48010
+ return false;
48011
+ }
48012
+ }
48013
+ function trackWritebackMirror(work, target, onSettled) {
48014
+ const key = JSON.stringify([target.runProjectId, target.sessionId, target.recordId]);
48015
+ const owner = {};
48016
+ mirrorSettlementOwners.set(key, owner);
48017
+ const refresh = Promise.resolve(work).then(async (state) => {
48018
+ if (mirrorSettlementOwners.get(key) !== owner) return state;
48019
+ onSettled?.(state);
48020
+ await onMirrorRefreshSettled?.({ ...target, state: state || "not-attempted" });
48021
+ return state;
48022
+ }).finally(() => {
48023
+ if (mirrorSettlementOwners.get(key) === owner) mirrorSettlementOwners.delete(key);
48024
+ });
48025
+ const pending = Promise.allSettled([pendingMirrorRefresh, refresh]).then(([, result]) => {
48026
+ if (result.status === "rejected") throw result.reason;
48027
+ return result.value;
47370
48028
  }).finally(() => {
47371
48029
  if (pendingMirrorRefresh === pending) pendingMirrorRefresh = null;
47372
48030
  });
47373
48031
  pendingMirrorRefresh = pending;
47374
48032
  return pending;
47375
48033
  }
48034
+ function invalidateMirrorAfterUndo(runId, projectId, result = {}, owner = {}) {
48035
+ if (!projectId || result?.changedDocument !== true && !result?.applied?.length) return Promise.resolve("not-attempted");
48036
+ const target = {
48037
+ recordId: runId,
48038
+ runProjectId: projectId,
48039
+ runAccountScopeId: owner.runAccountScopeId || getAccountScopeId?.() || "",
48040
+ sessionId: owner.sessionId || ""
48041
+ };
48042
+ const pending = Promise.resolve(pendingMirrorRefresh).catch(() => {
48043
+ }).then(() => invalidateWritebackMirror(target, {
48044
+ pageFirst: true,
48045
+ nativeTimeoutMs: MIRROR_INVALIDATION_TIMEOUT_MS,
48046
+ undo: true
48047
+ })).then((state) => state === "not-attempted" ? "invalidated" : state).finally(() => {
48048
+ if (pendingMirrorRefresh === pending) pendingMirrorRefresh = null;
48049
+ });
48050
+ pendingMirrorRefresh = pending;
48051
+ return pending;
48052
+ }
47376
48053
  function selectionViolationNextStep() {
47377
48054
  return tx(
47378
48055
  "Select the intended region again and request only changes within it. Valid written parts can be undone.",
@@ -47401,7 +48078,12 @@ ${lines}`
47401
48078
  }
47402
48079
  async function applySyncChangesToOverleaf(syncChanges = [], project = {}, options = {}) {
47403
48080
  const activeRun = getCurrentRunView?.();
47404
- const saveTarget = { runProjectId: activeRun?.runProjectId || "", recordId: activeRun?.recordId || "", sessionId: activeRun?.sessionId || "" };
48081
+ const saveTarget = {
48082
+ runProjectId: activeRun?.runProjectId || "",
48083
+ runAccountScopeId: activeRun?.runAccountScopeId || getAccountScopeId?.() || "",
48084
+ recordId: activeRun?.recordId || "",
48085
+ sessionId: activeRun?.sessionId || ""
48086
+ };
47405
48087
  const runMode = options.mode || getState().mode;
47406
48088
  const runRequireReviewing = typeof options.requireReviewing === "boolean" ? options.requireReviewing : getState().requireReviewing === true;
47407
48089
  const assistantMessage = cleanFinalAnswer(options.assistantMessage || getAssistantAnswerForCurrentRun());
@@ -47694,28 +48376,57 @@ ${tx("Sync note: no changes were written to Overleaf; every change was blocked b
47694
48376
  skipped: skippedEntries.length
47695
48377
  }
47696
48378
  });
47697
- const confirmation = appliedPaths.length ? await verifyPostWriteSaveState(applied, project, saveTarget) : { verification: { ok: false, state: "not_checked", reason: "No applied writeback operations were returned." } };
48379
+ let confirmation;
48380
+ try {
48381
+ confirmation = appliedPaths.length ? await verifyPostWriteSaveState(applied, project, saveTarget) : { verification: { ok: false, state: "not_checked", reason: "No applied writeback operations were returned." } };
48382
+ } catch (error) {
48383
+ if (!appliedPaths.length || error?.code !== "codex_cancelled" && error?.name !== "AbortError") throw error;
48384
+ const saveVerification2 = {
48385
+ ok: false,
48386
+ state: "unknown_timeout",
48387
+ signal: "unavailable",
48388
+ reason: "Save confirmation was cancelled.",
48389
+ recoveryReason: "codex_cancelled",
48390
+ attempts: []
48391
+ };
48392
+ error.writebackOutcome = {
48393
+ operations,
48394
+ applied,
48395
+ saveVerification: saveVerification2,
48396
+ settlement: buildSettlement({ operations, applyResult: applied, saveVerification: saveVerification2, mirror: { state: "unknown" } })
48397
+ };
48398
+ trackWritebackMirror(
48399
+ invalidateWritebackMirror(saveTarget, {
48400
+ pageFirst: true,
48401
+ nativeTimeoutMs: MIRROR_INVALIDATION_TIMEOUT_MS
48402
+ }),
48403
+ saveTarget,
48404
+ (state) => {
48405
+ error.writebackOutcome.settlement = buildSettlement({ operations, applyResult: applied, saveVerification: saveVerification2, mirror: { state } });
48406
+ }
48407
+ );
48408
+ try {
48409
+ await settleWritebackRecovery?.(applied, project, saveVerification2, saveTarget, runRequireReviewing);
48410
+ } catch (recoveryError) {
48411
+ appendRunRecordEvent?.(saveTarget.recordId, {
48412
+ kind: "technical",
48413
+ status: "warning",
48414
+ title: tx("Writeback recovery persistence failed", "\u5199\u56DE\u6062\u590D\u4FE1\u606F\u4FDD\u5B58\u5931\u8D25"),
48415
+ detail: { reason: recoveryError.message }
48416
+ });
48417
+ }
48418
+ throw error;
48419
+ }
47698
48420
  const saveVerification = confirmation.verification;
47699
48421
  await settleWritebackRecovery?.(applied, project, saveVerification, saveTarget, runRequireReviewing);
47700
48422
  if (appliedPaths.length) {
47701
48423
  appendPostWriteSaveVerificationWarning(saveVerification, saveTarget);
47702
48424
  appendRunEvent({ kind: "technical", title: "Post-write save confirmation", status: "info", detail: saveVerification });
47703
48425
  }
47704
- const mirrorSettlementTarget = saveTarget;
47705
- pendingMirrorRefresh = refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot }).catch(async (error) => {
48426
+ trackWritebackMirror(refreshProjectMirrorAfterWriteback(project, applied, saveVerification, { target: saveTarget, snapshot: confirmation.snapshot }).catch(async (error) => {
47706
48427
  appendRunRecordEvent(saveTarget.recordId, { kind: "technical", title: "Mirror refresh requires a fresh read", status: "info", detail: { reason: error.message } });
47707
48428
  return invalidateWritebackMirror(saveTarget);
47708
- }).then((state) => {
47709
- if (typeof onMirrorRefreshSettled !== "function") return;
47710
- return onMirrorRefreshSettled({
47711
- recordId: mirrorSettlementTarget?.recordId || "",
47712
- sessionId: mirrorSettlementTarget?.sessionId || "",
47713
- runProjectId: mirrorSettlementTarget?.runProjectId || "",
47714
- state: state || "not-attempted"
47715
- });
47716
- }).finally(() => {
47717
- pendingMirrorRefresh = null;
47718
- });
48429
+ }), saveTarget);
47719
48430
  const compileSummary = appliedPaths.length ? await autoRecompileAfterWriteback(appliedPaths, saveVerification, {
47720
48431
  autoRecompile: options.autoRecompile,
47721
48432
  mode: runMode,
@@ -47782,7 +48493,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
47782
48493
  // the run by inspecting the `applied.skipped` entries' failure codes.
47783
48494
  // Existing call sites that only read `summaryLine` / `hasSkippedOperations`
47784
48495
  // / `audit` are unaffected.
48496
+ operations,
47785
48497
  applied,
48498
+ saveVerification,
47786
48499
  settlement: buildSettlement({
47787
48500
  operations,
47788
48501
  applyResult: applied,
@@ -47831,27 +48544,65 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
47831
48544
  if (target.recordId) appendRunRecordEvent(target.recordId, { title, status: "warning" });
47832
48545
  else appendRunEvent({ title, status: "warning" });
47833
48546
  }
47834
- async function invalidateWritebackMirror(target = {}) {
48547
+ async function invalidateWritebackMirror(target = {}, options = {}) {
48548
+ const invalidatePage = async () => {
48549
+ if (getCurrentProjectId() !== target.runProjectId || target.runAccountScopeId && getAccountScopeId?.() !== target.runAccountScopeId) return true;
48550
+ try {
48551
+ resetContextProject();
48552
+ const response = await callPageBridge("invalidateProjectSnapshot", { invalidateFileList: true, runProjectId: target.runProjectId });
48553
+ return response?.ok === true;
48554
+ } catch (_error) {
48555
+ return false;
48556
+ }
48557
+ };
48558
+ const pageInvalidation = options.pageFirst === true ? invalidatePage() : null;
48559
+ const pendingFence = options.pageFirst === true && target.runProjectId ? await recordMirrorReuseFence(target) : null;
47835
48560
  let invalidated = false;
48561
+ let failureReason = "Native host did not confirm mirror invalidation.";
48562
+ let timer;
47836
48563
  try {
47837
48564
  if (target.runProjectId) {
47838
- const response = await sendBackgroundNative({ method: "mirror.invalidate", params: { projectId: target.runProjectId } });
48565
+ const pending = sendBackgroundNative({ method: "mirror.invalidate", params: { projectId: target.runProjectId } });
48566
+ pending.catch(() => {
48567
+ });
48568
+ const response = options.nativeTimeoutMs ? await Promise.race([
48569
+ pending,
48570
+ new Promise((resolve) => {
48571
+ timer = setTimeout(() => resolve(null), options.nativeTimeoutMs);
48572
+ })
48573
+ ]) : await pending;
47839
48574
  invalidated = response?.ok === true && response.result?.invalidated === true;
48575
+ if (!invalidated && response?.error?.message) failureReason = response.error.message;
47840
48576
  }
47841
- } catch (_error) {
48577
+ } catch (error) {
48578
+ failureReason = error?.message || failureReason;
48579
+ } finally {
48580
+ if (timer !== void 0) clearTimeout(timer);
47842
48581
  }
47843
- let pageInvalidated = getCurrentProjectId() !== target.runProjectId;
47844
- if (!pageInvalidated) {
47845
- resetContextProject();
47846
- try {
47847
- const response = await callPageBridge("invalidateProjectSnapshot", { invalidateFileList: true, runProjectId: target.runProjectId });
47848
- pageInvalidated = response?.ok === true;
47849
- } catch (_error) {
48582
+ const pageInvalidated = await (pageInvalidation || invalidatePage());
48583
+ if (invalidated && pageInvalidated) {
48584
+ if (pendingFence) {
48585
+ try {
48586
+ await clearMirrorReuseFenceGenerations(pendingFence.scope, [pendingFence.generation]);
48587
+ const current = getMirrorReuseFence(target.runProjectId, pendingFence.scope.accountScopeId);
48588
+ if (current) updateMirrorReuseFence(
48589
+ pendingFence.scope,
48590
+ current.generations.filter((id) => id !== pendingFence.generation),
48591
+ current.unknown
48592
+ );
48593
+ await refreshMirrorReuseFence(target.runProjectId, pendingFence.scope.accountScopeId);
48594
+ } catch (_error) {
48595
+ }
47850
48596
  }
48597
+ return "not-attempted";
47851
48598
  }
47852
- if (invalidated && pageInvalidated) return "not-attempted";
48599
+ if (target.runProjectId && !pendingFence) await recordMirrorReuseFence(target);
48600
+ if (invalidated && !pageInvalidated) failureReason = "The page did not confirm snapshot invalidation.";
47853
48601
  const event = {
47854
- title: tx(
48602
+ title: options.undo === true ? tx(
48603
+ `Overleaf undo finished, but local mirror invalidation failed: ${failureReason}. Refresh this project or reconnect the native host before running again.`,
48604
+ `Overleaf \u64A4\u9500\u5DF2\u5B8C\u6210\uFF0C\u4F46\u672C\u5730\u955C\u50CF\u5931\u6548\u5904\u7406\u5931\u8D25\uFF1A${failureReason}\u3002\u8BF7\u5237\u65B0\u9879\u76EE\u6216\u91CD\u65B0\u8FDE\u63A5 Native Host \u540E\u518D\u8FD0\u884C\u3002`
48605
+ ) : tx(
47855
48606
  "The local project cache could not be reset. Check the local connection before starting another task.",
47856
48607
  "\u672C\u5730\u9879\u76EE\u7F13\u5B58\u672A\u80FD\u91CD\u7F6E\u3002\u8BF7\u68C0\u67E5\u672C\u5730\u8FDE\u63A5\u540E\u518D\u5F00\u59CB\u4E0B\u4E00\u8F6E\u4EFB\u52A1\u3002"
47857
48608
  ),
@@ -47888,6 +48639,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
47888
48639
  if (!Array.isArray(applied?.applied) || !applied.applied.length) {
47889
48640
  return "not-attempted";
47890
48641
  }
48642
+ const mirrorFence = await refreshMirrorReuseFence(target.runProjectId, target.runAccountScopeId);
47891
48643
  if (!confirmation.snapshot && await tryConfirmMirrorWriteback(applied, target.runProjectId)) {
47892
48644
  appendRunEvent({
47893
48645
  title: tx(
@@ -47933,6 +48685,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
47933
48685
  }
47934
48686
  });
47935
48687
  if (response?.ok) {
48688
+ await settleFullMirrorSync(target.runProjectId, syncedProject, mirrorFence, response);
47936
48689
  appendRunEvent({
47937
48690
  title: tx("Local Codex workspace refreshed. The next run will start from the latest Overleaf content.", "\u5DF2\u5237\u65B0\u672C\u5730 Codex workspace\uFF0C\u4E0B\u4E00\u8F6E\u4F1A\u4ECE\u6700\u65B0 Overleaf \u5185\u5BB9\u5F00\u59CB\u3002"),
47938
48691
  status: "completed"
@@ -48139,6 +48892,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
48139
48892
  getCurrentRunView
48140
48893
  } = deps);
48141
48894
  assetTransferBroker = deps.assetTransferBroker;
48895
+ mirrorFenceStore = deps.mirrorFenceStore;
48896
+ getAccountScopeId = deps.getAccountScopeId;
48142
48897
  stageWritebackRecovery = deps.stageWritebackRecovery;
48143
48898
  settleWritebackRecovery = deps.settleWritebackRecovery;
48144
48899
  onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
@@ -48148,6 +48903,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
48148
48903
  applySyncChangesToOverleaf,
48149
48904
  resolveCompileLogContext,
48150
48905
  invalidateMirrorAfterUndo,
48906
+ getMirrorReuseFence,
48907
+ refreshMirrorReuseFence,
48908
+ syncFencedMirror,
48151
48909
  getPendingMirrorRefresh: () => pendingMirrorRefresh
48152
48910
  };
48153
48911
  }
@@ -48831,6 +49589,10 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
48831
49589
  showPluginToast(tx("Nothing to refill: the original task text is unavailable.", "\u65E0\u6CD5\u56DE\u586B\uFF1A\u539F\u4EFB\u52A1\u6587\u672C\u4E0D\u53EF\u7528\u3002"));
48832
49590
  return;
48833
49591
  }
49592
+ if (input.value.trim() && input.value.trim() !== task.trim()) {
49593
+ showPluginToast(tx("Kept your draft. Clear the composer, then try again to refill this task.", "\u5DF2\u4FDD\u7559\u4F60\u7684\u8349\u7A3F\u3002\u6E05\u7A7A\u8F93\u5165\u6846\u540E\u518D\u8BD5\u4E00\u6B21\uFF0C\u5373\u53EF\u56DE\u586B\u8FD9\u6761\u4EFB\u52A1\u3002"));
49594
+ return;
49595
+ }
48834
49596
  prepareRetryReplacement?.(run || null, task);
48835
49597
  input.value = task;
48836
49598
  getState().task = task;
@@ -50195,9 +50957,21 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50195
50957
  input.dataset.styleFocus = "project-search";
50196
50958
  input.placeholder = tx("Search all Overleaf projects", "\u641C\u7D22\u5168\u90E8 Overleaf \u9879\u76EE");
50197
50959
  input.setAttribute("aria-label", input.placeholder);
50198
- input.addEventListener("input", () => {
50960
+ let composing = false;
50961
+ const updateResults = () => {
50199
50962
  query = input.value;
50200
- render();
50963
+ const results = input.closest(".ws2-picker")?.querySelector(".ws2-projects");
50964
+ if (results) renderProjectResults(results);
50965
+ };
50966
+ input.addEventListener("compositionstart", () => {
50967
+ composing = true;
50968
+ });
50969
+ input.addEventListener("compositionend", () => {
50970
+ composing = false;
50971
+ updateResults();
50972
+ });
50973
+ input.addEventListener("input", (event) => {
50974
+ if (!composing && !event.isComposing) updateResults();
50201
50975
  });
50202
50976
  search.append(input);
50203
50977
  panel.append(search);
@@ -50215,6 +50989,22 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50215
50989
  const list = node("div", "ws2-projects");
50216
50990
  list.setAttribute("role", "group");
50217
50991
  list.setAttribute("aria-label", tx("Overleaf projects", "Overleaf \u9879\u76EE"));
50992
+ renderProjectResults(list);
50993
+ panel.append(list);
50994
+ const footer = node("div", "ws2-picker-footer");
50995
+ footer.append(
50996
+ node("span", "", tx(sources.length + " of 3 references selected", "\u5DF2\u9009 " + sources.length + " / 3 \u4EFD\u8D44\u6599")),
50997
+ button(tx("Done", "\u5B8C\u6210"), () => {
50998
+ pickerOpen = false;
50999
+ render();
51000
+ }, "ws2-button ws2-button-primary", "picker-done")
51001
+ );
51002
+ panel.append(footer);
51003
+ return panel;
51004
+ }
51005
+ function renderProjectResults(list) {
51006
+ const oldScroll = list.scrollTop;
51007
+ list.replaceChildren();
50218
51008
  if (projectLoading) {
50219
51009
  const loading = node("div", "ws2-picker-state");
50220
51010
  loading.append(node("span", "ws2-pulse"), node("span", "", tx("Loading project directory\u2026", "\u6B63\u5728\u52A0\u8F7D\u9879\u76EE\u76EE\u5F55\u2026")));
@@ -50263,17 +51053,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50263
51053
  query ? tx("No projects match this search.", "\u6CA1\u6709\u5339\u914D\u7684\u9879\u76EE\u3002") : tx("No projects in this group.", "\u6B64\u5206\u7EC4\u6682\u65E0\u9879\u76EE\u3002")
50264
51054
  ));
50265
51055
  }
50266
- panel.append(list);
50267
- const footer = node("div", "ws2-picker-footer");
50268
- footer.append(
50269
- node("span", "", tx(sources.length + " of 3 references selected", "\u5DF2\u9009 " + sources.length + " / 3 \u4EFD\u8D44\u6599")),
50270
- button(tx("Done", "\u5B8C\u6210"), () => {
50271
- pickerOpen = false;
50272
- render();
50273
- }, "ws2-button ws2-button-primary", "picker-done")
50274
- );
50275
- panel.append(footer);
50276
- return panel;
51056
+ list.scrollTop = oldScroll;
50277
51057
  }
50278
51058
  function renderSources() {
50279
51059
  const section = node("section", "ws2-library");
@@ -50381,6 +51161,36 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50381
51161
  }
50382
51162
  return section;
50383
51163
  }
51164
+ function replaceAround(parent, fresh, retained, replacement) {
51165
+ for (const child of Array.from(parent.childNodes)) {
51166
+ if (child !== retained) child.remove();
51167
+ }
51168
+ let before = true;
51169
+ for (const child of Array.from(fresh.childNodes)) {
51170
+ if (child === replacement) before = false;
51171
+ else parent.insertBefore(child, before ? retained : null);
51172
+ }
51173
+ }
51174
+ function renderKeepingProjectSearch(content) {
51175
+ const library = host.querySelector(".ws2-library");
51176
+ const picker = library?.querySelector(".ws2-picker");
51177
+ const search = picker?.querySelector(".ws2-search");
51178
+ const nextLibrary = content.querySelector(".ws2-library");
51179
+ const nextPicker = nextLibrary?.querySelector(".ws2-picker");
51180
+ if (!search || !nextPicker) {
51181
+ host.replaceChildren(content);
51182
+ return false;
51183
+ }
51184
+ const input = search.querySelector("input");
51185
+ const nextSearch = nextPicker.querySelector(".ws2-search");
51186
+ input.placeholder = nextSearch.querySelector("input").placeholder;
51187
+ input.setAttribute("aria-label", input.placeholder);
51188
+ picker.setAttribute("aria-label", nextPicker.getAttribute("aria-label"));
51189
+ replaceAround(picker, nextPicker, search, nextSearch);
51190
+ replaceAround(library, nextLibrary, picker, nextPicker);
51191
+ replaceAround(host, content, library, nextLibrary);
51192
+ return document.activeElement === input;
51193
+ }
50384
51194
  function render() {
50385
51195
  if (!host?.isConnected) return;
50386
51196
  alignScope();
@@ -50389,7 +51199,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50389
51199
  const heading = host.closest("[data-set-group]")?.querySelector("[data-writing-style-heading]");
50390
51200
  if (heading) heading.textContent = tx("Writing style", "\u5199\u4F5C\u98CE\u683C");
50391
51201
  host.classList.add("ws2");
50392
- host.replaceChildren();
51202
+ const content = document.createDocumentFragment();
50393
51203
  const top = node("div", "ws2-heading");
50394
51204
  const copy = node("div", "ws2-heading-copy");
50395
51205
  const title = node("label", "ws2-title", tx("Write in My Style", "\u6CBF\u7528\u5199\u4F5C\u98CE\u683C"));
@@ -50409,7 +51219,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50409
51219
  void toggle(toggleInput.checked);
50410
51220
  });
50411
51221
  top.append(copy, toggleInput);
50412
- host.append(top);
51222
+ content.append(top);
50413
51223
  if (meta?.ready) {
50414
51224
  const summary = node("div", "ws2-profile");
50415
51225
  const mark = node("span", "ws2-profile-icon");
@@ -50432,7 +51242,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50432
51242
  );
50433
51243
  view.disabled = previewBusy;
50434
51244
  summary.append(mark, info, view);
50435
- host.append(summary);
51245
+ content.append(summary);
50436
51246
  }
50437
51247
  const editing = expanded || working();
50438
51248
  const manage = button(
@@ -50448,9 +51258,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50448
51258
  manage.setAttribute("aria-expanded", String(editing));
50449
51259
  manage.disabled = working();
50450
51260
  manage.append(icon("arrow"));
50451
- host.append(manage);
51261
+ content.append(manage);
50452
51262
  if (editing) {
50453
- host.append(renderSources());
51263
+ content.append(renderSources());
50454
51264
  if (working()) {
50455
51265
  const progress = node("div", "ws2-progress");
50456
51266
  progress.setAttribute("role", "status");
@@ -50472,7 +51282,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50472
51282
  }, "ws2-button", "cancel-generation");
50473
51283
  stop.disabled = cancelling;
50474
51284
  progress.append(stop);
50475
- host.append(progress);
51285
+ content.append(progress);
50476
51286
  } else {
50477
51287
  const footer = node("div", "ws2-actions");
50478
51288
  const help = node("div", "ws2-action-copy");
@@ -50497,8 +51307,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50497
51307
  );
50498
51308
  generateButton.disabled = !scope || !meta || !sources.length || deps.isRunning();
50499
51309
  footer.append(generateButton);
50500
- host.append(footer);
50501
- if (deps.isRunning()) host.append(node("p", "ws2-caption", tx("Available after the current task finishes.", "\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u53EF\u751F\u6210\u3002")));
51310
+ content.append(footer);
51311
+ if (deps.isRunning()) content.append(node("p", "ws2-caption", tx("Available after the current task finishes.", "\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u53EF\u751F\u6210\u3002")));
50502
51312
  }
50503
51313
  }
50504
51314
  if (message || !meta) {
@@ -50512,12 +51322,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
50512
51322
  void ensureLoaded(true).catch(() => {
50513
51323
  });
50514
51324
  }, "ws2-button", "retry-settings"));
50515
- host.append(status);
51325
+ content.append(status);
50516
51326
  }
50517
- if (preview) host.append(renderPreview());
51327
+ if (preview) content.append(renderPreview());
51328
+ const searchKeptFocus = renderKeepingProjectSearch(content);
50518
51329
  const scroll = host.querySelector(".ws2-projects");
50519
51330
  if (scroll) scroll.scrollTop = oldScroll;
50520
- restoreFocus(focus);
51331
+ if (!searchKeptFocus) restoreFocus(focus);
50521
51332
  }
50522
51333
  function mount(panel) {
50523
51334
  const next = panel?.querySelector("[data-writing-style-settings]");
@@ -51256,6 +52067,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
51256
52067
  getSkippedEntries
51257
52068
  });
51258
52069
  const writebackOrchestrator = Modules.WritebackOrchestrator.create({
52070
+ mirrorFenceStore: root.CodexOverleafModuleRegistry.resolve("ScopedPersistenceBrowserAdapter"),
52071
+ getAccountScopeId: () => cachedAccountScopeId,
51259
52072
  tr,
51260
52073
  tx,
51261
52074
  getLocale,
@@ -51405,24 +52218,20 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
51405
52218
  });
51406
52219
  let dashboardRefreshTimer = null;
51407
52220
  function refreshDashboardSoon() {
51408
- if (panel?.dataset.view !== "recent-projects") {
52221
+ if (panel?.dataset.view !== "recent-projects" || document.visibilityState !== "visible") {
51409
52222
  return;
51410
52223
  }
51411
52224
  clearTimeout(dashboardRefreshTimer);
51412
52225
  dashboardRefreshTimer = setTimeout(() => {
51413
- if (panel?.dataset.view !== "recent-projects") {
52226
+ if (panel?.dataset.view !== "recent-projects" || document.visibilityState !== "visible") {
51414
52227
  return;
51415
52228
  }
51416
- if (panel.querySelector(".recent-projects-sessions input")) {
52229
+ if (panel.querySelector(".recent-projects-session-rename-input")) {
51417
52230
  refreshDashboardSoon();
51418
52231
  return;
51419
52232
  }
51420
52233
  const expanded = Array.from(panel.querySelectorAll('[data-project-row-wrap][data-expanded="true"]')).map((wrap) => wrap.getAttribute("data-project-row-wrap")).filter(Boolean);
51421
- const rootEl = panel.querySelector("[data-recent-projects-root]");
51422
- recentProjects.renderRecentProjectsVariant({
51423
- restoreExpanded: expanded,
51424
- restoreScrollTop: rootEl ? rootEl.scrollTop : void 0
51425
- }).catch(() => {
52234
+ recentProjects.refreshDashboardView({ restoreExpanded: expanded }).catch(() => {
51426
52235
  });
51427
52236
  }, 400);
51428
52237
  }
@@ -51993,7 +52802,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
51993
52802
  primeCodexLocalSkillsCatalog().catch(() => {
51994
52803
  });
51995
52804
  }
51996
- PanelRenderer.setVisible(panel, true);
52805
+ PanelRenderer.setVisible(panelRendererInstance, true);
51997
52806
  }
51998
52807
  let codexOverleafSkillsCatalogPromise = null;
51999
52808
  async function primeCodexOverleafSkillsCatalog({ required = false } = {}) {
@@ -52645,9 +53454,11 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
52645
53454
  return;
52646
53455
  }
52647
53456
  let focusFiles = [...executionSnapshot.focusFiles];
53457
+ const mirrorOwner = currentRunView, mirrorRouteGeneration = spaRouteGeneration;
52648
53458
  const warmStart = await awaitRunStep(resolveWarmRunStart({ focusFiles, mode: submittedMode }));
53459
+ const mirrorFence = await awaitRunStep(writebackOrchestrator.refreshMirrorReuseFence(mirrorOwner.runProjectId, mirrorOwner.runAccountScopeId));
52649
53460
  let project = warmStart.project || null;
52650
- let useExistingMirror = warmStart.useExistingMirror;
53461
+ let useExistingMirror = warmStart.useExistingMirror && !mirrorFence;
52651
53462
  let fileOverlays = warmStart.fileOverlays || null;
52652
53463
  let otWarmStart = warmStart.otWarmStart === true;
52653
53464
  if (!useExistingMirror) {
@@ -52656,6 +53467,17 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
52656
53467
  status: "running"
52657
53468
  });
52658
53469
  project = await awaitRunStep(getRunProjectSnapshot());
53470
+ if (mirrorFence) {
53471
+ const assertCurrent = () => {
53472
+ throwIfRunCancellationRequested();
53473
+ if (mirrorOwner !== currentRunView || mirrorRouteGeneration !== spaRouteGeneration || mirrorOwner.runProjectId !== getCurrentProjectId() || mirrorOwner.runAccountScopeId !== cachedAccountScopeId) throw createRunCancellationError();
53474
+ };
53475
+ await awaitRunStep(writebackOrchestrator.syncFencedMirror(mirrorOwner.runProjectId, project, {
53476
+ fence: mirrorFence,
53477
+ assertCurrent,
53478
+ sendNative: (payload) => nativeCompatibilityController.sendBackgroundNative(payload, { assertCurrent })
53479
+ }));
53480
+ }
52659
53481
  }
52660
53482
  throwIfRunCancellationRequested();
52661
53483
  if (!useExistingMirror) {
@@ -52800,7 +53622,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
52800
53622
  skillInvocation: submittedSkillInvocation,
52801
53623
  submittedMode
52802
53624
  })));
52803
- if (!response.ok && response.error?.code === "mirror_stale" && useExistingMirror) {
53625
+ if (!response.ok && ["mirror_stale", "document_identity_mismatch"].includes(response.error?.code) && useExistingMirror) {
52804
53626
  appendRunEvent({ title: tr("warmMirrorStaleRetryTitle"), status: "running" });
52805
53627
  const staleRetry = await awaitRunStep(prepareMirrorStaleRetry({
52806
53628
  project: await awaitRunStep(getRunProjectSnapshot()),
@@ -53041,7 +53863,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53041
53863
  });
53042
53864
  runSettlementPersistence.applyToCurrentRun(syncOutcome.settlement);
53043
53865
  if (runCancellationRequested) {
53044
- throw createRunCancellationError();
53866
+ throw Object.assign(createRunCancellationError(), { writebackOutcome: syncOutcome });
53045
53867
  }
53046
53868
  await finalizeAuditRecord(runAuditDraft, {
53047
53869
  ...syncOutcome.audit || {},
@@ -53130,7 +53952,10 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53130
53952
  }),
53131
53953
  nextStep: tr("failureReason_reviewing_unavailable_cancelled_next")
53132
53954
  });
53133
- } else appendRunCancelledReport();
53955
+ } else {
53956
+ runSettlementPersistence.applyToCurrentRun(error.writebackOutcome?.settlement);
53957
+ appendRunCancelledReport(error.writebackOutcome);
53958
+ }
53134
53959
  await finishRunView(tx("Cancelled", "\u5DF2\u53D6\u6D88"), "cancelled");
53135
53960
  void finalizeAuditRecord(runAuditDraft, { resultStatus: "cancelled" });
53136
53961
  return;
@@ -53218,9 +54043,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53218
54043
  stopRunElapsedTick();
53219
54044
  await flushQueuedSaveState().catch(() => {
53220
54045
  });
54046
+ if (settlement.continueQueue) {
54047
+ settlement.continueQueue = await waitForCancelledNativeRun(settledView);
54048
+ }
53221
54049
  currentRunView = null;
53222
54050
  runCancellationRequested = false;
53223
54051
  runCancellationController = null;
54052
+ syncComposerSendAvailability();
53224
54053
  activeTurnControl.release(settlement.requestId);
53225
54054
  if (isExperimentalOtEnabled()) {
53226
54055
  try {
@@ -53640,7 +54469,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53640
54469
  if (state?.activeSessionId !== sessionId || !isProjectEditorRoute(window.location)) return false;
53641
54470
  if (sessionManager.isTransitioning()) {
53642
54471
  deferredSessionStart = { sessionId, scope: getComposerDraftScope() };
53643
- return false;
54472
+ return "session_transition";
53644
54473
  }
53645
54474
  return true;
53646
54475
  }
@@ -53680,17 +54509,19 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53680
54509
  title: tx("Cancelling the current Codex task.", "\u6B63\u5728\u4E2D\u65AD\u5F53\u524D Codex \u4EFB\u52A1\u3002"),
53681
54510
  status: "running"
53682
54511
  });
53683
- const activeRequestId = nativeChannel.getActiveRequestId();
54512
+ const activeRequestId = currentRunView.nativeRequestId;
53684
54513
  const projectKey = currentRunView?.runProjectId || getCurrentProjectId() || "";
53685
54514
  const cancelTargets = [];
53686
- if (activeRequestId || projectKey) {
54515
+ if (activeRequestId != null && activeRequestId !== "") {
53687
54516
  cancelTargets.push(sendBackgroundNative({
53688
54517
  method: "codex.cancel",
53689
54518
  params: {
53690
- requestId: activeRequestId || void 0,
54519
+ requestId: activeRequestId,
53691
54520
  projectKey: projectKey || void 0
53692
54521
  }
53693
54522
  }));
54523
+ } else {
54524
+ cancelTargets.push(Promise.resolve(null));
53694
54525
  }
53695
54526
  cancelTargets.push(callPageBridge("cancelActiveWrite", {}).catch(() => ({ ok: false })));
53696
54527
  Promise.allSettled(cancelTargets).then((results) => {
@@ -53761,6 +54592,22 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53761
54592
  Promise.resolve(value).then((result) => settle(resolve, result), (error) => settle(reject, error));
53762
54593
  });
53763
54594
  }
54595
+ async function waitForCancelledNativeRun(runView) {
54596
+ const pending = runView?.pendingNativeRun;
54597
+ if (!pending) return true;
54598
+ const CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS = 2e4;
54599
+ let timer;
54600
+ try {
54601
+ return await Promise.race([
54602
+ pending.then(() => true, () => true),
54603
+ new Promise((resolve) => {
54604
+ timer = setTimeout(() => resolve(false), CANCELLED_NATIVE_SETTLEMENT_TIMEOUT_MS);
54605
+ })
54606
+ ]);
54607
+ } finally {
54608
+ clearTimeout(timer);
54609
+ }
54610
+ }
53764
54611
  function isRunCancellationError(error = {}) {
53765
54612
  return error.code === "codex_cancelled" || /cancelled by the user|was cancelled/i.test(error.message || "");
53766
54613
  }
@@ -53894,21 +54741,25 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
53894
54741
  method: "codex.run",
53895
54742
  params
53896
54743
  }, compatibilityGate.compatibility));
53897
- if (currentRunView) {
53898
- currentRunView.nativeRequestId = tracked.id;
53899
- const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
54744
+ if (owner) {
54745
+ owner.nativeRequestId = tracked.id;
54746
+ owner.pendingNativeRun = tracked.promise;
54747
+ const record = findRunRecord(owner.recordId, owner.sessionId);
53900
54748
  if (record) {
53901
54749
  record.nativeRequestId = tracked.id;
53902
54750
  }
53903
54751
  activeTurnControl.bind({
53904
54752
  requestId: tracked.id,
53905
- projectKey: currentRunView.runProjectId,
53906
- clientRunId: currentRunView.recordId,
53907
- sessionId: currentRunView.sessionId
54753
+ projectKey: owner.runProjectId,
54754
+ clientRunId: owner.recordId,
54755
+ sessionId: owner.sessionId
53908
54756
  });
53909
54757
  saveStateSoon();
53910
54758
  }
53911
54759
  return tracked.promise.finally(() => {
54760
+ if (owner?.pendingNativeRun === tracked.promise) {
54761
+ owner.pendingNativeRun = null;
54762
+ }
53912
54763
  if (currentRunView?.nativeRequestId === tracked.id) {
53913
54764
  currentRunView.activeTurn = null;
53914
54765
  renderPendingInputs();
@@ -54046,8 +54897,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54046
54897
  tx
54047
54898
  }).run(options);
54048
54899
  }
54049
- function appendRunCancelledReport() {
54050
- const hasQueuedFollowUp = (getActiveSession(state)?.pendingInputs || []).some((item) => item?.status === "queued");
54900
+ function appendRunCancelledReport(writebackOutcome = {}, continueQueue = true) {
54901
+ const hasQueuedFollowUp = continueQueue && (getActiveSession(state)?.pendingInputs || []).some((item) => item?.status === "queued");
54051
54902
  appendRunEvent({
54052
54903
  title: tx("Current Codex task was cancelled.", "\u5DF2\u4E2D\u65AD\u5F53\u524D Codex \u4EFB\u52A1\u3002"),
54053
54904
  status: "info",
@@ -54057,8 +54908,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54057
54908
  conclusionKey: "cancelled",
54058
54909
  conclusion: tx("This run was cancelled. It did not continue syncing or writing to Overleaf.", "\u8FD9\u8F6E\u5DF2\u4E2D\u65AD\uFF0C\u6CA1\u6709\u7EE7\u7EED\u540C\u6B65\u6216\u5199\u5165 Overleaf\u3002"),
54059
54910
  status: "cancelled",
54060
- operations: [],
54061
- applyResults: [],
54911
+ operations: writebackOutcome.operations || [],
54912
+ applyResults: writebackOutcome.applied ? [writebackOutcome.applied] : [],
54913
+ saveVerification: writebackOutcome.saveVerification,
54062
54914
  nextStep: hasQueuedFollowUp ? tx("The first queued follow-up will start next.", "\u63A5\u4E0B\u6765\u4F1A\u81EA\u52A8\u542F\u52A8\u961F\u9996\u7684\u540E\u7EED\u4EFB\u52A1\u3002") : tx("Edit the task and run again.", "\u53EF\u4EE5\u4FEE\u6539\u4EFB\u52A1\u540E\u91CD\u65B0\u8FD0\u884C\u3002")
54063
54915
  });
54064
54916
  }
@@ -54077,11 +54929,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54077
54929
  }
54078
54930
  const hasInput = Boolean(String(panel?.querySelector("[data-task]")?.value || "").trim());
54079
54931
  const running = typeof runningOverride === "boolean" ? runningOverride : Boolean(currentRunView);
54932
+ const runningInActiveSession = running && (!currentRunView || currentRunView.sessionId === state?.activeSessionId);
54080
54933
  const transitioning = sessionManager.isTransitioning();
54081
- const action = running && (!hasInput || transitioning) ? "cancel" : "send";
54082
- const label = action === "cancel" ? tr("cancelRun") : running ? tr("queueNextInput") : tr("send");
54934
+ const action = runningInActiveSession && (!hasInput || transitioning) ? "cancel" : "send";
54935
+ const attachmentsLoading = composerAttachmentController.hasPendingReads();
54936
+ const label = action === "cancel" ? tr("cancelRun") : attachmentsLoading ? tr("attachmentsLoading") : running && hasInput ? tr("queueNextInput") : tr("send");
54083
54937
  runButton.dataset.action = action;
54084
- runButton.disabled = action === "cancel" ? runCancellationRequested : !hasInput || transitioning;
54938
+ runButton.disabled = action === "cancel" ? runCancellationRequested : !hasInput || transitioning || attachmentsLoading;
54085
54939
  runButton.title = label;
54086
54940
  runButton.setAttribute("aria-label", label);
54087
54941
  if (currentRunView?.writebackRetry && hasInput && action !== "cancel") runButton.disabled = true;
@@ -54091,6 +54945,10 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54091
54945
  if (event.key === "Enter" && !event.isComposing && event.keyCode !== 229) event.preventDefault();
54092
54946
  return;
54093
54947
  }
54948
+ if (composerAttachmentController.hasPendingReads() && event.key === "Enter" && !event.shiftKey && !event.isComposing && event.keyCode !== 229) {
54949
+ event.preventDefault();
54950
+ return;
54951
+ }
54094
54952
  if (handleSlashMenuKeydown(event)) {
54095
54953
  return;
54096
54954
  }
@@ -54140,7 +54998,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54140
54998
  if (!isCodexOverleafSkillEnabled(id)) {
54141
54999
  continue;
54142
55000
  }
54143
- const title = String(skill?.title || skill?.name || id).trim().slice(0, 80) || id;
55001
+ const title = id === "reviewer-response" ? tx("Reviewer response", "\u5BA1\u7A3F\u56DE\u590D") : String(skill?.title || skill?.name || id).trim().slice(0, 80) || id;
54144
55002
  commands.push({
54145
55003
  id: `skill:${id}`,
54146
55004
  kind: "codex-overleaf-skill",
@@ -54754,6 +55612,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
54754
55612
  setState: (next) => {
54755
55613
  state = next;
54756
55614
  },
55615
+ isDashboard: () => panel?.dataset.view === "recent-projects",
55616
+ refreshDashboard: refreshDashboardSoon,
54757
55617
  // An in-flight Accept/Undo keeps its view until it settles; refresh applies afterwards.
54758
55618
  isBusy: () => Boolean(pendingDraftSave || readLiveRunViewForSaveStateGuard() || trackedChangeInFlight.size || runReviewController?.hasActiveReview?.() || document.querySelector("dialog[open]")),
54759
55619
  hasLocalDraft: (current) => Boolean(current.task) && composerDraftEditedHere,
@@ -55593,8 +56453,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55593
56453
  )
55594
56454
  };
55595
56455
  }
55596
- async function getRunProjectSnapshot() {
56456
+ async function getRunProjectSnapshot(expectedProjectId = getCurrentProjectId()) {
55597
56457
  const project = await callPageBridge("getProjectSnapshot", {
56458
+ projectId: expectedProjectId,
55598
56459
  force: true,
55599
56460
  maxAgeMs: 0,
55600
56461
  preferLightweight: true,
@@ -55606,11 +56467,12 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55606
56467
  zipTimeoutMs: RUN_SNAPSHOT_ZIP_TIMEOUT_MS,
55607
56468
  focusFiles: getActiveFocusFiles()
55608
56469
  });
55609
- const sanitizedProject = sanitizeRunProjectSnapshot(project);
56470
+ const sanitizedProject = sanitizeRunProjectSnapshot(project, expectedProjectId);
55610
56471
  if (sanitizedProject?.capabilities?.fullProjectSnapshot || sanitizedProject?.capabilities?.pathIdentityComplete === false) {
55611
56472
  return sanitizedProject;
55612
56473
  }
55613
56474
  const pageStateProject = await callPageBridge("getProjectSnapshot", {
56475
+ projectId: expectedProjectId,
55614
56476
  force: true,
55615
56477
  maxAgeMs: 0,
55616
56478
  preferLightweight: true,
@@ -55620,16 +56482,22 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55620
56482
  includeBinaryFiles: false,
55621
56483
  focusFiles: getActiveFocusFiles()
55622
56484
  });
55623
- const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject);
56485
+ const sanitizedPageStateProject = sanitizeRunProjectSnapshot(pageStateProject, expectedProjectId);
55624
56486
  if (sanitizedPageStateProject?.capabilities?.fullProjectSnapshot) {
55625
56487
  return sanitizedPageStateProject;
55626
56488
  }
55627
56489
  return sanitizedProject;
55628
56490
  }
55629
- function sanitizeRunProjectSnapshot(project) {
56491
+ function sanitizeRunProjectSnapshot(project, expectedProjectId = project?.id) {
55630
56492
  if (!project || typeof project !== "object") return project;
56493
+ if (project.id !== expectedProjectId) return { ...project, files: [], capabilities: {
56494
+ ...project.capabilities,
56495
+ fullProjectSnapshot: false,
56496
+ skipped: [{ path: "", reason: "document_identity_mismatch" }]
56497
+ } };
55631
56498
  const normalize = Modules.ProjectFiles.normalizeSafeProjectPath;
55632
- const files = Array.isArray(project.files) ? project.files : [];
56499
+ const files = Array.isArray(project.files) ? project.files.map((file) => file?.documentIdentity === void 0 ? file : { ...file, documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) }) : [];
56500
+ project = { ...project, files };
55633
56501
  const counts = /* @__PURE__ */ new Map();
55634
56502
  for (const file of files) {
55635
56503
  const path = normalize(file?.path);
@@ -55669,12 +56537,16 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55669
56537
  };
55670
56538
  }
55671
56539
  async function getMirrorFreshness() {
56540
+ const projectId = getCurrentProjectId(), accountScopeId = cachedAccountScopeId;
56541
+ if (await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId)) return null;
56542
+ if (getCurrentProjectId() !== projectId || cachedAccountScopeId !== accountScopeId) return null;
55672
56543
  try {
55673
56544
  const response = await sendBackgroundNative({
55674
56545
  method: "mirror.status",
55675
- params: { projectId: getCurrentProjectId() }
56546
+ params: { projectId }
55676
56547
  });
55677
- if (response?.ok) {
56548
+ const fence = await writebackOrchestrator.refreshMirrorReuseFence(projectId, accountScopeId);
56549
+ if (response?.ok && !fence && getCurrentProjectId() === projectId && cachedAccountScopeId === accountScopeId) {
55678
56550
  return response.result;
55679
56551
  }
55680
56552
  } catch (error) {
@@ -55702,7 +56574,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55702
56574
  return { useExistingMirror: false, reason: "missing_focus_overlay" };
55703
56575
  }
55704
56576
  const mirrorStatus = await getMirrorFreshness();
55705
- const mirrorHealth2 = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus);
56577
+ const mirrorHealth2 = Modules.MirrorHealth.classifyMirrorHealth(mirrorStatus || {});
55706
56578
  if (!mirrorStatus?.exists || !mirrorHealth2.reusable) {
55707
56579
  return { useExistingMirror: false, reason: "mirror_not_fresh", mirrorStatus };
55708
56580
  }
@@ -55727,12 +56599,27 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55727
56599
  });
55728
56600
  const seen = /* @__PURE__ */ new Set();
55729
56601
  return candidates.filter(
55730
- (file) => typeof file.content === "string" && Modules.ProjectFiles.isUsableProjectFileContent(file.content) && file.path && !seen.has(file.path) && seen.add(file.path)
56602
+ (file) => typeof file.content === "string" && (Modules.ProjectFiles.isUsableProjectFileContent(file.content) || file.content === "") && file.path && !seen.has(file.path) && seen.add(file.path)
55731
56603
  ).map((file) => ({
55732
56604
  path: file.path,
55733
- content: file.content
56605
+ content: file.content,
56606
+ kind: "text",
56607
+ source: file.source,
56608
+ ...file.documentIdentity !== void 0 ? { documentIdentity: snapshotDocumentIdentity(file.documentIdentity, file.path, project.id) } : {}
55734
56609
  }));
55735
56610
  }
56611
+ function snapshotDocumentIdentity(identity, path, projectId) {
56612
+ if (!identity || identity.version !== 1 || identity.source !== "cm-ranges-v1" || identity.path !== path || identity.projectId !== projectId || typeof identity.documentId !== "string" || !/^[a-f0-9]{24}$/.test(identity.documentId) || typeof identity.targetDocumentId !== "string" || !/^[a-f0-9]{24}$/.test(identity.targetDocumentId) || typeof identity.contentSha256 !== "string" || !/^[a-f0-9]{64}$/.test(identity.contentSha256)) return null;
56613
+ return {
56614
+ version: 1,
56615
+ source: "cm-ranges-v1",
56616
+ projectId,
56617
+ path,
56618
+ documentId: identity.documentId,
56619
+ targetDocumentId: identity.targetDocumentId,
56620
+ contentSha256: identity.contentSha256
56621
+ };
56622
+ }
55736
56623
  function normalizeSnapshotPath(path) {
55737
56624
  return Modules.ProjectFiles.normalizeSafeProjectPath(path);
55738
56625
  }
@@ -55954,10 +56841,15 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
55954
56841
  if (!accountScopeId || isScopedPersistenceIdentityFailure(error)) {
55955
56842
  throw error;
55956
56843
  }
56844
+ const fallbackProjectId = getCurrentProjectId();
56845
+ const fallbackGeneration = spaRouteGeneration;
55957
56846
  const fallbackKey = buildScopedFallbackStorageKey(storageKey, accountScopeId);
55958
56847
  const stored = await chrome.storage.local.get([fallbackKey]);
55959
56848
  const fallback = stored[fallbackKey] || {};
55960
56849
  if (fallback?.__codexOverleafCompactFallback === true && fallback.__codexOverleafFallbackAccountScopeId === accountScopeId) {
56850
+ if (accountScopeId === cachedAccountScopeId && fallbackProjectId === getCurrentProjectId() && fallbackGeneration === spaRouteGeneration) {
56851
+ Modules.StorageMigration.adoptProjectPreferenceBaseline(fallback, accountScopeId, fallbackProjectId);
56852
+ }
55961
56853
  return {
55962
56854
  mode: fallback.mode || "",
55963
56855
  providerId: fallback.providerId || "builtin",
@@ -56024,7 +56916,20 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
56024
56916
  compactState.customInstructionsByProject = state.customInstructionsByProject;
56025
56917
  compactState.reviewingAvailabilityByProject = state.reviewingAvailabilityByProject;
56026
56918
  compactState.governanceRulesByProject = state.governanceRulesByProject;
56919
+ const projectPreferenceBaseline = Migration.getProjectPreferenceBaseline(
56920
+ options?.draftScope?.accountScopeId || cachedAccountScopeId,
56921
+ projectId
56922
+ );
56027
56923
  const stateSnapshot = typeof structuredClone === "function" ? structuredClone(state) : JSON.parse(JSON.stringify(state));
56924
+ const projectPreferenceWrite = Migration.captureProjectPreferenceWrite({
56925
+ state: stateSnapshot,
56926
+ baseline: projectPreferenceBaseline,
56927
+ accountScopeId: options?.draftScope?.accountScopeId || cachedAccountScopeId,
56928
+ projectId,
56929
+ normalizeExperimentalOtByProject,
56930
+ normalizeGovernanceRulesByProject,
56931
+ normalizeCustomInstructionsByProject
56932
+ });
56028
56933
  const consumedSession = liveRunView?.draftConsumption && liveRunView.runProjectId === projectId && liveRunView.runAccountScopeId === cachedAccountScopeId ? stateSnapshot.sessions?.find((session) => session.id === liveRunView.sessionId) : null;
56029
56934
  if (consumedSession?.task === "") consumedSession.draftConsumption = { ...liveRunView.draftConsumption };
56030
56935
  const compactStateSnapshot = typeof structuredClone === "function" ? structuredClone(compactState) : JSON.parse(JSON.stringify(compactState));
@@ -56044,6 +56949,27 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
56044
56949
  normalizeExperimentalOtByProject,
56045
56950
  normalizeGovernanceRulesByProject,
56046
56951
  normalizeCustomInstructionsByProject,
56952
+ projectPreferenceBaseline,
56953
+ projectPreferenceWrite,
56954
+ applyProjectPreferences: (prefs, captured, fields) => {
56955
+ if (retentionScope.accountScopeId !== cachedAccountScopeId || retentionScope.projectId !== getCurrentProjectId() || retentionScope.generation !== spaRouteGeneration) return false;
56956
+ const adopted = [];
56957
+ for (const field of fields) {
56958
+ if (JSON.stringify(state[field]?.[projectId]) !== JSON.stringify(captured[field]?.[projectId])) continue;
56959
+ if (JSON.stringify(state[field]?.[projectId]) === JSON.stringify(prefs[field]?.[projectId])) continue;
56960
+ state[field] = { ...state[field], ...prefs[field] };
56961
+ if (!Object.hasOwn(prefs[field] || {}, projectId)) delete state[field][projectId];
56962
+ adopted.push(field);
56963
+ }
56964
+ if (adopted.includes("customInstructionsByProject")) syncCustomInstructionsEditorForProject(projectId);
56965
+ if (adopted.includes("governanceRulesByProject")) syncProjectSettingsEditorForProject();
56966
+ if (adopted.includes("experimentalOtByProject")) syncExperimentalOtToggleForProject(projectId);
56967
+ if (adopted.includes("reviewingAvailabilityByProject")) {
56968
+ syncReviewingTooltip();
56969
+ syncProbeStatus();
56970
+ }
56971
+ return true;
56972
+ },
56047
56973
  persistenceContext
56048
56974
  });
56049
56975
  if (!options?.draftScope) {
@@ -56441,11 +57367,17 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
56441
57367
  if (!composerDraftPersistence) composerDraftPersistence = Modules.ComposerAttachmentDraft.createController({
56442
57368
  getScope: () => ({ accountScopeId: cachedAccountScopeId, projectId: getCurrentProjectId() }),
56443
57369
  getAttachments: () => composerAttachmentController.serializePersistableAttachments(),
57370
+ getOmittedAttachmentNames: () => composerAttachmentController.getOmittedAttachmentNames(),
56444
57371
  setAttachments: (value) => {
56445
57372
  state.composerAttachments = value;
57373
+ syncComposerSendAvailability();
56446
57374
  },
56447
57375
  clear: () => composerAttachmentController.clear({ notify: false }),
56448
57376
  restore: (value) => composerAttachmentController.restorePersistedAttachments(value),
57377
+ onRestoreOmittedAttachments: ({ count, names }) => appendPlainLog(tx(
57378
+ `${count} attachment${count === 1 ? "" : "s"} could not be saved in the draft. Reattach before sending: ${names.join(", ")}.`,
57379
+ `${count} \u4E2A\u9644\u4EF6\u672A\u80FD\u4FDD\u5B58\u5230\u8349\u7A3F\u4E2D\uFF0C\u8BF7\u5728\u53D1\u9001\u524D\u91CD\u65B0\u6DFB\u52A0\uFF1A${names.join("\u3001")}\u3002`
57380
+ )),
56449
57381
  read: (scope) => Modules.StorageDb.readComposerAttachmentDraft(scope),
56450
57382
  write: (scope, value) => Modules.StorageDb.writeComposerAttachmentDraft(scope, value),
56451
57383
  scheduleSave: () => saveStateSoon()
@@ -56534,7 +57466,9 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
56534
57466
  writingStyleSettings.sync();
56535
57467
  applyPanelTheme(getThemePreference());
56536
57468
  if ((state.providerId || "builtin") === "builtin" && !modelSelectHasOption(state.model)) {
56537
- renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model);
57469
+ renderModelOptions(getModelCatalog().FALLBACK_MODELS, state.model, {
57470
+ provisionalSelected: getModelDiscovery().provisional === true
57471
+ });
56538
57472
  }
56539
57473
  panel.querySelector("[data-model]").value = state.model;
56540
57474
  const renderedModels = getRenderedModelEntries();
@@ -57444,11 +58378,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
57444
58378
  if (!record || record.forkSnapshot || !owner || owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId || owner.sessionId !== session.id || owner.runId !== record.id || record.undoStatus === "applied" || record.trackedChangeStatus === "rejected") return;
57445
58379
  const originalStatus = record.status;
57446
58380
  const retryAutoRecompile = typeof record.executionSnapshot?.autoRecompile === "boolean" ? record.executionSnapshot.autoRecompile : void 0;
57447
- const live = () => findRunRecord(runId, session.id) || record;
58381
+ const generation = spaRouteGeneration;
58382
+ const isOwnerScope = () => generation === spaRouteGeneration && owner.projectId === getCurrentProjectId() && owner.accountScopeId === cachedAccountScopeId;
58383
+ const live = () => isOwnerScope() && findRunRecord(runId, session.id) || record;
57448
58384
  let finished = false;
58385
+ let retryOutcome = null;
57449
58386
  runCancellationRequested = false;
57450
58387
  runCancellationController = new AbortController();
57451
- const generation = spaRouteGeneration;
57452
58388
  const requestId = record.nativeRequestId || `writeback:${owner.sessionId}:${runId}`;
57453
58389
  const ownerToken = crypto.randomUUID();
57454
58390
  const retryView = {
@@ -57466,7 +58402,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
57466
58402
  if (runCancellationRequested || currentRunView !== retryView) {
57467
58403
  throw Object.assign(new Error("Save checking was cancelled."), { code: "codex_cancelled" });
57468
58404
  }
57469
- if (generation !== spaRouteGeneration || owner.projectId !== getCurrentProjectId() || owner.accountScopeId !== cachedAccountScopeId) {
58405
+ if (!isOwnerScope()) {
57470
58406
  throw Object.assign(new Error("The project or account changed."), { code: "aborted_project_changed" });
57471
58407
  }
57472
58408
  };
@@ -57492,6 +58428,10 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
57492
58428
  runProcess: root2?.querySelector("[data-run-process]"),
57493
58429
  processLabel: root2?.querySelector("[data-run-process-summary]")
57494
58430
  });
58431
+ const pendingMirror = getPendingMirrorRefresh?.();
58432
+ if (pendingMirror) await awaitRunStep(pendingMirror.catch(() => {
58433
+ }));
58434
+ assertOwner();
57495
58435
  if (intent?.requestId) {
57496
58436
  const receipt = await callPageBridge("getWritebackReceipt", { requestId: intent.requestId, runProjectId: owner.projectId });
57497
58437
  if (receipt?.state === "running") throw new Error(tx("The previous upload is still finishing. Try again shortly.", "\u4E0A\u4E00\u8F6E\u4E0A\u4F20\u4ECD\u5728\u6536\u5C3E\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"));
@@ -57510,6 +58450,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
57510
58450
  assistantMessage: tx("Retried the remaining file transfers for this task.", "\u5DF2\u91CD\u8BD5\u672C\u8F6E\u672A\u5B8C\u6210\u7684\u6587\u4EF6\u540C\u6B65\u3002")
57511
58451
  }
57512
58452
  );
58453
+ retryOutcome = result;
57513
58454
  assertOwner();
57514
58455
  if (result.settlement?.facts) live().settlement = WritebackSettlement.mergeRetrySettlement(live().settlement, result.settlement.facts);
57515
58456
  await finishRunView(
@@ -57533,6 +58474,17 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
57533
58474
  }
57534
58475
  await flushQueuedSaveState();
57535
58476
  } catch (error) {
58477
+ const outcome = error.writebackOutcome || retryOutcome;
58478
+ const current = findRunRecord(runId, session.id);
58479
+ if (outcome && (runCancellationRequested || isRunCancellationError(error)) && currentRunView === retryView && isOwnerScope() && current && !current.forkSnapshot && current.runProjectId === owner.projectId) {
58480
+ if (outcome.settlement?.facts) current.settlement = WritebackSettlement.mergeRetrySettlement(
58481
+ current.settlement,
58482
+ outcome.settlement.facts
58483
+ );
58484
+ appendRunCancelledReport(outcome, false);
58485
+ await finishRunView(tx("Cancelled", "\u5DF2\u53D6\u6D88"), "cancelled");
58486
+ finished = true;
58487
+ }
57536
58488
  showPluginToast(error.message || String(error));
57537
58489
  } finally {
57538
58490
  if (currentRunView !== retryView) {