codex-overleaf-link 2.3.1 → 2.3.3

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 (39) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/content/assetTransferBroker.js +169 -0
  4. package/extension/src/content/contentRuntime.js +28 -1
  5. package/extension/src/content/generated/content.bundle.js +630 -68
  6. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  7. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  8. package/extension/src/content/markdownDomRenderer.js +70 -0
  9. package/extension/src/content/modelPicker.js +2 -2
  10. package/extension/src/content/modelPickerSupport.js +3 -3
  11. package/extension/src/content/pageBridgeClient.js +1 -0
  12. package/extension/src/content/pendingInputView.js +16 -3
  13. package/extension/src/content/providerSettingsDialog.js +1 -0
  14. package/extension/src/content/runResultActions.js +114 -0
  15. package/extension/src/content/runTimelineView.js +20 -0
  16. package/extension/src/content/sessionForkController.js +120 -0
  17. package/extension/src/content/writebackController.js +3 -0
  18. package/extension/src/content/writebackOrchestrator.js +3 -1
  19. package/extension/src/page/binaryAssetUploader.js +343 -0
  20. package/extension/src/page/trackedChangesLifecycle.js +2 -0
  21. package/extension/src/pageBridge.js +30 -1
  22. package/extension/src/shared/compatibility.js +4 -2
  23. package/extension/src/shared/i18n.js +16 -0
  24. package/extension/src/shared/models.js +13 -2
  25. package/extension/src/shared/pageRpcContract.js +5 -1
  26. package/extension/src/shared/sessionState.js +12 -4
  27. package/extension/src/shared/storageDb.js +1 -1
  28. package/extension/src/shared/storageRunActions.js +11 -1
  29. package/extension/styles/panel.css +232 -31
  30. package/native-host/src/codexModels.js +6 -2
  31. package/native-host/src/codexRuntimeIdentity.js +79 -4
  32. package/native-host/src/codexSessionRunner.js +5 -1
  33. package/native-host/src/codexThreadFork.js +103 -0
  34. package/native-host/src/mirrorWorkspace.js +5 -19
  35. package/native-host/src/nativeAssetTransfer.js +183 -0
  36. package/native-host/src/nativeEnvironment.js +4 -1
  37. package/native-host/src/taskRunnerRuntime.js +22 -0
  38. package/native-host/src/textPatch.js +707 -259
  39. package/package.json +1 -1
@@ -2,13 +2,14 @@
2
2
  "schemaVersion": 1,
3
3
  "entry": "extension/entries/content-entry.mjs",
4
4
  "output": "extension/src/content/generated/content.bundle.js",
5
- "sourceDigest": "ddbc07ac8d34d864aa3648c53211c5858e0312a73df815a0392bc52ec5eac1cc",
6
- "outputDigest": "42051c8624bdfca3094c8bef2430094e55b7e1ec49d475ac27f6df14629bf313",
7
- "byteLength": 2285557,
5
+ "sourceDigest": "4b5c2f7a7653947ac71ae42a3301c7547f789c2ca33e25f99f6781f31d1147be",
6
+ "outputDigest": "4e32bcf7d8be72541bb39508e38335ad0e32e41844afa45d7f0d34c9481a1cec",
7
+ "byteLength": 2313648,
8
8
  "inputs": [
9
9
  "extension/entries/content-entry.mjs",
10
10
  "extension/src/content/activeTurnControl.js",
11
11
  "extension/src/content/applyResultFormatters.js",
12
+ "extension/src/content/assetTransferBroker.js",
12
13
  "extension/src/content/composerAttachments.js",
13
14
  "extension/src/content/composerPanel.js",
14
15
  "extension/src/content/contentRuntime.js",
@@ -45,11 +46,13 @@
45
46
  "extension/src/content/runGuidanceController.js",
46
47
  "extension/src/content/runGuidanceView.js",
47
48
  "extension/src/content/runQueueScheduler.js",
49
+ "extension/src/content/runResultActions.js",
48
50
  "extension/src/content/runSettlementPersistence.js",
49
51
  "extension/src/content/runTimelineView.js",
50
52
  "extension/src/content/scopedPersistenceCoordinator.js",
51
53
  "extension/src/content/scopedPersistenceDetachedCommit.js",
52
54
  "extension/src/content/scopedPersistencePanelState.js",
55
+ "extension/src/content/sessionForkController.js",
53
56
  "extension/src/content/sessionManager.js",
54
57
  "extension/src/content/sessionMenuView.js",
55
58
  "extension/src/content/sessionPanel.js",
@@ -15,6 +15,7 @@
15
15
  'ActiveTurnControl',
16
16
  'AgentTranscript',
17
17
  'ApplyResultFormatters',
18
+ 'AssetTransferBroker',
18
19
  'AuditRecords',
19
20
  'Compatibility',
20
21
  'ComposerAttachments',
@@ -53,12 +54,14 @@
53
54
  'RunGuidanceController',
54
55
  'RunInputQueue',
55
56
  'RunQueueScheduler',
57
+ 'RunResultActions',
56
58
  'RunSettlementPersistence',
57
59
  'RunTimelineView',
58
60
  'ScopedPersistenceCoordinator',
59
61
  'ScopedPersistenceTransaction',
60
62
  'SensitiveScan',
61
63
  'SessionManager',
64
+ 'SessionForkController',
62
65
  'SessionPanel',
63
66
  'SessionPersistence',
64
67
  'SessionState',
@@ -442,9 +442,79 @@
442
442
  return error;
443
443
  }
