codex-overleaf-link 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +155 -392
  2. package/README.zh-CN.md +154 -393
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +283 -55
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +129 -16
  11. package/extension/src/backgroundUpdateCoordinator.js +242 -29
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +171 -8
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +104 -47
  17. package/extension/src/content/composerPanel.js +110 -12
  18. package/extension/src/content/contentRuntime.js +1555 -926
  19. package/extension/src/content/contextTray.js +8 -2
  20. package/extension/src/content/contextUsageView.js +267 -0
  21. package/extension/src/content/diagnosticsController.js +122 -96
  22. package/extension/src/content/diagnosticsExport.js +19 -5
  23. package/extension/src/content/diagnosticsPanel.js +48 -0
  24. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  25. package/extension/src/content/generated/content.bundle.js +11077 -3412
  26. package/extension/src/content/generated/content.bundle.meta.json +14 -3
  27. package/extension/src/content/globalPreferencesController.js +1 -1
  28. package/extension/src/content/legacyGlobalRegistry.js +10 -0
  29. package/extension/src/content/localSkillsPanel.js +104 -14
  30. package/extension/src/content/markdownDomRenderer.js +5 -3
  31. package/extension/src/content/markdownText.js +47 -11
  32. package/extension/src/content/modelPicker.js +8 -4
  33. package/extension/src/content/nativeCompatibilityController.js +74 -30
  34. package/extension/src/content/otWarmMirror.js +5 -6
  35. package/extension/src/content/otWarmMirrorController.js +17 -20
  36. package/extension/src/content/pageBridgeClient.js +17 -4
  37. package/extension/src/content/panelMaintenance.js +46 -14
  38. package/extension/src/content/panelRenderer.js +57 -27
  39. package/extension/src/content/pendingInputView.js +1 -1
  40. package/extension/src/content/projectProviderSelection.js +13 -4
  41. package/extension/src/content/projectSessionCleanup.js +109 -62
  42. package/extension/src/content/projectSettingsCoordinator.js +67 -6
  43. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  44. package/extension/src/content/providerSettingsDialog.js +73 -14
  45. package/extension/src/content/recentProjects.js +15 -8
  46. package/extension/src/content/runActivitySummary.js +160 -18
  47. package/extension/src/content/runCompletionMeta.js +532 -0
  48. package/extension/src/content/runController.js +39 -15
  49. package/extension/src/content/runFailureNotice.js +29 -13
  50. package/extension/src/content/runGuidanceController.js +72 -27
  51. package/extension/src/content/runGuidanceView.js +59 -10
  52. package/extension/src/content/runPresence.js +18 -8
  53. package/extension/src/content/runQueueScheduler.js +157 -47
  54. package/extension/src/content/runRecoveryActions.js +147 -0
  55. package/extension/src/content/runResultActions.js +28 -143
  56. package/extension/src/content/runReviewController.js +633 -0
  57. package/extension/src/content/runScrollLayout.js +21 -2
  58. package/extension/src/content/runTimelineView.js +367 -175
  59. package/extension/src/content/scopedPersistencePanelState.js +134 -20
  60. package/extension/src/content/selectionContextView.js +1 -1
  61. package/extension/src/content/sensitivePreflight.js +153 -0
  62. package/extension/src/content/sessionForkController.js +80 -17
  63. package/extension/src/content/sessionManager.js +50 -4
  64. package/extension/src/content/sessionMenuView.js +54 -18
  65. package/extension/src/content/sessionPersistence.js +25 -5
  66. package/extension/src/content/settingsPanel.js +22 -5
  67. package/extension/src/content/settingsWorkbench.js +132 -89
  68. package/extension/src/content/sharedSessionViewSync.js +49 -11
  69. package/extension/src/content/subagentActivityView.js +23 -7
  70. package/extension/src/content/updateIdle.js +4 -1
  71. package/extension/src/content/updateNotice.js +118 -12
  72. package/extension/src/content/updateSafetyReceiver.js +58 -0
  73. package/extension/src/content/writebackController.js +53 -4
  74. package/extension/src/content/writebackOrchestrator.js +116 -94
  75. package/extension/src/content/writingStyleSettings.js +35 -11
  76. package/extension/src/page/binaryAssetUploader.js +136 -14
  77. package/extension/src/page/compileBridge.js +193 -180
  78. package/extension/src/page/fileMover.js +256 -0
  79. package/extension/src/page/overleafCapabilities.js +4 -1
  80. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  81. package/extension/src/page/referenceProjects.js +1 -0
  82. package/extension/src/page/reviewContext.js +113 -0
  83. package/extension/src/page/saveState.js +6 -4
  84. package/extension/src/page/snapshotRouter.js +84 -39
  85. package/extension/src/page/textCoordinates.js +17 -1
  86. package/extension/src/page/textFileCreator.js +114 -26
  87. package/extension/src/page/trackedChangeCapture.js +12 -2
  88. package/extension/src/page/trackedChangeOwnership.js +19 -1
  89. package/extension/src/page/trackedChangeReplay.js +41 -21
  90. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  91. package/extension/src/page/treeOperations.js +7 -10
  92. package/extension/src/page/updateSafetyProbe.js +41 -0
  93. package/extension/src/page/writebackReceiptJournal.js +2 -0
  94. package/extension/src/page/writebackRouter.js +103 -155
  95. package/extension/src/pageBridge.js +126 -69
  96. package/extension/src/popup.js +62 -10
  97. package/extension/src/shared/agentTranscript.js +373 -17
  98. package/extension/src/shared/auditRecords.js +4 -0
  99. package/extension/src/shared/compatibility.js +22 -6
  100. package/extension/src/shared/compileAdapter.js +129 -66
  101. package/extension/src/shared/composerAttachmentDraft.js +156 -0
  102. package/extension/src/shared/contextUsage.js +125 -0
  103. package/extension/src/shared/directoryCleanupProof.js +43 -0
  104. package/extension/src/shared/failureReasons.js +349 -4
  105. package/extension/src/shared/globalPreferences.js +30 -13
  106. package/extension/src/shared/governanceRules.js +4 -37
  107. package/extension/src/shared/i18n.js +590 -24
  108. package/extension/src/shared/lineReferences.js +38 -31
  109. package/extension/src/shared/managedUpdateProjection.js +2 -2
  110. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  111. package/extension/src/shared/pageRpcContract.js +5 -4
  112. package/extension/src/shared/panelIcons.js +39 -0
  113. package/extension/src/shared/projectFiles.js +21 -44
  114. package/extension/src/shared/providerProfiles.js +5 -3
  115. package/extension/src/shared/reviewing.js +130 -1
  116. package/extension/src/shared/runActivityModel.js +70 -4
  117. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  118. package/extension/src/shared/runInputQueue.js +48 -5
  119. package/extension/src/shared/selectionContext.js +12 -3
  120. package/extension/src/shared/sessionState.js +132 -28
  121. package/extension/src/shared/sessionTitle.js +14 -1
  122. package/extension/src/shared/settlementFacts.js +10 -2
  123. package/extension/src/shared/sharedSessionBridge.js +118 -27
  124. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  125. package/extension/src/shared/staleGuard.js +166 -52
  126. package/extension/src/shared/storageDb.js +90 -26
  127. package/extension/src/shared/storageMigration.js +78 -112
  128. package/extension/src/shared/storageRunActions.js +267 -30
  129. package/extension/src/shared/subagentActivityModel.js +1 -1
  130. package/extension/src/shared/undoOperations.js +253 -32
  131. package/extension/src/shared/updateConsent.js +6 -4
  132. package/extension/src/shared/updateRuntimeIdentity.js +17 -3
  133. package/extension/src/shared/writebackIntent.js +16 -0
  134. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  135. package/extension/src/shared/writebackSettlement.js +58 -40
  136. package/extension/styles/panel.css +1123 -493
  137. package/native-host/src/anthropicBridgeRequest.js +4 -2
  138. package/native-host/src/anthropicBridgeResponse.js +6 -2
  139. package/native-host/src/anthropicMessagesBridge.js +24 -16
  140. package/native-host/src/bootstrapMigration.js +296 -0
  141. package/native-host/src/bootstrapMigrationContract.js +101 -0
  142. package/native-host/src/bootstrapUpdate.js +269 -0
  143. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  144. package/native-host/src/chatBridgeRequest.js +60 -6
  145. package/native-host/src/chatBridgeResponse.js +20 -1
  146. package/native-host/src/chatCompletionsBridge.js +48 -15
  147. package/native-host/src/codexConfigPaths.js +106 -0
  148. package/native-host/src/codexHome.js +66 -73
  149. package/native-host/src/codexHomeScope.js +124 -0
  150. package/native-host/src/codexInheritedAuth.js +90 -0
  151. package/native-host/src/codexLocalSkills.js +301 -0
  152. package/native-host/src/codexPromptAssembly.js +114 -127
  153. package/native-host/src/codexProviderTest.js +24 -26
  154. package/native-host/src/codexSessionRunner.js +234 -198
  155. package/native-host/src/codexSessionTiming.js +18 -8
  156. package/native-host/src/codexSkillIsolation.js +170 -0
  157. package/native-host/src/codexThreadFork.js +19 -3
  158. package/native-host/src/index.js +19 -7
  159. package/native-host/src/managedInstall.js +110 -35
  160. package/native-host/src/managedInstallRecovery.js +140 -0
  161. package/native-host/src/managedRuntimeEvidence.js +57 -0
  162. package/native-host/src/mirrorWorkspace.js +92 -22
  163. package/native-host/src/nativeDoctor.js +15 -0
  164. package/native-host/src/nativeHelperLifecycle.js +21 -0
  165. package/native-host/src/nativeHostRegistration.js +33 -0
  166. package/native-host/src/nativeRunLifecycle.js +71 -0
  167. package/native-host/src/processStopBarrier.js +42 -7
  168. package/native-host/src/projectPathPlatform.js +30 -0
  169. package/native-host/src/providerContextError.js +20 -0
  170. package/native-host/src/providerHttp.js +80 -0
  171. package/native-host/src/providerProfile.js +4 -2
  172. package/native-host/src/providerReasoning.js +2 -7
  173. package/native-host/src/providerRuntime.js +1 -0
  174. package/native-host/src/providerStore.js +72 -14
  175. package/native-host/src/responsesPassthroughBridge.js +266 -52
  176. package/native-host/src/runtimeInstaller.js +21 -1
  177. package/native-host/src/selectionScope.js +36 -8
  178. package/native-host/src/subagentAdmission.js +26 -0
  179. package/native-host/src/subagentBroker.js +79 -66
  180. package/native-host/src/subagentTelemetry.js +19 -10
  181. package/native-host/src/subagentWorkspacePath.js +5 -2
  182. package/native-host/src/taskRunnerRuntime.js +92 -57
  183. package/native-host/src/textPatch.js +473 -377
  184. package/native-host/src/tomlStatements.js +140 -0
  185. package/native-host/src/updateManager.js +409 -46
  186. package/native-host/src/writingStyleRuntime.js +57 -29
  187. package/native-host/src/writingStyleStore.js +73 -0
  188. package/package.json +5 -8
  189. package/scripts/install-managed.mjs +1 -0
