codex-overleaf-link 2.5.1 → 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 (93) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +78 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -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
+ });
@@ -19,9 +19,14 @@
19
19
  <div class="codex-diagnostics-wrap">
20
20
  <button type="button" class="codex-diagnostics-trigger" data-diagnostics-menu title="Diagnostics" aria-label="Diagnostics" aria-expanded="false">
21
21
  <span class="codex-diagnostics-dot" data-diagnostics-health-dot data-health="unknown"></span>
22
+ <span class="codex-diagnostics-label" data-diagnostics-health-label></span>
22
23
  </button>
23
24
  <div class="codex-diagnostics-menu" data-diagnostics-popover hidden>
24
25
  <div class="codex-diagnostics-hint" data-i18n="diagnosticsHint">Use when Codex cannot run, write, or read files</div>
26
+ <button type="button" data-refresh>
27
+ <span class="codex-diagnostics-refresh-title">${options.icons?.svg?.('refresh') || ''}<span data-i18n="refreshProbeTitle">Refresh file status</span></span>
28
+ <small data-i18n="refreshProbeSubtitle">Re-reads Overleaf state; does not sync or modify files</small>
29
+ </button>
25
30
  <button type="button" class="codex-diagnostics-runall" data-diagnostics-run-all>
26
31
  <span data-i18n="diagnosticsRunAllTitle">Run all checks</span>
27
32
  <small data-i18n="diagnosticsRunAllSubtitle">Connection, write access, project read, OT mirror</small>
@@ -81,6 +86,10 @@
81
86
 
82
87
  function bindStaticActions(instance) {
83
88
  const root = instance.container;
89
+ root.querySelector('[data-refresh]')?.addEventListener('click', () => {
90
+ closeMenu(instance);
91
+ instance.callbacks.onRefresh?.();
92
+ });
84
93
  root.querySelector('[data-diagnostics-menu]')?.addEventListener('click', () => toggleMenu(instance));
85
94
  root.querySelector('[data-diagnostics-run-all]')?.addEventListener('click', () => instance.callbacks.onRunAll?.());
86
95
  root.querySelector('[data-diagnostics-native-env]')?.addEventListener('click', () => instance.callbacks.onNativeEnvironment?.());
@@ -488,6 +497,15 @@
488
497
  const tooltip = t(instance, tooltipKey);
489
498
  trigger.title = tooltip;
490
499
  trigger.setAttribute('aria-label', tooltip);
500
+ trigger.dataset.health = health;
501
+ }
502
+ const label = instance?.container?.querySelector('[data-diagnostics-health-label]');
503
+ if (label) {
504
+ label.textContent = t(instance, {
505
+ ok: 'diagnosticsPillOk',
506
+ warn: 'diagnosticsPillWarn',
507
+ fail: 'diagnosticsPillFail'
508
+ }[health] || 'diagnosticsPillUnknown');
491
509
  }
492
510
  }
493
511