codex-overleaf-link 2.3.3 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>Codex Overleaf Link</h1>
4
4
  <p><strong>Empower Overleaf with Codex.</strong></p>
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-2.3.3-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-2.3.4-blue" alt="version">
7
7
  <img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
8
8
  <img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
@@ -38,14 +38,14 @@ One command installs the managed native host **and** managed extension runtime.
38
38
  macOS / Linux:
39
39
 
40
40
  ```bash
41
- CODEX_OVERLEAF_REF=v2.3.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.3/install.sh)"
41
+ CODEX_OVERLEAF_REF=v2.3.4 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.4/install.sh)"
42
42
  ```
43
43
 
44
44
  Windows PowerShell:
45
45
 
46
46
  ```powershell
47
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.3/install.ps1 -OutFile install.ps1
48
- $env:CODEX_OVERLEAF_REF='v2.3.3'
47
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.4/install.ps1 -OutFile install.ps1
48
+ $env:CODEX_OVERLEAF_REF='v2.3.4'
49
49
  powershell -ExecutionPolicy Bypass -File install.ps1
50
50
  ```
51
51
 
@@ -56,7 +56,7 @@ Then, in the `chrome://extensions` tab the script opened: enable **Developer mod
56
56
  `npm exec` installs the same managed native host and extension runtime without keeping a source checkout. Use it if you prefer a pinned npm package.
57
57
 
58
58
  ```bash
59
- npm exec --yes codex-overleaf-link@2.3.3 -- install-managed
59
+ npm exec --yes codex-overleaf-link@2.3.4 -- install-managed
60
60
  ```
61
61
 
62
62
  Then, in `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the managed extension path printed by the command. The Release extension zip remains available for explicitly unmanaged/manual installations.
@@ -88,9 +88,9 @@ npm installs, updates, and uninstalls the coordinated managed extension/native p
88
88
 
89
89
  | Action | Command |
90
90
  |--------|---------|
91
- | Install / update | `npm exec --yes codex-overleaf-link@2.3.3 -- install-managed` |
92
- | Diagnose | `npm exec --yes codex-overleaf-link@2.3.3 -- doctor` |
93
- | Uninstall | `npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed` |
91
+ | Install / update | `npm exec --yes codex-overleaf-link@2.3.4 -- install-managed` |
92
+ | Diagnose | `npm exec --yes codex-overleaf-link@2.3.4 -- doctor` |
93
+ | Uninstall | `npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed` |
94
94
 
95
95
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
96
96
 
@@ -104,13 +104,13 @@ Managed installations discover signed stable updates automatically and apply the
104
104
 
105
105
  v2.2.0 introduces Bootstrap protocol 2. Existing v2.1.x managed installations must run the pinned `install-managed` command once, then reload the extension and Overleaf. Later protocol-2 releases use the in-product signed updater for ordinary runtime, style, vendor, and Native Host feature changes.
106
106
 
107
- The v2.3.3 GitHub Release contains:
107
+ The v2.3.4 GitHub Release contains:
108
108
 
109
- - `codex-overleaf-link-extension-v2.3.3.zip`: loadable Chrome extension package for manual unpacked installation.
110
- - `codex-overleaf-native-host-v2.3.3.tar.gz`: native host runtime files used by the installer and release verification.
111
- - `codex-overleaf-link-2.3.3.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
112
- - `install.sh`: release-pinned macOS / Linux installer that defaults to `v2.3.3` when run directly from the release artifact.
113
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v2.3.3` when run directly from the release artifact.
109
+ - `codex-overleaf-link-extension-v2.3.4.zip`: loadable Chrome extension package for manual unpacked installation.
110
+ - `codex-overleaf-native-host-v2.3.4.tar.gz`: native host runtime files used by the installer and release verification.
111
+ - `codex-overleaf-link-2.3.4.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
112
+ - `install.sh`: release-pinned macOS / Linux installer that defaults to `v2.3.4` when run directly from the release artifact.
113
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v2.3.4` when run directly from the release artifact.
114
114
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
115
115
  - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
116
116
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
@@ -121,7 +121,7 @@ The v2.3.3 GitHub Release contains:
121
121
  Remove the native host (use `--browser chromium` on Linux Chromium):
122
122
 
123
123
  ```bash
124
- npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed
124
+ npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed
125
125
  ```
126
126
 
127
127
  The same command works on Windows PowerShell. If you installed from a manual checkout or source installer, you can also run `npm run uninstall:native` inside the repo, use `node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs` on macOS / Linux, or use `node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs` on Windows PowerShell.
@@ -156,13 +156,13 @@ Then remove the extension from `chrome://extensions`. To delete local data: on m
156
156
  Linux Chromium install or update:
157
157
 
158
158
  ```bash
159
- CODEX_OVERLEAF_REF=v2.3.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.3/install.sh)" -- --browser chromium
159
+ CODEX_OVERLEAF_REF=v2.3.4 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v2.3.4/install.sh)" -- --browser chromium
160
160
  ```
161
161
 
162
162
  Linux Chromium uninstall:
163
163
 
164
164
  ```bash
165
- npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed --browser chromium
165
+ npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed --browser chromium
166
166
  ```
167
167
 
168
168
  ## Features
@@ -298,7 +298,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
298
298
  Re-run any [native host installer](#install), reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
299
299
 
300
300
  ```bash
301
- npm exec --yes codex-overleaf-link@2.3.3 -- install-managed
301
+ npm exec --yes codex-overleaf-link@2.3.4 -- install-managed
302
302
  ```
303
303
 
304
304
  **The Windows popup or panel shows a Bash recovery command**
@@ -347,8 +347,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
347
347
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
348
348
  | Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
349
349
  | Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
350
- | Installer/update command | `npm exec --yes codex-overleaf-link@2.3.3 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- install-managed --browser chromium` |
351
- | Uninstall command | `npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-managed --browser chromium` |
350
+ | Installer/update command | `npm exec --yes codex-overleaf-link@2.3.4 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- install-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- install-managed --browser chromium` |
351
+ | Uninstall command | `npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed` | `npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-managed --browser chromium` |
352
352
  | Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
353
353
  | Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
354
354
  | Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
@@ -384,7 +384,7 @@ Contributions are welcome. Please open an issue before submitting large changes
384
384
  The recommended v1.9 installation owns one stable unpacked-extension directory and a versioned native-host directory:
385
385
 
386
386
  ```bash
387
- npm exec --yes codex-overleaf-link@2.3.3 -- install-managed
387
+ npm exec --yes codex-overleaf-link@2.3.4 -- install-managed
388
388
  ```
389
389
 
390
390
  Load the printed extension path once from `chrome://extensions`. Starting with the next signed stable release, Codex Overleaf Link checks GitHub Releases automatically, downloads and verifies the coordinated extension/native-host bundle, waits until every connected Overleaf tab is saved and idle, and then updates both components together. A failed health check restores the previous version automatically.
@@ -394,11 +394,11 @@ The stable updater ignores drafts and prereleases, performs no telemetry, and ca
394
394
  The legacy native-only installer remains available for source or unmanaged extension directories:
395
395
 
396
396
  ```bash
397
- npm exec --yes codex-overleaf-link@2.3.3 -- install-native
397
+ npm exec --yes codex-overleaf-link@2.3.4 -- install-native
398
398
  ```
399
399
 
400
400
  Legacy native-only installations can be removed with:
401
401
 
402
402
  ```bash
403
- npm exec --yes codex-overleaf-link@2.3.3 -- uninstall-native
403
+ npm exec --yes codex-overleaf-link@2.3.4 -- uninstall-native
404
404
  ```
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Codex Overleaf Link",
4
- "version": "2.3.3",
4
+ "version": "2.3.4",
5
5
  "description": "Connect the active Overleaf project to a local Codex bridge.",
6
6
  "icons": {
7
7
  "16": "assets/icons/icon16.png",
@@ -383,7 +383,8 @@
383
383
  getState: () => state,
384
384
  setState: next => { state = next; },
385
385
  getCurrentRunView: () => currentRunView,
386
- getSettingsPanelInstance: () => settingsPanelInstance
386
+ getSettingsPanelInstance: () => settingsPanelInstance,
387
+ prepareRetryReplacement: run => prepareRetryReplacement(run)
387
388
  });
