@opengeni/react 0.34.3 → 0.36.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.
@@ -1,4 +1,4 @@
1
- import type { ComposerDraft, EffectiveSessionControl, SessionEvent, SessionQueueSnapshot, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
1
+ import type { ComposerDraft, EffectiveSessionControl, McpPersonalConnectionSummary, SessionEvent, SessionQueueSnapshot, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
2
2
  import { type EmbeddedSessionClientOverride } from "../session-context";
3
3
  import { type SessionEventFeedOptions } from "./internal";
4
4
  /** Events that can change the authoritative prompt queue or effective control. */
@@ -15,6 +15,8 @@ export type UseTurnQueueResult = {
15
15
  pendingInputs: SessionPendingInputPreview[];
16
16
  /** Exact pending members projected to join an already-waiting prompt. */
17
17
  pendingInputAttachment: SessionQueueSnapshot["pendingInputAttachment"];
18
+ /** Secret-safe personal MCP summaries frozen on the exact active turn. */
19
+ activePersonalConnections: McpPersonalConnectionSummary[];
18
20
  effectiveControl: EffectiveSessionControl | null;
19
21
  /** The latest interrupted attempt has not yet durably proved physical quiescence. */
20
22
  stoppingPreviousAttempt: boolean;
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  useSessionLineage,
53
53
  useSessionMcpApprovalPolicy,
54
54
  useTurnQueue
55
- } from "./chunk-4NMH6UJ3.js";
55
+ } from "./chunk-OKKMZDQF.js";
56
56
  import {
57
57
  ActivityDisclosure,
58
58
  ActivityRail,
@@ -106,7 +106,7 @@ import {
106
106
  useLightboxOptional,
107
107
  useThemeType,
108
108
  v4aToGitFileDiff
109
- } from "./chunk-QRYJCWGD.js";
109
+ } from "./chunk-XT7JF2EH.js";
110
110
  import {
111
111
  buildTimeline,
112
112
  creditExhaustedFromEvents,
@@ -114,7 +114,7 @@ import {
114
114
  groupTimeline,
115
115
  sessionStatusFromEvents,
116
116
  toolDisplayName
117
- } from "./chunk-MEGI4FS6.js";
117
+ } from "./chunk-J2RVJ6JX.js";
118
118
  import {
119
119
  Actions,
120
120
  AttachButton,
@@ -6,8 +6,8 @@ import {
6
6
  SessionChrome,
7
7
  TimelineRow,
8
8
  sessionChromeGoalPillState
9
- } from "./chunk-QRYJCWGD.js";
10
- import "./chunk-MEGI4FS6.js";
9
+ } from "./chunk-XT7JF2EH.js";
10
+ import "./chunk-J2RVJ6JX.js";
11
11
  import "./chunk-ATSTR5P3.js";
12
12
  import "./chunk-TK7G6XLT.js";
13
13
  import "./chunk-LAKLF4PA.js";
package/dist/session.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  useSessionLineage,
19
19
  useSessionMcpApprovalPolicy,
20
20
  useTurnQueue
21
- } from "./chunk-4NMH6UJ3.js";
21
+ } from "./chunk-OKKMZDQF.js";
22
22
  import {
23
23
  buildTimeline,
24
24
  creditExhaustedFromEvents,
@@ -26,7 +26,7 @@ import {
26
26
  groupTimeline,
27
27
  sessionStatusFromEvents,
28
28
  toolDisplayName
29
- } from "./chunk-MEGI4FS6.js";
29
+ } from "./chunk-J2RVJ6JX.js";
30
30
  import {
31
31
  FILE_ONLY_MESSAGE_TEXT,
32
32
  composeSendInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "0.34.3",
3
+ "version": "0.36.0",
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": {
@@ -70,7 +70,7 @@
70
70
  "@dnd-kit/utilities": "3.2.2",
71
71
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
72
72
  "@fontsource-variable/noto-sans-jp": "5.2.10",
73
- "@opengeni/sdk": "^0.34.1",
73
+ "@opengeni/sdk": "^0.36.0",
74
74
  "clsx": "^2.1.1",
75
75
  "lucide-react": "^1.8.0",
76
76
  "motion": "^12.0.0",
@@ -248,7 +248,10 @@ function MarkdownCodeBlock({ children }: { children?: ReactNode }) {
248
248
  <CopyButton text={code} label="Copy code" reveal="group-hover" />
249
249
  </div>
250
250
  </div>
251
- <pre className="overflow-x-auto overflow-y-hidden rounded-og-md bg-og-surface-1/70 px-3 py-2.5 font-og-mono text-og-sm leading-5 text-og-fg-muted [scrollbar-gutter:stable] [&>code]:block [&>code]:border-0 [&>code]:bg-transparent [&>code]:p-0 [&>code]:leading-5 [&>code]:text-inherit">
251
+ <pre
252
+ tabIndex={0}
253
+ className="overflow-x-auto overflow-y-hidden rounded-og-md bg-og-surface-1/70 px-3 py-2.5 font-og-mono text-og-sm leading-5 text-og-fg-muted [scrollbar-gutter:stable] [&>code]:block [&>code]:border-0 [&>code]:bg-transparent [&>code]:p-0 [&>code]:leading-5 [&>code]:text-inherit"
254
+ >
252
255
  {children}
253
256
  </pre>
254
257
  </div>
@@ -268,7 +271,7 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
268
271
  />
269
272
  </div>
270
273
  </div>
271
- <div className="overflow-x-auto">
274
+ <div className="overflow-x-auto" tabIndex={0}>
272
275
  <table
273
276
  ref={tableRef}
274
277
  className={cn("w-full min-w-0 border-collapse text-og-base", className)}
@@ -22,6 +22,7 @@ import type { ComponentType } from "react";
22
22
  import { AnimatePresence, motion } from "motion/react";
23
23
  import { Collapsible } from "radix-ui";
24
24
  import {
25
+ Component,
25
26
  memo,
26
27
  useCallback,
27
28
  useEffect,
@@ -1158,21 +1159,34 @@ export function MessageTimeline({
1158
1159
  : 0;
1159
1160
  return (
1160
1161
  <div key={key} data-og-timeline-group-anchor="" data-og-group-key={key}>
1161
- <TimelineGroupView
1162
- group={group}
1163
- renderMessageText={renderMessageText}
1164
- onOpenSession={onOpenSession}
1165
- onMemoryClick={onMemoryClick}
1166
- onReconnect={onReconnect}
1167
- resolveProviderLogo={resolveProviderLogo}
1168
- toolRegistry={toolRegistry}
1169
- turnSummary={turnSummary}
1170
- foldLiveCluster={isAgentProgress(next)}
1171
- trailingAgentText={trailingAgentTextAfterTurn(group, next)}
1172
- contextCompactionCount={
1173
- contextCompactionCount > 0 ? contextCompactionCount : undefined
1174
- }
1175
- />
1162
+ <TimelineGroupRenderBoundary
1163
+ resetKeys={[
1164
+ group,
1165
+ renderMessageText,
1166
+ onOpenSession,
1167
+ onMemoryClick,
1168
+ onReconnect,
1169
+ resolveProviderLogo,
1170
+ toolRegistry,
1171
+ turnSummary,
1172
+ ]}
1173
+ >
1174
+ <TimelineGroupView
1175
+ group={group}
1176
+ renderMessageText={renderMessageText}
1177
+ onOpenSession={onOpenSession}
1178
+ onMemoryClick={onMemoryClick}
1179
+ onReconnect={onReconnect}
1180
+ resolveProviderLogo={resolveProviderLogo}
1181
+ toolRegistry={toolRegistry}
1182
+ turnSummary={turnSummary}
1183
+ foldLiveCluster={isAgentProgress(next)}
1184
+ trailingAgentText={trailingAgentTextAfterTurn(group, next)}
1185
+ contextCompactionCount={
1186
+ contextCompactionCount > 0 ? contextCompactionCount : undefined
1187
+ }
1188
+ />
1189
+ </TimelineGroupRenderBoundary>
1176
1190
  </div>
1177
1191
  );
1178
1192
  })}
@@ -1420,6 +1434,75 @@ function cancelFrame(id: number): void {
1420
1434
  window.clearTimeout(id);
1421
1435
  }
1422
1436
 
1437
+ type TimelineGroupRenderBoundaryProps = {
1438
+ children: ReactNode;
1439
+ resetKeys: readonly unknown[];
1440
+ };
1441
+
1442
+ type TimelineGroupRenderBoundaryState = {
1443
+ failed: boolean;
1444
+ resetKeys: readonly unknown[];
1445
+ };
1446
+
1447
+ function timelineRenderResetKeysChanged(
1448
+ previous: readonly unknown[],
1449
+ next: readonly unknown[],
1450
+ ): boolean {
1451
+ return (
1452
+ previous.length !== next.length || previous.some((key, index) => !Object.is(key, next[index]))
1453
+ );
1454
+ }
1455
+
1456
+ /**
1457
+ * A malformed historical payload or consumer renderer must not take down the
1458
+ * entire conversation. Keep the boundary outside the row component so React
1459
+ * can replace an invalid element type (error #130) with a bounded fallback.
1460
+ */
1461
+ class TimelineGroupRenderBoundary extends Component<
1462
+ TimelineGroupRenderBoundaryProps,
1463
+ TimelineGroupRenderBoundaryState
1464
+ > {
1465
+ state: TimelineGroupRenderBoundaryState = {
1466
+ failed: false,
1467
+ resetKeys: this.props.resetKeys,
1468
+ };
1469
+
1470
+ static getDerivedStateFromError(): Partial<TimelineGroupRenderBoundaryState> {
1471
+ return { failed: true };
1472
+ }
1473
+
1474
+ static getDerivedStateFromProps(
1475
+ props: TimelineGroupRenderBoundaryProps,
1476
+ state: TimelineGroupRenderBoundaryState,
1477
+ ): Partial<TimelineGroupRenderBoundaryState> | null {
1478
+ if (timelineRenderResetKeysChanged(state.resetKeys, props.resetKeys)) {
1479
+ return { failed: false, resetKeys: props.resetKeys };
1480
+ }
1481
+ return null;
1482
+ }
1483
+
1484
+ render(): ReactNode {
1485
+ if (this.state.failed) {
1486
+ return (
1487
+ <div
1488
+ data-testid="timeline-group-render-error"
1489
+ role="status"
1490
+ className="flex items-start gap-2 rounded-lg border border-og-border bg-og-surface-muted px-3 py-2 text-sm text-og-fg-muted"
1491
+ >
1492
+ <TriangleAlertIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
1493
+ <div>
1494
+ <p className="font-medium text-og-fg">Timeline item unavailable</p>
1495
+ <p>
1496
+ This item could not be displayed. The rest of the conversation is still available.
1497
+ </p>
1498
+ </div>
1499
+ </div>
1500
+ );
1501
+ }
1502
+ return this.props.children;
1503
+ }
1504
+ }
1505
+
1423
1506
  // The full loaded window stays mounted, so settled history rows must be cheap
1424
1507
  // on every commit: projection reuses group objects for unchanged groups, so
1425
1508
  // memo skips them. Live projection creates a new group object, and
@@ -1486,7 +1569,10 @@ const TimelineGroupView = memo(function TimelineGroupView({
1486
1569
  // Latch live→folded so a top-level shell that was already mounted open can
1487
1570
  // start the settle beat without remounting bare rail → wrapper.
1488
1571
  const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn);
1489
- const turnDefaultOpen = !insideTurn && group.kind === "turn" && group.outcome === "failed";
1572
+ const turnDefaultOpen =
1573
+ !insideTurn &&
1574
+ group.kind === "turn" &&
1575
+ (group.outcome === "failed" || timelineGroupContainsAuthNeeded(group));
1490
1576
  // activity-* → turn-* remount: carry resting state so settleFold does not
1491
1577
  // re-open a chip the reader already watched collapse.
1492
1578
  if (group.kind === "turn" && foldMemory && !insideTurn) {
@@ -1578,21 +1664,37 @@ const TimelineGroupView = memo(function TimelineGroupView({
1578
1664
  // more "N steps" was the redundant double fold).
1579
1665
  const nestClusters = foldableActivityClusterCount(group.groups) >= 2;
1580
1666
  const turnCopyText = collectTurnCopyText(group.groups, trailingAgentText);
1581
- const body = group.groups.map((child) => (
1582
- <TimelineGroupView
1583
- key={timelineGroupKey(child)}
1584
- group={child}
1585
- renderMessageText={renderMessageText}
1586
- onOpenSession={onOpenSession}
1587
- onMemoryClick={onMemoryClick}
1588
- onReconnect={onReconnect}
1589
- resolveProviderLogo={resolveProviderLogo}
1590
- toolRegistry={toolRegistry}
1591
- turnSummary={turnSummary}
1592
- insideTurn
1593
- nestClusterChips={nestClusters}
1594
- />
1595
- ));
1667
+ const body = group.groups.map((child) => {
1668
+ const key = timelineGroupKey(child);
1669
+ return (
1670
+ <TimelineGroupRenderBoundary
1671
+ key={key}
1672
+ resetKeys={[
1673
+ child,
1674
+ renderMessageText,
1675
+ onOpenSession,
1676
+ onMemoryClick,
1677
+ onReconnect,
1678
+ resolveProviderLogo,
1679
+ toolRegistry,
1680
+ turnSummary,
1681
+ ]}
1682
+ >
1683
+ <TimelineGroupView
1684
+ group={child}
1685
+ renderMessageText={renderMessageText}
1686
+ onOpenSession={onOpenSession}
1687
+ onMemoryClick={onMemoryClick}
1688
+ onReconnect={onReconnect}
1689
+ resolveProviderLogo={resolveProviderLogo}
1690
+ toolRegistry={toolRegistry}
1691
+ turnSummary={turnSummary}
1692
+ insideTurn
1693
+ nestClusterChips={nestClusters}
1694
+ />
1695
+ </TimelineGroupRenderBoundary>
1696
+ );
1697
+ });
1596
1698
  return (
1597
1699
  <TurnSummary
1598
1700
  items={activityItems}
@@ -1679,6 +1781,17 @@ function timelineGroupKey(group: TimelineGroup): string {
1679
1781
  }
1680
1782
  }
1681
1783
 
1784
+ function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean {
1785
+ switch (group.kind) {
1786
+ case "item":
1787
+ return group.item.kind === "auth-needed";
1788
+ case "activity":
1789
+ return false;
1790
+ case "turn":
1791
+ return group.groups.some(timelineGroupContainsAuthNeeded);
1792
+ }
1793
+ }
1794
+
1682
1795
  function timelineGroupItemIds(group: TimelineGroup): string[] {
1683
1796
  switch (group.kind) {
1684
1797
  case "item":
@@ -2433,7 +2546,9 @@ function AuthNeededRow({
2433
2546
  const [failed, setFailed] = useState(false);
2434
2547
  const provider = providerLabel(item.providerDomain);
2435
2548
  const unavailable =
2436
- item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
2549
+ item.reason === "personal_authority_unavailable" ||
2550
+ item.reason === "unsupported_auth" ||
2551
+ item.reason === "resource_scope_unavailable";
2437
2552
  const missing = item.reason === "missing_connection";
2438
2553
  const actionLabel = missing ? "Connect" : "Reconnect";
2439
2554
 
@@ -2587,6 +2702,8 @@ function authReasonLine(reason: AuthNeededItem["reason"]): string {
2587
2702
  case "expired":
2588
2703
  case "refresh_failed":
2589
2704
  return "Its access expired.";
2705
+ case "personal_authority_unavailable":
2706
+ return "This automation was not granted access to your personal connection.";
2590
2707
  case "unsupported_auth":
2591
2708
  return "This connection cannot authenticate the configured tool endpoint.";
2592
2709
  case "resource_scope_unavailable":
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ComposerDraft,
3
3
  EffectiveSessionControl,
4
+ McpPersonalConnectionSummary,
4
5
  SessionEvent,
5
6
  SessionQueueMutationResponse,
6
7
  SessionQueueSnapshot,
@@ -53,6 +54,8 @@ export type UseTurnQueueResult = {
53
54
  pendingInputs: SessionPendingInputPreview[];
54
55
  /** Exact pending members projected to join an already-waiting prompt. */
55
56
  pendingInputAttachment: SessionQueueSnapshot["pendingInputAttachment"];
57
+ /** Secret-safe personal MCP summaries frozen on the exact active turn. */
58
+ activePersonalConnections: McpPersonalConnectionSummary[];
56
59
  effectiveControl: EffectiveSessionControl | null;
57
60
  /** The latest interrupted attempt has not yet durably proved physical quiescence. */
58
61
  stoppingPreviousAttempt: boolean;
@@ -332,6 +335,7 @@ export function useTurnQueue(
332
335
  queue: visibleSnapshot?.items ?? [],
333
336
  pendingInputs: visibleSnapshot?.pendingInputs ?? [],
334
337
  pendingInputAttachment: visibleSnapshot?.pendingInputAttachment ?? null,
338
+ activePersonalConnections: visibleSnapshot?.activePersonalConnections ?? [],
335
339
  effectiveControl: visibleSnapshot?.effectiveControl ?? null,
336
340
  stoppingPreviousAttempt: visibleSnapshot?.stoppingPreviousAttempt ?? false,
337
341
  loading: identityMatches ? loading : enabled,
@@ -1507,6 +1507,7 @@ const AUTH_NEEDED_REASONS: ReadonlySet<string> = new Set([
1507
1507
  "expired",
1508
1508
  "insufficient_scope",
1509
1509
  "refresh_failed",
1510
+ "personal_authority_unavailable",
1510
1511
  "unsupported_auth",
1511
1512
  "resource_scope_unavailable",
1512
1513
  ]);