@opengeni/react 6.1.0-canary.3 → 6.1.0-canary.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "6.1.0-canary.3",
3
+ "version": "6.1.0-canary.4",
4
4
  "description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -133,8 +133,8 @@
133
133
  "@dnd-kit/utilities": "3.2.2",
134
134
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
135
135
  "@fontsource-variable/noto-sans-jp": "5.2.10",
136
- "@opengeni/connect": "^0.2.0-canary.14",
137
- "@opengeni/sdk": "^6.1.0-canary.3",
136
+ "@opengeni/connect": "^0.2.0-canary.15",
137
+ "@opengeni/sdk": "^6.1.0-canary.4",
138
138
  "clsx": "^2.1.1",
139
139
  "dequal": "2.0.3",
140
140
  "lucide-react": "^1.8.0",
@@ -26,6 +26,7 @@ import { tableElementToTsv } from "../lib/clipboard";
26
26
  import { prefersReducedMotion } from "../lib/motion";
27
27
  import { MOTION_INSPECT_SCALE } from "../lib/motion-inspect";
28
28
  import { CopyButton } from "./copy-button";
29
+ import { PreviewLoading } from "./preview-loading";
29
30
  import type { observeMarkdownTableLayout } from "./markdown-table-layout";
30
31
  import { softenStreamingMarkdown } from "./soften-streaming-markdown";
31
32
  import { createStreamReveal, rehypeStreamReveal, type StreamReveal } from "./stream-reveal";
@@ -434,13 +435,17 @@ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"
434
435
  if (!complete) {
435
436
  return (
436
437
  <div role="status" aria-live="polite" aria-busy={streaming === true} className="my-3">
437
- <ActivityDisclosure
438
- icon={<PanelsTopLeftIcon aria-hidden className="size-3.5" />}
439
- title={streaming ? "Preparing preview…" : "Preview incomplete"}
440
- running={streaming === true}
441
- expandable={false}
442
- preview={streaming ? undefined : "Generation stopped before the preview was ready."}
443
- />
438
+ {streaming ? (
439
+ <PreviewLoading />
440
+ ) : (
441
+ <ActivityDisclosure
442
+ icon={<PanelsTopLeftIcon aria-hidden className="size-3.5" />}
443
+ title="Preview incomplete"
444
+ running={false}
445
+ expandable={false}
446
+ preview="Generation stopped before the preview was ready."
447
+ />
448
+ )}
444
449
  </div>
445
450
  );
446
451
  }
