codex-overleaf-link 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +11 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +19 -6
  9. package/extension/src/content/contentRuntime.js +335 -317
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/otWarmMirror.js +1 -1
  17. package/extension/src/content/panelRenderer.js +112 -12
  18. package/extension/src/content/pendingInputView.js +106 -0
  19. package/extension/src/content/projectProviderSelection.js +68 -0
  20. package/extension/src/content/projectSessionCleanup.js +104 -0
  21. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  22. package/extension/src/content/providerSettingsDialog.js +2 -2
  23. package/extension/src/content/recentProjects.js +24 -13
  24. package/extension/src/content/runController.js +2 -2
  25. package/extension/src/content/runGuidanceController.js +70 -0
  26. package/extension/src/content/runGuidanceView.js +48 -0
  27. package/extension/src/content/runQueueScheduler.js +260 -0
  28. package/extension/src/content/runTimelineView.js +33 -80
  29. package/extension/src/content/sessionManager.js +2 -2
  30. package/extension/src/content/sessionPersistence.js +62 -0
  31. package/extension/src/content/settingsPanel.js +1 -1
  32. package/extension/src/content/updateNotice.js +80 -1
  33. package/extension/src/page/writebackRouter.js +3 -9
  34. package/extension/src/shared/compatibility.js +2 -1
  35. package/extension/src/shared/i18n.js +54 -0
  36. package/extension/src/shared/providerProfiles.js +3 -2
  37. package/extension/src/shared/runInputQueue.js +253 -0
  38. package/extension/src/shared/sessionState.js +45 -22
  39. package/extension/src/shared/storageDb.js +39 -44
  40. package/extension/src/shared/storageMigration.js +123 -3
  41. package/extension/src/shared/storageRunActions.js +34 -1
  42. package/extension/src/shared/updateConsent.js +15 -4
  43. package/extension/styles/panel.css +370 -23
  44. package/extension/vendor/katex/katex.min.css +1 -1
  45. package/extension/vendor/markdown-it/LICENSE +22 -0
  46. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  47. package/extension/vendor/markdown-it/VERSION +1 -0
  48. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  49. package/native-host/src/anthropicBridgeRequest.js +3 -3
  50. package/native-host/src/anthropicBridgeResponse.js +58 -17
  51. package/native-host/src/anthropicMessagesBridge.js +156 -23
  52. package/native-host/src/chatBridgeRequest.js +1 -1
  53. package/native-host/src/chatBridgeResponse.js +12 -8
  54. package/native-host/src/chatCompletionsBridge.js +22 -40
  55. package/native-host/src/codexPromptAssembly.js +3 -1
  56. package/native-host/src/codexProviderLaunch.js +6 -3
  57. package/native-host/src/codexSessionRunner.js +66 -66
  58. package/native-host/src/codexSessionTiming.js +45 -0
  59. package/native-host/src/diffEngine.js +10 -5
  60. package/native-host/src/index.js +58 -3
  61. package/native-host/src/managedInstall.js +45 -7
  62. package/native-host/src/providerProfile.js +2 -2
  63. package/native-host/src/providerStreamLifecycle.js +100 -0
  64. package/native-host/src/responsesPassthroughBridge.js +636 -0
  65. package/native-host/src/taskRunnerRuntime.js +81 -2
  66. package/native-host/src/updateArchive.js +4 -3
  67. package/native-host/src/updateManager.js +306 -19
  68. package/native-host/src/updateTrust.js +1 -1
  69. package/package.json +3 -1
@@ -36,6 +36,7 @@
36
36
  'mirror.status',
37
37
  'codex.models',
38
38
  'codex.run',
39
+ 'codex.steer',
39
40
  'codex.providers.list',
40
41
  'codex.providers.test',
41
42
  'codex.providers.test.cancel',
@@ -113,6 +114,10 @@
113
114
  const ContextTray = window.CodexOverleafContextTray;
114
115
  const LocalSkillsPanel = window.CodexOverleafLocalSkillsPanel;
115
116
  const runController = window.CodexOverleafRunController;
117
+ const RunInputQueue = window.CodexOverleafRunInputQueue;
118
+ const RunQueueScheduler = window.CodexOverleafRunQueueScheduler;
119
+ const ActiveTurnControl = window.CodexOverleafActiveTurnControl;
120
+ const PendingInputView = window.CodexOverleafPendingInputView;
116
121
  const mirrorHealth = window.CodexOverleafMirrorHealth;
117
122
  const otWarmMirrorController = window.CodexOverleafOtWarmMirrorController;
118
123
  const PanelRenderer = window.CodexOverleafPanelRenderer;
@@ -172,7 +177,8 @@
172
177
  tx,
173
178
  callPageBridge,
174
179
  getCurrentProjectReferenceFiles,
175
- showPluginToast
180
+ showPluginToast,
181
+ recordRenderingDiagnostic: detail => appendTechnicalEvent({ type: 'rendering.diagnostic', detail })
176
182
  });
177
183
  const {
178
184
  renderMarkdownInlineText,
@@ -597,6 +603,14 @@
597
603
  updateModelDisplay
598
604
  } = modelPicker;
599
605
 
606
+ const projectProviderSelection = window.CodexOverleafProjectProviderSelection.create({
607
+ tx, tr, getRunningSessionIds, showToast: showPluginToast, showConfirm: showPluginConfirm,
608
+ getState: () => state, setState: next => { state = next; }, getPanel: () => panel,
609
+ readSelectedModel: readSelectedModelInput, readSelectedSpeed: readSelectedSpeedInput,
610
+ getRunSelection: providerId => providerSettingsCoordinator.getRunSelection(providerId),
611
+ normalizeState: normalizePanelState, applyStateToPanel
612
+ });
613
+
600
614
  const providerSettingsCoordinator = window.CodexOverleafProviderSettingsCoordinator.create({
601
615
  tx,
602
616
  sendBackgroundNative,
@@ -604,7 +618,8 @@
604
618
  window,
605
619
  getSettingsPanelInstance: () => settingsPanelInstance,
606
620
  getSelectedModel: () => state?.model || '', getSelectedProviderId: () => state?.providerId || 'builtin',
607
- setSelectedProviderId: (providerId, model) => { state = updateActiveSession(state, { providerId: providerId || 'builtin', ...(model !== undefined ? { model } : {}) }); const modelSelect = panel?.querySelector('[data-model]'); if (model !== undefined && modelSelect) modelSelect.value = ''; },
621
+ setSelectedProviderId: projectProviderSelection.commit,
622
+ confirmProviderSwitch: projectProviderSelection.confirmSwitch,
608
623
  refreshModelOptions: loadModelOptions, persistInputs: persistPanelInputs
609
624
  });
610
625
  // v1.8.1 D3: the dashboard was a one-shot render — another tab finishing a
@@ -767,6 +782,9 @@
767
782
  let state = null;
768
783
  let storageKey = LEGACY_STORAGE_KEY;
769
784
  let currentRunView = null;
785
+ let runQueueScheduler = null;
786
+ let runInputCoordinator = null;
787
+ const activeTurnControl = ActiveTurnControl.create({ chrome });
770
788
  let saveStateTimer = null;
771
789
  // Two-phase saveState scheduling. `saveStateInFlight` flips to true while
772
790
  // an async saveState() is actually writing; `saveStateRunAfterFlight` is
@@ -787,7 +805,7 @@
787
805
  let renderedSlashCommands = new Map();
788
806
  let runCancellationRequested = false;
789
807
  let runCancellationController = null;
