@opengeni/react 3.1.0 → 3.3.2-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 (63) hide show
  1. package/dist/accounts.js +87 -11
  2. package/dist/accounts.js.map +1 -1
  3. package/dist/{browser-viewer-AEFHUYZK.js → browser-viewer-MKKFRRRV.js} +3 -3
  4. package/dist/{chunk-W6NFJXSA.js → chunk-2DZVYHVO.js} +20 -4
  5. package/dist/chunk-2DZVYHVO.js.map +1 -0
  6. package/dist/{chunk-YJFGAFBD.js → chunk-3AGBGTRT.js} +226 -95
  7. package/dist/chunk-3AGBGTRT.js.map +1 -0
  8. package/dist/{chunk-YFKJ7VZH.js → chunk-7EAFGICR.js} +45 -11
  9. package/dist/chunk-7EAFGICR.js.map +1 -0
  10. package/dist/{chunk-MZ2G2XBA.js → chunk-IOLVN74D.js} +31 -4
  11. package/dist/chunk-IOLVN74D.js.map +1 -0
  12. package/dist/{chunk-D3UIAJKY.js → chunk-JAQ5K7W6.js} +3 -2
  13. package/dist/{chunk-D3UIAJKY.js.map → chunk-JAQ5K7W6.js.map} +1 -1
  14. package/dist/{chunk-LAKLF4PA.js → chunk-VTOREEXF.js} +8 -7
  15. package/dist/chunk-VTOREEXF.js.map +1 -0
  16. package/dist/{chunk-RGALCTWO.js → chunk-XYRDDSXF.js} +4 -4
  17. package/dist/{chunk-HQVCKCLG.js → chunk-YG4QEQF5.js} +354 -255
  18. package/dist/chunk-YG4QEQF5.js.map +1 -0
  19. package/dist/{chunk-XXWH2JLC.js → chunk-YVCHAQZY.js} +2 -2
  20. package/dist/{chunk-OAM4WAAM.js → chunk-ZPBTI3TE.js} +2 -2
  21. package/dist/components/fleet-tile.d.ts +2 -2
  22. package/dist/components/model-policy-picker.d.ts +9 -0
  23. package/dist/components/tip-follow.d.ts +16 -0
  24. package/dist/composer.js +5 -5
  25. package/dist/hooks/use-session-events.d.ts +3 -3
  26. package/dist/index.js +76 -68
  27. package/dist/index.js.map +1 -1
  28. package/dist/interaction.js +2 -2
  29. package/dist/machines.js +2 -2
  30. package/dist/realtime.js +46 -7
  31. package/dist/realtime.js.map +1 -1
  32. package/dist/session-ui.js +4 -4
  33. package/dist/session.js +4 -4
  34. package/dist/timeline/entrance.d.ts +13 -1
  35. package/package.json +3 -2
  36. package/src/accounts.tsx +116 -14
  37. package/src/components/fleet-tile.tsx +4 -15
  38. package/src/components/message-timeline.tsx +237 -101
  39. package/src/components/model-policy-picker.tsx +24 -3
  40. package/src/components/tip-follow.ts +52 -0
  41. package/src/components/tooltip.tsx +38 -5
  42. package/src/hooks/use-available-models.ts +20 -14
  43. package/src/hooks/use-composer.ts +49 -3
  44. package/src/hooks/use-goal.ts +31 -7
  45. package/src/hooks/use-last-started-turn-policy.ts +11 -5
  46. package/src/hooks/use-session-events.ts +186 -61
  47. package/src/hooks/use-session-lineage.ts +20 -16
  48. package/src/hooks/use-session.ts +23 -19
  49. package/src/hooks/use-workspace-sessions.ts +31 -27
  50. package/src/lib/format.ts +8 -6
  51. package/src/realtime/realtime-control.tsx +55 -6
  52. package/src/timeline/entrance.tsx +28 -3
  53. package/src/timeline/projection.ts +4 -0
  54. package/dist/chunk-HQVCKCLG.js.map +0 -1
  55. package/dist/chunk-LAKLF4PA.js.map +0 -1
  56. package/dist/chunk-MZ2G2XBA.js.map +0 -1
  57. package/dist/chunk-W6NFJXSA.js.map +0 -1
  58. package/dist/chunk-YFKJ7VZH.js.map +0 -1
  59. package/dist/chunk-YJFGAFBD.js.map +0 -1
  60. /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-MKKFRRRV.js.map} +0 -0
  61. /package/dist/{chunk-RGALCTWO.js.map → chunk-XYRDDSXF.js.map} +0 -0
  62. /package/dist/{chunk-XXWH2JLC.js.map → chunk-YVCHAQZY.js.map} +0 -0
  63. /package/dist/{chunk-OAM4WAAM.js.map → chunk-ZPBTI3TE.js.map} +0 -0