444
444
 
445
+ function decorateCompletionReport(container, options = {}) {
446
+ const tx = typeof options.tx === 'function' ? options.tx : en => en;
447
+ const document = container?.ownerDocument || globalThis.document;
448
+ if (!container || !document) return;
449
+
450
+ for (const block of Array.from(container.children || [])) {
451
+ const textNode = Array.from(block.childNodes || [])
452
+ .find(node => node?.nodeType === 3 && String(node.textContent || '').trim());
453
+ if (!textNode) continue;
454
+ const text = String(textNode.textContent || '');
455
+ const match = text.match(/^(\s*)(Conclusion:|结论:|Changes:|修改:)(\s*)/);
456
+ if (!match || !textNode.parentNode) continue;
457
+ const label = document.createElement('span');
458
+ label.className = 'run-system-label';
459
+ label.dataset.systemLabel = ['Conclusion:', '结论:'].includes(match[2])
460
+ ? 'conclusion'
461
+ : 'changes';
462
+ label.textContent = `${match[2]}${match[3]}`;
463
+ textNode.textContent = `${match[1]}${text.slice(match[0].length)}`;
464
+ textNode.parentNode.insertBefore(label, textNode);
465
+ }
466
+ decorateChangeReceipts(container, document, tx);
467
+ }
468
+
469
+ function decorateChangeReceipts(container, document, tx) {
470
+ const label = container.querySelector?.('.run-system-label[data-system-label="changes"]');
471
+ const list = label?.closest?.('p')?.nextElementSibling;
472
+ if (!list || !['UL', 'OL'].includes(list.tagName)) return;
473
+
474
+ const receipts = [];
475
+ for (const item of Array.from(list.children || [])) {
476
+ const original = String(item.textContent || '').trim();
477
+ const match = original.match(/^(.+?):\s+(edit|create|rename|move|delete)(?:\s+\((.+)\))?$/i)
478
+ || original.match(/^(.+?):\s*(编辑|新建|重命名|移动|删除)(?:((.+)))?$/);
479
+ if (!match) return;
480
+ receipts.push({ item, original, file: match[1], operation: match[2] });
481
+ }
482
+ if (!receipts.length) return;
483
+
484
+ const syncedFiles = new Set(receipts.map(receipt => receipt.file)).size;
485
+ const summary = document.createElement('div');
486
+ summary.className = 'run-change-summary';
487
+ const check = document.createElement('span');
488
+ check.className = 'run-change-summary__check';
489
+ check.setAttribute('aria-hidden', 'true');
490
+ check.textContent = '✓';
491
+ const text = document.createElement('span');
492
+ text.textContent = tx(
493
+ `${syncedFiles} ${syncedFiles === 1 ? 'file' : 'files'} synced`,
494
+ `已同步 ${syncedFiles} 个文件`
495
+ );
496
+ summary.append(check, text);
497
+ list.parentNode?.insertBefore(summary, list);
498
+ list.classList.add('run-change-receipts');
499
+
500
+ for (const receipt of receipts) {
501
+ const file = document.createElement('span');
502
+ file.className = 'run-change-receipt__file';
503
+ file.textContent = receipt.file;
504
+ const operation = document.createElement('span');
505
+ operation.className = 'run-change-receipt__operation';
506
+ operation.textContent = receipt.operation;
507
+ receipt.item.classList.add('run-change-receipt');
508
+ receipt.item.setAttribute('aria-label', receipt.original);
509
+ receipt.item.title = receipt.original;
510
+ receipt.item.replaceChildren(file, operation);
511
+ }
512
+ }
513
+
445
514
  return {
446
515
  DEFAULT_LIMITS,
447
516
  countTokens,
517
+ decorateCompletionReport,
448
518
  renderMarkdown,
449
519
  stripEmptyHtmlComments
450
520
  };