388
389
  const {
389
390
  showUndoFileSelection,
@@ -840,6 +841,7 @@
840
841
  let state = null;
841
842
  let storageKey = LEGACY_STORAGE_KEY;
842
843
  let currentRunView = null;
844
+ let pendingRetryReplacement = null;
843
845
  let runQueueScheduler = null;
844
846
  let runInputCoordinator = null;
845
847
  const activeTurnControl = ActiveTurnControl.create({ chrome });
@@ -936,6 +938,13 @@
936
938
  sendResponse?.(getPanelStateResponse());
937
939
  return;
938
940
  }
941
+ if (message?.type === 'codex-overleaf/toggle-launcher') {
942
+ const visible = !PanelRenderer.isLauncherVisible(panelRendererInstance);
943
+ PanelRenderer.setLauncherVisible(panelRendererInstance, visible, { persist: true });
944
+ if (!visible) closePanel();
945
+ sendResponse?.(getPanelStateResponse());
946
+ return;
947
+ }
939
948
  if (message?.type === 'codex-overleaf/toggle-panel') {
940
949
  if (isPanelOpen()) {
941
950
  closePanel();
@@ -1186,7 +1195,12 @@
1186
1195
  maxWidth: PANEL_MAX_WIDTH,
1187
1196
  pageMinWidth: PAGE_MIN_WIDTH,
1188
1197
  initialWidth: state?.panelWidth || PANEL_DEFAULT_WIDTH,
1198
+ chromeApi: chrome,
1199
+ launcherLabel: tx('Show or hide Codex panel', '显示或隐藏 Codex 边栏'),
1189
1200
  callbacks: {
1201
+ onLauncherToggle: () => ensurePanelOpen(),
1202
+ onClosePanel: () => closePanel(),
1203
+ onLauncherVisibilityChange: visible => { if (!visible) closePanel(); },
1190
1204
  onRefresh: () => refreshProbe({ userInitiated: true }),
1191
1205
  onNewSession: () => startNewSession(),
1192
1206
  onChangeHistory: () => openChangeHistory(),
@@ -1364,7 +1378,8 @@
1364
1378
  function getPanelStateResponse() {
1365
1379
  return {
1366
1380
  ok: true,
1367
- open: isPanelOpen()
1381
+ open: isPanelOpen(),
1382
+ launcherVisible: PanelRenderer.isLauncherVisible(panelRendererInstance)
1368
1383
  };
1369
1384
  }
1370
1385
 
@@ -1789,6 +1804,12 @@
1789
1804
  // Freeze submitted run identity before the panel can change during the run.
1790
1805
  const submittedMode = executionSnapshot.mode;
1791
1806
  const submittedRequireReviewing = executionSnapshot.requireReviewing === true;
1807
+ const replaceRunId = queuedInput ? '' : runController.resolveRetryReplacement({
1808
+ candidate: pendingRetryReplacement,
1809
+ activeSessionId: state.activeSessionId,
1810
+ runs: state.runs,
1811
+ projectRunSettlement: run => WritebackSettlement.projectRunSettlement(run)
1812
+ });
1792
1813
 
1793
1814
  runCancellationRequested = false;
1794
1815
  runCancellationController = new AbortController();
@@ -1802,8 +1823,10 @@
1802
1823
  executionSnapshot,
1803
1824
  attachments: submittedAttachments,
1804
1825
  skillInvocation: submittedSkillInvocation,
1805
- queueItemId: queuedInput?.id || ''
1826
+ queueItemId: queuedInput?.id || '',
1827
+ replaceRunId
1806
1828
  });
1829
+ if (!queuedInput) pendingRetryReplacement = null;
1807
1830
  const runSessionId = currentRunView.sessionId;
1808
1831
  let runAuditDraft = null;
1809
1832
  try {
@@ -6127,6 +6150,17 @@
6127
6150
  renderPendingInputs();
6128
6151
  }
6129
6152
 
6153
+ function prepareRetryReplacement(run) {
6154
+ const sessionId = state?.activeSessionId || '';
6155
+ const source = findRunRecord(run?.id, sessionId);
6156
+ const eligible = source && runController.isRetryReplacementEligible(
6157
+ source,
6158
+ WritebackSettlement.projectRunSettlement(source)
6159
+ );
6160
+ pendingRetryReplacement = eligible ? { sessionId, runId: source.id } : null;
6161
+ return Boolean(eligible);
6162
+ }
6163
+
6130
6164
  function startRunView({
6131
6165
  task,
6132
6166
  mode,
@@ -6134,6 +6168,7 @@
6134
6168
  reasoningEffort,
6135
6169
  speedTier,
6136
6170
  queueItemId = '',
6171
+ replaceRunId = '',
6137
6172
  executionSnapshot = null
6138
6173
  }) {
6139
6174
  let attachments = [];
@@ -6185,13 +6220,14 @@
6185
6220
  }
6186
6221
  : {};
6187
6222
  state = updateActiveSession(state, {
6188
- runs: [...(state.runs || []), record].slice(-20),
6223
+ runs: runController.replaceRunForRetry(state.runs, replaceRunId, record),
6189
6224
  ...titlePatch
6190
6225
  });
6191
- saveStateSoon();
6226
+ saveStateSoon(replaceRunId ? 0 : 120);
6192
6227
 
6193
6228
  const log = panel.querySelector('[data-log]');
6194
6229
  removeEmptyRunsMessage(log);
6230
+ if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
6195
6231
  const root = renderRunCard(record);
6196
6232
  log.append(root);
6197
6233
  scrollLogToBottom({ force: true });
@@ -11368,7 +11368,7 @@ ${block}`;
11368
11368
  const MIN_NATIVE_VERSION = "1.0.0";
11369
11369
  const MIN_COMPATIBLE_NATIVE_VERSION = "1.0.0";
11370
11370
  const MIN_COMPATIBLE_EXTENSION_VERSION = "1.0.0";
11371
- const BUILD_TARGET_VERSION = "2.3.3";
11371
+ const BUILD_TARGET_VERSION = "2.3.4";
11372
11372
  const DEFAULT_CHROME_EXTENSION_ID = "illdpneeeopfffmiepaejglgmhpmdhdc";
11373
11373
  const REQUIRED_CAPABILITIES = Object.freeze([
11374
11374
  "bridgePing",
@@ -16058,11 +16058,39 @@ ${block}`;
16058
16058
  function defaultIsUsableProjectFileContent(content) {
16059
16059
  return String(content || "").trim().length > 0;
16060
16060
  }
16061
+ function isRetryReplacementEligible(run = {}, projection = {}) {
16062
+ if (!run.id || !["failed", "interrupted"].includes(run.status)) return false;
16063
+ if (run.trackedChangeStatus || run.undoStatus) return false;
16064
+ if (["undoOperations", "undoTrackedChanges", "undoExpectedFiles", "undoBaseFiles", "appliedOperations"].some((field) => Array.isArray(run[field]) && run[field].length)) return false;
16065
+ if (projection.canAccept || projection.canUndo || projection.changedDocument === true) return false;
16066
+ if (projection.failureTerminalState === "needs_review" || projection.terminalState === "needs_review") return false;
16067
+ return projection.changedDocument === false || run.mode === "ask" || hasZeroWriteReport(run.events);
16068
+ }
16069
+ function hasZeroWriteReport(events = []) {
16070
+ return (Array.isArray(events) ? events : []).some((event) => {
16071
+ const rows = Array.isArray(event?.detailStructured?.meta) ? event.detailStructured.meta : [];
16072
+ return rows.some((row) => row?.key === "writeResult" && /(?:wrote|写入)\s*0\b[\s\S]*(?:skipped|跳过)\s*0\b/i.test(String(row.value || "")));
16073
+ });
16074
+ }
16075
+ function resolveRetryReplacement({ candidate, activeSessionId, runs, projectRunSettlement } = {}) {
16076
+ if (!candidate?.runId || candidate.sessionId !== activeSessionId) return "";
16077
+ const source = (Array.isArray(runs) ? runs : []).find((run) => run?.id === candidate.runId);
16078
+ if (!source) return "";
16079
+ const projection = typeof projectRunSettlement === "function" ? projectRunSettlement(source) : {};
16080
+ return isRetryReplacementEligible(source, projection) ? source.id : "";
16081
+ }
16082
+ function replaceRunForRetry(runs, sourceRunId, nextRun, maxRuns = 20) {
16083
+ const retained = (Array.isArray(runs) ? runs : []).filter((run) => !sourceRunId || run?.id !== sourceRunId);
16084
+ return [...retained, nextRun].slice(-Math.max(1, maxRuns));
16085
+ }
16061
16086
  return {
16062
16087
  buildCodexRunParams,
16063
16088
  canUseFocusedPartialSnapshot,
16064
16089
  shouldRestrictWritebackToFocus,
16065
16090
  buildSessionHistoryResult,
16091
+ isRetryReplacementEligible,
16092
+ replaceRunForRetry,
16093
+ resolveRetryReplacement,
16066
16094
  truncateSessionHistoryText,
16067
16095
  normalizeComposerAttachments,
16068
16096
  normalizeSkillInvocation
@@ -16772,10 +16800,13 @@ ${block}`;
16772
16800
  const ROOT_CLASS = "codex-overleaf-panel-mounted";
16773
16801
  const RESIZE_CLASS = "codex-overleaf-panel-resizing";
16774
16802
  const COMPACT_CLASS = "codex-overleaf-panel-compact";
16803
+ const LAUNCHER_VISIBILITY_STORAGE_KEY = "codex-overleaf:launcher-visible";
16775
16804
  function codexIcon(name) {
16776
16805
  const icons = {
16777
16806
  refresh: '<path d="M4.2 5.3A5 5 0 1 1 3.6 10"/><path d="M4.2 2.2v3.1H1.1"/>',
16778
16807
  plus: '<path d="M8 3.2v9.6"/><path d="M3.2 8h9.6"/>',
16808
+ panel: '<rect x="2.3" y="2.3" width="11.4" height="11.4" rx="2"/><path d="M9.2 2.5v11"/><path d="M5.1 6.1h1.5M5.1 8h1.5M5.1 9.9h1.5"/>',
16809
+ collapse: '<path d="m6.1 3.3 4.7 4.7-4.7 4.7"/>',
16779
16810
  settings: '<path d="M3.4 5.2h5.9"/><path d="M11.7 5.2h.9"/><path d="M10.1 3.6v3.2"/><path d="M3.4 10.8h1"/><path d="M6.8 10.8h5.8"/><path d="M5.9 9.2v3.2"/>'
16780
16811
  };
16781
16812
  const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : "settings";
@@ -16785,9 +16816,11 @@ ${block}`;
16785
16816
  const doc = options2.document || document;
16786
16817
  const container = options2.container || doc.documentElement;
16787
16818
  const panelEl = doc.createElement("aside");
16819
+ const launcherEl = doc.createElement("button");
16788
16820
  const callbacks = options2.callbacks || {};
16789
16821
  const instance = {
16790
16822
  panelEl,
16823
+ launcherEl,
16791
16824
  callbacks,
16792
16825
  defaultWidth: options2.defaultWidth || 380,
16793
16826
  minWidth: options2.minWidth || 340,
@@ -16796,8 +16829,16 @@ ${block}`;
16796
16829
  lastDesktopWidth: options2.initialWidth || options2.defaultWidth || 380,
16797
16830
  listeners: [],
16798
16831
  document: doc,
16799
- window: options2.window || window
16832
+ window: options2.window || window,
16833
+ chromeApi: options2.chromeApi || (typeof chrome !== "undefined" ? chrome : null),
16834
+ storageChangeListener: null
16800
16835
  };
16836
+ launcherEl.id = "codex-overleaf-launcher";
16837
+ launcherEl.type = "button";
16838
+ launcherEl.title = options2.launcherLabel || "Show or hide Codex panel";
16839
+ launcherEl.setAttribute("aria-label", launcherEl.title);
16840
+ launcherEl.setAttribute("aria-expanded", "false");
16841
+ launcherEl.innerHTML = codexIcon("panel");
16801
16842
  panelEl.id = options2.panelId || "codex-overleaf-panel";
16802
16843
  panelEl.dataset.view = "session";
16803
16844
  panelEl.innerHTML = `
@@ -16809,6 +16850,7 @@ ${block}`;
16809
16850
  <div data-diagnostics-slot></div>
16810
16851
  <button type="button" data-new-session title="New Session" aria-label="New Session">${codexIcon("plus")}</button>
16811
16852
  <button type="button" data-custom-instructions-settings title="Project Settings" aria-label="Project Settings" aria-expanded="false">${codexIcon("settings")}</button>
16853
+ <button type="button" data-close-panel title="Close Codex panel" aria-label="Close Codex panel">${codexIcon("collapse")}</button>
16812
16854
  </div>
16813
16855
  </div>
16814
16856
  <div data-settings-slot></div>
@@ -16823,6 +16865,7 @@ ${block}`;
16823
16865
  <div data-composer-slot></div>
16824
16866
  `;
16825
16867
  container.append(panelEl);
16868
+ container.append(launcherEl);
16826
16869
  doc.documentElement.classList.add(ROOT_CLASS);
16827
16870
  instance.headerEl = panelEl.querySelector("[data-panel-header]");
16828
16871
  instance.bodyEl = panelEl.querySelector("[data-main]");
@@ -16835,6 +16878,8 @@ ${block}`;
16835
16878
  bind(instance, panelEl.querySelector("[data-refresh]"), "click", () => callbacks.onRefresh?.());
16836
16879
  bind(instance, panelEl.querySelector("[data-new-session]"), "click", () => callbacks.onNewSession?.());
16837
16880
  bind(instance, panelEl.querySelector("[data-custom-instructions-settings]"), "click", () => callbacks.onSettingsClick?.());
16881
+ bind(instance, panelEl.querySelector("[data-close-panel]"), "click", () => callbacks.onClosePanel?.());
16882
+ bind(instance, launcherEl, "click", () => callbacks.onLauncherToggle?.());
16838
16883
  bind(instance, panelEl.querySelector("[data-panel-resize-handle]"), "pointerdown", (event) => startResize(instance, event));
16839
16884
  bind(instance, panelEl.querySelector("[data-panel-resize-handle]"), "dblclick", (event) => {
16840
16885
  event?.preventDefault?.();
@@ -16848,8 +16893,10 @@ ${block}`;
16848
16893
  setWidth(instance, instance.lastDesktopWidth || instance.defaultWidth, { notify: true, persist: false });
16849
16894
  });
16850
16895
  setWidth(instance, options2.initialWidth || instance.defaultWidth, { notify: false });
16896
+ initializeLauncherVisibility(instance);
16851
16897
  return {
16852
16898
  panelEl,
16899
+ launcherEl,
16853
16900
  headerEl: instance.headerEl,
16854
16901
  bodyEl: instance.bodyEl,
16855
16902
  diagnosticsSlot: instance.diagnosticsSlot,
@@ -16944,10 +16991,58 @@ ${block}`;
16944
16991
  function setVisible(panelEl, visible) {
16945
16992
  panelEl?.classList?.toggle("is-open", Boolean(visible));
16946
16993
  document.documentElement.classList.toggle(ROOT_CLASS, Boolean(visible));
16994
+ const launcherEl = panelEl?.ownerDocument?.getElementById?.("codex-overleaf-launcher");
16995
+ launcherEl?.classList?.toggle("is-panel-open", Boolean(visible));
16996
+ launcherEl?.setAttribute?.("aria-expanded", visible ? "true" : "false");
16997
+ if (launcherEl) {
16998
+ launcherEl.hidden = Boolean(visible) || launcherEl.dataset.enabled === "false";
16999
+ launcherEl.setAttribute("aria-hidden", launcherEl.hidden ? "true" : "false");
17000
+ }
16947
17001
  if (!visible) {
16948
17002
  document.documentElement.classList.remove(RESIZE_CLASS, COMPACT_CLASS);
16949
17003
  }
16950
17004
  }
17005
+ function initializeLauncherVisibility(instance) {
17006
+ const storage = instance.chromeApi?.storage;
17007
+ if (!storage?.local?.get) {
17008
+ setLauncherVisible(instance, true, { persist: false });
17009
+ return;
17010
+ }
17011
+ Promise.resolve(storage.local.get({ [LAUNCHER_VISIBILITY_STORAGE_KEY]: true })).then((values) => applyLauncherVisibility(instance, values?.[LAUNCHER_VISIBILITY_STORAGE_KEY])).catch(() => applyLauncherVisibility(instance, true));
17012
+ if (storage.onChanged?.addListener) {
17013
+ instance.storageChangeListener = (changes, areaName) => {
17014
+ if (areaName === "local" && changes?.[LAUNCHER_VISIBILITY_STORAGE_KEY]) {
17015
+ applyLauncherVisibility(instance, changes[LAUNCHER_VISIBILITY_STORAGE_KEY].newValue);
17016
+ }
17017
+ };
17018
+ storage.onChanged.addListener(instance.storageChangeListener);
17019
+ }
17020
+ }
17021
+ function applyLauncherVisibility(instance, value) {
17022
+ const visible = value !== false;
17023
+ setLauncherVisible(instance, visible, { persist: false });
17024
+ instance.callbacks.onLauncherVisibilityChange?.(visible);
17025
+ }
17026
+ function setLauncherVisible(target, visible, options2 = {}) {
17027
+ const instance = target?._instance || target;
17028
+ const launcherEl = instance?.launcherEl;
17029
+ if (!launcherEl) return false;
17030
+ const nextVisible = Boolean(visible);
17031
+ launcherEl.dataset.enabled = nextVisible ? "true" : "false";
17032
+ launcherEl.hidden = !nextVisible || instance.panelEl?.classList?.contains("is-open") === true;
17033
+ launcherEl.setAttribute("aria-hidden", launcherEl.hidden ? "true" : "false");
17034
+ if (options2.persist === true) {
17035
+ Promise.resolve(instance.chromeApi?.storage?.local?.set?.({
17036
+ [LAUNCHER_VISIBILITY_STORAGE_KEY]: nextVisible
17037
+ })).catch(() => {
17038
+ });
17039
+ }
17040
+ return nextVisible;
17041
+ }
17042
+ function isLauncherVisible(target) {
17043
+ const instance = target?._instance || target;
17044
+ return Boolean(instance?.launcherEl && instance.launcherEl.dataset.enabled !== "false");
17045
+ }
16951
17046
  function setBadge(headerEl, badge = {}) {
16952
17047
  const actions = headerEl?.querySelector?.(".codex-vscode-head-actions");
16953
17048
  if (!actions) {
@@ -17075,12 +17170,18 @@ ${block}`;
17075
17170
  target.removeEventListener?.(type, listener, options2);
17076
17171
  }
17077
17172
  instance.panelEl?.remove?.();
17173
+ instance.launcherEl?.remove?.();
17174
+ if (instance.storageChangeListener) {
17175
+ instance.chromeApi?.storage?.onChanged?.removeListener?.(instance.storageChangeListener);
17176
+ }
17078
17177
  instance.document.documentElement.classList.remove(ROOT_CLASS, RESIZE_CLASS, COMPACT_CLASS);
17079
17178
  }
17080
17179
  window.CodexOverleafPanelRenderer = {
17081
17180
  create,
17082
17181
  createConfirmController,
17083
17182
  setVisible,
17183
+ setLauncherVisible,
17184
+ isLauncherVisible,
17084
17185
  setBadge,
17085
17186
  setWidth
17086
17187
  };
@@ -31470,7 +31571,7 @@ ${conclusion}` : `${label} ${conclusion}`;
31470
31571
  if (!efforts.length) {
31471
31572
  efforts = presentation === "none" ? ["none"] : ["low", "medium", "high", "xhigh"];
31472
31573
  }
31473
- const current = reasoningSelect.value || getState()?.reasoningEffort || "";
31574
+ const current = getState()?.reasoningEffort || reasoningSelect.value || "";
31474
31575
  const fallback = efforts.includes(model?.defaultReasoningEffort) ? model.defaultReasoningEffort : efforts.includes("high") ? "high" : efforts[0];
31475
31576
  reasoningSelect.textContent = "";
31476
31577
  for (const effort of efforts) {
@@ -34456,6 +34557,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
34456
34557
  setState,
34457
34558
  getCurrentRunView,
34458
34559
  getSettingsPanelInstance,
34560
+ prepareRetryReplacement,
34459
34561
  StorageDb
34460
34562
  } = deps;
34461
34563
  let auditHistoryRecords = [];
@@ -34608,6 +34710,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
34608
34710
  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"));
34609
34711
  return;
34610
34712
  }
34713
+ prepareRetryReplacement?.(run || null);
34611
34714
  input.value = task;
34612
34715
  getState().task = task;
34613
34716
  autosizeTaskTextarea();
@@ -35618,7 +35721,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
35618
35721
  state = next;
35619
35722
  },
35620
35723
  getCurrentRunView: () => currentRunView,
35621
- getSettingsPanelInstance: () => settingsPanelInstance
35724
+ getSettingsPanelInstance: () => settingsPanelInstance,
35725
+ prepareRetryReplacement: (run) => prepareRetryReplacement(run)
35622
35726
  });
35623
35727
  const {
35624
35728
  showUndoFileSelection,
@@ -36060,6 +36164,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36060
36164
  let state = null;
36061
36165
  let storageKey = LEGACY_STORAGE_KEY;
36062
36166
  let currentRunView = null;
36167
+ let pendingRetryReplacement = null;
36063
36168
  let runQueueScheduler = null;
36064
36169
  let runInputCoordinator = null;
36065
36170
  const activeTurnControl = ActiveTurnControl.create({ chrome });
@@ -36150,6 +36255,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36150
36255
  sendResponse?.(getPanelStateResponse());
36151
36256
  return;
36152
36257
  }
36258
+ if (message?.type === "codex-overleaf/toggle-launcher") {
36259
+ const visible = !PanelRenderer.isLauncherVisible(panelRendererInstance);
36260
+ PanelRenderer.setLauncherVisible(panelRendererInstance, visible, { persist: true });
36261
+ if (!visible) closePanel();
36262
+ sendResponse?.(getPanelStateResponse());
36263
+ return;
36264
+ }
36153
36265
  if (message?.type === "codex-overleaf/toggle-panel") {
36154
36266
  if (isPanelOpen()) {
36155
36267
  closePanel();
@@ -36363,7 +36475,14 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36363
36475
  maxWidth: PANEL_MAX_WIDTH,
36364
36476
  pageMinWidth: PAGE_MIN_WIDTH,
36365
36477
  initialWidth: state?.panelWidth || PANEL_DEFAULT_WIDTH,
36478
+ chromeApi: chrome,
36479
+ launcherLabel: tx("Show or hide Codex panel", "\u663E\u793A\u6216\u9690\u85CF Codex \u8FB9\u680F"),
36366
36480
  callbacks: {
36481
+ onLauncherToggle: () => ensurePanelOpen(),
36482
+ onClosePanel: () => closePanel(),
36483
+ onLauncherVisibilityChange: (visible) => {
36484
+ if (!visible) closePanel();
36485
+ },
36367
36486
  onRefresh: () => refreshProbe({ userInitiated: true }),
36368
36487
  onNewSession: () => startNewSession(),
36369
36488
  onChangeHistory: () => openChangeHistory(),
@@ -36527,7 +36646,8 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36527
36646
  function getPanelStateResponse() {
36528
36647
  return {
36529
36648
  ok: true,
36530
- open: isPanelOpen()
36649
+ open: isPanelOpen(),
36650
+ launcherVisible: PanelRenderer.isLauncherVisible(panelRendererInstance)
36531
36651
  };
36532
36652
  }
36533
36653
  function toggleDiagnosticsMenu() {
@@ -36887,6 +37007,12 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36887
37007
  ) : captureCurrentExecutionSnapshot(submittedPanelState);
36888
37008
  const submittedMode = executionSnapshot.mode;
36889
37009
  const submittedRequireReviewing = executionSnapshot.requireReviewing === true;
37010
+ const replaceRunId = queuedInput ? "" : runController.resolveRetryReplacement({
37011
+ candidate: pendingRetryReplacement,
37012
+ activeSessionId: state.activeSessionId,
37013
+ runs: state.runs,
37014
+ projectRunSettlement: (run) => WritebackSettlement.projectRunSettlement(run)
37015
+ });
36890
37016
  runCancellationRequested = false;
36891
37017
  runCancellationController = new AbortController();
36892
37018
  setRunning(true);
@@ -36899,8 +37025,10 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
36899
37025
  executionSnapshot,
36900
37026
  attachments: submittedAttachments,
36901
37027
  skillInvocation: submittedSkillInvocation,
36902
- queueItemId: queuedInput?.id || ""
37028
+ queueItemId: queuedInput?.id || "",
37029
+ replaceRunId
36903
37030
  });
37031
+ if (!queuedInput) pendingRetryReplacement = null;
36904
37032
  const runSessionId = currentRunView.sessionId;
36905
37033
  let runAuditDraft = null;
36906
37034
  try {
@@ -40473,6 +40601,16 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
40473
40601
  panel.dataset.cancelling = running && runCancellationRequested ? "true" : "false";
40474
40602
  renderPendingInputs();
40475
40603
  }
40604
+ function prepareRetryReplacement(run) {
40605
+ const sessionId = state?.activeSessionId || "";
40606
+ const source = findRunRecord(run?.id, sessionId);
40607
+ const eligible = source && runController.isRetryReplacementEligible(
40608
+ source,
40609
+ WritebackSettlement.projectRunSettlement(source)
40610
+ );
40611
+ pendingRetryReplacement = eligible ? { sessionId, runId: source.id } : null;
40612
+ return Boolean(eligible);
40613
+ }
40476
40614
  function startRunView({
40477
40615
  task,
40478
40616
  mode,
@@ -40480,6 +40618,7 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
40480
40618
  reasoningEffort,
40481
40619
  speedTier,
40482
40620
  queueItemId = "",
40621
+ replaceRunId = "",
40483
40622
  executionSnapshot = null
40484
40623
  }) {
40485
40624
  let attachments = [];
@@ -40527,12 +40666,13 @@ ${tx("Writeback note: Codex could not confirm any Overleaf write result entries,
40527
40666
  titleSource: "auto"
40528
40667
  } : {};
40529
40668
  state = updateActiveSession(state, {
40530
- runs: [...state.runs || [], record].slice(-20),
40669
+ runs: runController.replaceRunForRetry(state.runs, replaceRunId, record),
40531
40670
  ...titlePatch
40532
40671
  });
40533
- saveStateSoon();
40672
+ saveStateSoon(replaceRunId ? 0 : 120);
40534
40673
  const log = panel.querySelector("[data-log]");
40535
40674
  removeEmptyRunsMessage(log);
40675
+ if (replaceRunId) log.querySelector(`[data-run-id="${cssEscape(replaceRunId)}"]`)?.remove();
40536
40676
  const root2 = renderRunCard(record);
40537
40677
  log.append(root2);
40538
40678
  scrollLogToBottom({ force: true });
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "entry": "extension/entries/content-entry.mjs",
4
4
  "output": "extension/src/content/generated/content.bundle.js",
5
- "sourceDigest": "4b5c2f7a7653947ac71ae42a3301c7547f789c2ca33e25f99f6781f31d1147be",
6
- "outputDigest": "4e32bcf7d8be72541bb39508e38335ad0e32e41844afa45d7f0d34c9481a1cec",
7
- "byteLength": 2313648,
5
+ "sourceDigest": "48e7ba193b0723f8e135c4b52e3af1416b1fe35b3cb780207fb0600e7e0f8019",
6
+ "outputDigest": "866811d95e8bd58d369e0790bf2af5becc93946eb8ad987b80f0be0f80fa965f",
7
+ "byteLength": 2321914,
8
8
  "inputs": [
9
9
  "extension/entries/content-entry.mjs",
10
10
  "extension/src/content/activeTurnControl.js",
@@ -319,7 +319,7 @@
319
319
  if (!efforts.length) {
320
320
  efforts = presentation === 'none' ? ['none'] : ['low', 'medium', 'high', 'xhigh'];
321
321
  }
322
- const current = reasoningSelect.value || getState()?.reasoningEffort || '';
322
+ const current = getState()?.reasoningEffort || reasoningSelect.value || '';
323
323
  const fallback = efforts.includes(model?.defaultReasoningEffort)
324
324
  ? model.defaultReasoningEffort
325
325
  : efforts.includes('high') ? 'high' : efforts[0];
@@ -29,6 +29,7 @@
29
29
  setState,
30
30
  getCurrentRunView,
31
31
  getSettingsPanelInstance,
32
+ prepareRetryReplacement,
32
33
  StorageDb
33
34
  } = deps;
34
35
 
@@ -218,6 +219,7 @@
218
219
  showPluginToast(tx('Nothing to refill: the original task text is unavailable.', '无法回填:原任务文本不可用。'));
219
220
  return;
220
221
  }
222
+ prepareRetryReplacement?.(run || null);
221
223
  input.value = task;
222
224
  getState().task = task;
223
225
  autosizeTaskTextarea();
@@ -4,11 +4,14 @@
4
4
  const ROOT_CLASS = 'codex-overleaf-panel-mounted';
5
5
  const RESIZE_CLASS = 'codex-overleaf-panel-resizing';
6
6
  const COMPACT_CLASS = 'codex-overleaf-panel-compact';
7
+ const LAUNCHER_VISIBILITY_STORAGE_KEY = 'codex-overleaf:launcher-visible';
7
8
 
8
9
  function codexIcon(name) {
9
10
  const icons = {
10
11
  refresh: '<path d="M4.2 5.3A5 5 0 1 1 3.6 10"/><path d="M4.2 2.2v3.1H1.1"/>',
11
12
  plus: '<path d="M8 3.2v9.6"/><path d="M3.2 8h9.6"/>',
13
+ panel: '<rect x="2.3" y="2.3" width="11.4" height="11.4" rx="2"/><path d="M9.2 2.5v11"/><path d="M5.1 6.1h1.5M5.1 8h1.5M5.1 9.9h1.5"/>',
14
+ collapse: '<path d="m6.1 3.3 4.7 4.7-4.7 4.7"/>',
12
15
  settings: '<path d="M3.4 5.2h5.9"/><path d="M11.7 5.2h.9"/><path d="M10.1 3.6v3.2"/><path d="M3.4 10.8h1"/><path d="M6.8 10.8h5.8"/><path d="M5.9 9.2v3.2"/>'
13
16
  };
14
17
  const safeName = Object.prototype.hasOwnProperty.call(icons, name) ? name : 'settings';
@@ -19,9 +22,11 @@
19
22
  const doc = options.document || document;
20
23
  const container = options.container || doc.documentElement;
21
24
  const panelEl = doc.createElement('aside');
25
+ const launcherEl = doc.createElement('button');
22
26
  const callbacks = options.callbacks || {};
23
27
  const instance = {
24
28
  panelEl,
29
+ launcherEl,
25
30
  callbacks,
26
31
  defaultWidth: options.defaultWidth || 380,
27
32
  minWidth: options.minWidth || 340,
@@ -30,9 +35,18 @@
30
35
  lastDesktopWidth: options.initialWidth || options.defaultWidth || 380,
31
36
  listeners: [],
32
37
  document: doc,
33
- window: options.window || window
38
+ window: options.window || window,
39
+ chromeApi: options.chromeApi || (typeof chrome !== 'undefined' ? chrome : null),
40
+ storageChangeListener: null
34
41
  };
35
42
 
43
+ launcherEl.id = 'codex-overleaf-launcher';
44
+ launcherEl.type = 'button';
45
+ launcherEl.title = options.launcherLabel || 'Show or hide Codex panel';
46
+ launcherEl.setAttribute('aria-label', launcherEl.title);
47
+ launcherEl.setAttribute('aria-expanded', 'false');
48
+ launcherEl.innerHTML = codexIcon('panel');
49
+
36
50
  panelEl.id = options.panelId || 'codex-overleaf-panel';
37
51
  panelEl.dataset.view = 'session';
38
52
  panelEl.innerHTML = `
@@ -44,6 +58,7 @@
44
58
  <div data-diagnostics-slot></div>
45
59
  <button type="button" data-new-session title="New Session" aria-label="New Session">${codexIcon('plus')}</button>
46
60
  <button type="button" data-custom-instructions-settings title="Project Settings" aria-label="Project Settings" aria-expanded="false">${codexIcon('settings')}</button>
61
+ <button type="button" data-close-panel title="Close Codex panel" aria-label="Close Codex panel">${codexIcon('collapse')}</button>
47
62
  </div>
48
63
  </div>
49
64
  <div data-settings-slot></div>
@@ -59,6 +74,7 @@
59
74
  `;
60
75
 
61
76
  container.append(panelEl);
77
+ container.append(launcherEl);
62
78
  doc.documentElement.classList.add(ROOT_CLASS);
63
79
 
64
80
  instance.headerEl = panelEl.querySelector('[data-panel-header]');
@@ -73,6 +89,8 @@
73
89
  bind(instance, panelEl.querySelector('[data-refresh]'), 'click', () => callbacks.onRefresh?.());
74
90
  bind(instance, panelEl.querySelector('[data-new-session]'), 'click', () => callbacks.onNewSession?.());
75
91
  bind(instance, panelEl.querySelector('[data-custom-instructions-settings]'), 'click', () => callbacks.onSettingsClick?.());
92
+ bind(instance, panelEl.querySelector('[data-close-panel]'), 'click', () => callbacks.onClosePanel?.());
93
+ bind(instance, launcherEl, 'click', () => callbacks.onLauncherToggle?.());
76
94
  bind(instance, panelEl.querySelector('[data-panel-resize-handle]'), 'pointerdown', event => startResize(instance, event));
77
95
  bind(instance, panelEl.querySelector('[data-panel-resize-handle]'), 'dblclick', event => {
78
96
  event?.preventDefault?.();
@@ -87,9 +105,11 @@
87
105
  });
88
106
 
89
107
  setWidth(instance, options.initialWidth || instance.defaultWidth, { notify: false });
108
+ initializeLauncherVisibility(instance);
90
109
 
91
110
  return {
92
111
  panelEl,
112
+ launcherEl,
93
113
  headerEl: instance.headerEl,
94
114
  bodyEl: instance.bodyEl,
95
115
  diagnosticsSlot: instance.diagnosticsSlot,
@@ -198,11 +218,64 @@
198
218
  function setVisible(panelEl, visible) {
199
219
  panelEl?.classList?.toggle('is-open', Boolean(visible));
200
220
  document.documentElement.classList.toggle(ROOT_CLASS, Boolean(visible));
221
+ const launcherEl = panelEl?.ownerDocument?.getElementById?.('codex-overleaf-launcher');
222
+ launcherEl?.classList?.toggle('is-panel-open', Boolean(visible));
223
+ launcherEl?.setAttribute?.('aria-expanded', visible ? 'true' : 'false');
224
+ if (launcherEl) {
225
+ launcherEl.hidden = Boolean(visible) || launcherEl.dataset.enabled === 'false';
226
+ launcherEl.setAttribute('aria-hidden', launcherEl.hidden ? 'true' : 'false');
227
+ }
201
228
  if (!visible) {
202
229
  document.documentElement.classList.remove(RESIZE_CLASS, COMPACT_CLASS);
203
230
  }
204
231
  }
205
232
 
233
+ function initializeLauncherVisibility(instance) {
234
+ const storage = instance.chromeApi?.storage;
235
+ if (!storage?.local?.get) {
236
+ setLauncherVisible(instance, true, { persist: false });
237
+ return;
238
+ }
239
+ Promise.resolve(storage.local.get({ [LAUNCHER_VISIBILITY_STORAGE_KEY]: true }))
240
+ .then(values => applyLauncherVisibility(instance, values?.[LAUNCHER_VISIBILITY_STORAGE_KEY]))
241
+ .catch(() => applyLauncherVisibility(instance, true));
242
+ if (storage.onChanged?.addListener) {
243
+ instance.storageChangeListener = (changes, areaName) => {
244
+ if (areaName === 'local' && changes?.[LAUNCHER_VISIBILITY_STORAGE_KEY]) {
245
+ applyLauncherVisibility(instance, changes[LAUNCHER_VISIBILITY_STORAGE_KEY].newValue);
246
+ }
247
+ };
248
+ storage.onChanged.addListener(instance.storageChangeListener);
249
+ }
250
+ }
251
+
252
+ function applyLauncherVisibility(instance, value) {
253
+ const visible = value !== false;
254
+ setLauncherVisible(instance, visible, { persist: false });
255
+ instance.callbacks.onLauncherVisibilityChange?.(visible);
256
+ }
257
+
258
+ function setLauncherVisible(target, visible, options = {}) {
259
+ const instance = target?._instance || target;
260
+ const launcherEl = instance?.launcherEl;
261
+ if (!launcherEl) return false;
262
+ const nextVisible = Boolean(visible);
263
+ launcherEl.dataset.enabled = nextVisible ? 'true' : 'false';
264
+ launcherEl.hidden = !nextVisible || instance.panelEl?.classList?.contains('is-open') === true;
265
+ launcherEl.setAttribute('aria-hidden', launcherEl.hidden ? 'true' : 'false');
266
+ if (options.persist === true) {
267
+ Promise.resolve(instance.chromeApi?.storage?.local?.set?.({
268
+ [LAUNCHER_VISIBILITY_STORAGE_KEY]: nextVisible
269
+ })).catch(() => {});
270
+ }
271
+ return nextVisible;
272
+ }
273
+
274
+ function isLauncherVisible(target) {
275
+ const instance = target?._instance || target;
276
+ return Boolean(instance?.launcherEl && instance.launcherEl.dataset.enabled !== 'false');
277
+ }
278
+
206
279
  function setBadge(headerEl, badge = {}) {
207
280
  const actions = headerEl?.querySelector?.('.codex-vscode-head-actions');
208
281
  if (!actions) {
@@ -340,6 +413,10 @@
340
413
  target.removeEventListener?.(type, listener, options);
341
414
  }
342
415
  instance.panelEl?.remove?.();
416
+ instance.launcherEl?.remove?.();
417
+ if (instance.storageChangeListener) {
418
+ instance.chromeApi?.storage?.onChanged?.removeListener?.(instance.storageChangeListener);
419
+ }
343
420
  instance.document.documentElement.classList.remove(ROOT_CLASS, RESIZE_CLASS, COMPACT_CLASS);
344
421
  }
345
422
 
@@ -347,6 +424,8 @@
347
424
  create,
348
425
  createConfirmController,
349
426
  setVisible,
427
+ setLauncherVisible,
428
+ isLauncherVisible,
350
429
  setBadge,
351
430
  setWidth
352
431
  };
@@ -231,11 +231,46 @@
231
231
  return String(content || '').trim().length > 0;
232
232
  }
233
233
 
234
+ function isRetryReplacementEligible(run = {}, projection = {}) {
235
+ if (!run.id || !['failed', 'interrupted'].includes(run.status)) return false;
236
+ if (run.trackedChangeStatus || run.undoStatus) return false;
237
+ if (['undoOperations', 'undoTrackedChanges', 'undoExpectedFiles', 'undoBaseFiles', 'appliedOperations']
238
+ .some(field => Array.isArray(run[field]) && run[field].length)) return false;
239
+ if (projection.canAccept || projection.canUndo || projection.changedDocument === true) return false;
240
+ if (projection.failureTerminalState === 'needs_review' || projection.terminalState === 'needs_review') return false;
241
+ return projection.changedDocument === false || run.mode === 'ask' || hasZeroWriteReport(run.events);
242
+ }
243
+
244
+ function hasZeroWriteReport(events = []) {
245
+ return (Array.isArray(events) ? events : []).some(event => {
246
+ const rows = Array.isArray(event?.detailStructured?.meta) ? event.detailStructured.meta : [];
247
+ return rows.some(row => row?.key === 'writeResult'
248
+ && /(?:wrote|写入)\s*0\b[\s\S]*(?:skipped|跳过)\s*0\b/i.test(String(row.value || '')));
249
+ });
250
+ }
251
+
252
+ function resolveRetryReplacement({ candidate, activeSessionId, runs, projectRunSettlement } = {}) {
253
+ if (!candidate?.runId || candidate.sessionId !== activeSessionId) return '';
254
+ const source = (Array.isArray(runs) ? runs : []).find(run => run?.id === candidate.runId);
255
+ if (!source) return '';
256
+ const projection = typeof projectRunSettlement === 'function' ? projectRunSettlement(source) : {};
257
+ return isRetryReplacementEligible(source, projection) ? source.id : '';
258
+ }
259
+
260
+ function replaceRunForRetry(runs, sourceRunId, nextRun, maxRuns = 20) {
261
+ const retained = (Array.isArray(runs) ? runs : [])
262
+ .filter(run => !sourceRunId || run?.id !== sourceRunId);
263
+ return [...retained, nextRun].slice(-Math.max(1, maxRuns));
264
+ }
265
+
234
266
  return {
235
267
  buildCodexRunParams,
236
268
  canUseFocusedPartialSnapshot,
237
269
  shouldRestrictWritebackToFocus,
238
270
  buildSessionHistoryResult,
271
+ isRetryReplacementEligible,
272
+ replaceRunForRetry,
273
+ resolveRetryReplacement,
239
274
  truncateSessionHistoryText,
240
275
  normalizeComposerAttachments,
241
276
  normalizeSkillInvocation
@@ -18,7 +18,7 @@
18
18
  let currentInstallCommand = DEFAULT_INSTALL_COMMAND;
19
19
 
20
20
  installCommand.textContent = currentInstallCommand;
21
- button.textContent = 'Open panel in Overleaf';
21
+ button.textContent = 'Show Codex button in Overleaf';
22
22
  setVersionStatus({
23
23
  status: 'native_missing',
24
24
  classification: 'incompatible',
@@ -32,7 +32,7 @@
32
32
  return;
33
33
  }
34
34
 
35
- await chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/toggle-panel' });
35
+ await chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/toggle-launcher' });
36
36
  window.close();
37
37
  });
38
38
 
@@ -56,20 +56,20 @@
56
56
  const tab = await getActiveOverleafProjectTab();
57
57
  activeOverleafTab = tab;
58
58
  if (!tab?.id) {
59
- button.textContent = 'Open panel in Overleaf';
59
+ button.textContent = 'Show Codex button in Overleaf';
60
60
  return;
61
61
  }
62
62
 
63
63
  try {
64
64
  const response = await chrome.tabs.sendMessage(tab.id, { type: 'codex-overleaf/get-panel-state' });
65
- const open = response?.open === true;
66
- button.textContent = open ? 'Close panel in Overleaf' : 'Open panel in Overleaf';
67
- status.textContent = open
68
- ? 'Panel is open in this Overleaf tab.'
69
- : 'Panel is closed in this Overleaf tab.';
65
+ const visible = response?.launcherVisible !== false;
66
+ button.textContent = visible ? 'Hide Codex edge button' : 'Show Codex edge button';
67
+ status.textContent = visible
68
+ ? 'The edge button appears whenever the Codex panel is closed.'
69
+ : 'The Codex edge button is hidden in Overleaf.';
70
70
  } catch (_error) {
71
- button.textContent = 'Open panel in Overleaf';
72
- status.textContent = 'Refresh the Overleaf tab, then open the panel.';
71
+ button.textContent = 'Show Codex button in Overleaf';
72
+ status.textContent = 'Refresh the Overleaf tab, then show the Codex button.';
73
73
  }
74
74
  }
75
75
 
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '2.3.3';
15
+ const BUILD_TARGET_VERSION = '2.3.4';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -650,6 +650,78 @@
650
650
  overflow: hidden;
651
651
  }
652
652
 
653
+ #codex-overleaf-launcher {
654
+ position: fixed;
655
+ top: 50%;
656
+ right: 0;
657
+ z-index: 2147483646;
658
+ display: grid;
659
+ width: 26px;
660
+ height: 36px;
661
+ padding: 0;
662
+ place-items: center;
663
+ border: 1px solid #3a3a3a;
664
+ border-right: 0;
665
+ border-radius: 7px 0 0 7px;
666
+ background: #181818;
667
+ box-shadow: -1px 1px 5px rgba(0, 0, 0, 0.18);
668
+ color: #8a8a8a;
669
+ cursor: pointer;
670
+ opacity: 0.82;
671
+ transform: translateY(-50%);
672
+ transition: width 120ms ease, background 120ms ease, color 120ms ease, opacity 120ms ease;
673
+ appearance: none;
674
+ }
675
+
676
+ #codex-overleaf-launcher[hidden] { display: none !important; }
677
+
678
+ #codex-overleaf-launcher:hover {
679
+ width: 29px;
680
+ background: #232323;
681
+ color: #b7b7b7;
682
+ opacity: 1;
683
+ }
684
+
685
+ #codex-overleaf-launcher:active {
686
+ background: #2a2d2e;
687
+ }
688
+
689
+ #codex-overleaf-launcher:focus-visible {
690
+ outline: 2px solid #6aa4d8;
691
+ outline-offset: 2px;
692
+ }
693
+
694
+ #codex-overleaf-launcher .codex-icon,
695
+ #codex-overleaf-launcher .codex-icon svg {
696
+ display: block;
697
+ width: 14px;
698
+ height: 14px;
699
+ }
700
+
701
+ #codex-overleaf-launcher .codex-icon svg {
702
+ fill: none;
703
+ stroke: currentColor;
704
+ stroke-width: 1.45;
705
+ stroke-linecap: round;
706
+ stroke-linejoin: round;
707
+ }
708
+
709
+ #codex-overleaf-panel[data-theme="light"] ~ #codex-overleaf-launcher {
710
+ border-color: #d7dbe0;
711
+ background: #f3f4f6;
712
+ box-shadow: -1px 1px 5px rgba(31, 35, 40, 0.12);
713
+ color: #6a7280;
714
+ }
715
+
716
+ #codex-overleaf-panel[data-theme="light"] ~ #codex-overleaf-launcher:hover {
717
+ background: #eef0f3;
718
+ color: #3d4651;
719
+ }
720
+
721
+ @media (prefers-reduced-motion: reduce) {
722
+ #codex-overleaf-launcher { transition: none; }
723
+ }
724
+
653
725
  :root.codex-overleaf-panel-compact body {
654
726
  width: auto !important;
655
727
  max-width: none !important;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",