790
- let activePluginConfirmResolve = null;
808
+ let pluginConfirmController = null;
791
809
  const updateIdleClient = root.CodexOverleafUpdateIdle?.create({
792
810
  document,
793
811
  getBusyState: () => ({
@@ -795,7 +813,7 @@
795
813
  cancelling: runCancellationRequested,
796
814
  storage: saveStateInFlight || Boolean(saveStateTimer),
797
815
  reviewAction: trackedChangeInFlight.size > 0,
798
- dialog: Boolean(activePluginConfirmResolve)
816
+ dialog: Boolean(pluginConfirmController?.isOpen())
799
817
  }),
800
818
  checkSaved: () => callPageBridge('waitForSaveState', { deadlineMs: 1500, pollIntervalMs: 100, requirePositiveSignal: true, allowQuietEditorFallback: true, quietFallbackMs: 1000 }),
801
819
  onApplyingChange: applying => { if (panel) panel.dataset.updating = applying ? 'true' : 'false'; },
@@ -843,7 +861,7 @@
843
861
  return;
844
862
  }
845
863
  if (nativeChannel.shouldHandleNativeEvent(message)) {
846
- appendNativeEvent(message.event);
864
+ appendNativeEvent(message.event, message.journalSeq);
847
865
  }
848
866
  });
849
867
 
@@ -862,6 +880,15 @@
862
880
  async function init() {
863
881
  storageKey = getProjectStorageKey(LEGACY_STORAGE_KEY, window.location.href);
864
882
  state = normalizePanelState(await loadStoredState(), { restoreRunningRuns: true });
883
+ initializeRunQueueScheduler();
884
+ recoverInterruptedRunJournals()
885
+ .then(() => applyStateToPanel())
886
+ .catch(() => {});
887
+ setTimeout(() => {
888
+ recoverInterruptedRunJournals()
889
+ .then(() => applyStateToPanel())
890
+ .catch(() => {});
891
+ }, 900);
865
892
  ensurePanelOpen();
866
893
  root.CodexOverleafUpdateNotice?.mount?.(panel?.panelEl || panel, {
867
894
  getLocale: () => state?.locale || 'en'
@@ -1338,7 +1365,8 @@
1338
1365
  setElementTitleAndAria('[data-context-refresh]', tr('refreshFileList'), tr('refreshFileList'));
1339
1366
  setElementTitleAndAria('[data-reasoning]', tr('reasoningLabel'), tr('reasoningLabel'));
1340
1367
  setElementTitleAndAria('[data-speed]', tr('speedLabel'), tr('speedLabel'));
1341
- setElementTitleAndAria('[data-run]', currentRunView ? tr('cancelRun') : tr('send'), currentRunView ? tr('cancelRun') : tr('send'));
1368
+ setElementTitleAndAria('[data-run]', currentRunView ? tr('queueNextInput') : tr('send'), currentRunView ? tr('queueNextInput') : tr('send'));
1369
+ setElementTitleAndAria('[data-stop-run]', tr('cancelRun'), tr('cancelRun'));
1342
1370
 
1343
1371
  const actions = panel.querySelector('.codex-vscode-head-actions');
1344
1372
  if (actions) {
@@ -1422,27 +1450,15 @@
1422
1450
  return '';
1423
1451
  }
1424
1452
 
1425
- function installDiagnosticsDismiss() {
1426
- // DiagnosticsPanel installs its outside-click dismiss listener during create().
1427
- }
1453
+ function installDiagnosticsDismiss() {}
1428
1454
 
1429
- function closeDiagnosticsResult() {
1430
- DiagnosticsPanel.closeResult(diagnosticsPanelInstance);
1431
- }
1455
+ function closeDiagnosticsResult() { DiagnosticsPanel.closeResult(diagnosticsPanelInstance); }
1432
1456
 
1433
- function showDiagnosticsLoading(title, subtitle = tr('diagnosticsLoading')) {
1434
- DiagnosticsPanel.showLoading(diagnosticsPanelInstance, title, subtitle);
1435
- }
1457
+ function showDiagnosticsLoading(title, subtitle = tr('diagnosticsLoading')) { DiagnosticsPanel.showLoading(diagnosticsPanelInstance, title, subtitle); }
1436
1458
 
1437
- function showDiagnosticsResult(result = {}) {
1438
- DiagnosticsPanel.showResult(diagnosticsPanelInstance, result);
1439
- }
1459
+ function showDiagnosticsResult(result = {}) { DiagnosticsPanel.showResult(diagnosticsPanelInstance, result); }
1440
1460
 
1441
- // Push an overall health bucket (ok / warn / fail / unknown) to the
1442
- // diagnostics trigger's status dot.
1443
- function setDiagnosticsHealth(health) {
1444
- DiagnosticsPanel.updateStatus(diagnosticsPanelInstance, { health });
1445
- }
1461
+ function setDiagnosticsHealth(health) { DiagnosticsPanel.updateStatus(diagnosticsPanelInstance, { health }); }
1446
1462
 
1447
1463
  function installContextDismiss() {
1448
1464
  return contextTrayController.installContextDismiss();
@@ -1646,7 +1662,7 @@
1646
1662
  return contextTrayController.resetContextProject();
1647
1663
  }
1648
1664
 
1649
- async function runTask() {
1665
+ async function runTask(options = {}) {
1650
1666
  // Barrier on a still-flying background mirror refresh from the previous
1651
1667
  // turn (v1.7.5) BEFORE startRunView creates the new run view: the mirror's
1652
1668
  // wrap-up events must land in the run that produced them, not the new
@@ -1665,7 +1681,8 @@
1665
1681
  const submittedAttachments = getComposerAttachmentsForRun();
1666
1682
  const submittedSkillInvocation = getComposerSkillInvocationForRun();
1667
1683
 
1668
- const task = state.task.trim();
1684
+ const queuedInput = options.queuedInput || null;
1685
+ const task = String(queuedInput?.text || state.task || '').trim();
1669
1686
  if (!task) {
1670
1687
  appendLog(tx('Enter a task first.', '请先输入任务。'));
1671
1688
  return;
@@ -1681,9 +1698,13 @@
1681
1698
  reasoningEffort: state.reasoningEffort,
1682
1699
  speedTier: state.speedTier,
1683
1700
  attachments: submittedAttachments,
1684
- skillInvocation: submittedSkillInvocation
1701
+ skillInvocation: submittedSkillInvocation,
1702
+ queueItemId: queuedInput?.id || ''
1685
1703
  });
1686
1704
  const runSessionId = currentRunView.sessionId;
1705
+ if (queuedInput?.id) {
1706
+ await runQueueScheduler?.markExecuting(runSessionId, queuedInput.id, currentRunView.recordId);
1707
+ }
1687
1708
  let runAuditDraft = null;
1688
1709
  try {
1689
1710
  runAuditDraft = await awaitRunStep(createAuditDraftForRun({
@@ -1693,10 +1714,11 @@
1693
1714
  focusFiles: getActiveFocusFiles()
1694
1715
  }));
1695
1716
  announceCrossTabRunStart();
1696
- // Keep attachments across turns (v1.7.5): screenshots and files usually
1697
- // anchor several follow-up questions. The tray keeps them visible and
1698
- // hand-removable; only the task text resets after submit.
1699
- clearTaskComposer({ keepAttachments: true });
1717
+ // Binary attachments belong to this submitted turn. The run record has
1718
+ // already captured their snapshots, so clear them from the composer
1719
+ // together with the submitted text. Persistent @context remains intact.
1720
+ clearTaskComposer({ keepAttachments: false });
1721
+ syncComposerSendAvailability(true);
1700
1722
  appendRunEvent({
1701
1723
  title: submittedSkillInvocation?.id === 'skill-installer'
1702
1724
  ? tx('I will use the Codex skill installer for this request.', '我会用 Codex skill installer 处理这个请求。')
@@ -1727,13 +1749,10 @@
1727
1749
  return;
1728
1750
  }
1729
1751
  appendRunEvent({
1730
- // Wording must match behavior: a non-empty focus selection RESTRICTS
1731
- // reads and writes to these files (shouldRestrictWritebackToFocus),
1732
- // it does not merely "prioritize" them. The selection persists across
1733
- // turns until cleared via the + tray.
1752
+ // @context prioritizes these files; complete project runs may still update related files.
1734
1753
  title: tx(
1735
- `This run reads and writes ONLY: ${formatContextItems(getActiveFocusFiles())} (persists across turns; clear via +)`,
1736
- `本轮仅读写:${formatContextItems(getActiveFocusFiles())}(跨轮保留,可在 + 中清除)`
1754
+ `This run will prioritize: ${formatContextItems(getActiveFocusFiles())} (persists across turns; clear via +)`,
1755
+ `本轮将优先处理:${formatContextItems(getActiveFocusFiles())}(跨轮保留,可在 + 中清除)`
1737
1756
  ),
1738
1757
  status: 'completed'
1739
1758
  });
@@ -1785,7 +1804,7 @@
1785
1804
  focusFiles,
1786
1805
  isUsableProjectFileContent: window.CodexOverleafProjectFiles.isUsableProjectFileContent
1787
1806
  });
1788
- let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles });
1807
+ let restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot });
1789
1808
  if (snapshotWarnings.blocking.length && !warmMirrorReuse.useExistingMirror && !focusedPartialSnapshot) {
1790
1809
  for (const warning of snapshotWarnings.blocking) {
1791
1810
  appendLog(tx(`Cannot continue: ${formatProjectSnapshotWarning(warning)}`, `无法继续:${formatProjectSnapshotWarning(warning)}`));
@@ -1827,8 +1846,8 @@
1827
1846
  if (Array.isArray(warmMirrorReuse.focusFiles) && warmMirrorReuse.focusFiles.length) {
1828
1847
  focusFiles = warmMirrorReuse.focusFiles;
1829
1848
  }
1830
- restrictToFocusFiles = true;
1831
1849
  }
1850
+ restrictToFocusFiles = runController.shouldRestrictWritebackToFocus({ focusFiles, focusedPartialSnapshot: warmMirrorReuse.partialSnapshot, otWarmStart });
1832
1851
  appendRunEvent({
1833
1852
  title: warmMirrorReuse.warmStart
1834
1853
  ? tr('warmMirrorReuseTitle')
@@ -1912,9 +1931,7 @@
1912
1931
  await awaitRunStep(settleMirrorPrefetchBeforeRun());
1913
1932
  await awaitRunStep(providerSettingsCoordinator.ensureLoaded());
1914
1933
  appendRunEvent({ title: tx('Local Codex session is starting.', '本地 Codex session 开始运行。'), status: 'running' });
1915
- let response = await awaitRunStep(sendNative({
1916
- method: 'codex.run',
1917
- params: buildCodexRunParams({
1934
+ let response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
1918
1935
  task,
1919
1936
  project,
1920
1937
  useExistingMirror,
@@ -1929,8 +1946,7 @@
1929
1946
  attachments: submittedAttachments,
1930
1947
  skillInvocation: submittedSkillInvocation,
1931
1948
  submittedMode
1932
- })
1933
- }));
1949
+ })));
1934
1950
 