@@ -452,8 +452,8 @@
452
452
  if (effort === 'xhigh') return 'Max';
453
453
  }
454
454
  const labels = getLocale() === 'zh'
455
- ? { none: '关闭', minimal: '最小', low: '低', medium: '中', high: '高', xhigh: '超高' }
456
- : { none: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'XHigh' };
455
+ ? { none: '关闭', minimal: '最小', low: '低', medium: '中', high: '高', xhigh: '超高', max: '最大', ultra: '极限' }
456
+ : { none: 'Off', minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'XHigh', max: 'Max', ultra: 'Ultra' };
457
457
  return labels[effort] || effort || '';
458
458
  }
459
459
 
@@ -19,7 +19,7 @@
19
19
  }
20
20
 
21
21
  function normalizeReasoningEffortsForSelect(efforts) {
22
- const allowed = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
22
+ const allowed = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
23
23
  return Array.from(new Set((Array.isArray(efforts) ? efforts : [])
24
24
  .map(effort => normalizeModelOptionId(effort))
25
25
  .filter(effort => allowed.has(effort))));
@@ -92,12 +92,12 @@
92
92
  const normalized = {
93
93
  id,
94
94
  label: typeof model?.label === 'string' && model.label.length > 0 ? model.label : id,
95
- reasoningEfforts: Array.isArray(model?.reasoningEfforts) ? model.reasoningEfforts.slice() : [],
95
+ reasoningEfforts: normalizeReasoningEffortsForSelect(model?.reasoningEfforts),
96
96
  reasoningPresentation: typeof model?.reasoningPresentation === 'string' ? model.reasoningPresentation : '',
97
97
  speedTiers: normalizeSpeedTiersForSelect(model?.speedTiers)
98
98
  };
99
99
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultReasoningEffort')) {
100
- normalized.defaultReasoningEffort = model.defaultReasoningEffort;
100
+ normalized.defaultReasoningEffort = normalizeModelOptionId(model.defaultReasoningEffort);
101
101
  }
102
102
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultSpeedTier')) {
103
103
  normalized.defaultSpeedTier = model.defaultSpeedTier;
@@ -21,6 +21,7 @@
21
21
  ['src/page/overleafProjectSnapshot.js', 'codex-overleaf-project-snapshot-script'],
22
22
  ['src/page/pageBridgeCapability.js', 'codex-overleaf-page-bridge-capability-script'],
23
23
  ['src/page/treeOperations.js', 'codex-overleaf-tree-operations-script', true],
24
+ ['src/page/binaryAssetUploader.js', 'codex-overleaf-binary-asset-uploader-script', true],
24
25
  ['src/page/snapshotRouter.js', 'codex-overleaf-snapshot-router-script'],
25
26
  ['src/page/writeGuard.js', 'codex-overleaf-write-guard-script', true],
26
27
  ['src/page/trackedChangesLifecycle.js', 'codex-overleaf-tracked-changes-lifecycle-script', true],
@@ -16,10 +16,17 @@
16
16
  }
17
17
  container.replaceChildren();
18
18
  container.hidden = !items.length;
