@opengeni/runtime 2.5.3-canary.3 → 2.6.0-canary.1

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.
@@ -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>
@@ -15,6 +15,13 @@ provider-specific API wrapper, or OpenGeni-only build CLI.
15
15
 
16
16
  ## Start from the durable Site
17
17
 
18
+ - For an existing page the user wants to view and share, consider Sites before
19
+ choosing a temporary tunnel or another server. Inspect whether it can be
20
+ packaged as self-contained HTML, preserve its design and behavior, and retain
21
+ editable source when available. A local preview alone is still appropriate
22
+ when that is all the user requests. Explain workspace access for the published
23
+ link; do not imply it is public to anyone. If the app requires a backend that
24
+ Sites cannot host, explain that constraint instead of dropping functionality.
18
25
  - For a new Site, create a normal project directory with `package.json`,
19
26
  `index.html`, TypeScript/React source, styles, tests, and any ordinary build
20
27
  configuration the app needs.
@@ -112,6 +119,41 @@ Exercise the real interactions at desktop and mobile widths, inspect console
112
119
  errors, and take a screenshot when visual quality matters. Do not declare the
113
120
  Site complete from compilation alone.
114
121
 
122
+ ## Plain HTML without a browser build
123
+
124
+ For a small HTML-only Site, use ordinary HTML, CSS, and JavaScript. If it calls
125
+ OpenGeni tools or SDK methods, place this exact optional tag before your own
126
+ scripts (without async, defer, or type="module"):
127
+
128
+ ```html
129
+ <script src="/__opengeni/site-tools/client.js"></script>
130
+ <script>
131
+ const site = createOpenGeniSiteClient();
132
+ // Use the actual tool paths discovered with ogtool on site.tools.
133
+ // site.client is the same ordinary SDK client as in a React Site.
134
+ </script>
135
+ ```
136
+
137
+ The existing preview handler above serves this script. Serve the raw HTML with
138
+ `new Response(Bun.file("./index.html"))` alongside its tool route; no browser
139
+ build is needed. In the published viewer, the host resolves the tag to its
140
+ browser client. Both use the same SDK implementation and existing tool bridge.
141
+ Opening the file directly supplies no tool host. There are no credentials in
142
+ the tag or HTML. Declare requested tools and handle errors as for any Site.
143
+
144
+ Use the exact package pins above for the preview host. The preview runtime comes
145
+ from that installed SDK; the published runtime comes from the viewer's deployed
146
+ SDK. The tag does not install dependencies, replace React imports, or change Bun.
147
+ For TypeScript checking, React components, or imported browser dependencies,
148
+ use the normal typed SDK and build workflow above. Do not use both client-loading
149
+ paths in one document.
150
+
151
+ Publish the original HTML containing the tag, not the served client script or
152
+ transformed viewer document. For this path, upload `index.html` wherever the
153
+ publishing instructions below use `dist/index.html`. Inline ordinary styles,
154
+ scripts, and assets as usual; the OpenGeni client tag is resolved by the host.
155
+ An HTML-only Site can be a small widget; it does not require a full-page layout.
156
+
115
157
  ## Prefer OpenGeni's UI and typed client
116
158
 
117
159
  - Prefer `@opengeni/react` components and compiled CSS for OpenGeni-native
@@ -259,13 +301,38 @@ access limitation. Missing publishing access itself still blocks publication.
259
301
  SDK operations, with no direct `site.tools` calls, should publish
260
302
  `requestedTools: []`.
261
303
  4. The immutable requested set is only a maximum allowlist; publishing it grants
262
- no tool authority and requires no separate tool approval. Site calls do not open
263
- per-call approval dialogs.
304
+ no tool authority and requires no separate tool approval. Execution still follows
305
+ normal tool approval rules; handle approval-required responses in the UI.
264
306
  5. The host intersects that set with the viewer's
265
307
  live workspace, permission, and connection authority on every call. Handle
266
308
  missing tools, revoked connections, stale catalogs, and access loss as normal
267
309
  user-visible error states.
268
310
 
311
+ ## Show a visualization or Site inside chat
312
+
313
+ For an inline visualization stored directly in a chat response, read
314
+ `opengeni-visualize`. That path does not require publishing a Site.
315
+
316
+ To embed a published Site, use a fenced `opengeni-site` block containing JSON:
317
+
318
+ ```opengeni-site
319
+ {"siteId":"<artifact.id>","versionId":"<published version.id>"}
320
+ ```
321
+
322
+ Use the actual returned ids. `versionId` is optional; omitting it loads the
323
+ current version when the embed opens. Including it preserves the intended
324
+ version. The viewer can choose another version within the embed without
325
+ publishing or rolling back the Site. The embed uses the same Site renderer,
326
+ reload/full-screen controls, tool declarations and viewer permissions as the
327
+ Site page. Embeds resolve in the chat's workspace. Keep a normal Site link when
328
+ it helps the user reopen or share it. Archived or inaccessible Sites display
329
+ an unavailable state.
330
+
331
+ Use inline HTML for a visualization that belongs in this response. Use a Site
332
+ for a reusable app or durable page, and embed it when useful. Both can be small;
333
+ neither requires a dashboard or full-page layout. React Sites still use the
334
+ normal build and publish workflow.
335
+
269
336
  ## Publish one immutable version
