@opengeni/react 5.0.5-canary.3 → 5.2.0-canary.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 (81) hide show
  1. package/README.md +13 -1
  2. package/dist/artifacts.d.ts +2 -0
  3. package/dist/artifacts.js +1391 -2
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/chunk-2TKI3WEG.js +73 -0
  6. package/dist/chunk-2TKI3WEG.js.map +1 -0
  7. package/dist/{chunk-V3WX3TQG.js → chunk-45XEUVTK.js} +2 -2
  8. package/dist/{chunk-M2HNWZYR.js → chunk-IHQII5WO.js} +69 -18
  9. package/dist/chunk-IHQII5WO.js.map +1 -0
  10. package/dist/{chunk-HUMIRRYI.js → chunk-IYCR4343.js} +19 -19
  11. package/dist/{chunk-HUMIRRYI.js.map → chunk-IYCR4343.js.map} +1 -1
  12. package/dist/{chunk-6J56T3MG.js → chunk-NYESIADM.js} +14 -5
  13. package/dist/chunk-NYESIADM.js.map +1 -0
  14. package/dist/{chunk-7S2KWUJD.js → chunk-QXJCNVSD.js} +2283 -2215
  15. package/dist/chunk-QXJCNVSD.js.map +1 -0
  16. package/dist/{chunk-BV5A4OTE.js → chunk-T5FRXMC3.js} +21 -2
  17. package/dist/{chunk-BV5A4OTE.js.map → chunk-T5FRXMC3.js.map} +1 -1
  18. package/dist/{chunk-BRFMGY4L.js → chunk-UGMKPUEM.js} +28 -3
  19. package/dist/chunk-UGMKPUEM.js.map +1 -0
  20. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  21. package/dist/components/artifacts/published-html-artifact-frame.d.ts +2 -0
  22. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  23. package/dist/components/markdown.d.ts +13 -1
  24. package/dist/components/message-timeline.d.ts +4 -1
  25. package/dist/components/session-conversation.d.ts +3 -0
  26. package/dist/components/user-message-body.d.ts +10 -1
  27. package/dist/composer.js +2 -2
  28. package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-AA4JH2AP.js} +2 -2
  29. package/dist/connect.js +1 -1
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +9 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/interaction.js +1 -1
  34. package/dist/{platform-activity-row-27LKAWZD.js → platform-activity-row-UW4UVVM4.js} +6 -3
  35. package/dist/platform-activity-row-UW4UVVM4.js.map +1 -0
  36. package/dist/session-ui.d.ts +1 -1
  37. package/dist/session-ui.js +6 -5
  38. package/dist/session-ui.js.map +1 -1
  39. package/dist/session.js +2 -2
  40. package/dist/sites-ui.d.ts +2 -1
  41. package/dist/sites.js +12 -4
  42. package/dist/sites.js.map +1 -1
  43. package/dist/timeline/activity-rail.d.ts +1 -0
  44. package/dist/timeline/platform-activity-row.d.ts +1 -1
  45. package/dist/timeline/retained-image.d.ts +16 -0
  46. package/dist/timeline/rolling-activity.d.ts +9 -0
  47. package/dist/timeline/shared.d.ts +4 -1
  48. package/dist/timeline/turn-summary.d.ts +4 -1
  49. package/package.json +4 -4
  50. package/src/artifacts.ts +2 -0
  51. package/src/components/artifacts/inline-html-document.ts +24 -0
  52. package/src/components/artifacts/published-html-artifact-frame.tsx +32 -2
  53. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  54. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  55. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  56. package/src/components/copy-button.tsx +7 -2
  57. package/src/components/markdown.tsx +111 -34
  58. package/src/components/message-timeline.tsx +61 -4
  59. package/src/components/session-conversation.tsx +5 -0
  60. package/src/components/user-message-body.tsx +20 -2
  61. package/src/hooks/use-composer.ts +15 -2
  62. package/src/hooks/use-computer-session.ts +36 -1
  63. package/src/index.ts +10 -2
  64. package/src/session-ui.ts +4 -1
  65. package/src/sites-ui.tsx +23 -6
  66. package/src/timeline/activity-rail.tsx +20 -3
  67. package/src/timeline/platform-activity-row.tsx +5 -2
  68. package/src/timeline/retained-image.ts +91 -0
  69. package/src/timeline/rolling-activity.tsx +83 -0
  70. package/src/timeline/shared.tsx +33 -12
  71. package/src/timeline/tool-renderers.tsx +7 -91
  72. package/src/timeline/turn-summary.tsx +21 -15
  73. package/styles/compiled.css +122 -0
  74. package/styles/index.css +49 -0
  75. package/dist/chunk-6J56T3MG.js.map +0 -1
  76. package/dist/chunk-7S2KWUJD.js.map +0 -1
  77. package/dist/chunk-BRFMGY4L.js.map +0 -1
  78. package/dist/chunk-M2HNWZYR.js.map +0 -1
  79. package/dist/platform-activity-row-27LKAWZD.js.map +0 -1
  80. /package/dist/{chunk-V3WX3TQG.js.map → chunk-45XEUVTK.js.map} +0 -0
  81. /package/dist/{computer-viewer-7TEZC6Y2.js.map → computer-viewer-AA4JH2AP.js.map} +0 -0