19
+ if (!items.length) {
20
+ return;
21
+ }
19
22
  for (const item of items) {
20
23
  const row = document.createElement('article');
21
24
  row.className = 'codex-pending-input';
22
25
  row.dataset.status = item.status || 'queued';
26
+ const marker = document.createElement('span');
27
+ marker.className = 'codex-pending-input-marker';
28
+ marker.setAttribute('aria-hidden', 'true');
29
+ marker.append(createIcon('queue'));
23
30
  const copy = document.createElement('div');
24
31
  copy.className = 'codex-pending-input-copy';
25
32
  const status = document.createElement('span');
@@ -34,7 +41,10 @@
34
41
  const text = document.createElement('span');
35
42
  text.className = 'codex-pending-input-text';
36
43
  text.textContent = item.text;
37
- copy.append(status, text);
44
+ copy.append(text);
45
+ if (item.status && item.status !== 'queued') {
46
+ copy.append(status);
47
+ }
38
48
  const actions = document.createElement('div');
39
49
  actions.className = 'codex-pending-input-actions';
40
50
  if (item.status === 'paused') {
@@ -60,7 +70,8 @@
60
70
  remove.disabled = item.status === 'steering' || item.status === 'executing';
61
71
  text.title = item.text;
62
72
  actions.append(remove);
63
- row.append(copy, actions);
73
+ row.setAttribute('aria-label', `${status.textContent || options.tr?.('queuedInputQueued') || 'Queued'}: ${item.text}`);
74
+ row.append(marker, copy, actions);
64
75
  container.append(row);
65
76
  }
66
77
  }
@@ -93,7 +104,9 @@
93
104
  svg.setAttribute('focusable', 'false');
94
105
  const paths = icon === 'guide'
95
106
  ? ['M4 4.5v4a4 4 0 0 0 4 4h7', 'm12 9 3.5 3.5L12 16']
96
- : ['M6 6h8', 'M8 6V4.5h4V6', 'm7.2 8 0.6 7.5h4.4l.6-7.5'];
107
+ : icon === 'queue'
108
+ ? ['M4 5.5h12', 'M4 10h12', 'M4 14.5h8']
109
+ : ['M6 6h8', 'M8 6V4.5h4V6', 'm7.2 8 0.6 7.5h4.4l.6-7.5'];
97
110
  for (const value of paths) {
98
111
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
99
112
  path.setAttribute('d', value);
@@ -985,6 +985,7 @@
985
985
  return;
986
986
  }
987
987
  const style = instance.document.defaultView.getComputedStyle(panel);
988
+ instance.root.dataset.theme = panel.dataset.theme === 'light' ? 'light' : 'dark';
988
989
  instance.root.style.setProperty('--provider-panel-bg', style.backgroundColor || '#151716');
989
990
  instance.root.style.setProperty('--provider-panel-color', style.color || '#eceeea');
990
991
  }