1935
1951
  // Handle mirror_stale error by retrying with full sync
1936
1952
  if (!response.ok && response.error?.code === 'mirror_stale' && useExistingMirror) {
@@ -1948,9 +1964,7 @@
1948
1964
  fileOverlays = null;
1949
1965
  otWarmStart = false;
1950
1966
  restrictToFocusFiles = staleRetry.restrictToFocusFiles;
1951
- response = await awaitRunStep(sendNative({
1952
- method: 'codex.run',
1953
- params: buildCodexRunParams({
1967
+ response = await awaitRunStep(sendTrackedCodexRun(buildCodexRunParams({
1954
1968
  task,
1955
1969
  project,
1956
1970
  useExistingMirror: false,
@@ -1965,8 +1979,7 @@
1965
1979
  attachments: submittedAttachments,
1966
1980
  skillInvocation: submittedSkillInvocation,
1967
1981
  submittedMode
1968
- })
1969
- }));
1982
+ })));
1970
1983
  }
1971
1984
 
1972
1985
  if (!response.ok && response.error?.code === 'thread_resume_failed') {
@@ -1982,9 +1995,7 @@
1982
1995
  }
1983
1996
  }
1984
1997
  appendRunEvent({ title: tx('Creating a new Codex conversation thread.', '正在新建 Codex 会话线程。'), status: 'running' });
1985
- response = await sendNative({
1986
- method: 'codex.run',
1987
- params: buildCodexRunParams({
1998
+ response = await sendTrackedCodexRun(buildCodexRunParams({
1988
1999
  task,
1989
2000
  project,
1990
2001
  useExistingMirror,
@@ -1999,8 +2010,7 @@
1999
2010
  attachments: submittedAttachments,
2000
2011
  skillInvocation: submittedSkillInvocation,
2001
2012
  submittedMode
2002
- })
2003
- });
2013
+ }));
2004
2014
  } else {
2005
2015
  appendRunEvent({ title: tx('Cancelled: user chose not to create a new thread.', '已取消:用户选择不新建线程。'), status: 'rejected' });
2006
2016
  await finalizeAuditRecord(runAuditDraft, { resultStatus: 'rejected' });
@@ -2161,6 +2171,9 @@
2161
2171
  mode: submittedMode,
2162
2172
  requireReviewing: submittedRequireReviewing
2163
2173
  });
2174
+ if (runCancellationRequested) {
2175
+ throw createRunCancellationError();
2176
+ }
2164
2177
  await finalizeAuditRecord(runAuditDraft, {
2165
2178
  ...(syncOutcome.audit || {}),
2166
2179
  sensitiveFindings: sensitiveFindings.findings,
@@ -2309,6 +2322,15 @@
2309
2322
  });
2310
2323
  finishRunView(tx('Task failed', '任务失败'), 'failed');
2311
2324
  } finally {
2325
+ const settledView = currentRunView;
2326
+ const settledRecord = settledView ? findRunRecord(settledView.recordId, settledView.sessionId) : null;
2327
+ const settlement = {
2328
+ sessionId: settledView?.sessionId || '',
2329
+ status: settledView?.terminalStatus || settledRecord?.status || 'failed',
2330
+ queueItemId: settledView?.queueItemId || '',
2331
+ requestId: settledView?.nativeRequestId || '',
2332
+ continueQueue: runCancellationRequested
2333
+ };
2312
2334
  setRunning(false);
2313
2335
  nativeChannel.clearActiveRequest();
2314
2336
  stopRunElapsedTick();
@@ -2318,7 +2340,12 @@
2318
2340
  if (isExperimentalOtEnabled()) {
2319
2341
  await resumeOtWarmMirror('run-settled');
2320
2342
  }
2321
- saveStateSoon();
2343
+ await saveState().catch(() => {});
2344
+ activeTurnControl.release(settlement.requestId);
2345
+ if (settlement.requestId) {
2346
+ activeTurnControl.acknowledge(settlement.requestId).catch(() => {});
2347
+ }
2348
+ runQueueScheduler?.afterSettlement(settlement).catch(() => {});
2322
2349
  }
2323
2350
  }
2324
2351
 
@@ -2557,7 +2584,7 @@
2557
2584
  return;
2558
2585
  }
2559
2586
  if (currentRunView) {
2560
- return;
2587
+ return queueComposerInput();
2561
2588
  }
2562
2589
  runTask().catch(error => {
2563
2590
  setRunning(false);
@@ -2570,13 +2597,73 @@
2570
2597
  });