@@ -6,6 +6,8 @@
6
6
  const tx = typeof options.tx === 'function' ? options.tx : (en) => en;
7
7
  const tr = typeof options.tr === 'function' ? options.tr : (key) => key;
8
8
  const appendPlainLog = typeof options.appendPlainLog === 'function' ? options.appendPlainLog : () => {};
9
+ const onChange = typeof options.onChange === 'function' ? options.onChange : () => {};
10
+ const icons = options.icons || null;
9
11
  const limits = options.limits || {};
10
12
  const maxAttachments = Number(limits.maxAttachments) || 8;
11
13
  const maxAttachmentBytes = Number(limits.maxAttachmentBytes) || 12 * 1024 * 1024;
@@ -15,6 +17,8 @@
15
17
  let attachments = [];
16
18
  const pendingAttachmentKeys = new Set();
17
19
  let pendingAttachmentBytes = 0;
20
+ let attachmentGeneration = 0;
21
+ let attachmentRevision = 0;
18
22
 
19
23
  function handlePaste(event) {
20
24
  const files = collectFilesFromDataTransfer(event.clipboardData);
@@ -59,51 +63,68 @@
59
63
  }
60
64
 
61
65
  async function addFiles(files) {
62
- for (const file of Array.from(files || [])) {
63
- const dedupeKey = buildAttachmentDedupeKey(file);
64
- if (isDuplicateAttachmentFile(dedupeKey)) {
65
- continue;
66
- }
67
- if (attachments.length + pendingAttachmentKeys.size >= maxAttachments) {
68
- appendPlainLog(tx(
69
- `Attachment limit reached (${maxAttachments}).`,
70
- `附件数量已达上限(${maxAttachments} 个)。`
71
- ));
72
- break;
73
- }
74
- validateAttachmentFile(file);
75
- const fileSize = Number(file.size) || 0;
76
- if (!canReserveAttachmentBytes(fileSize)) {
77
- appendPlainLog(tx(
78
- `Attachment total limit reached (${formatFileSize(maxAttachmentTotalBytes)}). Remove attachments or use smaller files.`,
79
- `附件总大小已达上限(${formatFileSize(maxAttachmentTotalBytes)})。请移除部分附件或使用更小的文件。`
80
- ));
81
- continue;
82
- }
83
- pendingAttachmentKeys.add(dedupeKey);
84
- pendingAttachmentBytes += fileSize;
85
- try {
86
- const dataUrl = await readFileAsDataUrl(file);
87
- const preview = await buildAttachmentPreviewData(file, dataUrl);
88
- const attachment = {
89
- id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
90
- name: normalizeAttachmentName(file.name),
91
- mimeType: String(file.type || ''),
92
- size: Number(file.size) || 0,
93
- kind: preview.kind,
94
- previewDataUrl: preview.previewDataUrl,
95
- contentBase64: extractBase64FromDataUrl(dataUrl),
96
- dedupeKey
97
- };
98
- if (!attachments.some(existing => buildAttachmentDedupeKey(existing) === dedupeKey)) {
99
- attachments.push(attachment);
66
+ const generation = attachmentGeneration;
67
+ try {
68
+ for (const file of Array.from(files || [])) {
69
+ if (generation !== attachmentGeneration) return;
70
+ const dedupeKey = buildAttachmentDedupeKey(file);
71
+ if (isDuplicateAttachmentFile(dedupeKey)) {
72
+ continue;
73
+ }
74
+ if (attachments.length + pendingAttachmentKeys.size >= maxAttachments) {
75
+ appendPlainLog(tx(
76
+ `Attachment limit reached (${maxAttachments}).`,
77
+ `附件数量已达上限(${maxAttachments} 个)。`
78
+ ));
79
+ break;
80
+ }
81
+ validateAttachmentFile(file);
82
+ const fileSize = Number(file.size) || 0;
83
+ if (!canReserveAttachmentBytes(fileSize)) {
84
+ appendPlainLog(tx(
85
+ `Attachment total limit reached (${formatFileSize(maxAttachmentTotalBytes)}). Remove attachments or use smaller files.`,
86
+ `附件总大小已达上限(${formatFileSize(maxAttachmentTotalBytes)})。请移除部分附件或使用更小的文件。`
87
+ ));
88
+ continue;
89
+ }
90
+ attachmentRevision += 1;
91
+ pendingAttachmentKeys.add(dedupeKey);
92
+ pendingAttachmentBytes += fileSize;
93
+ try {
94
+ const dataUrl = await readFileAsDataUrl(file, tx);
95
+ if (generation !== attachmentGeneration) return;
96
+ const preview = await buildAttachmentPreviewData(file, dataUrl);
97
+ if (generation !== attachmentGeneration) return;
98
+ const attachment = {
99
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
100
+ name: normalizeAttachmentName(file.name),
101
+ mimeType: String(file.type || ''),
102
+ size: Number(file.size) || 0,
103
+ kind: preview.kind,
104
+ previewDataUrl: preview.previewDataUrl,
105
+ contentBase64: extractBase64FromDataUrl(dataUrl),
106
+ dedupeKey
107
+ };
108
+ if (!attachments.some(existing => buildAttachmentDedupeKey(existing) === dedupeKey)) {
109
+ attachments.push(attachment);
110
+ attachmentRevision += 1;
111
+ onChange();
112
+ }
113
+ } catch (error) {
114
+ if (generation !== attachmentGeneration) return;
115
+ throw error;
116
+ } finally {
117
+ // Cleared work no longer owns reservations in the current composer.
118
+ if (generation === attachmentGeneration) {
119
+ pendingAttachmentBytes = Math.max(0, pendingAttachmentBytes - fileSize);
120
+ pendingAttachmentKeys.delete(dedupeKey);
121
+ }
100
122
  }
101
- } finally {
102
- pendingAttachmentBytes = Math.max(0, pendingAttachmentBytes - fileSize);
103
- pendingAttachmentKeys.delete(dedupeKey);
104
123
  }
124
+ } finally {
125
+ // A partial batch may have accepted files before validation or reading failed.
126
+ if (generation === attachmentGeneration) renderComposerAttachments();
105
127
  }
106
- renderComposerAttachments();
107
128
  }
108
129
 
109
130
  function getAttachmentsForRun() {
@@ -139,20 +160,27 @@
139
160
  root: getPanel() || document.body,
140
161
  tr,
141
162
  tx,
163
+ icons,
142
164
  removable: true,
143
165
  onRemove: removeAttachment
144
166
  });
145
167
  }
146
168
 
147
- function clear() {
169
+ function clear(options = {}) {
170
+ attachmentGeneration += 1;
171
+ attachmentRevision += 1;
148
172
  attachments = [];
149
173
  pendingAttachmentKeys.clear();
174
+ pendingAttachmentBytes = 0;
150
175
  renderComposerAttachments();
176
+ if (options.notify !== false) onChange();
151
177
  }
152
178
 
153
179
  function removeAttachment(id) {
180
+ const previousLength = attachments.length;
154
181
  attachments = attachments.filter(attachment => attachment.id !== id);
155
182
  renderComposerAttachments();
183
+ if (attachments.length !== previousLength) { attachmentRevision += 1; onChange(); }
156
184
  }
157
185
 
158
186
  function isDuplicateAttachmentFile(dedupeKey) {
@@ -260,14 +288,36 @@
260
288
  if (!usable.length) {
261
289
  return 0;
262
290
  }
291
+ attachmentRevision += 1;
263
292
  attachments = usable.map(item => ({ ...item }));
264
293
  renderComposerAttachments();
265
294
  return attachments.length;
266
295
  }
267
296
 
297
+ // Only a just-rejected local-skill submission uses this in-memory recovery.
298
+ // It keeps the normal composer limits, without durable-draft truncation.
299
+ function restoreSubmittedAttachments(value, expectedRevision) {
300
+ if (expectedRevision !== attachmentRevision || attachments.length || pendingAttachmentBytes || pendingAttachmentKeys.size) return false;
301
+ if (!Array.isArray(value) || value.length > maxAttachments) return false;
302
+ let total = 0;
303
+ for (const item of value) {
304
+ const size = Number(item?.size);
305
+ if (!item?.name || typeof item.contentBase64 !== 'string' || !Number.isFinite(size) || size < 0 || size > maxAttachmentBytes) return false;
306
+ total += size;
307
+ }
308
+ if (total > maxAttachmentTotalBytes) return false;
309
+ attachments = value.map((item, index) => ({ ...item, id: `restored-${Date.now()}-${index}`, dedupeKey: buildAttachmentDedupeKey(item) }));
310
+ attachmentRevision += 1;
311
+ renderComposerAttachments();
312
+ onChange();
313
+ return true;
314
+ }
315
+
268
316
  return {
269
317
  serializePersistableAttachments,
270
318
  restorePersistedAttachments,
319
+ restoreSubmittedAttachments,
320
+ getRevision: () => attachmentRevision,
271
321
  handlePaste,
272
322
  handleDragOver,
273
323
  handleDragLeave,
@@ -285,6 +335,7 @@
285
335
  root: getPanel() || document.body,
286
336
  tr,
287
337
  tx,
338
+ icons,
288
339
  ...options
289
340
  }),
290
341
  clear,
@@ -360,6 +411,12 @@
360
411
  .slice(0, 180) || '';
361
412
  }