@@ -0,0 +1,114 @@
1
+ (function initCodexOverleafRunResultActions(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafRunResultActions = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function runResultActionsFactory() {
6
+ 'use strict';
7
+
8
+ function create(deps = {}) {
9
+ const {
10
+ tr,
11
+ getLocale,
12
+ forkRunFromNode,
13
+ document: documentRef = globalThis.document,
14
+ navigator: navigatorRef = globalThis.navigator
15
+ } = deps;
16
+
17
+ function configureResultActions(root, run) {
18
+ const actions = root.querySelector('.run-result-actions');
19
+ const reports = root.querySelectorAll('.run-completion-report');
20
+ const report = reports[reports.length - 1];
21
+ if (!actions || !report) return;
22
+ report.append(actions);
23
+ actions.hidden = false;
24
+ configureCopyButton(root);
25
+ configureForkButton(root, run);
26
+ configureCompletedTime(actions, run);
27
+ }
28
+
29
+ function configureForkButton(root, run) {
30
+ const existing = root.querySelector('[data-run-fork]');
31
+ const button = existing.cloneNode(true);
32
+ existing.replaceWith(button);
33
+ const available = run.forkSnapshot !== true
34
+ && run.status !== 'running'
35
+ && Boolean(run.codexTurnId)
36
+ && typeof forkRunFromNode === 'function';
37
+ button.hidden = false;
38
+ button.disabled = !available;
39
+ button.title = tr(available ? 'forkRunTitle' : 'forkRunUnavailable');
40
+ button.setAttribute('aria-label', button.title);
41
+ button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h3.5c3.5 0 4.2-4 7.5-4h2m-2.5-2.5L16 6l-2.5 2.5M6.5 10c3.5 0 4.2 4 7.5 4h2m-2.5-2.5L16 14l-2.5 2.5"/></svg>';
42
+ if (!available) return;
43
+ button.addEventListener('click', async event => {
44
+ event.stopPropagation();
45
+ button.disabled = true;
46
+ button.classList.add('is-running');
47
+ try {
48
+ await forkRunFromNode(run.id);
49
+ } catch (_error) {
50
+ button.disabled = false;
51
+ button.classList.remove('is-running');
52
+ }
53
+ });
54
+ }
55
+
56
+ function configureCompletedTime(actions, run) {
57
+ const target = actions.querySelector('[data-run-completed-time]');
58
+ const finishedAt = String(run.finishedAt || '').trim();
59
+ const date = finishedAt ? new Date(finishedAt) : null;
60
+ if (!target || !date || !Number.isFinite(date.getTime())) return;
61
+ target.hidden = false;
62
+ target.textContent = new Intl.DateTimeFormat(getLocale(), {
63
+ hour: '2-digit',
64
+ minute: '2-digit'
65
+ }).format(date);
66
+ target.title = date.toLocaleString(getLocale());
67
+ }
68
+
69
+ function configureCopyButton(root) {
70
+ const existing = root.querySelector('[data-run-copy]');
71
+ const button = existing.cloneNode(true);
72
+ existing.replaceWith(button);
73
+ const finalAnswer = root.querySelector('.run-completion-report .run-final-answer');
74
+ const copyText = String(finalAnswer?.innerText || finalAnswer?.textContent || '').trim();
75
+ button.hidden = false;
76
+ button.disabled = !copyText;
77
+ button.title = tr('copyConclusionTitle');
78
+ button.setAttribute('aria-label', button.title);
79
+ button.innerHTML = '<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9" rx="2"/><path d="M13.5 6.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6.5a2 2 0 0 0 2 2h1.5"/></svg>';
80
+ if (!copyText) return;
81
+ button.addEventListener('click', async event => {
82
+ event.stopPropagation();
83
+ try {
84
+ await writeClipboardText(copyText);
85
+ button.title = tr('copyConclusionDone');
86
+ } catch (_error) {
87
+ button.title = tr('copyConclusionFailed');
88
+ }
89
+ button.setAttribute('aria-label', button.title);
90
+ });
91
+ }
92
+
93
+ async function writeClipboardText(text) {
94
+ if (navigatorRef?.clipboard?.writeText) {
95
+ await navigatorRef.clipboard.writeText(text);
96
+ return;
97
+ }
98
+ const textarea = documentRef.createElement('textarea');
99
+ textarea.value = text;
100
+ textarea.setAttribute('readonly', '');
101
+ textarea.style.position = 'fixed';
102
+ textarea.style.opacity = '0';
103
+ documentRef.body.append(textarea);
104
+ textarea.select();
105
+ const copied = documentRef.execCommand('copy');
106
+ textarea.remove();
107
+ if (!copied) throw new Error('clipboard_copy_failed');
108
+ }
109
+
110
+ return Object.freeze({ configureResultActions });
111
+ }
112
+
113
+ return Object.freeze({ create });
114
+ });
@@ -13,6 +13,7 @@
13
13
  function create(deps = {}) {
14
14
  const {
15
15
  RunGuidanceView,
16
+ RunResultActions,
16
17
  tr,
17
18
  tx,
18
19
  getLocale,
@@ -29,6 +30,7 @@
29
30
  cssEscape,
30
31
  renderMarkdownInlineText,
31
32
  renderMarkdownBlockText,
33
+ decorateCompletionReport,
32
34
  sanitizeAssistantVisibleText,
33
35
  sanitizeAssistantVisibleValue,
34
36
  stripEmptyHtmlCommentPlaceholders = value => value,
@@ -346,6 +348,11 @@
346
348
  </summary>
347
349
  </details>
348
350
  <div class="run-activity-list" data-run-events></div>
351
+ <div class="run-result-actions" hidden>
352
+ <button type="button" class="run-copy-action" data-run-copy hidden></button>
353
+ <button type="button" class="run-fork-action" data-run-fork hidden></button>
354
+ <span class="run-completed-time" data-run-completed-time hidden></span>
355
+ </div>
349
356
  <div class="run-report" data-run-report aria-live="polite" hidden></div>
350
357
  </div>
351
358
  `;
@@ -399,6 +406,7 @@
399
406
  }
400
407
  }
401
408
 
409
+ RunResultActions.configureResultActions(root, run);
402
410
  configureAcceptButton(root, run);
403
411
  configureUndoButton(root, run);
404
412
  return root;
@@ -702,6 +710,7 @@
702
710
  }
703
711
  if (mainSections.length) {
704
712
  renderMarkdownBlockText(main, mainSections.join('\n\n'));
713
+ decorateCompletionReport(main, { tx });
705
714
  report.append(main);
706
715
  }
707
716
 
@@ -722,6 +731,7 @@
722
731
  const body = document.createElement('div');
723
732
  body.className = 'run-final-answer';
724
733
  renderMarkdownBlockText(body, split.body || flatText);
734
+ decorateCompletionReport(body, { tx });
725
735
  report.append(body);
726
736
  appendCompileFix(report);
727
737
  appendRejectedRedo(report);
@@ -886,6 +896,11 @@
886
896
  const button = existing.cloneNode(true);
887
897
  existing.replaceWith(button);
888
898
 
899
+ if (run.forkSnapshot === true) {
900
+ button.hidden = true;
901
+ return;
902
+ }
903
+
889
904
  if (isTrackedChangeLifecycleRun(run)) {
890
905
  configureLifecycleUndoButton(button, run);
891
906
  return;
@@ -982,6 +997,11 @@
982
997
  const button = existing.cloneNode(true);
983
998
  existing.replaceWith(button);
984
999
 
1000
+ if (run.forkSnapshot === true) {
1001
+ button.hidden = true;
1002
+ return;
1003
+ }
1004
+
985
1005
  if (!isTrackedChangeLifecycleRun(run)) {
986
1006
  button.hidden = true;
987
1007
  return;
@@ -0,0 +1,120 @@
1
+ (function initCodexOverleafSessionForkController(root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafSessionForkController = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function sessionForkControllerFactory() {
6
+ 'use strict';
7
+
8
+ function create(deps = {}) {
9
+ const {
10
+ SessionState, getState, setState, sendBackgroundNative, saveState,
11
+ applyStateToPanel, showPluginToast, tr
12
+ } = deps;
13
+
14
+ async function forkRunFromNode(runId) {
15
+ const previousState = getState();
16
+ const source = findSource(previousState, runId);
17
+ if (!source) return failFork('session_fork_source_missing', tr('forkRunUnavailable'));
18
+ const { session, run, runIndex } = source;
19
+ if (run.status === 'running' || !session.codexThreadId || !run.codexTurnId) {
20
+ return failFork('session_fork_unavailable', tr('forkRunUnavailable'));
21
+ }
22
+ const response = await sendBackgroundNative({
23
+ method: 'codex.thread.fork',
24
+ params: {
25
+ threadId: session.codexThreadId,
26
+ lastTurnId: run.codexTurnId,
27
+ speedTier: session.speedTier,
28
+ loadCodexLocalSkills: previousState.loadCodexLocalSkills !== false,
29
+ loadCodexOverleafSkills: previousState.loadCodexOverleafSkills !== false
30
+ }
31
+ });
32
+ if (!response?.ok || !response.result?.threadId) {
33
+ return failFork(response?.error?.code || 'session_fork_failed', response?.error?.message || tr('forkRunFailed'));
34
+ }
35
+ const copiedRuns = session.runs.slice(0, runIndex + 1).map(copyRunSnapshot);
36
+ const titleBase = String(session.title || copiedRuns[0]?.task || tr('newSessionFallback')).trim();
37
+ const forkTitle = buildUniqueForkTitle(previousState.sessions, titleBase);
38
+ const forked = SessionState.createSession({
39
+ title: forkTitle,
40
+ titleSource: 'manual',
41
+ runs: copiedRuns,
42
+ task: '',
43
+ mode: session.mode,
44
+ providerId: session.providerId,
45
+ model: session.model,
46
+ reasoningEffort: session.reasoningEffort,
47
+ speedTier: session.speedTier,
48
+ requireReviewing: session.requireReviewing,
49
+ focusFiles: session.focusFiles,
50
+ projectReferenceFiles: session.projectReferenceFiles,
51
+ codexThreadId: response.result.threadId,
52
+ pendingInputs: [],
53
+ forkedFromSessionId: session.id,
54
+ forkedFromRunId: run.id
55
+ });
56
+ setState(SessionState.normalizePanelState({
57
+ ...previousState,
58
+ sessions: [...(previousState.sessions || []), forked],
59
+ activeSessionId: forked.id
60
+ }));
61
+ try {
62
+ await saveState();
63
+ } catch (error) {
64
+ setState(previousState);
65
+ await sendBackgroundNative({
66
+ method: 'codex.history.clearPlugin',
67
+ params: { sessionId: forked.id, threadId: response.result.threadId }
68
+ }).catch(() => null);
69
+ showPluginToast(error.message || tr('forkRunFailed'), { status: 'failed', sticky: true });
70
+ throw error;
71
+ }
72
+ applyStateToPanel();
73
+ showPluginToast(tr('forkRunDone'), { status: 'completed' });
74
+ return forked;
75
+ }
76
+
77
+ function failFork(code, message) {
78
+ showPluginToast(message, { status: 'failed', sticky: true });
79
+ throw codedError(code, message);
80
+ }
81
+
82
+ return { forkRunFromNode };
83
+ }
84
+
85
+ function buildUniqueForkTitle(sessions = [], sourceTitle = '') {
86
+ const fallback = String(sourceTitle || 'New Session').trim() || 'New Session';
87
+ const baseTitle = fallback
88
+ .replace(/\s+\(fork\)$/i, '')
89
+ .replace(/\s+\(\d+\)$/, '')
90
+ .trim() || fallback;
91
+ const usedTitles = new Set((sessions || []).map(session => String(session?.title || '').trim()));
92
+ let index = 1;
93
+ while (usedTitles.has(`${baseTitle} (${index})`)) index += 1;
94
+ return `${baseTitle} (${index})`;
95
+ }
96
+
97
+ function findSource(state, runId) {
98
+ for (const session of state?.sessions || []) {
99
+ const runIndex = (session.runs || []).findIndex(run => run.id === runId);
100
+ if (runIndex >= 0) return { session, run: session.runs[runIndex], runIndex };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function copyRunSnapshot(run) {
106
+ const copy = JSON.parse(JSON.stringify(run || {}));
107
+ return {
108
+ ...copy,
109
+ id: `fork_${String(copy.id || 'run')}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
110
+ forkSnapshot: true,
111
+ forkSourceRunId: String(copy.id || ''),
112
+ attachments: [], appliedOperations: [], undoOperations: [], undoBaseFiles: [],
113
+ undoTrackedChanges: [], undoExpectedFiles: [], undoStatus: '',
114
+ trackedChangeStatus: '', nativeRequestId: '', queueItemId: '', interruptedDraft: undefined
115
+ };
116
+ }
117
+
118
+ function codedError(code, message) { const error = new Error(message); error.code = code; return error; }
119
+ return { copyRunSnapshot, create };
120
+ });
@@ -15,6 +15,9 @@
15
15
  type: change.type,
