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
@@ -32,6 +32,7 @@
32
32
  closeDiagnosticsMenu = () => {},
33
33
  closeCustomInstructionsSettings = () => {},
34
34
  closeSlashMenu = () => {},
35
+ icons = null,
35
36
  projectFiles: projectFilesDep,
36
37
  exactFileListZipTimeoutMs: exactFileListZipTimeoutMsDep
37
38
  } = {}) {
@@ -39,6 +40,11 @@
39
40
  || root.document
40
41
  || (typeof globalThis !== 'undefined' ? globalThis.document : null);
41
42
  const getLocationHref = getLocationHrefDep || (() => root.location?.href || '');
43
+ const setCloseGlyph = button => {
44
+ const glyph = icons?.svg?.('close', 'codex-close-glyph');
45
+ if (glyph) button.innerHTML = glyph;
46
+ else button.textContent = '×';
47
+ };
42
48
  const projectFiles = projectFilesDep || ProjectFiles;
43
49
  const exactFileListZipTimeoutMs = Number(exactFileListZipTimeoutMsDep) ||
44
50
  DEFAULT_CONTEXT_EXACT_FILE_LIST_ZIP_TIMEOUT_MS;
@@ -556,7 +562,7 @@
556
562
  clear.dataset.contextClear = '';
557
563
  clear.title = tr('clearFiles');
558
564
  clear.setAttribute('aria-label', tr('clearFiles'));
559
- clear.textContent = '×';
565
+ setCloseGlyph(clear);
560
566
  clear.addEventListener('click', clearFocusFile);
561
567
  chip.append(clear);
562
568
  }
@@ -603,7 +609,7 @@
603
609
  remove.setAttribute('data-context-summary-remove', '');
604
610
  remove.title = tr('removeContextFile', { path });
605
611
  remove.setAttribute('aria-label', tr('removeContextFile', { path }));