@@ -200,6 +200,11 @@ export function tipFollowNoteGrowth(
200
200
  if (previous <= 0) {
201
201
  return { ...state, lastHeight: height };
202
202
  }
203
+ if (height < previous && previous - height <= TIP_FOLLOW_SHRINK_EPS_PX) {
204
+ // Match viewport-shrink deadband memory: the shell retains the paired
205
+ // pre-shrink scrollTop and this state retains the corresponding height.
206
+ return state;
207
+ }
203
208
  if (height <= previous) {
204
209
  return { ...state, lastHeight: height };
205
210
  }
@@ -441,6 +446,53 @@ export function tipFollowCompensateShrink(
441
446
  return Math.max(0, Math.min(maxScroll, scrollTop - delta));
442
447
  }
443
448
 
449
+ export type TipFollowContentShrinkBaseline = {
450
+ scrollHeight: number;
451
+ scrollTop: number;
452
+ };
453
+
454
+ export type TipFollowContentShrinkObservation = {
455
+ baseline: TipFollowContentShrinkBaseline | null;
456
+ compensatedScrollTop: number | null;
457
+ };
458
+
459
+ /**
460
+ * Retain the geometry from the first sub-epsilon content-shrink frame until
461
+ * the cumulative collapse crosses the deadband. Browsers may clamp scrollTop
462
+ * after every tiny frame, so compensating from the latest DOM top would count
463
+ * those already-applied clamps twice and make the result animation-cadence
464
+ * dependent.
465
+ */
466
+ export function tipFollowObserveContentShrink(
467
+ baseline: TipFollowContentShrinkBaseline | null,
468
+ previousHeight: number,
469
+ previousScrollTop: number,
470
+ nextHeight: number,
471
+ clientHeight: number,
472
+ epsPx: number = TIP_FOLLOW_SHRINK_EPS_PX,
473
+ ): TipFollowContentShrinkObservation {
474
+ if (previousHeight <= 0 || nextHeight >= previousHeight) {
475
+ return { baseline: null, compensatedScrollTop: null };
476
+ }
477
+ const retained = baseline ?? {
478
+ scrollHeight: previousHeight,
479
+ scrollTop: previousScrollTop,
480
+ };
481
+ if (retained.scrollHeight - nextHeight <= epsPx) {
482
+ return { baseline: retained, compensatedScrollTop: null };
483
+ }
484
+ return {
485
+ baseline: null,
486
+ compensatedScrollTop: tipFollowCompensateShrink(
487
+ retained.scrollTop,
488
+ retained.scrollHeight,
489
+ nextHeight,
490
+ clientHeight,
491
+ epsPx,
492
+ ),
493
+ };
494
+ }
495
+
444
496
  /**
445
497
  * Viewport shrank (SessionChrome / composer / window). maxScroll rises by ≈Δc;
446
498
  * keep the same tip distance: scrollTop += Δc. Without this, pinned follow only
@@ -8,10 +8,19 @@
8
8
  * tips and look like a blank white bubble.
9
9
  */
10
10
  import { Tooltip as TooltipPrimitive } from "radix-ui";
11
- import { createContext, useContext, type ComponentProps, type CSSProperties } from "react";
11
+ import {
12
+ createContext,
13
+ useContext,
14
+ useLayoutEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState,
18
+ type ComponentProps,
19
+ type CSSProperties,
20
+ } from "react";
12
21
 
13
22
  import { cn } from "../lib/cn";
14
- import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
23
+ import { usePortalTokenStyle } from "../lib/use-portal-token-style";
15
24
 
16
25
  type TooltipSourceContextValue = {
17
26
  source: HTMLElement | null;
@@ -28,10 +37,34 @@ function TooltipProvider({
28
37
  }
29
38
 
30
39
  function Tooltip({ children, ...props }: ComponentProps<typeof TooltipPrimitive.Root>) {
31
- const source = usePortalTokenSource<HTMLElement>();
40
+ const { onOpenChange, ...rootProps } = props;
41
+ const sourceRef = useRef<HTMLElement | null>(null);
42
+ const sourcePublishedRef = useRef(false);
43
+ const [source, setSource] = useState<HTMLElement | null>(null);
44
+ const ref = useRef((node: HTMLElement | null) => {
45
+ sourceRef.current = node;
46
+ if (sourcePublishedRef.current) setSource(node);
47
+ }).current;
48
+ useLayoutEffect(() => {
49
+ if (rootProps.open === true || rootProps.defaultOpen === true) {
50
+ sourcePublishedRef.current = true;
51
+ setSource(sourceRef.current);
52
+ }
53
+ }, [rootProps.defaultOpen, rootProps.open]);
54
+ const sourceContext = useMemo(() => ({ source, ref }), [ref, source]);
32
55
  return (
33
- <TooltipSourceContext.Provider value={source}>
34
- <TooltipPrimitive.Root data-slot="tooltip" {...props}>
56
+ <TooltipSourceContext.Provider value={sourceContext}>
57
+ <TooltipPrimitive.Root
58
+ data-slot="tooltip"
59
+ onOpenChange={(open) => {
60
+ if (open) {
61
+ sourcePublishedRef.current = true;
62
+ setSource(sourceRef.current);
63
+ }
64
+ onOpenChange?.(open);
65
+ }}
66
+ {...rootProps}
67
+ >
35
68
  {children}
36
69
  </TooltipPrimitive.Root>
37
70
  </TooltipSourceContext.Provider>
@@ -43,7 +43,10 @@ export function useAvailableModels(
43
43
  options: UseAvailableModelsOptions = {},
44
44
  ): UseAvailableModelsResult {
45
45
  const client = useOpenGeniClient(options);
46
- const load = useCallback(async () => await client.getClientConfig(), [client]);
46
+ const load = useCallback(
47
+ async (signal?: AbortSignal) => await client.getClientConfig({ signal }),
48
+ [client],
49
+ );
47
50
  const state = usePolledValue(load, {
48
51
  pollIntervalMs: options.pollIntervalMs,
49
52
  enabled: options.enabled,
@@ -62,19 +65,22 @@ export function useWorkspaceModelCatalog(
62
65
  options: UseWorkspaceModelCatalogOptions,
63
66
  ): UseWorkspaceModelCatalogResult {
64
67
  const client = useOpenGeniClient(options);
65
- const load = useCallback(async () => {
66
- if (!options.workspaceId) {
67
- return { models: [], defaultModel: null };
68
- }
69
- const [catalog, config] = await Promise.all([
70
- client.getWorkspaceModelCatalog(options.workspaceId),
71
- client.getClientConfig(),
72
- ]);
73
- return {
74
- models: catalog.models,
75
- defaultModel: config.defaultModel,
76
- };
77
- }, [client, options.workspaceId]);
68
+ const load = useCallback(
69
+ async (signal?: AbortSignal) => {
70
+ if (!options.workspaceId) {
71
+ return { models: [], defaultModel: null };
72
+ }
73
+ const [catalog, config] = await Promise.all([
74
+ client.getWorkspaceModelCatalog(options.workspaceId, { signal }),
75
+ client.getClientConfig({ signal }),
76
+ ]);
77
+ return {
78
+ models: catalog.models,
79
+ defaultModel: config.defaultModel,
80
+ };
81
+ },
82
+ [client, options.workspaceId],
83
+ );
78
84
  const state = usePolledValue(load, {
79
85
  pollIntervalMs: options.pollIntervalMs,
80
86
  enabled: options.enabled !== false && Boolean(options.workspaceId),
@@ -569,9 +569,17 @@ function reconcileOptimisticSendFromEvents(
569
569
  );
570
570
  const triggerEventId = operation.triggerEventId ?? accepted?.id ?? null;
571
571
  const turnId = operation.turnId ?? promptTurnIdForTrigger(triggerEventId, events);
572
+ const acceptedRouting = promptRoutingFromAcceptedEvent(accepted ?? null);
573
+ const destination =
574
+ acceptedRouting === "queued_for_execution"
575
+ ? "queue"
576
+ : acceptedRouting === "accepted_for_execution" || acceptedRouting === "accepted_for_steering"
577
+ ? "chat"
578
+ : operation.destination;
572
579
  const needsAdmissionUpdate =
573
580
  accepted !== undefined &&
574
581
  (operation.state !== "queued" ||
582
+ operation.destination !== destination ||
575
583
  operation.triggerEventId !== triggerEventId ||
576
584
  operation.turnId !== turnId ||
577
585
  operation.error !== undefined ||
@@ -580,6 +588,7 @@ function reconcileOptimisticSendFromEvents(
580
588
  ? {
581
589
  ...operation,
582
590
  state: "queued",
591
+ destination,
583
592
  triggerEventId,
584
593
  turnId,
585
594
  error: undefined,
@@ -697,6 +706,7 @@ export function useComposer(
697
706
  const targetGeneration = useRef(0);
698
707
  const draftReadGeneration = useRef(0);
699
708
  const draftReadInFlightRef = useRef<string | null>(null);
709
+ const draftReadAbortRef = useRef<AbortController | null>(null);
700
710
  const draftReadRetryRef = useRef<{
701
711
  targetKey: string;
702
712
  failures: number;
@@ -733,6 +743,8 @@ export function useComposer(
733
743
  if (draftReadRetryRef.current.timer !== null) {
734
744
  clearTimeout(draftReadRetryRef.current.timer);
735
745
  }
746
+ draftReadAbortRef.current?.abort();
747
+ draftReadAbortRef.current = null;
736
748
  draftReadRetryRef.current = { targetKey, failures: 0, timer: null };
737
749
  targetKeyRef.current = targetKey;
738
750
  targetGeneration.current += 1;
@@ -791,6 +803,11 @@ export function useComposer(
791
803
  if (draftReadRetryRef.current.timer !== null) {
792
804
  clearTimeout(draftReadRetryRef.current.timer);
793
805
  }
806
+ targetGeneration.current += 1;
807
+ draftReadGeneration.current += 1;
808
+ draftReadInFlightRef.current = null;
809
+ draftReadAbortRef.current?.abort();
810
+ draftReadAbortRef.current = null;
794
811
  },
795
812
  [],
796
813
  );
@@ -929,6 +946,9 @@ export function useComposer(
929
946
  draftReadInFlightRef.current = targetKey;
930
947
  const generation = targetGeneration.current;
931
948
  const readTicket = ++draftReadGeneration.current;
949
+ const requestAbort = new AbortController();
950
+ draftReadAbortRef.current?.abort();
951
+ draftReadAbortRef.current = requestAbort;
932
952
  const localAtStart = localEditRevision.current;
933
953
  const baseAtStart = draftRef.current;
934
954
  const policyAtStart = policyRef.current;
@@ -958,7 +978,9 @@ export function useComposer(
958
978
  setDraftLoading(true);
959
979
  }
960
980
  try {
961
- const fetched = await client.getComposerDraft(workspaceId, sessionId);
981
+ const fetched = await client.getComposerDraft(workspaceId, sessionId, {
982
+ signal: requestAbort.signal,
983
+ });
962
984
  if (
963
985
  generation !== targetGeneration.current ||
964
986
  targetKeyRef.current !== targetKey ||
@@ -1049,6 +1071,9 @@ export function useComposer(
1049
1071
  if (draftReadInFlightRef.current === targetKey) {
1050
1072
  draftReadInFlightRef.current = null;
1051
1073
  }
1074
+ if (draftReadAbortRef.current === requestAbort) {
1075
+ draftReadAbortRef.current = null;
1076
+ }
1052
1077
  }
1053
1078
  },
1054
1079
  [client, durableDrafts, sessionId, targetKey, workspaceId],
@@ -1463,10 +1488,13 @@ export function useComposer(
1463
1488
  ) {
1464
1489
  return;
1465
1490
  }
1491
+ const acceptedRouting =
1492
+ acceptedResult?.routing ?? promptRoutingFromAcceptedEvent(acceptedEvent);
1466
1493
  const acceptedDestination =
1467
- acceptedResult?.routing === "queued_for_execution"
1494
+ acceptedRouting === "queued_for_execution"
1468
1495
  ? "queue"
1469
- : acceptedResult?.routing === "accepted_for_execution"
1496
+ : acceptedRouting === "accepted_for_execution" ||
1497
+ acceptedRouting === "accepted_for_steering"
1470
1498
  ? "chat"
1471
1499
  : operation.destination;
1472
1500
  if (options.events === undefined && acceptedDestination === "chat") {
@@ -2743,6 +2771,24 @@ function asError(cause: unknown): Error {
2743
2771
  return cause instanceof Error ? cause : new Error(String(cause));
2744
2772
  }
2745
2773
 
2774
+ function promptRoutingFromAcceptedEvent(
2775
+ event: SessionEvent | null,
2776
+ ): "accepted_for_execution" | "queued_for_execution" | "accepted_for_steering" | null {
2777
+ if (
2778
+ typeof event?.payload !== "object" ||
2779
+ event.payload === null ||
2780
+ Array.isArray(event.payload)
2781
+ ) {
2782
+ return null;
2783
+ }
2784
+ const routing = (event.payload as Record<string, unknown>).routing;
2785
+ return routing === "accepted_for_execution" ||
2786
+ routing === "queued_for_execution" ||
2787
+ routing === "accepted_for_steering"
2788
+ ? routing
2789
+ : null;
2790
+ }
2791
+
2746
2792
  function isDraftConflictError(error: Error): boolean {
2747
2793
  const apiError = error as Partial<OpenGeniApiError>;
2748
2794
  return (
@@ -83,14 +83,26 @@ export function useGoal(
83
83
  const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
84
84
  const generation = useRef(0);
85
85
  const targetKeyRef = useRef<string | null>(null);
86
+ const loadAbort = useRef<AbortController | null>(null);
87
+
88
+ const retireLoads = useCallback(() => {
89
+ generation.current += 1;
90
+ loadAbort.current?.abort();
91
+ loadAbort.current = null;
92
+ }, []);
86
93
 
87
94
  const load = useCallback(async (): Promise<void> => {
88
95
  if (!sessionId) {
89
96
  return;
90
97
  }
91
98
  const ticket = ++generation.current;
99
+ loadAbort.current?.abort();
100
+ const controller = new AbortController();
101
+ loadAbort.current = controller;
92
102
  try {
93
- const fetched = await client.getGoal(workspaceId, sessionId);
103
+ const fetched = await client.getGoal(workspaceId, sessionId, {
104
+ signal: controller.signal,
105
+ });
94
106
  if (ticket === generation.current) {
95
107
  setGoal(fetched);
96
108
  setError(null);
@@ -108,6 +120,8 @@ export function useGoal(
108
120
  setError(cause instanceof Error ? cause : new Error(String(cause)));
109
121
  }
110
122
  setLoading(false);
123
+ } finally {
124
+ if (loadAbort.current === controller) loadAbort.current = null;
111
125
  }
112
126
  }, [client, workspaceId, sessionId]);
113
127
 
@@ -119,13 +133,14 @@ export function useGoal(
119
133
  setError(null);
120
134
  }
121
135
  if (!enabled || !pageLive) {
136
+ retireLoads();
122
137
  setLoading(false);
123
138
  return;
124
139
  }
125
140
  if (sharedFeed) {
126
141
  setLoading(false);
127
142
  return () => {
128
- generation.current += 1;
143
+ retireLoads();
129
144
  };
130
145
  }
131
146
  setLoading(true);
@@ -133,7 +148,7 @@ export function useGoal(
133
148
  if (pollIntervalMs === undefined || pollIntervalMs <= 0) {
134
149
  void load();
135
150
  return () => {
136
- generation.current += 1;
151
+ retireLoads();
137
152
  };
138
153
  }
139
154
  let cancelled = false;
@@ -149,9 +164,18 @@ export function useGoal(
149
164
  return () => {
150
165
  cancelled = true;
151
166
  if (timer !== null) clearTimeout(timer);
152
- generation.current += 1;
167
+ retireLoads();
153
168
  };
154
- }, [load, enabled, pageLive, workspaceId, sessionId, options.pollIntervalMs, sharedFeed]);
169
+ }, [
170
+ load,
171
+ enabled,
172
+ pageLive,
173
+ workspaceId,
174
+ sessionId,
175
+ options.pollIntervalMs,
176
+ retireLoads,
177
+ sharedFeed,
178
+ ]);
155
179
 
156
180
  const scheduleRefresh = useDebouncedCallback(() => void load());
157
181
  useSessionEventTrigger(client, workspaceId, sessionId, isGoalRefreshEvent, scheduleRefresh, {
@@ -203,11 +227,11 @@ export function useGoal(
203
227
  return true as const;
204
228
  });
205
229
  if (ok) {
206
- generation.current += 1;
230
+ retireLoads();
207
231
  setGoal(null);
208
232
  setError(null);
209
233
  }
210
- }, [client, workspaceId, sessionId, run]);
234
+ }, [client, workspaceId, sessionId, run, retireLoads]);
211
235
 
212
236
  return {
213
237
  goal,
@@ -51,11 +51,17 @@ export function useLastStartedTurnPolicy(
51
51
  useOpenGeni(options);
52
52
  const enabled = (options.enabled ?? true) && Boolean(sessionId);
53
53
 
54
- const load = useCallback(async (): Promise<LastStartedTurnPolicy | null> => {
55
- if (!sessionId) return null;
56
- const turns = await client.listTurns(workspaceId, sessionId, { latestStarted: true });
57
- return policyFromTurn(turns[0]);
58
- }, [client, workspaceId, sessionId]);
54
+ const load = useCallback(
55
+ async (signal?: AbortSignal): Promise<LastStartedTurnPolicy | null> => {
56
+ if (!sessionId) return null;
57
+ const turns = await client.listTurns(workspaceId, sessionId, {
58
+ latestStarted: true,
59
+ signal,
60
+ });
61
+ return policyFromTurn(turns[0]);
62
+ },
63
+ [client, workspaceId, sessionId],
64
+ );
59
65
 
60
66
  const { data, loading, error, refresh } = usePolledValue(load, {
61
67
  pollIntervalMs: options.pollIntervalMs,