362
413
 
414
+ function setCloseGlyph(button, icons) {
415
+ const glyph = icons?.svg?.('close', 'codex-close-glyph');
416
+ if (glyph) button.innerHTML = glyph;
417
+ else button.textContent = '×';
418
+ }
419
+
363
420
  function renderAttachmentPreviewList(attachments = [], container, options = {}) {
364
421
  if (!container) return;
365
422
  container.replaceChildren();
@@ -407,7 +464,7 @@
407
464
  const remove = document.createElement('button');
408
465
  remove.type = 'button';
409
466
  remove.className = 'codex-attachment-preview-remove';
410
- remove.textContent = '×';
467
+ setCloseGlyph(remove, options.icons);
411
468
  const removeLabel = options.tx ? options.tx('Remove ' + label, '移除 ' + label) : 'Remove ' + label;
412
469
  remove.title = removeLabel;
413
470
  remove.setAttribute('aria-label', removeLabel);
@@ -438,7 +495,7 @@
438
495
  const close = document.createElement('button');
439
496
  close.type = 'button';
440
497
  close.className = 'codex-attachment-preview-dialog-close';
441
- close.textContent = '×';
498
+ setCloseGlyph(close, options.icons);
442
499
  close.title = options.tr ? options.tr('close') : 'Close';
443
500
  close.setAttribute('aria-label', options.tr ? options.tr('close') : 'Close');
444
501
  const image = document.createElement('img');
@@ -496,13 +553,13 @@
496
553
  /\.(?:png|jpe?g|gif|webp|bmp|svg)$/i.test(String(attachment.name || ''));
497
554
  }