@@ -0,0 +1,349 @@
1
+ <!--__INLINE_VISUALIZATION_FRAGMENT__-->
2
+ <script src="https://unpkg.com/@floating-ui/core@1.7.3/dist/floating-ui.core.umd.min.js"></script>
3
+ <script src="https://unpkg.com/@floating-ui/dom@1.7.4/dist/floating-ui.dom.umd.min.js"></script>
4
+ <script>
5
+ (() => {
6
+ if (window.FloatingUIDOM == null) {
7
+ return;
8
+ }
9
+ const { autoUpdate, computePosition, flip, offset, shift, size } = window.FloatingUIDOM;
10
+ const tooltipPlacements = new Set(["top", "right", "bottom", "left"]);
11
+ const tooltipId = `codex-visualization-tooltip-${
12
+ window.crypto?.randomUUID?.() ?? Date.now().toString(36)
13
+ }`;
14
+ const tooltipGap = 5;
15
+ const hoverDelay = 700;
16
+ const skipDelay = 300;
17
+ let activeTrigger = null;
18
+ let pendingTrigger = null;
19
+ let tooltip = null;
20
+ let openTimeout = null;
21
+ let autoUpdateCleanup = null;
22
+ let positionRequest = 0;
23
+ let skipDelayUntil = 0;
24
+ let lastPointerWasTouch = false;
25
+ let pinnedByTouch = false;
26
+
27
+ const getTrigger = (target) =>
28
+ target?.nodeType === Node.ELEMENT_NODE ? target.closest("[data-tooltip]") : null;
29
+ const containsTarget = (trigger, target) =>
30
+ target?.nodeType != null && trigger.contains(target);
31
+ const getContent = (trigger) => trigger.getAttribute("data-tooltip")?.trim() ?? "";
32
+ const getPlacement = (trigger) => {
33
+ const placement = trigger.getAttribute("data-tooltip-placement");
34
+ return tooltipPlacements.has(placement) ? placement : "top";
35
+ };
36
+ const getTooltip = () => {
37
+ if (tooltip == null) {
38
+ tooltip = document.createElement("div");
39
+ tooltip.id = tooltipId;
40
+ tooltip.className = "tooltip";
41
+ tooltip.setAttribute("role", "tooltip");
42
+ }
43
+ return tooltip;
44
+ };
45
+ const setDescribedBy = (trigger, described) => {
46
+ const ids = new Set(
47
+ (trigger.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean),
48
+ );
49
+ if (described) {
50
+ ids.add(tooltipId);
51
+ } else {
52
+ ids.delete(tooltipId);
53
+ }
54
+ if (ids.size === 0) {
55
+ trigger.removeAttribute("aria-describedby");
56
+ } else {
57
+ trigger.setAttribute("aria-describedby", [...ids].join(" "));
58
+ }
59
+ };
60
+ const clearOpenTimeout = () => {
61
+ if (openTimeout != null) {
62
+ window.clearTimeout(openTimeout);
63
+ openTimeout = null;
64
+ }
65
+ pendingTrigger = null;
66
+ };
67
+ const closeTooltip = (activateSkipDelay = true) => {
68
+ clearOpenTimeout();
69
+ pinnedByTouch = false;
70
+ positionRequest += 1;
71
+ autoUpdateCleanup?.();
72
+ autoUpdateCleanup = null;
73
+ if (activeTrigger == null) {
74
+ return;
75
+ }
76
+ setDescribedBy(activeTrigger, false);
77
+ activeTrigger = null;
78
+ tooltip?.remove();
79
+ if (activateSkipDelay) {
80
+ skipDelayUntil = Date.now() + skipDelay;
81
+ }
82
+ };
83
+ const updatePosition = (trigger, floating) => {
84
+ const request = ++positionRequest;
85
+ void computePosition(trigger, floating, {
86
+ middleware: [
87
+ offset(tooltipGap),
88
+ flip({ padding: tooltipGap }),
89
+ shift({ padding: tooltipGap }),
90
+ size({
91
+ padding: tooltipGap,
92
+ apply({ availableHeight, availableWidth, elements }) {
93
+ elements.floating.style.setProperty(
94
+ "--tooltip-available-width",
95
+ `${Math.max(0, availableWidth)}px`,
96
+ );
97
+ elements.floating.style.setProperty(
98
+ "--tooltip-available-height",
99
+ `${Math.max(0, availableHeight)}px`,
100
+ );
101
+ },
102
+ }),
103
+ ],
104
+ placement: getPlacement(trigger),
105
+ strategy: "fixed",
106
+ })
107
+ .then(({ x, y }) => {
108
+ if (activeTrigger !== trigger || request !== positionRequest) {
109
+ return;
110
+ }
111
+ const scale = window.devicePixelRatio || 1;
112
+ floating.style.transform = `translate(${Math.round(x * scale) / scale}px, ${Math.round(y * scale) / scale}px)`;
113
+ floating.style.visibility = "visible";
114
+ })
115
+ .catch(() => {
116
+ if (activeTrigger === trigger && request === positionRequest) {
117
+ closeTooltip(false);
118
+ }
119
+ });
120
+ };
121
+ const openTooltip = (trigger) => {
122
+ const content = getContent(trigger);
123
+ if (!trigger.isConnected || content.length === 0) {
124
+ return;
125
+ }
126
+ clearOpenTimeout();
127
+ if (activeTrigger === trigger) {
128
+ return;
129
+ }
130
+ closeTooltip();
131
+ const floating = getTooltip();
132
+ floating.textContent = content;
133
+ floating.style.visibility = "hidden";
134
+ floating.style.transform = "translate(0, 0)";
135
+ document.body.appendChild(floating);
136
+ activeTrigger = trigger;
137
+ setDescribedBy(trigger, true);
138
+ autoUpdateCleanup = autoUpdate(trigger, floating, () => {
139
+ updatePosition(trigger, floating);
140
+ });
141
+ };
142
+ const requestOpen = (trigger, immediate = false) => {
143
+ if (
144
+ getContent(trigger).length === 0 ||
145
+ activeTrigger === trigger ||
146
+ pendingTrigger === trigger
147
+ ) {
148
+ return;
149
+ }
150
+ clearOpenTimeout();
151
+ pendingTrigger = trigger;
152
+ const delay =
153
+ immediate || activeTrigger != null || Date.now() < skipDelayUntil ? 0 : hoverDelay;
154
+ if (delay === 0) {
155
+ openTooltip(trigger);
156
+ return;
157
+ }
158
+ openTimeout = window.setTimeout(() => {
159
+ openTimeout = null;
160
+ pendingTrigger = null;
161
+ openTooltip(trigger);
162
+ }, delay);
163
+ };
164
+ const handlePointerOpen = (event) => {
165
+ if (event.pointerType === "touch") {
166
+ return;
167
+ }
168
+ const trigger = getTrigger(event.target);
169
+ if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
170
+ return;
171
+ }
172
+ requestOpen(trigger);
173
+ };
174
+ const handlePointer = (event) => {
175
+ lastPointerWasTouch = event.type === "pointerdown" && event.pointerType === "touch";
176
+ };
177
+ const handleTouchClick = (event) => {
178
+ const isTouch = event.pointerType === "touch" || lastPointerWasTouch;
179
+ lastPointerWasTouch = false;
180
+ if (!isTouch || event.detail === 0) {
181
+ return;
182
+ }
183
+ const trigger = getTrigger(event.target);
184
+ if (
185
+ trigger == null ||
186
+ !trigger.isConnected ||
187
+ getContent(trigger).length === 0 ||
188
+ (pinnedByTouch && activeTrigger === trigger)
189
+ ) {
190
+ if (pinnedByTouch) {
191
+ closeTooltip();
192
+ }
193
+ return;
194
+ }
195
+ openTooltip(trigger);
196
+ pinnedByTouch = activeTrigger === trigger;
197
+ window.setTimeout(() => {
198
+ if (activeTrigger === trigger && !trigger.isConnected) {
199
+ closeTooltip(false);
200
+ }
201
+ }, 0);
202
+ };
203
+ const handleLeave = (event) => {
204
+ if (pinnedByTouch) {
205
+ return;
206
+ }
207
+ const trigger = getTrigger(event.target);
208
+ if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
209
+ return;
210
+ }
211
+ if (activeTrigger === trigger || pendingTrigger === trigger) {
212
+ closeTooltip();
213
+ }
214
+ };
215
+ const handleFocusIn = (event) => {
216
+ const trigger = getTrigger(event.target);
217
+ if (trigger?.matches(":focus-visible")) {
218
+ requestOpen(trigger, true);
219
+ }
220
+ };
221
+ const handleKeyDown = (event) => {
222
+ if (event.key === "Escape") {
223
+ closeTooltip();
224
+ }
225
+ };
226
+ const handleContextMenu = (event) => {
227
+ lastPointerWasTouch = false;
228
+ const trigger = getTrigger(event.target);
229
+ if (activeTrigger === trigger || pendingTrigger === trigger) {
230
+ closeTooltip();
231
+ }
232
+ };
233
+ const destroy = () => {
234
+ closeTooltip(false);
235
+ lastPointerWasTouch = false;
236
+ document.removeEventListener("pointerover", handlePointerOpen);
237
+ document.removeEventListener("pointerdown", handlePointer);
238
+ document.removeEventListener("pointercancel", handlePointer);
239
+ document.removeEventListener("click", handleTouchClick);
240
+ document.removeEventListener("pointerout", handleLeave);
241
+ document.removeEventListener("focusin", handleFocusIn);
242
+ document.removeEventListener("focusout", handleLeave);
243
+ document.removeEventListener("keydown", handleKeyDown);
244
+ document.removeEventListener("contextmenu", handleContextMenu);
245
+ tooltip = null;
246
+ };
247
+
248
+ document.addEventListener("pointerover", handlePointerOpen);
249
+ document.addEventListener("pointerdown", handlePointer);
250
+ document.addEventListener("pointercancel", handlePointer);
251
+ document.addEventListener("click", handleTouchClick);
252
+ document.addEventListener("pointerout", handleLeave);
253
+ document.addEventListener("focusin", handleFocusIn);
254
+ document.addEventListener("focusout", handleLeave);
255
+ document.addEventListener("keydown", handleKeyDown);
256
+ document.addEventListener("contextmenu", handleContextMenu);
257
+ window.addEventListener("pagehide", destroy, { once: true });
258
+ })();
259
+ </script>
260
+ <script id="codex-visualization-tabs">
261
+ (() => {
262
+ const selector = '.nav[role="tablist"] [role="tab"]';
263
+ const disabled = ':disabled,[aria-disabled="true"]';
264
+ const selectTab = (selected) => {
265
+ const panelId = selected.getAttribute("aria-controls");
266
+ for (const tab of selected.closest('[role="tablist"]').querySelectorAll('[role="tab"]')) {
267
+ const active = tab === selected;
268
+ tab.classList.toggle("active", active);
269
+ tab.setAttribute("aria-selected", String(active));
270
+ const id = tab.getAttribute("aria-controls");
271
+ const panel = id && document.getElementById(id);
272
+ if (panel != null) {
273
+ panel.hidden = id !== panelId;
274
+ if (!panel.hidden && selected.id) {
275
+ panel.setAttribute("aria-labelledby", selected.id);
276
+ }
277
+ }
278
+ }
279
+ };
280
+ const interact = (event) => {
281
+ const tab = event.target?.closest?.(selector);
282
+ if (event.defaultPrevented || tab == null || tab.matches(disabled)) {
283
+ return;
284
+ }
285
+ if (event.type === "click") {
286
+ selectTab(tab);
287
+ return;
288
+ }
289
+ const tabs = Array.from(
290
+ tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]'),
291
+ ).filter((candidate) => !candidate.matches(disabled));
292
+ const current = tabs.indexOf(tab);
293
+ let next;
294
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
295
+ next = (current + 1) % tabs.length;
296
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
297
+ next = (current + tabs.length - 1) % tabs.length;
298
+ } else if (event.key === "Home") {
299
+ next = 0;
300
+ } else if (event.key === "End") {
301
+ next = tabs.length - 1;
302
+ } else {
303
+ return;
304
+ }
305
+ event.preventDefault();
306
+ tabs[next].focus();
307
+ selectTab(tabs[next]);
308
+ };
309
+
310
+ for (const list of document.querySelectorAll('.nav[role="tablist"]')) {
311
+ const selected =
312
+ list.querySelector('[role="tab"][aria-selected="true"]') ??
313
+ list.querySelector('[role="tab"].active') ??
314
+ list.querySelector('[role="tab"]');
315
+ if (selected != null) {
316
+ selectTab(selected);
317
+ }
318
+ }
319
+ document.addEventListener("click", interact);
320
+ document.addEventListener("keydown", interact);
321
+ window.addEventListener(
322
+ "pagehide",
323
+ () => {
324
+ document.removeEventListener("click", interact);
325
+ document.removeEventListener("keydown", interact);
326
+ },
327
+ { once: true },
328
+ );
329
+ })();
330
+ </script>
331
+ <script
332
+ id="codex-visualization-lucide"
333
+ async
334
+ src="https://unpkg.com/lucide@1.17.0/dist/umd/lucide.js"
335
+ ></script>
336
+ <script>
337
+ (() => {
338
+ const initialize = () => {
339
+ globalThis.lucide?.createIcons({ attrs: { width: 16, height: 16 } });
340
+ };
341
+ if (globalThis.lucide != null) {
342
+ initialize();
343
+ return;
344
+ }
345
+ document
346
+ .getElementById("codex-visualization-lucide")
347
+ ?.addEventListener("load", initialize, { once: true });
348
+ })();
349
+ </script>
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/generate-visualization-assets.ts.
2
+ export const VISUALIZATION_CSS = "/* Adapted from Codex visualize 1.0.32; shared with the OpenGeni visualization skill. */\n:root {\n color-scheme: light dark;\n background-color: var(\n --background,\n var(--color-background-primary, light-dark(rgb(255 255 255), rgb(24 24 24)))\n ) !important;\n\n /* Agent-facing contract; keep in sync with SKILL.md. */\n --background: var(\n --color-background-primary,\n light-dark(rgb(255 255 255), rgb(24 24 24))\n );\n --foreground: var(\n --color-text-primary,\n light-dark(rgb(26 28 31), rgb(255 255 255))\n );\n --card: color-mix(in oklab, var(--foreground) 5%, var(--background));\n --card-foreground: var(\n --color-text-primary,\n light-dark(rgb(26 28 31), rgb(255 255 255))\n );\n --popover: var(\n --color-background-secondary,\n light-dark(rgb(255 255 255), rgb(45 45 45))\n );\n --popover-foreground: var(\n --color-text-primary,\n light-dark(rgb(26 28 31), rgb(255 255 255))\n );\n --primary: var(\n --color-text-info,\n light-dark(rgb(51 156 255), rgb(131 195 255))\n );\n --primary-foreground: var(\n --color-text-inverse,\n light-dark(rgb(255 255 255), rgb(13 13 13))\n );\n --secondary: var(\n --color-background-secondary,\n light-dark(rgb(255 255 255 / 96%), rgb(54 54 54 / 96%))\n );\n --secondary-foreground: var(\n --color-text-primary,\n light-dark(rgb(26 28 31), rgb(255 255 255))\n );\n --muted: color-mix(in srgb, var(--foreground) 10%, transparent);\n --muted-foreground: var(\n --color-text-secondary,\n light-dark(rgb(26 28 31 / 49.4%), rgb(255 255 255 / 49.8%))\n );\n --accent: var(\n --color-background-info,\n light-dark(rgb(229 242 255), rgb(13 39 63))\n );\n --accent-foreground: var(\n --color-text-info,\n light-dark(rgb(51 156 255), rgb(131 195 255))\n );\n --destructive: var(\n --color-text-warning,\n light-dark(rgb(226 85 7), rgb(255 133 73))\n );\n --border: var(\n --color-border-secondary,\n light-dark(rgb(26 28 31 / 8%), rgb(255 255 255 / 8.2%))\n );\n --input: var(\n --color-border-primary,\n light-dark(\n rgb(26 28 31 / 11.8%),\n color-mix(in oklab, rgb(0 0 0) 10%, transparent)\n )\n );\n --ring: var(\n --color-ring-primary,\n light-dark(rgb(51 156 255), rgb(131 195 255 / 76%))\n );\n --font-size-base: var(--font-text-md-size, 14px);\n --blue: light-dark(rgb(51 156 255), rgb(51 156 255));\n --orange: light-dark(rgb(226 85 7), rgb(251 106 34));\n --green: light-dark(rgb(0 162 64), rgb(64 201 119));\n --red: light-dark(rgb(224 46 42), rgb(255 103 100));\n --purple: light-dark(rgb(146 79 247), rgb(173 123 249));\n --yellow: light-dark(rgb(255 195 0), rgb(255 210 64));\n --viz-series-1: var(--primary);\n --viz-series-2: light-dark(rgb(243 136 59), rgb(245 154 86));\n --viz-series-3: light-dark(rgb(93 201 119), rgb(116 213 139));\n --viz-series-4: light-dark(rgb(235 119 177), rgb(240 143 192));\n --viz-series-5: light-dark(rgb(155 121 236), rgb(170 145 239));\n --viz-series-6: light-dark(rgb(58 185 177), rgb(90 203 194));\n\n /* Internal implementation variables; not part of the agent contract. */\n --font-sans: -apple-system, system-ui, \"Segoe UI\", sans-serif;\n --font-mono:\n ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\",\n monospace;\n --font-size-normal: max(11px, var(--font-size-base));\n --font-size-tooltip: calc(var(--font-size-base) - 1px);\n --font-size-small: max(11px, calc(var(--font-size-base) - 2px));\n --font-size-h1: calc(var(--font-size-normal) * 1.7142857143);\n --font-size-h2: calc(var(--font-size-normal) * 1.4285714286);\n --font-size-h3: calc(var(--font-size-normal) * 1.2857142857);\n --font-weight-normal: 430;\n --font-weight-medium: 500;\n --icon-size: 16px;\n --line-height-normal: calc(var(--font-size-normal) * 1.5);\n --line-height-tooltip: calc(var(--font-size-tooltip) * 1.4285714286);\n --line-height-small: calc(var(--font-size-small) + 4px);\n --corner-shape: round;\n --radius: var(--border-radius-lg, 10px);\n --radius-sm: calc(var(--radius) * 0.6);\n --radius-md: calc(var(--radius) * 0.8);\n --radius-lg: var(--radius);\n --radius-2xl: calc(var(--radius) * 1.6);\n --radius-full: 9999px;\n --shadow-sm: 0 1px 2px -1px rgb(0 0 0 / 8%);\n --checkmark-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17 17'%3E%3Cpath d='M 12.8961 3.64101C13.1297 3.41418 13.4984 3.37523 13.7779 3.56581C14.0571 3.75635 14.1554 4.11331 14.0299 4.41347L13.9615 4.53847L7.71151 13.7045C7.59411 13.8767 7.4063 13.9877 7.19881 14.0072C6.99136 14.0267 6.78564 13.9533 6.63826 13.806L2.88826 10.056L2.79842 9.9457C2.6192 9.67407 2.64927 9.30496 2.88826 9.06581C3.12738 8.82669 3.49647 8.79676 3.76815 8.97597L3.8785 9.06581L7.03084 12.2182L12.8053 3.74941L12.8961 3.64101Z'/%3E%3C/svg%3E\");\n\n /* Legacy aliases; not part of the current agent contract. */\n --viz-bg: transparent;\n --viz-panel: var(--card);\n --viz-border: var(--border);\n --viz-text: var(--foreground);\n --viz-muted: var(--muted-foreground);\n --viz-accent: var(--primary);\n --viz-accent-text: var(--primary-foreground);\n --viz-accent-bg: var(--accent);\n --viz-font-size: var(--font-size-base);\n --viz-warning: var(--destructive);\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n}\n\n:root[data-theme=\"dark\"] {\n color-scheme: dark;\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml > body {\n /* Preserve MCP aliases for older inline renderers without creating :root cycles. */\n --color-background-primary: var(\n --background,\n light-dark(rgb(255 255 255), rgb(24 24 24))\n );\n --color-text-primary: var(\n --foreground,\n light-dark(rgb(26 28 31), rgb(255 255 255))\n );\n --color-border-secondary: var(\n --border,\n light-dark(rgb(26 28 31 / 8%), rgb(255 255 255 / 8.2%))\n );\n\n margin: 0;\n padding: var(--visualize-paint-gutter, 16px);\n color: var(--foreground);\n background: transparent !important;\n font-family: var(--font-sans) !important;\n font-size: var(--font-size-normal);\n font-weight: var(--font-weight-normal);\n line-height: var(--line-height-normal);\n}\n\na {\n color: color-mix(in srgb, var(--viz-accent) 80%, var(--viz-text) 20%);\n cursor: pointer;\n font-weight: var(--font-weight-medium, 500);\n text-decoration: none;\n text-underline-offset: 2px;\n}\n\na:is(:hover, :focus-visible) {\n text-decoration-line: underline;\n text-decoration-style: dashed;\n text-decoration-thickness: 0.5px;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np {\n margin-block: 0;\n}\n\nh1 {\n font-size: var(--font-size-h1);\n font-weight: var(--font-weight-medium);\n line-height: 1.25;\n}\n\nh2 {\n font-size: var(--font-size-h2);\n font-weight: var(--font-weight-medium);\n line-height: 1.25;\n}\n\nh3,\nh4,\nh5,\nh6 {\n font-size: var(--font-size-h3);\n font-weight: var(--font-weight-medium);\n line-height: 1.3;\n}\n\nb,\nstrong,\nth {\n font-weight: var(--font-weight-medium);\n}\n\ncode:not(pre code) {\n display: inline;\n padding: 1px 6px;\n border-radius: var(--radius-sm);\n corner-shape: var(--corner-shape);\n background: var(--muted);\n font-family: var(--font-mono);\n font-size: 0.92em;\n overflow-wrap: anywhere;\n -webkit-box-decoration-break: clone;\n box-decoration-break: clone;\n word-break: break-word;\n}\n\n.table-responsive {\n width: 100%;\n overflow-x: auto;\n scrollbar-width: thin;\n}\n\n.table {\n width: 100%;\n border-collapse: collapse;\n color: var(--foreground);\n font: inherit;\n text-align: start;\n}\n\n.table :is(th, td) {\n padding-block: 10px;\n padding-inline: 0 24px;\n overflow-wrap: anywhere;\n border-bottom: 1px solid var(--border);\n text-align: start;\n vertical-align: top;\n}\n\n.table-responsive > .table :is(th, td) {\n overflow-wrap: break-word;\n}\n\n.table :is(th, td):last-child {\n padding-inline-end: 0;\n}\n\n.table :is(caption, thead th) {\n font-weight: 600;\n}\n\n.table thead th {\n padding-block: 8px;\n border-bottom-color: color-mix(in srgb, var(--foreground) 16%, transparent);\n}\n\n.table tbody tr:last-child :is(th, td) {\n border-bottom: 0;\n}\n\n.table.table-sm :is(th, td) {\n padding-block: 6px;\n}\n\n.table.table-sm :is(th, td):not(:last-child) {\n padding-inline-end: 16px;\n}\n\n.table :is(.text-end, [align=\"right\"]) {\n text-align: end;\n font-variant-numeric: tabular-nums;\n}\n\n.table :is(.text-center, [align=\"center\"]) {\n text-align: center;\n}\n\n.table .text-nowrap {\n white-space: nowrap;\n}\n\n#widget {\n display: flex;\n flex-direction: column;\n gap: 12px;\n width: 100%;\n padding: 0;\n background: transparent !important;\n}\n\n.card {\n min-width: 0;\n padding: 12px;\n overflow: hidden;\n overflow-wrap: break-word;\n border-radius: var(--radius-2xl);\n corner-shape: var(--corner-shape);\n color: var(--card-foreground);\n background: var(--card);\n}\n\n#widget > :not(.card, .progress) {\n width: 100% !important;\n max-width: none !important;\n margin: 0 !important;\n padding: 0 !important;\n border: 0 !important;\n border-radius: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n}\n\n.tooltip {\n position: fixed;\n z-index: 50;\n top: 0;\n left: 0;\n width: max-content;\n max-width: min(\n 20rem,\n var(--tooltip-available-width, calc(100vw - 10px)),\n calc(100vw - 10px)\n );\n max-height: min(\n var(--tooltip-available-height, calc(100vh - 10px)),\n calc(100vh - 10px)\n );\n padding: 4px 8px;\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n corner-shape: var(--corner-shape);\n color: var(--popover-foreground);\n background: var(--popover);\n box-shadow: none;\n font-size: var(--font-size-tooltip);\n line-height: var(--line-height-tooltip);\n overflow-wrap: break-word;\n white-space: normal;\n pointer-events: none;\n user-select: none;\n}\n\n.viz-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(max(180px, 24%), 1fr));\n gap: 10px;\n}\n\n.viz-stat {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.viz-stat-value {\n font-size: var(--font-size-h2);\n font-weight: var(--font-weight-medium);\n line-height: 1.25;\n}\n\n.viz-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\nhr {\n width: 100%;\n height: 1px;\n margin-block: 6px;\n border: 0;\n background: var(--border);\n}\n\n.nav {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: 4px;\n}\n\n.progress {\n display: flex;\n height: 8px;\n margin-block: calc((var(--line-height-normal, 1.5em) - 8px) / 2);\n overflow: hidden;\n border: 0;\n border-radius: var(--radius-full);\n background: var(--muted);\n}\n\n.progress-bar:only-child {\n border-radius: var(--radius-full);\n}\n\n.progress-bar {\n height: 100%;\n flex-shrink: 0;\n border: 0;\n background: var(--viz-series-1);\n}\n\n.viz-badge {\n padding: 3px 8px;\n border-radius: var(--radius-full);\n color: var(--accent-foreground);\n background: var(--accent);\n font-weight: var(--font-weight-medium);\n}\n\nsmall,\n.text-small,\n.viz-badge {\n font-size: var(--font-size-small);\n line-height: var(--line-height-small);\n}\n\n.text-muted {\n color: var(--muted-foreground);\n}\n\n.tabular-nums {\n font-variant-numeric: tabular-nums;\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.viz-controls {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n\n.viz-controls > .form-label {\n display: grid;\n min-width: min(100%, 260px);\n flex: 1 1 280px;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: baseline;\n gap: 2px 12px;\n margin-bottom: 0;\n}\n\n.viz-controls > .form-label > :is(.form-control, .form-range, .form-select) {\n grid-column: 1 / -1;\n}\n\n.btn,\n.nav-link,\n.form-check-input,\n.form-control,\n.form-range,\n.form-select {\n font: inherit;\n}\n\n.btn,\n.nav-link {\n appearance: button;\n display: inline-flex;\n inline-size: fit-content;\n max-inline-size: 100%;\n min-height: 28px;\n align-items: center;\n justify-content: center;\n gap: 4px;\n margin: 0;\n padding: 0 8px;\n -webkit-app-region: no-drag;\n border: 1px solid var(--input);\n border-radius: var(--radius-lg);\n corner-shape: var(--corner-shape);\n color: var(--secondary-foreground);\n background: var(--secondary);\n cursor: var(--cursor-interaction, pointer);\n text-align: center;\n text-decoration: none;\n white-space: nowrap;\n user-select: none;\n -webkit-font-smoothing: antialiased;\n}\n\n.btn:is(.btn-block, .viz-tile) {\n inline-size: 100%;\n}\n\n.btn.viz-tile {\n min-width: 0;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\na.btn {\n cursor: pointer;\n text-decoration: none;\n}\n\n.btn:not(:disabled):hover,\n.nav-link:not(:disabled):not([aria-disabled=\"true\"]):hover {\n background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));\n}\n\n.btn-primary {\n border-color: transparent;\n color: var(--primary-foreground);\n background: var(--foreground);\n background-clip: padding-box;\n}\n\n.btn-primary .text-muted {\n color: color-mix(in srgb, var(--primary-foreground) 50%, transparent);\n}\n\n.btn-primary:not(:disabled):hover {\n background: color-mix(in srgb, var(--foreground) 80%, transparent);\n background-clip: padding-box;\n}\n\n.btn-ghost {\n border-color: transparent;\n color: var(--muted-foreground);\n background: transparent;\n}\n\n.btn-ghost:not(:disabled):hover {\n color: var(--foreground);\n background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));\n}\n\n.nav-pills .nav-link {\n min-width: 0;\n border-color: transparent;\n color: var(--muted-foreground);\n background: transparent;\n}\n\n.nav-justified .nav-link {\n flex: 1 1 0;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\n.nav-pills .nav-link.active {\n color: var(--foreground);\n background: var(--muted);\n}\n\n.btn:disabled,\n.nav-link:disabled,\n.nav-link[aria-disabled=\"true\"] {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n[data-lucide] {\n stroke-width: 1.6;\n}\n\n.form-label {\n display: block;\n margin-bottom: 6px;\n color: var(--foreground);\n}\n\n.form-control {\n display: block;\n width: 100%;\n min-height: 28px;\n padding: 0 8px;\n outline: none;\n border: 1px solid var(--input);\n border-radius: var(--radius-lg);\n corner-shape: var(--corner-shape);\n color: var(--foreground);\n background: var(--secondary);\n}\n\n.form-control::placeholder {\n color: var(--muted-foreground);\n}\n\n.form-control[type=\"file\"] {\n padding: 0;\n overflow: hidden;\n cursor: var(--cursor-interaction, pointer);\n}\n\n.form-control[type=\"file\"]::file-selector-button {\n min-height: 26px;\n margin-right: 8px;\n padding: 0 8px;\n border: 0;\n border-right: 1px solid var(--input);\n color: var(--secondary-foreground);\n background: var(--secondary);\n cursor: inherit;\n font: inherit;\n}\n\n.form-control[type=\"file\"]:not(:disabled):hover::file-selector-button {\n background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));\n}\n\n.form-control-color[type=\"color\"] {\n width: 40px;\n height: 28px;\n padding: 3px;\n cursor: var(--cursor-interaction, pointer);\n}\n\n.form-control-color[type=\"color\"]::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n.form-control-color[type=\"color\"]::-webkit-color-swatch {\n border: 0;\n border-radius: calc(var(--radius-lg) - 4px);\n corner-shape: var(--corner-shape);\n}\n\ntextarea.form-control {\n height: auto;\n min-height: 72px;\n padding: 8px 10px;\n resize: vertical;\n}\n\n.form-control:focus-visible {\n border-color: var(--ring);\n box-shadow: inset 0 0 0 1px var(--ring);\n}\n\n.form-control:disabled,\n.form-select:disabled {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.form-select {\n appearance: none;\n display: block;\n width: 100%;\n min-height: 28px;\n margin: 0;\n padding: 0 32px 0 8px;\n outline: none;\n border: 1px solid var(--input);\n border-radius: var(--radius-lg);\n corner-shape: var(--corner-shape);\n color: var(--foreground);\n background-color: var(--secondary);\n background-image:\n linear-gradient(45deg, transparent 50%, var(--muted-foreground) 50%),\n linear-gradient(135deg, var(--muted-foreground) 50%, transparent 50%);\n background-position:\n calc(100% - 14px) 50%,\n calc(100% - 10px) 50%;\n background-repeat: no-repeat;\n background-size: 4px 4px;\n cursor: var(--cursor-interaction, default);\n}\n\n.form-select:not(:disabled):hover {\n background-color: color-mix(in srgb, var(--foreground) 6%, var(--secondary));\n}\n\n.form-select:focus-visible {\n border-color: var(--ring);\n box-shadow: inset 0 0 0 1px var(--ring);\n}\n\n.form-check {\n display: flex;\n min-height: 20px;\n align-items: center;\n gap: 6px;\n}\n\n.form-check-input {\n appearance: none;\n width: 14px;\n height: 14px;\n flex: 0 0 auto;\n margin: 0;\n border: 1px solid var(--input);\n color: var(--primary-foreground);\n background-color: transparent;\n cursor: var(--cursor-interaction, default);\n transition:\n background-color 150ms,\n border-color 150ms,\n box-shadow 150ms;\n}\n\n.form-check:not(.form-switch) .form-check-input[type=\"checkbox\"] {\n border-color: var(--input);\n border-radius: var(--radius-sm);\n corner-shape: var(--corner-shape);\n background-color: var(--secondary);\n box-shadow: var(--shadow-sm);\n}\n\n.form-check:not(.form-switch)\n .form-check-input:not(:disabled):not(:checked):hover {\n background-color: var(--card);\n}\n\n.form-check:not(.form-switch) .form-check-input[type=\"checkbox\"]:checked {\n border-color: var(--primary);\n background-color: var(--primary);\n}\n\n.form-check:not(.form-switch)\n .form-check-input[type=\"checkbox\"]:checked::before {\n display: block;\n width: 100%;\n height: 100%;\n background: var(--primary-foreground);\n content: \"\";\n mask: var(--checkmark-image) center / 12px 12px no-repeat;\n}\n\n.form-check-input[type=\"radio\"] {\n width: 14px;\n height: 14px;\n border-radius: var(--radius-full);\n}\n\n.form-check-input[type=\"radio\"]:checked {\n border: 2px solid var(--primary);\n background:\n radial-gradient(circle, var(--primary-foreground) 0 2.5px, transparent 3px),\n var(--primary);\n}\n\n.form-check:not(.form-switch) .form-check-input:focus-visible {\n outline: 2px solid var(--ring);\n outline-offset: 2px;\n}\n\n.form-check:not(.form-switch) .form-check-input:disabled {\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.form-check:not(.form-switch) .form-check-input:disabled + .form-check-label {\n cursor: not-allowed;\n}\n\n.form-switch .form-check-input:disabled,\n.form-switch .form-check-input:disabled + .form-check-label {\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.form-check-label {\n color: var(--foreground);\n cursor: var(--cursor-interaction, default);\n}\n\n.form-switch .form-check-input {\n position: relative;\n width: 32px;\n height: 20px;\n border: 0;\n border-radius: var(--radius-full);\n background: var(--muted);\n box-shadow: none;\n transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n.form-switch .form-check-input::before {\n position: absolute;\n top: 50%;\n left: 0;\n width: 16px;\n height: 16px;\n box-sizing: border-box;\n border: 1px solid light-dark(var(--primary-foreground), var(--foreground));\n border-radius: var(--radius-full);\n background: light-dark(var(--primary-foreground), var(--foreground));\n box-shadow: var(--shadow-sm);\n content: \"\";\n transform: translate(2px, -50%);\n transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n.form-switch .form-check-input:checked {\n background: var(--primary);\n}\n\n.form-switch .form-check-input:checked::before {\n transform: translate(14px, -50%);\n}\n\n.form-switch .form-check-input:focus-visible {\n box-shadow: 0 0 0 2px var(--ring);\n}\n\n.form-range {\n appearance: none;\n display: block;\n width: 100%;\n height: 28px;\n flex: 1;\n margin: 0;\n padding: 0;\n outline: none;\n border: 0;\n accent-color: var(--primary);\n background: linear-gradient(\n color-mix(in srgb, var(--foreground) 7%, transparent),\n color-mix(in srgb, var(--foreground) 7%, transparent)\n )\n center / 100% 2px no-repeat;\n}\n\n.form-range::-webkit-slider-runnable-track {\n height: 28px;\n background: transparent;\n}\n\n.form-range::-webkit-slider-thumb {\n appearance: none;\n width: 20px;\n height: 20px;\n margin-top: 4px;\n border: 1px solid var(--border);\n border-radius: var(--radius-full);\n background: light-dark(var(--primary-foreground), var(--foreground));\n}\n\n.form-range:focus-visible::-webkit-slider-thumb {\n border-color: var(--ring);\n box-shadow: inset 0 0 0 1px var(--ring);\n}\n\n.form-range::-moz-range-track {\n height: 28px;\n background: transparent;\n}\n\n.form-range::-moz-range-thumb {\n width: 20px;\n height: 20px;\n border: 1px solid var(--border);\n border-radius: var(--radius-full);\n background: light-dark(var(--primary-foreground), var(--foreground));\n}\n\n.form-range:focus-visible::-moz-range-thumb {\n border-color: var(--ring);\n box-shadow: inset 0 0 0 1px var(--ring);\n}\n\n.form-range:disabled {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.btn:not(.btn-primary, .viz-tile):is(\n [aria-pressed=\"true\"],\n [aria-selected=\"true\"],\n .is-selected\n) {\n border-color: var(--primary);\n color: var(--primary-foreground);\n background: var(--primary);\n}\n\n.btn.viz-tile:is([aria-pressed=\"true\"], [aria-selected=\"true\"], .is-selected) {\n border-color: var(--primary);\n box-shadow: inset 0 0 0 1px var(--primary);\n}\n\n.btn:focus-visible,\n.nav-link:focus-visible {\n outline: 2px solid var(--ring);\n outline-offset: 2px;\n}\n\n@media (pointer: coarse) {\n .btn,\n .nav-link,\n .form-control,\n .form-select {\n min-width: 44px;\n min-height: 44px;\n }\n\n /* Keep small-screen WebKit from zooming when a text field receives focus. */\n .form-control,\n .form-select {\n font-size: max(16px, var(--font-size-normal));\n }\n\n .form-control[type=\"file\"]::file-selector-button {\n min-width: 44px;\n min-height: 44px;\n }\n\n .form-control-color[type=\"color\"] {\n width: 44px;\n height: 44px;\n }\n\n .form-control:is(\n [type=\"date\"],\n [type=\"datetime-local\"],\n [type=\"month\"],\n [type=\"time\"],\n [type=\"week\"]\n ) {\n min-width: 0;\n max-width: 100%;\n padding-block: 8px;\n }\n\n .form-check {\n min-height: 44px;\n padding-inline: 15px;\n }\n\n .form-check-input {\n position: relative;\n }\n\n .form-check-input::after {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 44px;\n height: 44px;\n content: \"\";\n transform: translate(-50%, -50%);\n }\n\n .form-check-label {\n display: inline-flex;\n min-width: 44px;\n min-height: 44px;\n align-items: center;\n }\n\n .form-range {\n height: 44px;\n touch-action: pan-y;\n }\n\n .form-range::-webkit-slider-runnable-track {\n height: 44px;\n }\n\n .form-range::-moz-range-track {\n height: 44px;\n }\n\n .form-range::-webkit-slider-thumb {\n margin-top: 12px;\n }\n}\n\nsvg {\n display: block;\n max-width: 100%;\n height: auto;\n}\n\n#widget > svg {\n width: 100%;\n}\n\n/* Model-authored chart rules can otherwise stretch icons after these styles load. */\nsvg.lucide {\n display: block;\n width: var(--icon-size) !important;\n height: var(--icon-size) !important;\n flex: none;\n margin: 0 !important;\n stroke-width: 1.6;\n}\n\n.text-warning,\n.text-destructive {\n color: var(--destructive);\n}\n";
3
+ export const VISUALIZATION_HELPERS = "\n<script src=\"https://unpkg.com/@floating-ui/core@1.7.3/dist/floating-ui.core.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@floating-ui/dom@1.7.4/dist/floating-ui.dom.umd.min.js\"></script>\n<script>\n (() => {\n if (window.FloatingUIDOM == null) {\n return;\n }\n const { autoUpdate, computePosition, flip, offset, shift, size } = window.FloatingUIDOM;\n const tooltipPlacements = new Set([\"top\", \"right\", \"bottom\", \"left\"]);\n const tooltipId = `codex-visualization-tooltip-${\n window.crypto?.randomUUID?.() ?? Date.now().toString(36)\n }`;\n const tooltipGap = 5;\n const hoverDelay = 700;\n const skipDelay = 300;\n let activeTrigger = null;\n let pendingTrigger = null;\n let tooltip = null;\n let openTimeout = null;\n let autoUpdateCleanup = null;\n let positionRequest = 0;\n let skipDelayUntil = 0;\n let lastPointerWasTouch = false;\n let pinnedByTouch = false;\n\n const getTrigger = (target) =>\n target?.nodeType === Node.ELEMENT_NODE ? target.closest(\"[data-tooltip]\") : null;\n const containsTarget = (trigger, target) =>\n target?.nodeType != null && trigger.contains(target);\n const getContent = (trigger) => trigger.getAttribute(\"data-tooltip\")?.trim() ?? \"\";\n const getPlacement = (trigger) => {\n const placement = trigger.getAttribute(\"data-tooltip-placement\");\n return tooltipPlacements.has(placement) ? placement : \"top\";\n };\n const getTooltip = () => {\n if (tooltip == null) {\n tooltip = document.createElement(\"div\");\n tooltip.id = tooltipId;\n tooltip.className = \"tooltip\";\n tooltip.setAttribute(\"role\", \"tooltip\");\n }\n return tooltip;\n };\n const setDescribedBy = (trigger, described) => {\n const ids = new Set(\n (trigger.getAttribute(\"aria-describedby\") ?? \"\").split(/\\s+/).filter(Boolean),\n );\n if (described) {\n ids.add(tooltipId);\n } else {\n ids.delete(tooltipId);\n }\n if (ids.size === 0) {\n trigger.removeAttribute(\"aria-describedby\");\n } else {\n trigger.setAttribute(\"aria-describedby\", [...ids].join(\" \"));\n }\n };\n const clearOpenTimeout = () => {\n if (openTimeout != null) {\n window.clearTimeout(openTimeout);\n openTimeout = null;\n }\n pendingTrigger = null;\n };\n const closeTooltip = (activateSkipDelay = true) => {\n clearOpenTimeout();\n pinnedByTouch = false;\n positionRequest += 1;\n autoUpdateCleanup?.();\n autoUpdateCleanup = null;\n if (activeTrigger == null) {\n return;\n }\n setDescribedBy(activeTrigger, false);\n activeTrigger = null;\n tooltip?.remove();\n if (activateSkipDelay) {\n skipDelayUntil = Date.now() + skipDelay;\n }\n };\n const updatePosition = (trigger, floating) => {\n const request = ++positionRequest;\n void computePosition(trigger, floating, {\n middleware: [\n offset(tooltipGap),\n flip({ padding: tooltipGap }),\n shift({ padding: tooltipGap }),\n size({\n padding: tooltipGap,\n apply({ availableHeight, availableWidth, elements }) {\n elements.floating.style.setProperty(\n \"--tooltip-available-width\",\n `${Math.max(0, availableWidth)}px`,\n );\n elements.floating.style.setProperty(\n \"--tooltip-available-height\",\n `${Math.max(0, availableHeight)}px`,\n );\n },\n }),\n ],\n placement: getPlacement(trigger),\n strategy: \"fixed\",\n })\n .then(({ x, y }) => {\n if (activeTrigger !== trigger || request !== positionRequest) {\n return;\n }\n const scale = window.devicePixelRatio || 1;\n floating.style.transform = `translate(${Math.round(x * scale) / scale}px, ${Math.round(y * scale) / scale}px)`;\n floating.style.visibility = \"visible\";\n })\n .catch(() => {\n if (activeTrigger === trigger && request === positionRequest) {\n closeTooltip(false);\n }\n });\n };\n const openTooltip = (trigger) => {\n const content = getContent(trigger);\n if (!trigger.isConnected || content.length === 0) {\n return;\n }\n clearOpenTimeout();\n if (activeTrigger === trigger) {\n return;\n }\n closeTooltip();\n const floating = getTooltip();\n floating.textContent = content;\n floating.style.visibility = \"hidden\";\n floating.style.transform = \"translate(0, 0)\";\n document.body.appendChild(floating);\n activeTrigger = trigger;\n setDescribedBy(trigger, true);\n autoUpdateCleanup = autoUpdate(trigger, floating, () => {\n updatePosition(trigger, floating);\n });\n };\n const requestOpen = (trigger, immediate = false) => {\n if (\n getContent(trigger).length === 0 ||\n activeTrigger === trigger ||\n pendingTrigger === trigger\n ) {\n return;\n }\n clearOpenTimeout();\n pendingTrigger = trigger;\n const delay =\n immediate || activeTrigger != null || Date.now() < skipDelayUntil ? 0 : hoverDelay;\n if (delay === 0) {\n openTooltip(trigger);\n return;\n }\n openTimeout = window.setTimeout(() => {\n openTimeout = null;\n pendingTrigger = null;\n openTooltip(trigger);\n }, delay);\n };\n const handlePointerOpen = (event) => {\n if (event.pointerType === \"touch\") {\n return;\n }\n const trigger = getTrigger(event.target);\n if (trigger == null || containsTarget(trigger, event.relatedTarget)) {\n return;\n }\n requestOpen(trigger);\n };\n const handlePointer = (event) => {\n lastPointerWasTouch = event.type === \"pointerdown\" && event.pointerType === \"touch\";\n };\n const handleTouchClick = (event) => {\n const isTouch = event.pointerType === \"touch\" || lastPointerWasTouch;\n lastPointerWasTouch = false;\n if (!isTouch || event.detail === 0) {\n return;\n }\n const trigger = getTrigger(event.target);\n if (\n trigger == null ||\n !trigger.isConnected ||\n getContent(trigger).length === 0 ||\n (pinnedByTouch && activeTrigger === trigger)\n ) {\n if (pinnedByTouch) {\n closeTooltip();\n }\n return;\n }\n openTooltip(trigger);\n pinnedByTouch = activeTrigger === trigger;\n window.setTimeout(() => {\n if (activeTrigger === trigger && !trigger.isConnected) {\n closeTooltip(false);\n }\n }, 0);\n };\n const handleLeave = (event) => {\n if (pinnedByTouch) {\n return;\n }\n const trigger = getTrigger(event.target);\n if (trigger == null || containsTarget(trigger, event.relatedTarget)) {\n return;\n }\n if (activeTrigger === trigger || pendingTrigger === trigger) {\n closeTooltip();\n }\n };\n const handleFocusIn = (event) => {\n const trigger = getTrigger(event.target);\n if (trigger?.matches(\":focus-visible\")) {\n requestOpen(trigger, true);\n }\n };\n const handleKeyDown = (event) => {\n if (event.key === \"Escape\") {\n closeTooltip();\n }\n };\n const handleContextMenu = (event) => {\n lastPointerWasTouch = false;\n const trigger = getTrigger(event.target);\n if (activeTrigger === trigger || pendingTrigger === trigger) {\n closeTooltip();\n }\n };\n const destroy = () => {\n closeTooltip(false);\n lastPointerWasTouch = false;\n document.removeEventListener(\"pointerover\", handlePointerOpen);\n document.removeEventListener(\"pointerdown\", handlePointer);\n document.removeEventListener(\"pointercancel\", handlePointer);\n document.removeEventListener(\"click\", handleTouchClick);\n document.removeEventListener(\"pointerout\", handleLeave);\n document.removeEventListener(\"focusin\", handleFocusIn);\n document.removeEventListener(\"focusout\", handleLeave);\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.removeEventListener(\"contextmenu\", handleContextMenu);\n tooltip = null;\n };\n\n document.addEventListener(\"pointerover\", handlePointerOpen);\n document.addEventListener(\"pointerdown\", handlePointer);\n document.addEventListener(\"pointercancel\", handlePointer);\n document.addEventListener(\"click\", handleTouchClick);\n document.addEventListener(\"pointerout\", handleLeave);\n document.addEventListener(\"focusin\", handleFocusIn);\n document.addEventListener(\"focusout\", handleLeave);\n document.addEventListener(\"keydown\", handleKeyDown);\n document.addEventListener(\"contextmenu\", handleContextMenu);\n window.addEventListener(\"pagehide\", destroy, { once: true });\n })();\n</script>\n<script id=\"codex-visualization-tabs\">\n (() => {\n const selector = '.nav[role=\"tablist\"] [role=\"tab\"]';\n const disabled = ':disabled,[aria-disabled=\"true\"]';\n const selectTab = (selected) => {\n const panelId = selected.getAttribute(\"aria-controls\");\n for (const tab of selected.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]')) {\n const active = tab === selected;\n tab.classList.toggle(\"active\", active);\n tab.setAttribute(\"aria-selected\", String(active));\n const id = tab.getAttribute(\"aria-controls\");\n const panel = id && document.getElementById(id);\n if (panel != null) {\n panel.hidden = id !== panelId;\n if (!panel.hidden && selected.id) {\n panel.setAttribute(\"aria-labelledby\", selected.id);\n }\n }\n }\n };\n const interact = (event) => {\n const tab = event.target?.closest?.(selector);\n if (event.defaultPrevented || tab == null || tab.matches(disabled)) {\n return;\n }\n if (event.type === \"click\") {\n selectTab(tab);\n return;\n }\n const tabs = Array.from(\n tab.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]'),\n ).filter((candidate) => !candidate.matches(disabled));\n const current = tabs.indexOf(tab);\n let next;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (current + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (current + tabs.length - 1) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n tabs[next].focus();\n selectTab(tabs[next]);\n };\n\n for (const list of document.querySelectorAll('.nav[role=\"tablist\"]')) {\n const selected =\n list.querySelector('[role=\"tab\"][aria-selected=\"true\"]') ??\n list.querySelector('[role=\"tab\"].active') ??\n list.querySelector('[role=\"tab\"]');\n if (selected != null) {\n selectTab(selected);\n }\n }\n document.addEventListener(\"click\", interact);\n document.addEventListener(\"keydown\", interact);\n window.addEventListener(\n \"pagehide\",\n () => {\n document.removeEventListener(\"click\", interact);\n document.removeEventListener(\"keydown\", interact);\n },\n { once: true },\n );\n })();\n</script>\n<script\n id=\"codex-visualization-lucide\"\n async\n src=\"https://unpkg.com/lucide@1.17.0/dist/umd/lucide.js\"\n></script>\n<script>\n (() => {\n const initialize = () => {\n globalThis.lucide?.createIcons({ attrs: { width: 16, height: 16 } });\n };\n if (globalThis.lucide != null) {\n initialize();\n return;\n }\n document\n .getElementById(\"codex-visualization-lucide\")\n ?.addEventListener(\"load\", initialize, { once: true });\n })();\n</script>\n";
@@ -142,11 +142,16 @@ export function CopyHoverFrame({
142
142
  <div
143
143
  data-og-annotation-chrome=""
144
144
  className={cn(
145
- "mt-1 flex h-7 items-center gap-1.5",
145
+ "flex h-6 items-center gap-1.5 pointer-coarse:h-11",
146
146
  align === "end" ? "justify-end" : "justify-start",
147
147
  )}
148
148
  >
149
- <CopyButton text={copyText} label={label} reveal="group-hover" />
149
+ <CopyButton
150
+ text={copyText}
151
+ label={label}
152
+ reveal="group-hover"
153
+ className="size-6 pointer-coarse:size-11"
154
+ />
150
155
  {trailing}
151
156
  </div>
152
157
  </div>
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  Children,
3
+ createContext,
4
+ useContext,
3
5
  isValidElement,
4
6
  memo,
5
7
  useEffect,
@@ -13,6 +15,7 @@ import {
13
15
  import ReactMarkdown, {
14
16
  defaultUrlTransform,
15
17
  type Components,
18
+ type ExtraProps,
16
19
  type UrlTransform,
17
20
  } from "react-markdown";
18
21
  import remarkGfm from "remark-gfm";
@@ -40,7 +43,17 @@ import { TooltipProvider } from "./tooltip";
40
43
  * the tokens land. Consumers who want a different renderer can still pass
41
44
  * `renderMessageText` to `MessageTimeline` to override this entirely.
42
45
  */
46
+ export type MarkdownInteractiveBlock = { kind: "html" | "site"; content: string };
47
+ const InteractiveContext = createContext<{
48
+ source: string;
49
+ render?: (block: MarkdownInteractiveBlock) => ReactNode;
50
+ renderImage?: (image: { src: string; alt: string }) => ReactNode;
51
+ }>({ source: "" });
52
+
43
53
  export type MarkdownProps = {
54
+ renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
55
+ /** Host opt-in for assistant-authored interactive fences. */
56
+ renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
44
57
  children: string;
45
58
  className?: string | undefined;
46
59
  /**
@@ -171,7 +184,7 @@ const baseComponents: Components = {
171
184
  </code>
172
185
  ),
173
186
  // Fenced code — quiet mono block (no card / no nested vertical scroll).
174
- pre: ({ children }) => <MarkdownCodeBlock>{children}</MarkdownCodeBlock>,
187
+ pre: InteractiveCodeBlock,
175
188
  // Tables stay unboxed (hairline rules); hover reveals a TSV copy control.
176
189
  table: ({ children, ...props }) => <MarkdownTable {...props}>{children}</MarkdownTable>,
177
190
  thead: ({ children, ...props }) => <thead {...props}>{children}</thead>,
@@ -191,19 +204,7 @@ const baseComponents: Components = {
191
204
  {children}
192
205
  </td>
193
206
  ),
194
- img: ({ alt, src, ...props }) =>
195
- src ? (
196
- <img
197
- alt={alt ?? ""}
198
- src={src}
199
- className="my-3 max-w-full rounded-og-md border border-og-border"
200
- {...props}
201
- />
202
- ) : alt ? (
203
- <span className="text-og-fg-subtle" aria-disabled="true">
204
- {alt}
205
- </span>
206
- ) : null,
207
+ img: MarkdownImage,
207
208
  };
208
209
 
209
210
  const MARKDOWN_LINK_CLASS =
@@ -343,8 +344,30 @@ export function sandboxFilePathFromHref(href: string | undefined): string | null
343
344
  return sandboxFileLocationFromHref(href)?.path ?? null;
344
345
  }
345
346
 
347
+ export function retainedImageId(src: string): string | null {
348
+ return (
349
+ /^artifact:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i
350
+ .exec(src)?.[1]
351
+ ?.toLowerCase() ?? null
352
+ );
353
+ }
354
+ function MarkdownImage({ src, alt }: ComponentPropsWithoutRef<"img">) {
355
+ const { renderImage } = useContext(InteractiveContext);
356
+ if (!src) return <span>{alt ?? "Image unavailable"}</span>;
357
+ if (retainedImageId(src))
358
+ return renderImage ? (
359
+ <>{renderImage({ src, alt: alt ?? "" })}</>
360
+ ) : (
361
+ <span>{alt || "Image"} (preview unavailable)</span>
362
+ );
363
+ return <img src={src} alt={alt ?? ""} loading="lazy" className="my-3 max-w-full rounded-og-md" />;
364
+ }
365
+
346
366
  const markdownUrlTransform: UrlTransform = (url, key, node) =>
347
- key === "href" && node.tagName === "a" && isSandboxHref(url) ? url : defaultUrlTransform(url);
367
+ (key === "href" && node.tagName === "a" && isSandboxHref(url)) ||
368
+ (key === "src" && node.tagName === "img" && retainedImageId(url))
369
+ ? url
370
+ : defaultUrlTransform(url);
348
371
 
349
372
  /** How long after the stream ends the reveal pipeline stays for trailing animations. */
350
373
  /** Trailing ink window after stream end — keep ≥ {@link INK_FADE_MS}. */
@@ -382,6 +405,42 @@ function fenceLanguage(children: ReactNode): string | null {
382
405
  return found;
383
406
  }
384
407
 
408
+ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"> & ExtraProps) {
409
+ const { source, render } = useContext(InteractiveContext);
410
+ const language = fenceLanguage(children);
411
+ if (render && (language === "opengeni-html" || language === "opengeni-site")) {
412
+ const start = node?.position?.start.offset;
413
+ const end = node?.position?.end.offset;
414
+ const original =
415
+ start !== undefined && end !== undefined && end <= source.length
416
+ ? source.slice(start, end)
417
+ : "";
418
+ const lines = original.trimEnd().split("\n");
419
+ const opening = lines[0]?.match(/^ {0,3}([\x60]{3,}|~{3,})/);
420
+
421
+ const closing = lines
422
+ .at(-1)
423
+ ?.replace(/^\s*(?:>\s*)+/, "")
424
+ .trim();
425
+ const complete =
426
+ opening &&
427
+ lines.length > 1 &&
428
+ closing &&
429
+ closing.length >= opening[1]!.length &&
430
+ [...closing].every((c) => c === opening[1]![0]);
431
+ if (!complete) return <p role="status">Preparing preview…</p>;
432
+ return (
433
+ <>
434
+ {render({
435
+ kind: language === "opengeni-html" ? "html" : "site",
436
+ content: nodeText(children).replace(/\n$/, ""),
437
+ })}
438
+ </>
439
+ );
440
+ }
441
+ return <MarkdownCodeBlock>{children}</MarkdownCodeBlock>;
442
+ }
443
+
385
444
  function MarkdownCodeBlock({ children }: { children?: ReactNode }) {
386
445
  const code = nodeText(children).replace(/\n$/, "");
387
446
  const language = fenceLanguage(children);
@@ -462,7 +521,14 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
462
521
  );
463
522
  }
464
523
 
465
- function MarkdownImpl({ children, className, streaming = false, onSandboxFile }: MarkdownProps) {
524
+ function MarkdownImpl({
525
+ children,
526
+ className,
527
+ streaming = false,
528
+ onSandboxFile,
529
+ renderInteractiveBlock,
530
+ renderImage,
531
+ }: MarkdownProps) {
466
532
  // Tip-ink engine for THIS body: created on the first streaming render, kept
467
533
  // through a short linger after the stream ends (so the last age window can
468
534
  // finish), then dropped so settled bodies pay zero cost. Observing during
@@ -553,28 +619,39 @@ function MarkdownImpl({ children, className, streaming = false, onSandboxFile }:
553
619
  const parseText = streaming || revealActive ? softenStreamingMarkdown(children) : children;
554
620
  const components = useMemo(() => markdownComponents(onSandboxFile), [onSandboxFile]);
555
621
 
622
+ const interactiveContext = useMemo(
623
+ () => ({
624
+ source: children,
625
+ ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
626
+ ...(renderImage ? { renderImage } : {}),
627
+ }),
628
+ [children, renderInteractiveBlock, renderImage],
629
+ );
630
+
556
631
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
557
632
  // long links and code blocks wrap/scroll instead of forcing overflow.
558
633
  return (
559
- <TooltipProvider delayDuration={400}>
560
- <div
561
- ref={bodyRef}
562
- className={cn(
563
- "og-markdown-body min-w-0 break-words",
564
- settling && "og-markdown-settle",
565
- className,
566
- )}
567
- >
568
- <ReactMarkdown
569
- remarkPlugins={[remarkGfm]}
570
- rehypePlugins={rehypePlugins}
571
- components={components}
572
- urlTransform={markdownUrlTransform}
634
+ <InteractiveContext.Provider value={interactiveContext}>
635
+ <TooltipProvider delayDuration={400}>
636
+ <div
637
+ ref={bodyRef}
638
+ className={cn(
639
+ "og-markdown-body min-w-0 break-words",
640
+ settling && "og-markdown-settle",
641
+ className,
642
+ )}
573
643
  >
574
- {parseText}
575
- </ReactMarkdown>
576
- </div>
577
- </TooltipProvider>
644
+ <ReactMarkdown
645
+ remarkPlugins={[remarkGfm]}
646
+ rehypePlugins={rehypePlugins}
647
+ components={components}
648
+ urlTransform={markdownUrlTransform}
649
+ >
650
+ {parseText}
651
+ </ReactMarkdown>
652
+ </div>
653
+ </TooltipProvider>
654
+ </InteractiveContext.Provider>
578
655
  );
579
656
  }
580
657