606
- remove.textContent = '×';
612
+ setCloseGlyph(remove);
607
613
  remove.addEventListener('click', event => {
608
614
  event.preventDefault();
609
615
  event.stopPropagation();
@@ -0,0 +1,267 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) module.exports = factory();
3
+ else root.CodexOverleafModuleRegistry.define('ContextUsageView', [], factory);
4
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function () {
5
+ 'use strict';
6
+
7
+ let nextId = 0;
8
+
9
+ // State and ownership are supplied by the runtime. The view never estimates
10
+ // context from conversation text, cumulative billing totals, or model names.
11
+ function create(deps = {}) {
12
+ let state = { usage: null, compacting: false, compactingKind: '', pending: false, compacted: false };
13
+ let host = null;
14
+ let button = null;
15
+ let popover = null;
16
+ let compactButton = null;
17
+ let panel = null;
18
+ let open = false;
19
+ let observer = null;
20
+ const listeners = [];
21
+ const tr = (key, params) => deps.tr(key, params);
22
+ const locale = () => deps.getLocale?.() === 'zh' ? 'zh-CN' : 'en-US';
23
+
24
+ function bind(target, type, listener, options) {
25
+ if (!target?.addEventListener) return;
26
+ target.addEventListener(type, listener, options);
27
+ listeners.push(() => target.removeEventListener(type, listener, options));
28
+ }
29
+
30
+ function unbind() {
31
+ listeners.splice(0).forEach(remove => remove());
32
+ observer?.disconnect();
33
+ observer = null;
34
+ }
35
+
36
+ function mount() {
37
+ const currentPanel = deps.getPanel?.();
38
+ const currentHost = currentPanel?.querySelector('[data-context-usage]');
39
+ if (currentHost === host) return Boolean(host);
40
+ unbind();
41
+ host = currentHost || null;
42
+ panel = currentPanel;
43
+ button = host?.querySelector('[data-context-usage-toggle]');
44
+ popover = host?.querySelector('[data-context-usage-popover]');
45
+ compactButton = host?.querySelector('[data-context-usage-compact]');
46
+ open = false;
47
+ if (!button || !popover) return false;
48
+ popover.id = 'codex-context-usage-' + (++nextId);
49
+ button.setAttribute('aria-controls', popover.id);
50
+ const doc = host.ownerDocument;
51
+ const view = doc.defaultView;
52
+ bind(button, 'click', event => {
53
+ event.preventDefault();
54
+ if (open) close();
55
+ else {
56
+ deps.onOpen?.();
57
+ open = true;
58
+ popover.hidden = false;
59
+ button.setAttribute('aria-expanded', 'true');
60
+ render();
61
+ }
62
+ });
63
+ bind(compactButton, 'click', event => {
64
+ event.preventDefault();
65
+ if (compactButton.disabled) return;
66
+ close();
67
+ deps.onCompact?.();
68
+ });
69
+ bind(doc, 'pointerdown', event => { if (!host.contains(event.target)) close(); }, true);
70
+ bind(doc, 'focusin', event => { if (!host.contains(event.target)) close(); });
71
+ bind(doc, 'keydown', event => {
72
+ if (open && event.key === 'Escape') {
73
+ const restoreFocus = popover.contains(doc.activeElement);
74
+ close();
75
+ if (restoreFocus) button.focus();
76
+ event.preventDefault();
77
+ }
78
+ });
79
+ bind(view, 'resize', position);
80
+ bind(view?.visualViewport, 'resize', position);
81
+ bind(view?.visualViewport, 'scroll', position);
82
+ bind(doc, 'scroll', position, true);
83
+ if (typeof view?.ResizeObserver === 'function') {
84
+ observer = new view.ResizeObserver(position);
85
+ observer.observe(panel);
86
+ observer.observe(button);
87
+ }
88
+ return true;
89
+ }
90
+
91
+ function close() {
92
+ open = false;
93
+ if (popover) popover.hidden = true;
94
+ button?.setAttribute('aria-expanded', 'false');
95
+ }
96
+
97
+ function position() {
98
+ if (!open || !popover || !button) return;
99
+ const view = host.ownerDocument.defaultView;
100
+ const viewport = view.visualViewport;
101
+ const viewportLeft = viewport?.offsetLeft || 0;
102
+ const viewportTop = viewport?.offsetTop || 0;
103
+ const viewportRight = viewportLeft + (viewport?.width || view.innerWidth);
104
+ const viewportBottom = viewportTop + (viewport?.height || view.innerHeight);
105
+ const panelRect = panel.getBoundingClientRect();
106
+ const rect = button.getBoundingClientRect();
107
+ const left = Math.max(viewportLeft, panelRect.left) + 8;
108
+ const right = Math.min(viewportRight, panelRect.right) - 8;
109
+ const top = Math.max(viewportTop, panelRect.top) + 8;
110
+ const bottom = Math.min(viewportBottom, panelRect.bottom) - 8;
111
+ const width = Math.max(0, Math.min(284, right - left));
112
+ const above = Math.max(0, rect.top - top - 8);
113
+ const below = Math.max(0, bottom - rect.bottom - 8);
114
+ const placeAbove = above >= Math.min(160, below);
115
+ popover.style.width = width + 'px';
116
+ popover.style.maxHeight = Math.max(0, placeAbove ? above : below) + 'px';
117
+ popover.style.left = Math.max(left, Math.min(rect.right - width, right - width)) + 'px';
118
+ const height = popover.getBoundingClientRect().height;
119
+ popover.style.top = (placeAbove ? Math.max(top, rect.top - height - 8) : rect.bottom + 8) + 'px';
120
+ }
121
+
122
+ function number(value) {
123
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null;
124
+ return value;
125
+ }
126
+
127
+ function formatTokens(value) {
128
+ if (value === null) return '—';
129
+ const unit = value >= 1e9 ? 1e9 : value >= 1e6 ? 1e6 : value >= 1e3 ? 1e3 : 1;
130
+ const suffix = unit === 1e9 ? 'B' : unit === 1e6 ? 'M' : unit === 1e3 ? 'K' : '';
131
+ return new Intl.NumberFormat(locale(), { maximumFractionDigits: unit === 1 ? 0 : 1 }).format(value / unit) + suffix;
132
+ }
133
+
134
+ function formatPercent(value, digits = 1) {
135
+ return value === null ? '—' : new Intl.NumberFormat(locale(), { style: 'percent', maximumFractionDigits: digits }).format(value);
136
+ }
137
+
138
+ function formatLatestReply(value) {
139
+ const at = Date.parse(value || '');
140
+ if (!Number.isFinite(at)) return '';
141
+ const seconds = Math.max(0, Math.round((Date.now() - at) / 1000));
142
+ const minutes = Math.round(seconds / 60), hours = Math.round(minutes / 60), days = Math.round(hours / 24);
143
+ const time = seconds < 45 ? tr('contextUsageJustNow')
144
+ : minutes < 60 ? tr('contextUsageMinutesAgo', { count: Math.max(1, minutes) })
145
+ : hours < 24 ? tr('contextUsageHoursAgo', { count: hours })
146
+ : tr(days === 1 ? 'contextUsageDayAgo' : 'contextUsageDaysAgo', { count: days });
147
+ return tr('contextUsageLatestReply', { time });
148
+ }
149
+
150
+ function text(name, value) {
151
+ const node = host.querySelector('[data-context-usage-' + name + ']');
152
+ if (node) node.textContent = value;
153
+ return node;
154
+ }
155
+
156
+ // The bar spans the reported window; its filled part splits by the latest
157
+ // response's counters. Cached input is drawn inside input, never added to it.
158
+ function renderBreakdown(used, capacity) {
159
+ const input = number(state.usage?.inputTokens), output = number(state.usage?.outputTokens);
160
+ const cachedValue = number(state.usage?.cachedInputTokens);
161
+ const cached = input !== null && cachedValue !== null && cachedValue <= input ? cachedValue : null;
162
+ const total = input !== null && output !== null && Number.isSafeInteger(input + output)
163
+ && input + output > 0 ? input + output : null;
164
+ const filled = used !== null && capacity !== null ? Math.min(1, used / capacity) : 0;
165
+ const widths = total === null
166
+ ? { cached: 0, input: filled, output: 0 }
167
+ : { cached: filled * (cached || 0) / total, input: filled * (input - (cached || 0)) / total, output: filled * output / total };
168
+ for (const [name, width] of Object.entries(widths)) {
169
+ const node = host.querySelector('[data-context-usage-bar] [data-seg="' + name + '"]');
170
+ if (node) node.style.width = (width * 100).toFixed(3) + '%';
171
+ }
172
+ const legend = host.querySelector('[data-context-usage-breakdown]');
173
+ legend.hidden = input === null && output === null;
174
+ text('input-label', tr('contextUsageInput'));
175
+ text('output-label', tr('contextUsageOutput'));
176
+ text('cached-label', tr('contextUsageCacheHit'));
177
+ text('input', formatTokens(input));
178
+ text('output', formatTokens(output));
179
+ text('cached', formatTokens(cached));
180
+ text('input-share', total === null ? '' : formatPercent(input / total, 0));
181
+ text('output-share', total === null ? '' : formatPercent(output / total, 0));
182
+ const rate = text('cache-rate', input > 0 && cached !== null ? formatPercent(cached / input, 0) : '—');
183
+ if (rate) rate.title = tr('contextUsageCacheHitRate');
184
+ legend.title = total === null ? '' : tr('contextUsageBreakdownBasis');
185
+ host.querySelector('[data-context-usage-cache]').hidden = input === null || cached === null;
186
+ }
187
+
188
+ function render() {
189
+ if (!mount()) return;
190
+ const used = number(state.usage?.usedTokens);
191
+ const capacityValue = number(state.usage?.contextWindow);
192
+ const capacity = capacityValue > 0 ? capacityValue : null;
193
+ const percent = used !== null && capacity !== null ? used / capacity * 100 : null;
194
+ const level = percent === null ? '' : deps.level?.(percent) || 'normal';
195
+ const percentage = percent === null ? '' : new Intl.NumberFormat(locale(), { maximumFractionDigits: 1 }).format(percent);
196
+ const amount = tr('contextUsageAmount', { used: formatTokens(used), capacity: formatTokens(capacity) });
197
+ const summary = tr('contextUsageSummary', { amount, percentage: percent === null ? '—' : percentage + '%' });
198
+ const compactingText = state.compactingKind === 'auto' ? tr('contextUsageCompactingAuto') : tr('contextUsageCompacting');
199
+ const compacting = state.compacting;
200
+ const pending = !compacting && state.pending;
201
+ const availability = deps.getCompactAvailability?.() || { ok: true };
202
+ // No sample and nothing in progress: show the empty state. Before the first
203
+ // message there is no thread, so the line says when usage will appear.
204
+ const empty = used === null && !compacting && !pending;
205
+ const noThread = availability.ok === false && availability.reason === 'compactNoThread';
206
+ const emptyText = noThread ? tr('contextUsageBeforeFirstMessage') : tr('contextUsageUnavailable');
207
+ host.dataset.compacting = compacting ? 'true' : 'false';
208
+ host.dataset.empty = empty ? 'true' : 'false';
209
+ host.dataset.unknown = percent === null ? 'true' : 'false';
210
+ host.dataset.pending = pending ? 'true' : 'false';
211
+ host.dataset.level = level;
212
+ host.style.setProperty('--context-usage-percent', String(Math.max(0, Math.min(100, percent || 0))));
213
+ button.title = compacting ? compactingText : pending ? tr(state.compacted ? 'contextUsagePending' : 'contextUsagePendingDetail')
214
+ : used === null ? emptyText : summary;
215
+ button.setAttribute('aria-label', button.title);
216
+ text('title', tr('contextUsageTitle'));
217
+ // Without a usable sample, the line under the title carries the state.
218
+ text('amount', used !== null ? amount : compacting ? compactingText
219
+ : pending ? tr('contextUsagePendingDetail') : emptyText);
220
+ host.querySelector('[data-context-usage-spinner]').hidden = !compacting;
221
+ const chip = text('chip', tr('contextUsageCompactedChip'));
222
+ if (chip) chip.hidden = !pending || !state.compacted;
223
+ host.querySelector('[data-context-usage-percent-wrap]').hidden = compacting || pending;
224
+ text('percent', percent === null ? '—' : percentage);
225
+ host.querySelector('[data-context-usage-percent-sign]').hidden = percent === null;
226
+ const figure = host.querySelector('[data-context-usage-figure]');
227
+ if (figure) figure.title = percent === null ? '' : tr('contextUsageEstimate');
228
+ const levelNode = text('level', level === 'near-limit' ? tr('contextUsageNearLimit') : level === 'high' ? tr('contextUsageHigh') : '');
229
+ if (levelNode) levelNode.hidden = compacting || (level !== 'high' && level !== 'near-limit');
230
+ // A sample shown while compacting gets an explicit note; so does a missing capacity.
231
+ const noteText = used === null ? '' : compacting ? compactingText : capacity === null ? tr('contextUsageCapacityUnknown') : '';
232
+ text('note-text', noteText);
233
+ const note = host.querySelector('[data-context-usage-note]');
234
+ if (note) {
235
+ note.hidden = !noteText;
236
+ note.dataset.kind = compacting ? 'compacting' : 'info';
237
+ }
238
+ renderBreakdown(used, capacity);
239
+ const reported = text('reported', used === null ? '' : formatLatestReply(state.usage?.observedAt));
240
+ if (reported) reported.hidden = !reported.textContent;
241
+ if (compactButton) {
242
+ // Without a conversation there is nothing to compact; a busy state keeps the disabled button.
243
+ compactButton.hidden = noThread;
244
+ compactButton.disabled = availability.ok === false;
245
+ compactButton.title = availability.ok === false ? tr(availability.reason || 'compactBusy') : tr('contextUsageCompactHint');
246
+ text('compact-label', tr('contextUsageCompactAction'));
247
+ }
248
+ const foot = host.querySelector('[data-context-usage-foot]');
249
+ if (foot) foot.hidden = (!compactButton || compactButton.hidden) && (!reported || reported.hidden);
250
+ popover.setAttribute('aria-label', tr('contextUsageTitle'));
251
+ position();
252
+ }
253
+
254
+ return {
255
+ setState(value = {}) {
256
+ state = { usage: value.usage || null, compacting: Boolean(value.compacting),
257
+ compactingKind: value.compactingKind || '', pending: Boolean(value.pending), compacted: Boolean(value.compacted) };
258
+ render();
259
+ },
260
+ render,
261
+ close,
262
+ destroy() { close(); unbind(); host = button = popover = compactButton = panel = null; }
263
+ };
264
+ }
265
+
266
+ return { create };
267
+ });