498
555
 
499
- function readFileAsDataUrl(file) {
556
+ function readFileAsDataUrl(file, tx = en => en) {
500
557
  return new Promise((resolve, reject) => {
501
558
  const reader = new FileReader();
502
559
  reader.onload = () => {
503
560
  resolve(String(reader.result || ''));
504
561
  };
505
- reader.onerror = () => reject(reader.error || new Error('Could not read selected file'));
562
+ reader.onerror = () => reject(new Error(tx('Could not read selected file.', '无法读取所选文件。'), { cause: reader.error }));
506
563
  reader.readAsDataURL(file);
507
564
  });
508
565
  }
@@ -13,6 +13,7 @@
13
13
  attachmentController: options.attachmentController || null,
14
14
  listeners: []
15
15
  };
16
+ const icon = name => options.icons?.svg(name) || '';
16
17
 
17
18
  container.innerHTML = `
18
19
  <div class="codex-pending-inputs" data-pending-inputs hidden></div>
@@ -20,9 +21,9 @@
20
21
  <div class="codex-attachment-strip codex-attachment-preview-list" data-attachment-strip hidden></div>
21
22
  <div class="codex-composer-selection" data-composer-selection hidden></div>
22
23
  <div class="codex-composer-skill-context" data-composer-skill-context hidden>
23
- <span class="codex-composer-skill-icon" aria-hidden="true">◇</span>
24
+ <span class="codex-composer-skill-icon" aria-hidden="true">${icon('sparkle')}</span>
24
25
  <span data-composer-skill-label></span>
25
- <button type="button" data-composer-skill-clear title="Clear skill" aria-label="Clear skill">×</button>
26
+ <button type="button" data-composer-skill-clear title="Clear skill" aria-label="Clear skill">${icon('close')}</button>
26
27
  </div>
27
28
  <textarea data-task rows="3" placeholder="Ask Codex anything. Type @ to add context"></textarea>
28
29
  <div class="codex-context-summary" data-context-summary hidden></div>
@@ -60,8 +61,43 @@
60
61
  </div>
61
62
  </div>
62
63
  <div class="codex-composer-toolbar">
63
- <button type="button" data-add-context title="Add @ context" aria-label="Add @ context" aria-expanded="false">+</button>
64
+ <button type="button" data-add-context title="Add @ context" aria-label="Add @ context" aria-expanded="false">${icon('plus')}</button>
64
65
  <div class="codex-model-config" data-model-config>
66
+ <div class="codex-context-usage" data-context-usage data-unknown="true">
67
+ <button type="button" class="codex-context-usage-button" data-context-usage-toggle aria-expanded="false">
68
+ <svg class="codex-context-usage-ring" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
69
+ <circle class="codex-context-usage-track" cx="8" cy="8" r="6.4" pathLength="100"></circle>
70
+ <circle class="codex-context-usage-fill" cx="8" cy="8" r="2.55" pathLength="100"></circle>
71
+ <circle class="codex-context-usage-orbit" cx="8" cy="8" r="6.4" pathLength="100"></circle>
72
+ </svg>
73
+ </button>
74
+ <div class="codex-context-usage-popover" data-context-usage-popover role="region" tabindex="0" hidden>
75
+ <div class="codex-context-usage-head">
76
+ <svg class="codex-context-usage-empty-ring" data-context-usage-empty-ring viewBox="0 0 26 26" aria-hidden="true" focusable="false"><circle cx="13" cy="13" r="10.5"></circle><circle class="codex-context-usage-empty-dot" cx="13" cy="13" r="2"></circle></svg>
77
+ <div class="codex-context-usage-head-text">
78
+ <div class="codex-context-usage-title" data-context-usage-title></div>
79
+ <div class="codex-context-usage-amount" data-context-usage-amount></div>
80
+ </div>
81
+ <div class="codex-context-usage-figure" data-context-usage-figure>
82
+ <span class="codex-context-usage-spinner" data-context-usage-spinner hidden>${icon('spin')}</span>
83
+ <span class="codex-context-usage-chip" data-context-usage-chip hidden></span>
84
+ <span class="codex-context-usage-percent" data-context-usage-percent-wrap><span data-context-usage-percent></span><small data-context-usage-percent-sign>%</small></span>
85
+ <span class="codex-context-usage-level" data-context-usage-level hidden></span>
86
+ </div>
87
+ </div>
88
+ <div class="codex-context-usage-note" data-context-usage-note hidden>${icon('compact')}<span data-context-usage-note-text></span></div>
89
+ <div class="codex-context-usage-bar" data-context-usage-bar aria-hidden="true"><i data-seg="cached"></i><i data-seg="input"></i><i data-seg="output"></i></div>
90
+ <div class="codex-context-usage-legend" data-context-usage-breakdown hidden>
91
+ <div class="codex-context-usage-row"><i class="codex-context-usage-swatch" data-seg="input"></i><span data-context-usage-input-label></span><b data-context-usage-input></b><em data-context-usage-input-share></em></div>
92
+ <div class="codex-context-usage-row" data-sub data-context-usage-cache><i class="codex-context-usage-swatch" data-seg="cached"></i><span data-context-usage-cached-label></span><b data-context-usage-cached></b><em data-context-usage-cache-rate></em></div>
93
+ <div class="codex-context-usage-row"><i class="codex-context-usage-swatch" data-seg="output"></i><span data-context-usage-output-label></span><b data-context-usage-output></b><em data-context-usage-output-share></em></div>
94
+ </div>
95
+ <div class="codex-context-usage-foot" data-context-usage-foot>
96
+ <span data-context-usage-reported hidden></span>
97
+ <button type="button" class="codex-context-usage-compact" data-context-usage-compact>${icon('compact')}<span data-context-usage-compact-label></span></button>
98
+ </div>
99
+ </div>
100
+ </div>
65
101
  <button type="button" class="codex-model-config-button" data-model-config-toggle aria-haspopup="menu" aria-expanded="false">
66
102
  <span class="codex-model-speed-indicator" data-speed-indicator hidden aria-hidden="true">
67
103
  <svg viewBox="0 0 16 16" focusable="false">
@@ -99,7 +135,10 @@
99
135
  </div>
100
136
  </div>
101
137
  </div>
102
- <button type="submit" data-run data-action="send" title="Send" aria-label="Send">↑</button>
138
+ <button type="submit" data-run data-action="send" title="Send" aria-label="Send">
139
+ <span class="codex-run-glyph" data-run-glyph="send">${icon('send')}</span>
140
+ <span class="codex-run-glyph" data-run-glyph="cancel">${icon('stop')}</span>
141
+ </button>
103
142
  </div>
104
143
  <div class="codex-slash-menu" data-slash-menu hidden>
105
144
  <button type="button" data-slash-command="install-skill" data-slash-command-kind="installer">
@@ -119,9 +158,9 @@
119
158
  </svg>
120
159
  <span class="codex-context-sync-copy" data-context-sync-copy></span>
121
160
  </span>
122
- <button type="button" class="codex-attach-button" data-attach-file title="Attach files (PDF, images)" aria-label="Attach files (PDF, images)">⌲</button>
161
+ <button type="button" class="codex-attach-button" data-attach-file title="Attach files (PDF, images)" aria-label="Attach files (PDF, images)">${icon('attach')}</button>
123
162
  <input type="file" data-attach-input multiple hidden>
124
- <button type="button" data-context-refresh title="Refresh file list" aria-label="Refresh file list">↻</button>
163
+ <button type="button" data-context-refresh title="Refresh file list" aria-label="Refresh file list">${icon('refresh')}</button>
125
164
  </span>
126
165
  </div>
127
166
  <div class="codex-context-selection" data-context-selection></div>
@@ -132,9 +171,11 @@
132
171
  `;