16
16
  path: change.path,
17
17
  contentBase64: change.contentBase64 || '',
18
+ assetRef: change.assetRef && typeof change.assetRef === 'object' ? { ...change.assetRef } : undefined,
19
+ sha256: change.sha256 || change.assetRef?.sha256 || '',
20
+ mimeType: change.mimeType || change.assetRef?.mimeType || '',
18
21
  size: Number.isFinite(Number(change.size)) ? Number(change.size) : undefined,
19
22
  previousExists: change.previousExists === true,
20
23
  previousKind: change.previousKind || '',
@@ -61,6 +61,7 @@
61
61
  let onMirrorRefreshSettled;
62
62
  let writebackSettlement;
63
63
  let injectedWritebackSettlement;
64
+ let assetTransferBroker;
64
65
 
65
66
  function buildSettlement(input = {}) {
66
67
  return writebackSettlement?.settle instanceof Function
@@ -283,7 +284,7 @@
283
284
  };
284
285
  }
285
286
  const applied = operations.length
286
- ? mergeApplyResultSkipped(await callPageBridge('applyOperations', {
287
+ ? mergeApplyResultSkipped(await assetTransferBroker.applyOperations({
287
288
  operations,
288
289
  baseFiles: project?.files || [],
289
290
  requireReviewing: runRequireReviewing,
@@ -859,6 +860,7 @@
859
860
  getState,
860
861
  getCurrentRunView,
861
862
  } = deps);
863
+ assetTransferBroker = deps.assetTransferBroker;
862
864
  onMirrorRefreshSettled = deps.onMirrorRefreshSettled;
863
865
  writebackSettlement = deps.writebackSettlement;
864
866
  compileAdapter = deps.compileAdapter;