270
337
 
271
338
  1. Call `opengeni__artifacts_prepare_upload` (no hashes or sizes required).
@@ -354,7 +354,8 @@ export class GmailRestMcpServer implements LocalMcpBridgeServer {
354
354
  forceRefresh: false,
355
355
  ...(this.options.subjectId ? { subjectId: this.options.subjectId } : {}),
356
356
  });
357
- if (result.status !== "ok") {
357
+ if (result.status === "auth_needed") {
358
+ await this.reportAuthNeeded(result);
358
359
  throw new GmailRestAuthError("Authentication required for Gmail");
359
360
  }
360
361
  this.options.onResolvedConnectionId?.(result.connectionId);
@@ -683,6 +684,28 @@ export class GmailRestMcpServer implements LocalMcpBridgeServer {
683
684
  };
684
685
  }
685
686
 
687
+ private async reportAuthNeeded(
688
+ result: Extract<ResolveCredentialResult, { status: "auth_needed" }>,
689
+ toolName?: string,
690
+ ): Promise<void> {
691
+ await this.options.onAuthNeeded?.({
692
+ serverId: this.options.serverId,
693
+ ...(toolName ? { toolName } : {}),
694
+ providerDomain: result.providerDomain,
695
+ ...(result.provider ? { provider: result.provider } : {}),
696
+ reason: result.reason,
697
+ ...(result.connectionId ? { connectionId: result.connectionId } : {}),
698
+ ...(result.authoritySource === "host" || this.options.connectionRef.authoritySource === "host"
699
+ ? { authoritySource: "host" as const }
700
+ : {}),
701
+ ...(result.scopes ? { scopes: result.scopes } : {}),
702
+ ...(result.resource ? { resource: result.resource } : {}),
703
+ ...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
704
+ ...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
705
+ ...(this.options.subjectId ? { subjectId: this.options.subjectId } : {}),
706
+ });
707
+ }
708
+
686
709
  private async request<T>(
687
710
  toolName: string,
688
711
  urlInput: string | URL,
@@ -708,23 +731,7 @@ export class GmailRestMcpServer implements LocalMcpBridgeServer {
708
731
  ...(this.options.subjectId ? { subjectId: this.options.subjectId } : {}),
709
732
  });
710
733
  if (result.status === "auth_needed") {
711
- await this.options.onAuthNeeded?.({
712
- serverId: this.options.serverId,
713
- toolName,
714
- providerDomain: result.providerDomain,
715
- ...(result.provider ? { provider: result.provider } : {}),
716
- reason: result.reason,
717
- ...(result.connectionId ? { connectionId: result.connectionId } : {}),
718
- ...(result.authoritySource === "host" ||
719
- this.options.connectionRef.authoritySource === "host"
720
- ? { authoritySource: "host" as const }
721
- : {}),
722
- ...(result.scopes ? { scopes: result.scopes } : {}),
723
- ...(result.resource ? { resource: result.resource } : {}),
724
- ...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
725
- ...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
726
- ...(this.options.subjectId ? { subjectId: this.options.subjectId } : {}),
727
- });
734
+ await this.reportAuthNeeded(result, toolName);
728
735
  throw new GmailRestAuthError("Authentication required for Gmail");
729
736
  }
730
737
  this.options.onResolvedConnectionId?.(result.connectionId);
@@ -72,21 +72,11 @@ Your answer is being rendered by an application for the user. Follow these guide
72
72
  * Do not provide ranges of lines.
73
73
  * Avoid repeating the same filename multiple times when one grouping is clearer.
74
74
 
75
- ### Visualizations
75
+ ### Visuals in chat
76
76
 
77
- Use a visualization only when it makes an important relationship materially easier to understand than prose or a short list. Do not add one merely because an answer has components or steps.
77
+ Use inline HTML when an interactive visualization materially helps the user; read the opengeni-visualize skill first. For building, publishing, or embedding a saved Site, read opengeni-sites when available. Use ordinary Markdown for simple explanations and tables.
78
78
 
79
- Good candidates include:
80
-
81
- - several exact mappings or repeated-field comparisons;
82
- - one source, component, or decision affecting three or more downstream consumers or branches;
83
- - three or more dependent steps, or state that changes across an event sequence;
84
- - hierarchy, ownership, nesting, or layout;
85
- - a bug or interaction whose relationships are difficult to explain linearly.
86
-
87
- Prefer the smallest useful visual: a table for mappings or comparisons, a flow or timeline for sequence or change, a tree for hierarchy or branching, and a wireframe for layout.
88
-
89
- Usually skip visuals for single facts, one-step actions, simple edits, basic instructions, or information already clear in a short paragraph or list. Compact notation and small examples do not count as visualizations.
79
+ Display images with ![descriptive alt text](artifact:<artifactId>). Use the exact retained artifact id from an image tool or sandbox_file_publish receipt. For a sandbox image, publish the file first; a sandbox path is not an inline image source. Keep image bytes, credentials, and temporary download URLs out of the response. Ordinary public image URLs also work. For custom image sizes or galleries, follow opengeni-visualize; raw HTML image tags in ordinary Markdown are displayed as text.
90
80
 
91
81
  # Rules for getting work done
92
82