133
172
 
134
173
  bindEvents(instance);
174
+ bindMenuLayout(instance);
135
175
 
136
176
  return {
137
177
  setRunning: running => setRunning(instance, running),
178
+ setSessionTransition: pending => setSessionTransition(instance, pending),
138
179
  setModels: models => setModels(instance, models),
139
180
  setMode: mode => setMode(instance, mode),
140
181
  focus: () => focus(instance),
@@ -156,14 +197,31 @@
156
197
  bind(instance, form, 'drop', event => instance.callbacks.onDrop?.(event));
157
198
  bind(instance, instance.container.querySelector('[data-run]'), 'click', event => {
158
199
  event.preventDefault();
159
- if (instance.callbacks.isRunning?.() && !String(task?.value || '').trim()) {
200
+ if (instance.container.querySelector('[data-run]')?.dataset.action === 'cancel') {
160
201
  instance.callbacks.onCancel?.();
161
202
  return;
162
203
  }
163
204
  form?.requestSubmit?.();
164
205
  });
165
206
  bind(instance, task, 'keydown', event => instance.callbacks.onTaskKeydown?.(event));
166
- bind(instance, task, 'input', event => instance.callbacks.onTaskInput?.(event));
207
+ bind(instance, task, 'input', event => {
208
+ if (acceptTaskInput(instance, task)) instance.callbacks.onTaskInput?.(event);
209
+ });
210
+ bind(instance, task, 'compositionstart', () => {
211
+ const composition = { scope: instance.callbacks.getTaskScope?.() };
212
+ composition.done = new Promise(resolve => { composition.resolve = resolve; });
213
+ instance.composition = composition;
214
+ });
215
+ bind(instance, task, 'compositionend', event => {
216
+ const composition = instance.composition;
217
+ if (acceptTaskInput(instance, task)) instance.callbacks.onInputChange?.(event);
218
+ // Chrome may emit the final input after compositionend. Keep its original
219
+ // owner until that event batch has finished, before a session can change.
220
+ if (composition) setTimeout(() => {
221
+ if (instance.composition === composition) instance.composition = null;
222
+ composition.resolve();
223
+ }, 0);
224
+ });
167
225
  bind(instance, instance.container.querySelector('[data-slash-menu]'), 'click', event => instance.callbacks.onSlashMenuClick?.(event));
168
226
  bind(instance, instance.container.querySelector('[data-composer-skill-clear]'), 'click', () => instance.callbacks.onClearSkillInvocation?.());
169
227
  bind(instance, instance.container.querySelector('[data-add-context]'), 'click', () => instance.callbacks.onAddContext?.());
@@ -180,8 +238,11 @@
180
238
  }
181
239
  for (const selector of ['[data-model]', '[data-reasoning]', '[data-speed]', '[data-mode]', '[data-task]', '[data-require-reviewing]', '[data-auto-recompile]']) {
182
240
  const element = instance.container.querySelector(selector);
183
- bind(instance, element, 'change', event => instance.callbacks.onInputChange?.(event));
184
- bind(instance, element, 'input', event => instance.callbacks.onInputChange?.(event));
241
+ const changed = event => {
242
+ if (element !== task || acceptTaskInput(instance, task)) instance.callbacks.onInputChange?.(event);
243
+ };
244
+ bind(instance, element, 'change', changed);
245
+ bind(instance, element, 'input', changed);
185
246
  }
186
247
  }