@@ -0,0 +1,154 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /** Private loading surface: never receives or executes the unfinished fence. */
4
+ export function PreviewLoading() {
5
+ const ref = useRef<HTMLCanvasElement>(null);
6
+ useEffect(() => {
7
+ const canvas = ref.current;
8
+ if (!canvas) return;
9
+ let ctx: CanvasRenderingContext2D | null;
10
+ try {
11
+ ctx = canvas.getContext("2d");
12
+ } catch {
13
+ return; // The CSS grid and status remain when canvas is unavailable.
14
+ }
15
+ if (!ctx) return;
16
+ const context = ctx;
17
+ const motion = window.matchMedia?.("(prefers-reduced-motion: reduce)");
18
+ let frame: number | undefined;
19
+ let time = 2;
20
+ let last: number | undefined;
21
+ let disposed = false;
22
+ // Without visibility observation, keep a static surface rather than doing
23
+ // unbounded work for previews that may be outside the viewport.
24
+ let visible = false;
25
+ const canAnimate = () => !disposed && visible && !document.hidden && motion?.matches === false;
26
+
27
+ function draw() {
28
+ const w = canvas!.clientWidth;
29
+ const h = canvas!.clientHeight;
30
+ if (!w || !h || document.hidden || disposed) return;
31
+ const d = Math.min(window.devicePixelRatio || 1, 2);
32
+ if (canvas!.width !== Math.round(w * d) || canvas!.height !== Math.round(h * d)) {
33
+ canvas!.width = Math.round(w * d);
34
+ canvas!.height = Math.round(h * d);
35
+ }
36
+ const style = getComputedStyle(canvas!);
37
+ const ink = style.getPropertyValue("--_og-preview-ink").trim() || "#bca3ef";
38
+ const glowColor = style.getPropertyValue("--_og-preview-glow").trim() || "#8f6ccc";
39
+ context.setTransform(d, 0, 0, d, 0, 0);
40
+ context.clearRect(0, 0, w, h);
41
+ const cx = w * (0.5 + 0.13 * Math.sin(time * 0.21));
42
+ const cy = h * (0.5 + 0.12 * Math.cos(time * 0.27));
43
+ const glow = context.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.65);
44
+ glow.addColorStop(0, glowColor);
45
+ glow.addColorStop(1, "transparent");
46
+ context.globalAlpha = 0.09;
47
+ context.fillStyle = glow;
48
+ context.fillRect(0, 0, w, h);
49
+ const rows = Math.ceil(h / 18) + 7;
50
+ const cols = Math.ceil(w / 18) + 7;
51
+ const points = Array.from({ length: rows }, (_row, r) =>
52
+ Array.from({ length: cols }, (_col, c) => {
53
+ const x = (c - 3) * 18;
54
+ const y = (r - 3) * 18;
55
+ const dist = Math.hypot((x - cx) * 0.8, y - cy);
56
+ const wave = Math.sin(dist * 0.032 - time * 0.95);
57
+ return {
58
+ x: x + ((x - cx) / Math.max(dist, 1)) * wave * 4,
59
+ y: y + ((y - cy) / Math.max(dist, 1)) * wave * 4,
60
+ l: Math.pow((wave + 1) / 2, 5),
61
+ };
62
+ }),
63
+ );
64
+ context.strokeStyle = ink;
65
+ context.fillStyle = ink;
66
+ context.lineWidth = 0.65;
67
+ points.forEach((row, r) =>
68
+ row.forEach((p, c) => {
69
+ context.globalAlpha = 0.035 + p.l * 0.055;
70
+ context.beginPath();
71
+ if (c + 1 < cols) {
72
+ context.moveTo(p.x, p.y);
73
+ context.lineTo(row[c + 1]!.x, row[c + 1]!.y);
74
+ }
75
+ if (r + 1 < rows) {
76
+ context.moveTo(p.x, p.y);
77
+ context.lineTo(points[r + 1]![c]!.x, points[r + 1]![c]!.y);
78
+ }
79
+ context.stroke();
80
+ context.globalAlpha = 0.15 + p.l * 0.55;
81
+ context.beginPath();
82
+ context.arc(p.x, p.y, 0.85 + p.l * 0.65, 0, Math.PI * 2);
83
+ context.fill();
84
+ }),
85
+ );
86
+ context.globalAlpha = 1;
87
+ canvas!.dataset.painted = "true";
88
+ }
89
+
90
+ function stop() {
91
+ if (frame !== undefined) window.cancelAnimationFrame(frame);
92
+ frame = undefined;
93
+ last = undefined;
94
+ }
95
+ function tick(stamp: number) {
96
+ frame = undefined;
97
+ if (!canAnimate()) return;
98
+ if (last !== undefined) time += Math.min((stamp - last) / 1000, 0.05);
99
+ last = stamp;
100
+ draw();
101
+ frame = window.requestAnimationFrame(tick);
102
+ }
103
+ function refresh() {
104
+ stop();
105
+ if (disposed || document.hidden || (!visible && intersection)) return;
106
+ draw();
107
+ if (canAnimate() && typeof window.requestAnimationFrame === "function") {
108
+ frame = window.requestAnimationFrame(tick);
109
+ }
110
+ }
111
+ const intersection =
112
+ typeof IntersectionObserver === "function"
113
+ ? new IntersectionObserver(([entry]) => {
114
+ visible = entry?.isIntersecting === true;
115
+ refresh();
116
+ })
117
+ : undefined;
118
+ intersection?.observe(canvas);
119
+ const resize = typeof ResizeObserver === "function" ? new ResizeObserver(refresh) : undefined;
120
+ resize?.observe(canvas);
121
+ // Observe only ancestors, not the document subtree. This also updates the
122
+ // static reduced-motion canvas when a host changes its local theme.
123
+ const theme =
124
+ typeof MutationObserver === "function" ? new MutationObserver(refresh) : undefined;
125
+ for (let ancestor = canvas.parentElement; ancestor; ancestor = ancestor.parentElement) {
126
+ theme?.observe(ancestor, {
127
+ attributes: true,
128
+ attributeFilter: ["class", "style", "data-og-theme"],
129
+ });
130
+ }
131
+ motion?.addEventListener?.("change", refresh);
132
+ document.addEventListener("visibilitychange", refresh);
133
+ window.addEventListener("resize", refresh);
134
+ // Old hosts get one still frame; observed surfaces wait until onscreen.
135
+ if (!intersection) draw();
136
+ return () => {
137
+ disposed = true;
138
+ stop();
139
+ intersection?.disconnect();
140
+ resize?.disconnect();
141
+ theme?.disconnect();
142
+ motion?.removeEventListener?.("change", refresh);
143
+ document.removeEventListener("visibilitychange", refresh);
144
+ window.removeEventListener("resize", refresh);
145
+ };
146
+ }, []);
147
+
148
+ return (
149
+ <div className="og-preview-loading">
150
+ <canvas ref={ref} aria-hidden="true" />
151
+ <span className="og-preview-loading-label">Preparing preview…</span>
152
+ </div>
153
+ );
154
+ }
@@ -35,6 +35,8 @@ export type ConnectSetupProps = {
35
35
  controller: ConnectController;
36
36
  /** Called synchronously in the click handler so a host can open a popup. */
37
37
  onAuthorize: (attempt: ConnectAttempt) => void | Promise<unknown>;
38
+ /** Called synchronously on Continue; hosts can reserve a popup before advancing. */
39
+ onSelectAccount?: (accountId: string) => void | Promise<unknown>;
38
40
  /** Host pagination must retain selections across pages and submit the final set. */
39
41
  onBrowseResources?: (attempt: ConnectAttempt) => void | Promise<unknown>;
40
42
  className?: string;
@@ -61,11 +63,13 @@ function ScopedSetup({
61
63
  controller,
62
64
  onAuthorize,
63
65
  onBrowseResources,
66
+ onSelectAccount,
64
67
  className,
65
68
  authorizeLabel = "Authorize connection",
66
69
  }: ConnectSetupProps) {
67
70
  const view = useConnect(controller);
68
71
  const [localError, setLocalError] = useState(false);
72
+ const [navigating, setNavigating] = useState(false);
69
73
  const invoke = (operation: () => unknown | Promise<unknown>) => {
70
74
  setLocalError(false);
71
75
  try {
@@ -91,7 +95,7 @@ function ScopedSetup({
91
95
  event.preventDefault();
92
96
  const form = event.currentTarget;
93
97
  const data = new FormData(form);
94
- if (view.busy) return;
98
+ if (view.busy || navigating) return;
95
99
  const key = crypto.randomUUID();
96
100
  if (action.type === "credentials") {
97
101
  const values = Object.fromEntries(
@@ -105,7 +109,15 @@ function ScopedSetup({
105
109
  if (
106
110
  action.accounts.some((account) => account.id === accountId && account.status !== "disabled")
107
111
  )
108
- invoke(() => view.advance({ type: "account", accountId }, key));
112
+ invoke(async () => {
113
+ if (!onSelectAccount) return view.advance({ type: "account", accountId }, key);
114
+ setNavigating(true);
115
+ try {
116
+ await onSelectAccount(accountId);
117
+ } finally {
118
+ setNavigating(false);
119
+ }
120
+ });
109
121
  } else if (action.type === "select_resources") {
110
122
  if (action.cursor) return;
111
123
  const resourceIds = data.getAll("resource").map(String);
@@ -129,7 +141,7 @@ function ScopedSetup({
129
141
  <section
130
142
  className={["og-connect-setup", className].filter(Boolean).join(" ")}
131
143
  aria-label="Connection setup"
132
- aria-busy={view.busy}
144
+ aria-busy={view.busy || navigating}
133
145
  >
134
146
  <div className="og-connect-setup-summary">
135
147
  <p className="og-connect-setup-scope">
@@ -154,7 +166,7 @@ function ScopedSetup({
154
166
  )}
155
167
  {!terminal && (
156
168
  <form key={`${attempt.id}:${attempt.revision}`} onSubmit={submit} autoComplete="off">
157
- <fieldset disabled={view.busy}>
169
+ <fieldset disabled={view.busy || navigating}>
158
170
  {["credentials", "select_account", "select_resources", "preview"].includes(
159
171
  action.type,
160
172
  ) ? (
@@ -261,7 +273,7 @@ function ScopedSetup({
261
273
  className="og-connect-setup-primary"
262
274
  onClick={() => invoke(() => onAuthorize(structuredClone(attempt)))}
263
275
  >
264
- {authorizeLabel}
276
+ {navigating ? "Waiting for authorization…" : authorizeLabel}
265
277
  </button>
266
278
  )}
267
279
  {action.type === "wait" && (
@@ -1478,6 +1478,9 @@ export function useComposer(
1478
1478
  ...(wireInput.connectionAuthorities
1479
1479
  ? { connectionAuthorities: wireInput.connectionAuthorities }
1480
1480
  : {}),
1481
+ ...(wireInput.selectedHostMcpDelegations
1482
+ ? { selectedHostMcpDelegations: wireInput.selectedHostMcpDelegations }
1483
+ : {}),
1481
1484
  ...(wireInput.personalResourceAttachment
1482
1485
  ? { personalResourceAttachment: wireInput.personalResourceAttachment }
1483
1486
  : {}),
@@ -1754,6 +1757,9 @@ export function useComposer(
1754
1757
  ...(input.connectionAuthorities
1755
1758
  ? { connectionAuthorities: input.connectionAuthorities }
1756
1759
  : {}),
1760
+ ...(input.selectedHostMcpDelegations
1761
+ ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
1762
+ : {}),
1757
1763
  ...(input.personalResourceAttachment
1758
1764
  ? { personalResourceAttachment: input.personalResourceAttachment }
1759
1765
  : {}),
@@ -1105,16 +1105,14 @@ export function RealtimeVoiceModelPanel(props: {
1105
1105
 
1106
1106
  return (
1107
1107
  <div className="flex min-h-0 flex-1 flex-col" data-testid="realtime-voice-model-panel">
1108
- <div className="flex shrink-0 items-start gap-1 px-2 pt-1 pb-1.5">
1108
+ <div className="flex min-h-10 shrink-0 items-center gap-2 border-b border-og-border px-2 pb-2">
1109
1109
  {props.leading}
1110
- <div className="min-w-0">
1111
- <div className="text-og-menu font-medium text-og-fg">Voice model</div>
1112
- <p className="mt-0.5 text-og-control text-og-fg-subtle">
1113
- Used when you start a voice conversation.
1114
- </p>
1115
- </div>
1110
+ <div className="min-w-0 flex-1 text-og-menu font-medium text-og-fg">Voice model</div>
1116
1111
  </div>
1117
- <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-1.5 pb-1.5">
1112
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2">
1113
+ <p className="mb-3 text-og-control text-og-fg-muted">
1114
+ Used when you start a voice conversation.
1115
+ </p>
1118
1116
  <RealtimeModelPickerMenu
1119
1117
  models={props.models}
1120
1118
  selectedModel={props.selectedModel}
@@ -608,6 +608,54 @@
608
608
  --_og-session-chrome-ease: var(--og-session-chrome-ease, var(--og-ease-out));
609
609
  --_og-session-chrome-row-hover: var(--og-session-chrome-row-hover, var(--og-color-surface-3));
610
610
  }
611
+ :where(.og-root).og-preview-loading,:where(.og-root) .og-preview-loading {
612
+ --_og-preview-ink: #bca3ef;
613
+ --_og-preview-glow: #8f6ccc;
614
+ position: relative;
615
+ width: 100%;
616
+ overflow: hidden;
617
+ border-radius: 10px;
618
+ background: #17161d;
619
+ container: og-preview / inline-size;
620
+ }
621
+ :where(.og-root):is([data-og-theme="light"], .og-light) .og-preview-loading, :is([data-og-theme="light"], .og-light) :where(.og-root) .og-preview-loading,:where(.og-root) :is([data-og-theme="light"], .og-light) .og-preview-loading {
622
+ --_og-preview-ink: #76629c;
623
+ --_og-preview-glow: #ba8ee0;
624
+ background: #faf9fd;
625
+ }
626
+ :where(.og-root).og-preview-loading canvas,:where(.og-root) .og-preview-loading canvas {
627
+ display: block;
628
+ width: 100%;
629
+ height: 320px;
630
+ background-image: radial-gradient(circle, var(--_og-preview-ink) 1px, transparent 1px);
631
+ @supports (color: color-mix(in lab, red, red)) {
632
+ background-image: radial-gradient(circle, color-mix(in srgb, var(--_og-preview-ink) 30%, transparent) 1px, transparent 1px);
633
+ }
634
+ background-size: 18px 18px;
635
+ }
636
+ :where(.og-root).og-preview-loading canvas[data-painted="true"],:where(.og-root) .og-preview-loading canvas[data-painted="true"] {
637
+ background-image: none;
638
+ }
639
+ :where(.og-root).og-preview-loading-label,:where(.og-root) .og-preview-loading-label {
640
+ position: absolute;
641
+ left: 16px;
642
+ bottom: 14px;
643
+ color: var(--og-color-fg-muted);
644
+ font-family: var(--og-font-sans);
645
+ font-size: var(--og-font-size-xs);
646
+ line-height: var(--og-line-height-xs);
647
+ font-weight: 400;
648
+ }
649
+ @container og-preview (max-width: 500px) {
650
+ :where(.og-root).og-preview-loading canvas,:where(.og-root) .og-preview-loading canvas {
651
+ height: 260px;
652
+ }
653
+ }
654
+ @media (max-width: 500px) {
655
+ :where(.og-root).og-preview-loading canvas,:where(.og-root) .og-preview-loading canvas {
656
+ height: 260px;
657
+ }
658
+ }
611
659
  .og-root :where([hidden]:not([hidden="until-found"])) {
612
660
  display: none !important;
613
661
  }
package/styles/index.css CHANGED
@@ -17,6 +17,51 @@
17
17
  @import "./tokens.css";
18
18
  @import "./effective-tokens.css";
19
19
 
20
+ /* Full-surface preview preparation. Private palette preserves the accepted
21
+ ripple study; the steady label uses the host's typography and text tokens. */
22
+ .og-preview-loading {
23
+ --_og-preview-ink: #bca3ef;
24
+ --_og-preview-glow: #8f6ccc;
25
+ position: relative;
26
+ width: 100%;
27
+ overflow: hidden;
28
+ border-radius: 10px;
29
+ background: #17161d;
30
+ container: og-preview / inline-size;
31
+ }
32
+ .og-root:is([data-og-theme="light"], .og-light) .og-preview-loading,
33
+ :is([data-og-theme="light"], .og-light) .og-root .og-preview-loading,
34
+ .og-root :is([data-og-theme="light"], .og-light) .og-preview-loading {
35
+ --_og-preview-ink: #76629c;
36
+ --_og-preview-glow: #ba8ee0;
37
+ background: #faf9fd;
38
+ }
39
+ .og-preview-loading canvas {
40
+ display: block;
41
+ width: 100%;
42
+ height: 320px;
43
+ /* Non-canvas / pre-effect fallback, removed only after a successful paint. */
44
+ background-image: radial-gradient(circle, color-mix(in srgb, var(--_og-preview-ink) 30%, transparent) 1px, transparent 1px);
45
+ background-size: 18px 18px;
46
+ }
47
+ .og-preview-loading canvas[data-painted="true"] { background-image: none; }
48
+ .og-preview-loading-label {
49
+ position: absolute;
50
+ left: 16px;
51
+ bottom: 14px;
52
+ color: var(--og-color-fg-muted);
53
+ font-family: var(--og-font-sans);
54
+ font-size: var(--og-font-size-xs);
55
+ line-height: var(--og-line-height-xs);
56
+ font-weight: 400;
57
+ }
58
+ @container og-preview (max-width: 500px) {
59
+ .og-preview-loading canvas { height: 260px; }
60
+ }
61
+ @media (max-width: 500px) {
62
+ .og-preview-loading canvas { height: 260px; }
63
+ }
64
+
20
65
  /* Standalone embeds do not necessarily load Tailwind Preflight. Keep the
21
66
  control reset inside SDK roots, before utilities, without resetting the host.
22
67
  :where keeps controls at the same specificity as component utilities. */