2571
2598
  }
2572
2599
 
2600
+ const runGuidanceController = window.CodexOverleafRunGuidanceController.create({
2601
+ findRunRecord, appendRunEvent, getCurrentRunView: () => currentRunView,
2602
+ renderRunEvent, cssEscape, bumpUnreadIfDetached
2603
+ });
2604
+
2605
+ function initializeRunQueueScheduler() {
2606
+ runInputCoordinator = RunQueueScheduler.createCoordinator({
2607
+ getSession: sessionId => findSessionById(sessionId),
2608
+ getActiveSession: () => getActiveSession(state),
2609
+ setQueue: (sessionId, pendingInputs) => updateSessionById(sessionId, { pendingInputs }),
2610
+ getCurrentRun: () => currentRunView,
2611
+ canStart: sessionId => state?.activeSessionId === sessionId && isProjectEditorRoute(window.location),
2612
+ readInputs: readPanelInputs,
2613
+ getTask: () => panel?.querySelector('[data-task]')?.value || '',
2614
+ hasAttachments: () => getComposerAttachmentsForRun().length > 0,
2615
+ capturePayload: () => ({
2616
+ mode: state.mode,
2617
+ autoRecompile: state.autoRecompile !== false,
2618
+ requireReviewing: state.requireReviewing,
2619
+ focusFiles: getActiveFocusFiles()
2620
+ }),
2621
+ clearTask: () => {
2622
+ panel.querySelector('[data-task]').value = '';
2623
+ updateSessionById(state.activeSessionId, { task: '' });
2624
+ autosizeTaskTextarea();
2625
+ syncComposerSendAvailability();
2626
+ },
2627
+ applyQueuedInput: item => {
2628
+ state.autoRecompile = item.payload?.autoRecompile !== false;
2629
+ state = updateActiveSession(state, {
2630
+ task: item.text,
2631
+ mode: item.payload?.mode || state.mode,
2632
+ requireReviewing: item.payload?.requireReviewing !== false,
2633
+ focusFiles: item.payload?.focusFiles || []
2634
+ });
2635
+ applyStateToPanel();
2636
+ },
2637
+ run: item => runTask({ queuedInput: item }),
2638
+ sendSteer: params => sendBackgroundNative({ method: 'codex.steer', params }),
2639
+ getContainer: () => panel?.querySelector('[data-pending-inputs]'),
2640
+ createView: options => PendingInputView.create(options),
2641
+ appendEvent: (title, status) => appendRunEvent({ title, status }),
2642
+ appendGuidance: runGuidanceController.upsert,
2643
+ removeGuidance: runGuidanceController.remove,
2644
+ toast: (message, status) => showPluginToast(message, { status }),
2645
+ tr,
2646
+ save: () => saveState(),
2647
+ saveSoon: () => saveStateSoon(),
2648
+ randomUUID: () => crypto.randomUUID()
2649
+ });
2650
+ runQueueScheduler = runInputCoordinator.scheduler;
2651
+ }
2652
+
2653
+ function queueComposerInput() {
2654
+ return runInputCoordinator?.queueComposerInput();
2655
+ }
2656
+
2657
+ function renderPendingInputs() {
2658
+ return runInputCoordinator?.render();
2659
+ }
2660
+
2573
2661
  async function cancelActiveRun() {
2574
2662
  if (!currentRunView || runCancellationRequested) {
2575
2663
  return;
2576
2664
  }
2577
2665
  runCancellationRequested = true;
2578
2666
  runCancellationController?.abort();
2579
- cancelActivePageBridgeRequests();
2580
2667
  panel.dataset.cancelling = 'true';
2581
2668
  setRunning(true);
2582
2669
  appendRunEvent({
@@ -2761,125 +2848,16 @@
2761
2848
  return confirmed ? 'new' : 'cancel';
2762
2849
  }
2763
2850
 
2764
- async function showPluginConfirm({
2765
- title = tr('confirmDefaultTitle'),
2766
- message = '',
2767
- confirmLabel = tr('confirmDefaultConfirm'),
2768
- cancelLabel = tr('confirmDefaultCancel'),
2769
- destructive = false
2770
- } = {}) {
2771
- if (!panel) {
2772
- ensurePanelOpen();
2773
- }
2774
-
2775
- if (activePluginConfirmResolve) {
2776
- activePluginConfirmResolve(false);
2777
- activePluginConfirmResolve = null;
2778
- }
2779
-
2780
- return new Promise(resolve => {
2781
- let settled = false;
2782
- const overlay = document.createElement('div');
2783
- overlay.className = 'codex-plugin-confirm';
2784
- overlay.setAttribute('data-plugin-confirm', 'true');
2785
- overlay.setAttribute('role', 'dialog');
2786
- overlay.setAttribute('aria-modal', 'true');
2787
- overlay.setAttribute('aria-label', title);
2788
-
2789
- const card = document.createElement('section');
2790
- card.className = 'codex-plugin-confirm-card';
2791
-
2792
- const head = document.createElement('div');
2793
- head.className = 'codex-plugin-confirm-head';
2794
- const icon = document.createElement('img');
2795
- icon.className = 'codex-plugin-confirm-icon';
2796
- icon.alt = '';
2797
- icon.setAttribute('aria-hidden', 'true');
2798
- icon.src = chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png');
2799
- const titleWrap = document.createElement('div');
2800
- const brand = document.createElement('div');
2801
- brand.className = 'codex-plugin-confirm-brand';
2802
- brand.textContent = tr('confirmBrand');
2803
- const titleEl = document.createElement('div');
2804
- titleEl.className = 'codex-plugin-confirm-title';
2805
- titleEl.textContent = title;
2806
- titleWrap.append(brand, titleEl);
2807
- head.append(icon, titleWrap);
2808
-
2809
- const body = document.createElement('div');
2810
- body.className = 'codex-plugin-confirm-body';
2811
- body.textContent = String(message || '');
2812
-
2813
- const actions = document.createElement('div');
2814
- actions.className = 'codex-plugin-confirm-actions';
2815
- const cancel = document.createElement('button');
2816
- cancel.type = 'button';
2817
- cancel.className = 'codex-plugin-confirm-cancel';
2818
- cancel.textContent = cancelLabel;
2819
- const confirm = document.createElement('button');
2820
- confirm.type = 'button';
2821
- confirm.className = 'codex-plugin-confirm-confirm';
2822
- if (destructive) {
2823
- confirm.dataset.destructive = 'true';
2824
- }
2825
- confirm.textContent = confirmLabel;
2826
- actions.append(cancel, confirm);
2827
- card.append(head, body, actions);
2828
- overlay.append(card);
2829
- panel.append(overlay);
2830
-
2831
- const cleanup = value => {
2832
- if (settled) {
2833
- return;
2834
- }
2835
- settled = true;
2836
- activePluginConfirmResolve = null;
2837
- document.removeEventListener('keydown', onKeydown, true);
2838
- overlay.remove();
2839
- resolve(value);
2840
- };
2841
- activePluginConfirmResolve = cleanup;
2842
- const onKeydown = event => {
2843
- if (event.key === 'Escape') {
2844
- event.preventDefault();
2845
- cleanup(false);
2846
- return;
2847
- }
2848
- if (event.key === 'Enter') {
2849
- event.preventDefault();
2850
- // Destructive dialogs never treat a bare Enter as consent — the
2851
- // user must deliberately reach the (red) confirm button. Enter on
2852
- // a focused button still activates it via the click handler.
2853
- if (!destructive && event.target !== cancel) {
2854
- cleanup(true);
2855
- } else if (event.target === cancel) {
2856
- cleanup(false);
2857
- } else if (event.target === confirm) {
2858
- cleanup(true);
2859
- }
2860
- return;
2861
- }
2862
- if (event.key === 'Tab') {
2863
- // aria-modal contract: keep Tab cycling inside the two-button card
2864
- // instead of drifting onto the Overleaf page behind the overlay.
2865
- event.preventDefault();
2866
- const next = event.target === cancel ? confirm : cancel;
2867
- next.focus();
2868
- }
2869
- };
2870
-
2871
- overlay.addEventListener('click', event => {
2872
- if (event.target === overlay) {
2873
- cleanup(false);
2874
- }
2851
+ async function showPluginConfirm(options = {}) {
2852
+ if (!pluginConfirmController) {
2853
+ pluginConfirmController = PanelRenderer.createConfirmController({
2854
+ getPanel: () => panel,
2855
+ ensurePanelOpen,
2856
+ getIconUrl: () => chrome.runtime.getURL('assets/icons/codex-overleaf-dialog-icon.png'),
2857
+ tr
2875
2858
  });
2876
- cancel.addEventListener('click', () => cleanup(false));
2877
- confirm.addEventListener('click', () => cleanup(true));
2878
- document.addEventListener('keydown', onKeydown, true);
2879
- // Destructive dialogs open on the SAFE button so a stray keystroke
2880
- // cannot delete anything; benign confirms keep the fast path.
2881
- (destructive ? cancel : confirm).focus();
2882
- });
2859
+ }
2860
+ return pluginConfirmController.show(options);
2883
2861
  }
2884
2862
 
2885
2863
  function getEnabledCodexOverleafSkillIds() {
@@ -2933,10 +2911,97 @@
2933
2911
  });
2934
2912
  return {
2935
2913
  ...params,
2914
+ clientRunId: currentRunView?.recordId || '',
2915
+ clientSessionId: currentRunView?.sessionId || '',
2936
2916
  providerSelection: providerSettingsCoordinator.getRunSelection(state?.providerId)
2937
2917
  };
2938
2918
  }
2939
2919
 
2920
+ async function sendTrackedCodexRun(params) {
2921
+ const compatibilityGate = await ensureNativeCompatibilityForMethod('codex.run');
2922
+ if (!compatibilityGate.ok) {
2923
+ return compatibilityGate.response;
2924
+ }
2925
+ throwIfCancelledBeforeNativeDispatch('codex.run');
2926
+ const tracked = nativeChannel.sendNativeTracked(attachNativeCompatibilityEvidence({
2927
+ method: 'codex.run',
2928
+ params
2929
+ }, compatibilityGate.compatibility));
2930
+ if (currentRunView) {
2931
+ currentRunView.nativeRequestId = tracked.id;
2932
+ const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
2933
+ if (record) {
2934
+ record.nativeRequestId = tracked.id;
2935
+ }
2936
+ activeTurnControl.bind({
2937
+ requestId: tracked.id,
2938
+ projectKey: currentRunView.runProjectId,
2939
+ clientRunId: currentRunView.recordId,
2940
+ sessionId: currentRunView.sessionId
2941
+ });
2942
+ saveStateSoon();
2943
+ }
2944
+ return tracked.promise.finally(() => {
2945
+ if (currentRunView?.nativeRequestId === tracked.id) {
2946
+ currentRunView.activeTurn = null;
2947
+ renderPendingInputs();
2948
+ }
2949
+ });
2950
+ }
2951
+
2952
+ async function recoverInterruptedRunJournals() {
2953
+ const projectKey = getCurrentProjectId();
2954
+ const recovery = await activeTurnControl.recoverJournals({
2955
+ projectKey,
2956
+ findSession: findSessionById,
2957
+ getActiveSession: () => getActiveSession(state),
2958
+ createInterruptedRun: journal => ({
2959
+ id: journal.clientRunId,
2960
+ task: journal.task || 'interrupted task',
2961
+ mode: journal.mode || 'ask',
2962
+ model: journal.model || '',
2963
+ status: 'interrupted',
2964
+ statusText: tr('restoredRunStoppedStatus'),
2965
+ runProjectId: projectKey,
2966
+ startedAt: journal.createdAt || new Date().toISOString(),
2967
+ finishedAt: journal.updatedAt || new Date().toISOString(),
2968
+ events: [],
2969
+ attachments: [],
2970
+ undoOperations: [],
2971
+ undoTrackedChanges: [],
2972
+ undoExpectedFiles: []
2973
+ }),
2974
+ mapEvent: (raw, journal) => {
2975
+ const activity = mapAgentEventToActivity(raw, { locale: getLocale() });
2976
+ if (!activity?.visible || activity.kind === 'technical') {
2977
+ return null;
2978
+ }
2979
+ return {
2980
+ kind: activity.kind || 'activity',
2981
+ title: sanitizeAssistantVisibleText(activity.title) || 'Event',
2982
+ status: activity.status || 'running',
2983
+ detail: sanitizeAssistantVisibleValue(activity.detail),
2984
+ timestamp: raw.timestamp || journal.updatedAt || new Date().toISOString(),
2985
+ streamKey: sanitizeAssistantVisibleText(activity.streamKey),
2986
+ streamRole: sanitizeAssistantVisibleText(activity.streamRole),
2987
+ appendText: sanitizeAssistantVisibleText(activity.appendText),
2988
+ replaceText: activity.replaceText
2989
+ };
2990
+ },
2991
+ upsertStream: upsertRunStreamRecordEvent,
2992
+ pauseSessionQueue: (session, reason) => {
2993
+ session.pendingInputs = RunInputQueue.pauseAll(session.pendingInputs || [], reason);
2994
+ },
2995
+ maxEvents: MAX_RUN_EVENTS
2996
+ });
2997
+ if (recovery.changed) {
2998
+ await saveState();
2999
+ for (const requestId of recovery.acknowledgeIds) {
3000
+ await activeTurnControl.acknowledge(requestId).catch(() => false);
3001
+ }
3002
+ }
3003
+ }
3004
+
2940
3005
  async function createAuditDraftForRun(input = {}) {
2941
3006
  const StorageDb = window.CodexOverleafStorageDb;
2942
3007
  if (!StorageDb || !AuditRecords) {
@@ -3126,6 +3191,8 @@
3126
3191
  }
3127
3192
 
3128
3193
  function appendRunCancelledReport() {
3194
+ const hasQueuedFollowUp = (getActiveSession(state)?.pendingInputs || [])
3195
+ .some(item => item?.status === 'queued');
3129
3196
  appendRunEvent({
3130
3197
  title: tx('Current Codex task was cancelled.', '已中断当前 Codex 任务。'),
3131
3198
  status: 'failed'
@@ -3135,7 +3202,9 @@
3135
3202
  status: 'rejected',
3136
3203
  operations: [],
3137
3204
  applyResults: [],
3138
- nextStep: tx('Edit the task and run again.', '可以修改任务后重新运行。')
3205
+ nextStep: hasQueuedFollowUp
3206
+ ? tx('The first queued follow-up will start next.', '接下来会自动启动队首的后续任务。')
3207
+ : tx('Edit the task and run again.', '可以修改任务后重新运行。')
3139
3208
  });
3140
3209
  }
3141
3210
 
@@ -3148,17 +3217,27 @@
3148
3217
  task.style.height = `${Math.min(task.scrollHeight, 160)}px`;
3149
3218
  }
3150
3219
 
3151
- function syncComposerSendAvailability() {
3220
+ function syncComposerSendAvailability(runningOverride) {
3152
3221
  const runButton = panel?.querySelector('[data-run]');
3153
3222
  if (!runButton) {
3154
3223
  return;
3155
3224
  }
3156
- // While a run is active the button is Cancel and must stay clickable.
3157
- if (currentRunView) {
3158
- runButton.disabled = false;
3159
- return;
3160
- }
3161
- runButton.disabled = !String(panel?.querySelector('[data-task]')?.value || '').trim();
3225
+ const hasInput = Boolean(String(panel?.querySelector('[data-task]')?.value || '').trim());
3226
+ const running = typeof runningOverride === 'boolean'
3227
+ ? runningOverride
3228
+ : Boolean(currentRunView);
3229
+ const action = running && !hasInput ? 'cancel' : 'send';
3230
+ const label = action === 'cancel'
3231
+ ? tr('cancelRun')
3232
+ : running
3233
+ ? tr('queueNextInput')
3234
+ : tr('send');
3235
+ runButton.dataset.action = action;
3236
+ runButton.disabled = action === 'cancel'
3237
+ ? runCancellationRequested
3238
+ : !hasInput;
3239
+ runButton.title = label;
3240
+ runButton.setAttribute('aria-label', label);
3162
3241
  }
3163
3242
 
3164
3243
  function handleTaskInputKeydown(event) {
@@ -3175,6 +3254,7 @@
3175
3254
  function handleTaskInput() {
3176
3255
  autosizeTaskTextarea();
3177
3256
  syncComposerSendAvailability();
3257
+ renderPendingInputs();
3178
3258
  scheduleMirrorPrefetch({
3179
3259
  reason: 'composer-input',
3180
3260
  delayMs: mirrorHealth?.PREFETCH_DEBOUNCE_MS || 1200
@@ -5243,102 +5323,23 @@
5243
5323
  }
5244
5324
 
5245
5325
  function formatProjectSnapshotUserLog(project) {
5246
- const files = project?.files || [];
5247
- if (!files.length) {
5248
- return tx(
5249
- 'No Overleaf project files were read yet. Make sure the project has loaded, or open a .tex file in Overleaf and retry.',
5250
- '还没有读到 Overleaf 项目文件。请确认项目已加载完成,或在 Overleaf 点开一个 .tex 文件后重试。'
5251
- );
5252
- }
5253
-
5254
- const textCount = files.filter(isTextSnapshotFile).length;
5255
- const binaryCount = files.length - textCount;
5256
- const resourceText = binaryCount ? tx(`, ${binaryCount} asset file(s)`, `,${binaryCount} 个资源文件`) : '';
5257
- const activePath = project.activePath ? tx(`, current file: ${project.activePath}`, `,当前文件:${project.activePath}`) : '';
5258
- const focusFiles = getActiveFocusFiles();
5259
- const focusText = focusFiles.length ? tx(`, focus: ${focusFiles.join(', ')}`, `,优先处理:${focusFiles.join(', ')}`) : '';
5260
- return tx(
5261
- `Read Overleaf project: ${textCount} text file(s)${resourceText}${activePath}${focusText}.`,
5262
- `已读取 Overleaf 项目:${textCount} 个文本文件${resourceText}${activePath}${focusText}。`
5263
- );
5326
+ return DiagnosticsPanel.formatProjectSnapshotUserLog(project, { tx, getFocusFiles: getActiveFocusFiles });
5264
5327
  }
5265
5328
 
5266
5329
  function formatProjectSnapshotFileSize(file) {
5267
- if (isTextSnapshotFile(file)) {
5268
- return `${String(file.content || '').length} chars`;
5269
- }
5270
- if (Number.isFinite(Number(file?.size))) {
5271
- return `${Number(file.size)} bytes`;
5272
- }
5273
- return file?.contentBase64 ? `${String(file.contentBase64).length} base64` : 'binary';
5330
+ return DiagnosticsPanel.formatProjectSnapshotFileSize(file);
5274
5331
  }
5275
5332
 
5276
5333
  function formatProjectSnapshotWarning(warning) {
5277
- if (/No source files were captured/i.test(warning)) {
5278
- return tx('No project source files were read. Make sure Overleaf has loaded, or open a .tex file and retry.', '没有读取到项目源文件。请确认 Overleaf 页面加载完成,或点开一个 .tex 文件后重试。');
5279
- }
5280
- if (/Full project snapshot was not captured/i.test(warning)) {
5281
- return tx('The full Overleaf project was not read. To avoid refreshing the local workspace from an incomplete snapshot, reload Overleaf or retry later.', '没有读到完整的 Overleaf 项目。为了避免本地 workspace 用残缺快照覆盖项目,请刷新 Overleaf 或稍后重试。');
5282
- }
5283
- if (/empty or still loading/i.test(warning)) {
5284
- return tx('Some file content is still loading. Wait for Overleaf to finish loading, then retry.', '读取到的文件内容还在加载中。请等 Overleaf 加载完成后重试。');
5285
- }
5286
- if (/suspiciously short|shorter than 80 characters/i.test(warning)) {
5287
- return tx('Some file content is very short and may not have fully loaded. Results may be incomplete.', '部分文件内容很短,可能还没完全加载。结果可能不完整。');
5288
- }
5289
- if (/identical captured content/i.test(warning)) {
5290
- return tx('Several files appear to have identical captured content, so Overleaf file switching may have failed. Reload and retry.', '多个文件内容看起来相同,Overleaf 文件切换可能没有成功。请刷新页面后重试。');
5291
- }
5292
- if (/were skipped/i.test(warning)) {
5293
- return tx('Some project files were skipped, usually images, PDFs, or unreadable files.', '有些项目文件被跳过,通常是图片、PDF 或无法读取的文件。');
5294
- }
5295
- return warning;
5334
+ return DiagnosticsPanel.formatProjectSnapshotWarning(warning, tx);
5296
5335
  }
5297
5336
 
5298
5337
  function appendEditorDiagnostics(editorDiagnostics, projectDiagnostics) {
5299
- if (editorDiagnostics) {
5300
- const active = editorDiagnostics.active;
5301
- appendLog(`Editor probe: active ${active?.tag || 'none'} ${active?.ariaLabel || active?.className || ''} len=${active?.valueLength || 0}; textareas=${editorDiagnostics.textareaCount || 0}; editables=${editorDiagnostics.editableCount || 0}; iframes=${editorDiagnostics.iframeCount || 0}.`);
5302
- if (editorDiagnostics.documentStats) {
5303
- const stats = editorDiagnostics.documentStats;
5304
- appendLog(`DOM stats: elements=${stats.elementCount || 0}; textareas=${stats.textareaCount || 0}; cm=${stats.cmCount || 0}; role textbox=${stats.roleTextboxCount || 0}.`);
5305
- }
5306
- if (editorDiagnostics.unstableStore) {
5307
- const store = editorDiagnostics.unstableStore;
5308
- const readable = (store.readable || [])
5309
- .map(item => `${item.path}:${item.present ? item.type : 'missing'}`)
5310
- .join(', ');
5311
- appendLog(`Overleaf store: ${store.present ? 'present' : 'missing'}${readable ? `; ${readable}` : ''}.`);
5312
- }
5313
- if (editorDiagnostics.codeMirrorView) {
5314
- const view = editorDiagnostics.codeMirrorView;
5315
- appendLog(`CodeMirror view: docLength=${view.docLength || 0}; dispatch=${Boolean(view.hasDispatch)}; source=${view.source || 'unknown'}.`);
5316
- }
5317
- if (editorDiagnostics.globals) {
5318
- const globals = editorDiagnostics.globals;
5319
- appendLog(`Overleaf globals: overleaf=${globals.overleaf || 'missing'}; Overleaf=${globals.Overleaf || 'missing'}; _ide=${globals._ide || 'missing'}.`);
5320
- }
5321
- for (const item of [...(editorDiagnostics.textareas || []), ...(editorDiagnostics.editables || [])].slice(0, 3)) {
5322
- appendLog(`Editor candidate: ${item.tag || 'node'} ${item.ariaLabel || item.className || item.id || ''} len=${item.valueLength || 0}.`);
5323
- }
5324
- }
5325
- if (projectDiagnostics) {
5326
- const records = projectDiagnostics.docRecords || [];
5327
- appendLog(`Project probe: doc records=${projectDiagnostics.docRecordCount || 0}; roots=${(projectDiagnostics.internalRootKeys || []).slice(0, 6).join(', ') || 'none'}.`);
5328
- for (const record of records.slice(0, 4)) {
5329
- appendLog(`Doc record: ${record.path} id=${record.id} source=${record.source || 'internal'}.`);
5330
- }
5331
- }
5338
+ DiagnosticsPanel.appendEditorDiagnostics(editorDiagnostics, projectDiagnostics, appendLog);
5332
5339
  }
5333
5340
 
5334
5341
  function appendProjectWarnings(project) {
5335
- const warnings = getProjectSnapshotWarnings(project);
5336
- for (const warning of warnings.blocking) {
5337
- appendLog(`Snapshot blocked: ${warning}`);
5338
- }
5339
- for (const warning of warnings.nonBlocking) {
5340
- appendLog(`Snapshot warning: ${warning}`);
5341
- }
5342
+ DiagnosticsPanel.appendProjectWarnings(project, { getWarnings: getProjectSnapshotWarnings, appendLog });
5342
5343
  }
5343
5344
 
5344
5345
  function getProjectSnapshotWarnings(project) {
@@ -5388,40 +5389,15 @@
5388
5389
  }
5389
5390
 
5390
5391
  function isTextSnapshotFile(file) {
5391
- return Boolean(file && file.kind !== 'binary' && typeof file.content === 'string');
5392
+ return DiagnosticsPanel.isTextSnapshotFile(file);
5392
5393
  }
5393
5394
 
5394
5395
  function uniqueContentSignatures(files) {
5395
- const signatures = files.map(file => {
5396
- const content = String(file.content || '');
5397
- return `${content.length}:${content.slice(0, 120)}:${content.slice(-120)}`;
5398
- });
5399
- return Array.from(new Set(signatures));
5396
+ return DiagnosticsPanel.uniqueContentSignatures(files);
5400
5397
  }
5401
5398
 
5402
5399
  function appendReviewingDiagnostics(diagnostics) {
5403
- if (!diagnostics) {
5404
- appendLog(tx('Probe diagnostics unavailable.', '诊断探针不可用。'));
5405
- return;
5406
- }
5407
-
5408
- appendLog(`Probe saw ${diagnostics.controlCount || 0} controls; body Reviewing=${Boolean(diagnostics.bodyTextHasReviewing)}, text Reviewing=${Boolean(diagnostics.textContentHasReviewing)}.`);
5409
- const controls = diagnostics.reviewLikeControls || [];
5410
- if (!controls.length) {
5411
- appendLog('Probe did not see review/track/suggest controls in the page DOM.');
5412
- return;
5413
- }
5414
-
5415
- for (const control of controls.slice(0, 4)) {
5416
- const label = [
5417
- control.text,
5418
- control.ariaLabel && `aria:${control.ariaLabel}`,
5419
- control.title && `title:${control.title}`,
5420
- control.dataTestId && `test:${control.dataTestId}`,
5421
- control.id && `id:${control.id}`
5422
- ].filter(Boolean).join(' | ');
5423
- appendLog(`Review-like ${control.tag || 'node'}: ${label || control.htmlSnippet || '(no label)'}`);
5424
- }
5400
+ DiagnosticsPanel.appendReviewingDiagnostics(diagnostics, { appendLog, tx });
5425
5401
  }
5426
5402
 
5427
5403
  function sendNative(payload) {
@@ -5991,6 +5967,7 @@
5991
5967
  runs: Array.isArray(session.runs) ? session.runs : [],
5992
5968
  history: Array.isArray(session.history) ? session.history : [],
5993
5969
  task: typeof session.task === 'string' ? session.task : '',
5970
+ pendingInputs: Array.isArray(session.pendingInputs) ? session.pendingInputs : [],
5994
5971
  mode: session.mode || prefs.mode || 'confirm', providerId: session.providerId || 'builtin',
5995
5972
  model: session.model || prefs.model || 'gpt-5.4',
5996
5973
  reasoningEffort: session.reasoningEffort || prefs.reasoningEffort || 'high',
@@ -6152,14 +6129,11 @@
6152
6129
  updatedAt: session.updatedAt
6153
6130
  }, { preserveRunActionPayload: true })
6154
6131
  ));
6155
- if (sessionRecords.length) {
6156
- await StorageDb.putRecords('sessions', sessionRecords);
6157
- }
6158
- const keepSessionIds = new Set(sessionRecords.map(record => record.id));
6159
- const existingSessions = await StorageDb.getAllByIndex('sessions', 'projectId', projectId);
6160
- await Promise.all(existingSessions
6161
- .filter(session => session?.id && !keepSessionIds.has(session.id))
6162
- .map(session => StorageDb.deleteRecord('sessions', session.id)));
6132
+ await window.CodexOverleafSessionPersistence.writeSessions({
6133
+ Migration, StorageDb, projectId,
6134
+ deletedSessionIds: options?.deletedSessionIds,
6135
+ sessionRecords
6136
+ });
6163
6137
  } catch (error) {
6164
6138
  // Fallback: try legacy save
6165
6139
  try {
@@ -6313,7 +6287,7 @@
6313
6287
  saveStateSoon();
6314
6288
  return;
6315
6289
  }
6316
- if (event?.target?.matches?.('[data-speed]')) state = updateActiveSession(state, { speedTier: readSelectedSpeedInput() }); renderReasoningOptions(getRenderedModelEntries());
6290
+ if (event?.target?.matches?.('[data-speed]')) renderReasoningOptions(getRenderedModelEntries());
6317
6291
  renderSpeedOptions(getRenderedModelEntries());
6318
6292
  readPanelInputs();
6319
6293
  renderModelConfigChoices();
@@ -6367,14 +6341,21 @@
6367
6341
  syncExperimentalOtToggleForProject(projectId);
6368
6342
  }
6369
6343
  persistComposerAttachmentsToState();
6344
+ const model = readSelectedModelInput();
6345
+ const reasoningEffort = panel.querySelector('[data-reasoning]').value;
6346
+ const speedTier = readSelectedSpeedInput();
6370
6347
  state = updateActiveSession(state, {
6371
- model: readSelectedModelInput(),
6372
- reasoningEffort: panel.querySelector('[data-reasoning]').value,
6373
- speedTier: readSelectedSpeedInput(),
6374
6348
  mode: panel.querySelector('[data-mode]').value,
6375
6349
  task: panel.querySelector('[data-task]').value,
6376
6350
  requireReviewing: panel.querySelector('[data-require-reviewing]').checked
6377
6351
  });
6352
+ state.providerId = state.providerId || 'builtin';
6353
+ state.model = model;
6354
+ state.reasoningEffort = reasoningEffort;
6355
+ state.speedTier = speedTier;
6356
+ if (state.session) {
6357
+ state.session.providerId = state.providerId;
6358
+ }
6378
6359
  state.autoRecompile = panel.querySelector('[data-auto-recompile]')?.checked !== false;
6379
6360
  const experimentalOtCheckbox = panel.querySelector('[data-experimental-ot]');
6380
6361
  if (experimentalOtCheckbox) {
@@ -6519,12 +6500,7 @@
6519
6500
 
6520
6501
 
6521
6502
  function setRunning(running) {
6522
- const runButton = panel.querySelector('[data-run]');
6523
- // Running -> the button is Cancel and must never be disabled; idle -> it
6524
- // is Send and disables on an empty composer (feedback instead of no-op).
6525
- runButton.disabled = running ? false : !String(panel.querySelector('[data-task]')?.value || '').trim();
6526
- runButton.title = running ? tr('cancelRun') : tr('send');
6527
- runButton.setAttribute('aria-label', running ? tr('cancelRun') : tr('send'));
6503
+ syncComposerSendAvailability(running);
6528
6504
  panel.querySelector('[data-new-session]').disabled = false;
6529
6505
  panel.querySelector('[data-diagnostics-snapshot]').disabled = running;
6530
6506
  if (running) {
@@ -6532,9 +6508,10 @@
6532
6508
  }
6533
6509
  panel.dataset.running = running ? 'true' : 'false';
6534
6510
  panel.dataset.cancelling = running && runCancellationRequested ? 'true' : 'false';
6511
+ renderPendingInputs();
6535
6512
  }
6536
6513
 
6537
- function startRunView({ task, mode, model, reasoningEffort, speedTier }) {
6514
+ function startRunView({ task, mode, model, reasoningEffort, speedTier, queueItemId = '' }) {
6538
6515
  let attachments = [];
6539
6516
  if (Array.isArray(arguments[0]?.attachments)) {
6540
6517
  attachments = arguments[0].attachments;
@@ -6558,6 +6535,10 @@
6558
6535
  undoTrackedChanges: [],
6559
6536
  undoExpectedFiles: [],
6560
6537
  undoStatus: '',
6538
+ queueItemId,
6539
+ nativeRequestId: '',
6540
+ codexTurnId: '',
6541
+ nativeEventSeq: 0,
6561
6542
  // Welcome-panel + write-guard:
6562
6543
  // Immutable per-run capture of the project this run was submitted
6563
6544
  // against. Every writeback / accept / undo dispatch attaches this id
@@ -6602,7 +6583,10 @@
6602
6583
  report: root.querySelector('[data-run-report]'),
6603
6584
  status: root.querySelector('[data-run-status]'),
6604
6585
  projectFiles: captureProjectReferenceFiles(arguments[0]?.project),
6605
- startedAt: Date.now()
6586
+ startedAt: Date.now(),
6587
+ queueItemId,
6588
+ nativeRequestId: '',
6589
+ activeTurn: null
6606
6590
  };
6607
6591
  }
6608
6592
 
@@ -6619,6 +6603,8 @@
6619
6603
  record.status = status;
6620
6604
  record.statusText = statusText;
6621
6605
  record.finishedAt = new Date().toISOString();
6606
+ currentRunView.terminalStatus = status;
6607
+ runGuidanceController.settleView(currentRunView);
6622
6608
  // Welcome-panel + write-guard:
6623
6609
  // when the user navigated away mid-run, the URL-derived projectId now
6624
6610
  // belongs to a different project (or /project). Routing this finish
@@ -6649,11 +6635,16 @@
6649
6635
  }
6650
6636
 
6651
6637
 
6652
- function appendNativeEvent(event) {
6638
+ function appendNativeEvent(event, journalSeq = 0) {
6653
6639
  if (!event) {
6654
6640
  return;
6655
6641
  }
6656
6642
 
6643
+ if (event.type === 'codex.turn.bound') {
6644
+ bindActiveTurn(event.detail || {}, journalSeq);
6645
+ return;
6646
+ }
6647
+
6657
6648
  const activity = mapAgentEventToActivity(event, { locale: getLocale() });
6658
6649
  if (!activity?.visible || activity.kind === 'technical') {
6659
6650
  return;
@@ -6669,10 +6660,30 @@
6669
6660
  streamKey: activity.streamKey,
6670
6661
  streamRole: activity.streamRole,
6671
6662
  appendText: activity.appendText,
6672
- replaceText: activity.replaceText
6663
+ replaceText: activity.replaceText,
6664
+ nativeEventSeq: journalSeq
6673
6665
  });
6674
6666
  }
6675
6667
 
6668
+ function bindActiveTurn(detail = {}, journalSeq = 0) {
6669
+ if (!currentRunView) {
6670
+ return;
6671
+ }
6672
+ const threadId = String(detail.threadId || '');
6673
+ const turnId = String(detail.turnId || '');
6674
+ currentRunView.activeTurn = { threadId, turnId };
6675
+ const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
6676
+ if (record) {
6677
+ record.codexTurnId = turnId;
6678
+ record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(journalSeq || 0));
6679
+ }
6680
+ if (threadId) {
6681
+ updateSessionById(currentRunView.sessionId, { codexThreadId: threadId });
6682
+ }
6683
+ renderPendingInputs();
6684
+ saveState().catch(() => {});
6685
+ }
6686
+
6676
6687
  function appendTechnicalEvent(event) {
6677
6688
  void event;
6678
6689
  }
@@ -6703,6 +6714,7 @@
6703
6714
  kind: input.kind || 'activity',
6704
6715
  subagent: input.subagent === true ? true : undefined,
6705
6716
  technicalDetail: sanitizeAssistantVisibleValue(input.technicalDetail),
6717
+ guidanceId: sanitizeAssistantVisibleText(input.guidanceId),
6706
6718
  streamKey: sanitizeAssistantVisibleText(input.streamKey),
6707
6719
  streamRole: sanitizeAssistantVisibleText(input.streamRole),
6708
6720
  appendText: typeof input.appendText === 'string' ? sanitizeAssistantVisibleText(input.appendText) : input.appendText,
@@ -6734,6 +6746,9 @@
6734
6746
  const record = findRunRecord(currentRunView.recordId, currentRunView.sessionId);
6735
6747
  let renderedEvent = event;
6736
6748
  if (record) {
6749
+ if (Number.isFinite(Number(input.nativeEventSeq))) {
6750
+ record.nativeEventSeq = Math.max(Number(record.nativeEventSeq || 0), Number(input.nativeEventSeq || 0));
6751
+ }
6737
6752
  // v1.8.1: keep the session's activity timestamp honest DURING a run —
6738
6753
  // it was pinned to run start, so the dashboard's 30-minute zombie
6739
6754
  // heuristic mislabeled any long live run as interrupted (fleet P1).
@@ -6807,6 +6822,7 @@
6807
6822
  currentRunView.root = root;
6808
6823
  currentRunView.runProcess = root.querySelector('[data-run-process]');
6809
6824
  currentRunView.processLabel = root.querySelector('[data-run-process-summary]');
6825
+ currentRunView.guidance = root.querySelector('[data-run-guidance]');
6810
6826
  currentRunView.events = root.querySelector('[data-run-events]');
6811
6827
  currentRunView.report = root.querySelector('[data-run-report]');
6812
6828
  currentRunView.status = root.querySelector('[data-run-status]');
@@ -6824,6 +6840,7 @@
6824
6840
  if (event.kind === 'technical') {
6825
6841
  return;
6826
6842
  }
6843
+ if (runGuidanceController.appendToView(event, view)) return;
6827
6844
  if (event.kind === 'stream') {
6828
6845
  // Stream deltas update an existing line in place — they must not inflate
6829
6846
  // the unread-step counter.
@@ -8092,7 +8109,8 @@
8092
8109
  detail: event.detail,
8093
8110
  timestamp: event.timestamp || new Date().toISOString(),
8094
8111
  kind: event.kind || 'activity',
8095
- technicalDetail: event.technicalDetail
8112
+ technicalDetail: event.technicalDetail,
8113
+ guidanceId: sanitizeAssistantVisibleText(event.guidanceId)
8096
8114
  };
8097
8115
  record.events = [...(record.events || []), normalized].slice(-MAX_RUN_EVENTS);
8098
8116
  saveStateSoon();