187
248
 
@@ -193,6 +254,41 @@
193
254
  instance.listeners.push({ target, type, listener });
194
255
  }
195
256
 
257
+ function bindMenuLayout(instance) {
258
+ const view = instance.container.ownerDocument?.defaultView || window;
259
+ const update = () => instance.callbacks.onMenuLayoutChange?.();
260
+ bind(instance, view, 'resize', update);
261
+ bind(instance, view.visualViewport, 'resize', update);
262
+ bind(instance, view.visualViewport, 'scroll', update);
263
+ const form = instance.container.querySelector('[data-composer-form]');
264
+ if (form && typeof view.ResizeObserver === 'function') {
265
+ instance.menuResizeObserver = new view.ResizeObserver(update);
266
+ instance.menuResizeObserver.observe(form);
267
+ instance.menuResizeObserver.observe(instance.container);
268
+ const panel = instance.container.closest?.('#codex-overleaf-panel');
269
+ if (panel && panel !== instance.container) instance.menuResizeObserver.observe(panel);
270
+ }
271
+ }
272
+
273
+ function acceptTaskInput(instance, task) {
274
+ if (instance.composition && instance.composition.scope !== instance.callbacks.getTaskScope?.()) {
275
+ task.value = instance.callbacks.getTaskValue?.() || '';
276
+ return false;
277
+ }
278
+ return !instance.sessionTransition || Boolean(instance.composition);
279
+ }
280
+
281
+ function setSessionTransition(instance, pending) {
282
+ const task = instance.container.querySelector('[data-task]');
283
+ if (pending && !instance.sessionTransition) instance.previousReadOnly = task?.readOnly === true;
284
+ instance.sessionTransition = pending;
285
+ const composition = instance.composition;
286
+ if (pending && composition) task?.blur();
287
+ if (task) task.readOnly = pending || instance.previousReadOnly === true;
288
+ setRunning(instance, Boolean(instance.callbacks.isRunning?.()));
289
+ return pending ? composition?.done : undefined;
290
+ }
291
+
196
292
  function setRunning(target, running) {
197
293
  const instance = target?._instance || target;
198
294
  const runButton = instance?.container?.querySelector('[data-run]');
@@ -200,14 +296,14 @@
200
296
  return;
201
297
  }
202
298
  const hasInput = Boolean(String(instance?.container?.querySelector('[data-task]')?.value || '').trim());
203
- const action = running && !hasInput ? 'cancel' : 'send';
299
+ const action = running && (!hasInput || instance.sessionTransition) ? 'cancel' : 'send';
204
300
  const label = action === 'cancel'
205
301
  ? t(instance, 'cancelRun')
206
302
  : running
207
303
  ? t(instance, 'queueNextInput')
208
304
  : t(instance, 'send');
209
305
  runButton.dataset.action = action;
210
- runButton.disabled = action === 'send' && !hasInput;
306
+ runButton.disabled = action === 'send' && (!hasInput || instance.sessionTransition);
211
307
  runButton.title = label;
212
308
  runButton.setAttribute('aria-label', label);
213
309
  }
@@ -276,6 +372,8 @@
276
372
  }
277
373
 
278
374
  function destroy(instance) {
375
+ instance.composition?.resolve();
376
+ instance.menuResizeObserver?.disconnect();
279
377
  for (const { target, type, listener } of instance.listeners.splice(0)) {
280
378
  target.removeEventListener?.(type, listener);
281
379
  }