@opengeni/react 6.0.0-canary.1 → 6.1.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.
@@ -98,8 +98,8 @@ export type MessageTimelineProps = {
98
98
  hasNewer?: boolean | undefined;
99
99
  /** A newer history page is being fetched. */
100
100
  loadingNewer?: boolean | undefined;
101
- /** Page forward through history without loading the whole gap to the tip. */
102
- onLoadNewer?: (() => void) | undefined;
101
+ /** Page forward through history. Return the request promise to enable inline error/retry. */
102
+ onLoadNewer?: (() => unknown) | undefined;
103
103
  /**
104
104
  * Reload the live tip window. When omitted, Jump to latest only re-pins and
105
105
  * scrolls the in-memory window.
package/dist/index.js CHANGED
@@ -108,7 +108,7 @@ import {
108
108
  useWorkspaceModelCatalog,
109
109
  userMessageLikelyNeedsDisclosure,
110
110
  v4aToGitFileDiff
111
- } from "./chunk-CC7AVRIJ.js";
111
+ } from "./chunk-TDSEILUH.js";
112
112
  import "./chunk-YBM6CQRU.js";
113
113
  import "./chunk-5M6VTFJ5.js";
114
114
  import "./chunk-ZYRGEKWS.js";
@@ -219,7 +219,7 @@ import {
219
219
  useSession,
220
220
  useSessionEvents,
221
221
  useTurnQueue
222
- } from "./chunk-PZ27MCZP.js";
222
+ } from "./chunk-L2NBTKRX.js";
223
223
  import {
224
224
  FILE_ONLY_MESSAGE_TEXT,
225
225
  composeSendInput,
@@ -18,7 +18,7 @@ import {
18
18
  setStartupDetails,
19
19
  useStartupDetails,
20
20
  userMessageLikelyNeedsDisclosure
21
- } from "./chunk-CC7AVRIJ.js";
21
+ } from "./chunk-TDSEILUH.js";
22
22
  import "./chunk-YBM6CQRU.js";
23
23
  import "./chunk-5M6VTFJ5.js";
24
24
  import "./chunk-ZYRGEKWS.js";
@@ -33,7 +33,7 @@ import "./chunk-R4KRSFGA.js";
33
33
  import "./chunk-VZTQ73XT.js";
34
34
  import {
35
35
  createOlderHistoryLoadReceipt
36
- } from "./chunk-PZ27MCZP.js";
36
+ } from "./chunk-L2NBTKRX.js";
37
37
  import "./chunk-NYESIADM.js";
38
38
  import "./chunk-PLYRL5ER.js";
39
39
  import "./chunk-KZ73RL76.js";
package/dist/session.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  useSession,
29
29
  useSessionEvents,
30
30
  useTurnQueue
31
- } from "./chunk-PZ27MCZP.js";
31
+ } from "./chunk-L2NBTKRX.js";
32
32
  import {
33
33
  FILE_ONLY_MESSAGE_TEXT,
34
34
  composeSendInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "6.0.0-canary.1",
3
+ "version": "6.1.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": {
@@ -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.10",
137
- "@opengeni/sdk": "^6.0.0-canary.1",
136
+ "@opengeni/connect": "^0.2.0",
137
+ "@opengeni/sdk": "^6.1.0",
138
138
  "clsx": "^2.1.1",
139
139
  "dequal": "2.0.3",
140
140
  "lucide-react": "^1.8.0",
@@ -229,8 +229,8 @@ export type MessageTimelineProps = {
229
229
  hasNewer?: boolean | undefined;
230
230
  /** A newer history page is being fetched. */
231
231
  loadingNewer?: boolean | undefined;
232
- /** Page forward through history without loading the whole gap to the tip. */
233
- onLoadNewer?: (() => void) | undefined;
232
+ /** Page forward through history. Return the request promise to enable inline error/retry. */
233
+ onLoadNewer?: (() => unknown) | undefined;
234
234
  /**
235
235
  * Reload the live tip window. When omitted, Jump to latest only re-pins and
236
236
  * scrolls the in-memory window.
@@ -461,6 +461,67 @@ export function MessageTimeline({
461
461
  // can acquire a different first-delta id when older text arrives).
462
462
  const sourceItems = events ?? items;
463
463
  const olderBoundaryKey = sourceItems?.[0]?.id;
464
+ const newerBoundaryKey = `${events?.[0]?.sessionId ?? ""}:${olderBoundaryKey ?? ""}`;
465
+ const newerScopeRef = useRef(newerBoundaryKey);
466
+ newerScopeRef.current = newerBoundaryKey;
467
+ const newerAttemptRef = useRef<{ pending: boolean } | null>(null);
468
+ const newerRetryButtonRef = useRef<HTMLButtonElement | null>(null);
469
+ const [newerRetryPending, setNewerRetryPending] = useState(false);
470
+ const [newerFailure, setNewerFailure] = useState<{ key: string; message: string } | null>(null);
471
+ useEffect(() => {
472
+ newerScopeRef.current = newerBoundaryKey;
473
+ newerAttemptRef.current = null;
474
+ setNewerFailure(null);
475
+ setNewerRetryPending(false);
476
+ return () => {
477
+ newerScopeRef.current = "";
478
+ newerAttemptRef.current = null;
479
+ };
480
+ }, [newerBoundaryKey]);
481
+ const requestNewer = useCallback(
482
+ (explicitRetry = false) => {
483
+ if (
484
+ newerScopeRef.current !== newerBoundaryKey ||
485
+ !onLoadNewer ||
486
+ loadingNewer ||
487
+ (newerAttemptRef.current && (!explicitRetry || newerAttemptRef.current.pending))
488
+ ) {
489
+ return;
490
+ }
491
+ const attempt = { pending: true };
492
+ newerAttemptRef.current = attempt;
493
+ if (explicitRetry) setNewerRetryPending(true);
494
+ const isCurrent = () =>
495
+ newerAttemptRef.current === attempt && newerScopeRef.current === newerBoundaryKey;
496
+ // Both synchronous host errors and rejected promises belong to this
497
+ // boundary. Retain the failed attempt so observers cannot hot-retry it.
498
+ void Promise.resolve()
499
+ .then(() => (isCurrent() ? onLoadNewer() : undefined))
500
+ .then(
501
+ () => {
502
+ if (!isCurrent()) return;
503
+ newerAttemptRef.current = null;
504
+ // The successful page removes the recovery control. Return focus
505
+ // to the reading surface without moving the reader's viewport.
506
+ if (document.activeElement === newerRetryButtonRef.current) {
507
+ scrollRef.current?.focus({ preventScroll: true });
508
+ }
509
+ setNewerFailure(null);
510
+ setNewerRetryPending(false);
511
+ },
512
+ (reason: unknown) => {
513
+ if (!isCurrent()) return;
514
+ attempt.pending = false;
515
+ setNewerRetryPending(false);
516
+ setNewerFailure({
517
+ key: newerBoundaryKey,
518
+ message: reason instanceof Error ? reason.message : String(reason),
519
+ });
520
+ },
521
+ );
522
+ },
523
+ [onLoadNewer, loadingNewer, newerBoundaryKey],
524
+ );
464
525
  const previousSourceIdsRef = useRef(new Set<string>());
465
526
  const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
466
527
  const readingAnchorRef = useRef<TimelineAnchor | null>(null);
@@ -1586,14 +1647,14 @@ export function MessageTimeline({
1586
1647
  // An underfilled history window has both sentinels visible. Advancing
1587
1648
  // it automatically would undo an explicit older-page navigation.
1588
1649
  if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
1589
- onLoadNewer();
1650
+ requestNewer();
1590
1651
  }
1591
1652
  },
1592
1653
  { root, rootMargin: "0px 0px 1200px 0px" },
1593
1654
  );
1594
1655
  observer.observe(target);
1595
1656
  return () => observer.disconnect();
1596
- }, [hasNewer, loadingNewer, onLoadNewer, firstGroupKey]);
1657
+ }, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);
1597
1658
 
1598
1659
  // Late layout that React commits cannot see (images decoding, fonts, code
1599
1660
  // blocks) grows content without a commit. While pinned, soft-follow the tip;
@@ -2061,6 +2122,29 @@ export function MessageTimeline({
2061
2122
  {groups.length > 0 && trailingState ? (
2062
2123
  <div data-og-timeline-trailing-state="">{trailingState}</div>
2063
2124
  ) : null}
2125
+ {hasNewer && newerFailure?.key === newerBoundaryKey ? (
2126
+ <div
2127
+ data-og-newer-error=""
2128
+ className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
2129
+ >
2130
+ <p role="status" className="max-w-prose [overflow-wrap:anywhere]">
2131
+ Couldn’t load later activity. {newerFailure.message}
2132
+ </p>
2133
+ <button
2134
+ ref={newerRetryButtonRef}
2135
+ type="button"
2136
+ data-og-retry-newer=""
2137
+ className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
2138
+ aria-disabled={loadingNewer || newerRetryPending}
2139
+ aria-busy={newerRetryPending}
2140
+ onClick={() => requestNewer(true)}
2141
+ >
2142
+ {newerRetryPending
2143
+ ? "Retrying later activity…"
2144
+ : "Retry later activity"}
2145
+ </button>
2146
+ </div>
2147
+ ) : null}
2064
2148
  {hasNewer ? (
2065
2149
  <div
2066
2150
  ref={bottomSentinelRef}
@@ -84,9 +84,7 @@ function Conversation({
84
84
  onLoadOlder={feed.loadOlder}
85
85
  hasNewer={feed.hasNewer}
86
86
  loadingNewer={feed.loadingNewer}
87
- onLoadNewer={() => {
88
- void feed.loadNewer();
89
- }}
87
+ onLoadNewer={feed.loadNewer}
90
88
  onJumpToStart={async () => {
91
89
  await feed.loadOldest();
92
90
  }}
@@ -140,6 +140,7 @@ export function useSessionEvents(
140
140
  const [eventWindow, setEventWindow] = useState<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
141
141
  const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
142
142
  const [error, setError] = useState<Error | null>(null);
143
+ const [newerError, setNewerError] = useState<Error | null>(null);
143
144
  const [hasOlder, setHasOlder] = useState(false);
144
145
  const [hasNewer, setHasNewer] = useState(false);
145
146
  const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
@@ -186,6 +187,7 @@ export function useSessionEvents(
186
187
  // Navigation belongs to the session/client lifetime, not the transport.
187
188
  useEffect(() => {
188
189
  navigationGenerationRef.current += 1;
190
+ setNewerError(null);
189
191
  loadingOlderRef.current = false;
190
192
  loadingNewerRef.current = false;
191
193
  loadingOldestRef.current = false;
@@ -691,6 +693,7 @@ export function useSessionEvents(
691
693
  initialWindowLoadedRef.current = true;
692
694
  viewModeRef.current = "history";
693
695
  loadingOldestRef.current = false;
696
+ setNewerError(null);
694
697
  startTransition(() => {
695
698
  if (status !== undefined) {
696
699
  setSessionStatusProjection(status);
@@ -736,6 +739,7 @@ export function useSessionEvents(
736
739
  return false;
737
740
  }
738
741
  if (window.events.length === 0) {
742
+ setNewerError(null);
739
743
  hasNewerRef.current = false;
740
744
  setHasNewer(false);
741
745
  // Caught up with durable history — resume the live tip stream.
@@ -788,6 +792,7 @@ export function useSessionEvents(
788
792
  resumeLive = true;
789
793
  }
790
794
  loadingNewerRef.current = false;
795
+ setNewerError(null);
791
796
  startTransition(() => {
792
797
  if (status !== undefined) {
793
798
  setSessionStatusProjection(status);
@@ -803,6 +808,16 @@ export function useSessionEvents(
803
808
  });
804
809
  published = true;
805
810
  return newer;
811
+ } catch (reason) {
812
+ // A settled request from an old navigation lifetime is not a failure of
813
+ // the current session. Preserve current loading/error state as well.
814
+ if (navigationGenerationRef.current !== generation) {
815
+ return false;
816
+ }
817
+ setNewerError(reason instanceof Error ? reason : new Error(String(reason)));
818
+ // Keep authorization and integrity failures actionable for callers;
819
+ // timeline-owned invocations attach their own explicit recovery UI.
820
+ throw reason;
806
821
  } finally {
807
822
  if (!published && navigationGenerationRef.current === generation) {
808
823
  loadingNewerRef.current = false;
@@ -817,6 +832,7 @@ export function useSessionEvents(
817
832
  }
818
833
  loadingLatestRef.current = true;
819
834
  setLoadingLatest(true);
835
+ setNewerError(null);
820
836
  let published = false;
821
837
  try {
822
838
  streamAbortRef.current?.abort();
@@ -873,7 +889,7 @@ export function useSessionEvents(
873
889
  loadOldest,
874
890
  loadingLatest: !identityMatches ? false : loadingLatest,
875
891
  jumpToLatest,
876
- error: identityMatches ? error : null,
892
+ error: identityMatches ? (newerError ?? error) : null,
877
893
  };
878
894
  }
879
895
 
@@ -1785,6 +1785,9 @@
1785
1785
  :where(.og-root).max-w-md,:where(.og-root) .max-w-md {
1786
1786
  max-width: var(--container-md);
1787
1787
  }
1788
+ :where(.og-root).max-w-prose,:where(.og-root) .max-w-prose {
1789
+ max-width: 65ch;
1790
+ }
1788
1791
  :where(.og-root).max-w-sm,:where(.og-root) .max-w-sm {
1789
1792
  max-width: var(--container-sm);
1790
1793
  }