@opengeni/react 3.1.0 → 3.3.2

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
@@ -11,15 +11,15 @@ import {
11
11
  sessionChromeGoalPillLabel,
12
12
  sessionChromeGoalPillState,
13
13
  userMessageLikelyNeedsDisclosure
14
- } from "./chunk-HQVCKCLG.js";
14
+ } from "./chunk-YG4QEQF5.js";
15
15
  import {
16
16
  createOlderHistoryLoadReceipt
17
- } from "./chunk-D3UIAJKY.js";
17
+ } from "./chunk-JAQ5K7W6.js";
18
18
  import "./chunk-WSK7G5LE.js";
19
- import "./chunk-YFKJ7VZH.js";
19
+ import "./chunk-7EAFGICR.js";
20
20
  import "./chunk-VZTQ73XT.js";
21
21
  import "./chunk-JVYQRRC4.js";
22
- import "./chunk-LAKLF4PA.js";
22
+ import "./chunk-VTOREEXF.js";
23
23
  import "./chunk-22KP6LR5.js";
24
24
  export {
25
25
  BUILT_IN_TURN_SUMMARY_FACET_IDS,
package/dist/session.js CHANGED
@@ -17,14 +17,14 @@ import {
17
17
  useSessionLineage,
18
18
  useSessionMcpApprovalPolicy,
19
19
  useTurnQueue
20
- } from "./chunk-YJFGAFBD.js";
20
+ } from "./chunk-3AGBGTRT.js";
21
21
  import {
22
22
  FILE_ONLY_MESSAGE_TEXT,
23
23
  composeSendInput,
24
24
  shouldSteerOnKey,
25
25
  shouldSubmitOnKey,
26
26
  useComposer
27
- } from "./chunk-MZ2G2XBA.js";
27
+ } from "./chunk-IOLVN74D.js";
28
28
  import {
29
29
  buildTimeline,
30
30
  createOlderHistoryLoadReceipt,
@@ -35,10 +35,10 @@ import {
35
35
  projectPendingHumanInputRequests,
36
36
  sessionStatusFromEvents,
37
37
  toolDisplayName
38
- } from "./chunk-D3UIAJKY.js";
38
+ } from "./chunk-JAQ5K7W6.js";
39
39
  import "./chunk-KZ73RL76.js";
40
40
  import "./chunk-NEXLLTP4.js";
41
- import "./chunk-LAKLF4PA.js";
41
+ import "./chunk-VTOREEXF.js";
42
42
  export {
43
43
  FILE_ONLY_MESSAGE_TEXT,
44
44
  approvalsFromRequiresAction,
@@ -1,4 +1,16 @@
1
- export declare const EntranceAnimationProvider: import("react").Provider<boolean>;
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * Freeze the mount-time gate for this subtree while publishing a separate live
4
+ * gate to activity rails that need later appends to animate after a bulk paint.
5
+ * MessageTimeline mounts one provider per durable group, so existing groups do
6
+ * not receive a context invalidation when a prepend toggles bulk mode and new
7
+ * groups still capture the value from the commit that created them.
8
+ */
9
+ export declare function EntranceAnimationProvider({ value, liveValue, children, }: {
10
+ value: boolean;
11
+ liveValue?: boolean | undefined;
12
+ children: ReactNode;
13
+ }): import("react").JSX.Element;
2
14
  /**
3
15
  * Live entrance gate from the nearest provider. Prefer
4
16
  * {@link useEntranceAnimation} for elements that must freeze the decision at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "3.1.0",
3
+ "version": "3.3.2",
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": {
@@ -120,8 +120,9 @@
120
120
  "@dnd-kit/utilities": "3.2.2",
121
121
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
122
122
  "@fontsource-variable/noto-sans-jp": "5.2.10",
123
- "@opengeni/sdk": "^3.1.0",
123
+ "@opengeni/sdk": "^3.3.2",
124
124
  "clsx": "^2.1.1",
125
+ "dequal": "2.0.3",
125
126
  "lucide-react": "^1.8.0",
126
127
  "motion": "^12.0.0",
127
128
  "radix-ui": "^1.4.3",
package/src/accounts.tsx CHANGED
@@ -197,6 +197,7 @@ export function BrowserAccountsProvider({
197
197
  expectedGeneration: string;
198
198
  } | null>(null);
199
199
  const pendingCommitSequenceRef = useRef<number | null>(null);
200
+ const pendingServerMutationSequenceRef = useRef<number | null>(null);
200
201
  const invalidatedDuringPendingCommitRef = useRef(false);
201
202
  const sequenceRef = useRef(0);
202
203
  const transitionAbortRef = useRef<AbortController | null>(null);
@@ -262,6 +263,7 @@ export function BrowserAccountsProvider({
262
263
  sequence: number,
263
264
  publish: boolean,
264
265
  authorityResetConfirmed = false,
266
+ initiatingSurfaceFenced = false,
265
267
  ): Promise<ManagedAuthSessionSetProjection | null> => {
266
268
  let source = from;
267
269
  let target = accepted;
@@ -272,7 +274,7 @@ export function BrowserAccountsProvider({
272
274
  throw new Error("Browser account response regressed the accepted actor epoch");
273
275
  }
274
276
  }
275
- if (sameSelectedActor(source, target)) {
277
+ if (!initiatingSurfaceFenced && sameSelectedActor(source, target)) {
276
278
  setProjection(target);
277
279
  setPhase("ready");
278
280
  setError(null);
@@ -287,7 +289,13 @@ export function BrowserAccountsProvider({
287
289
  // and abort old-actor work throughout that entire window.
288
290
  if (publish) publishActorHint(target);
289
291
  setPhase("loading");
290
- await onActorTransition({ kind, from: source, to: target, signal: controller.signal });
292
+ await onActorTransition({
293
+ kind,
294
+ from: initiatingSurfaceFenced ? null : source,
295
+ to: target,
296
+ signal: controller.signal,
297
+ });
298
+ initiatingSurfaceFenced = false;
291
299
  if (controller.signal.aborted || sequenceRef.current !== sequence) return null;
292
300
  if (!target) {
293
301
  setProjection(null);
@@ -393,21 +401,44 @@ export function BrowserAccountsProvider({
393
401
  pending.expectedGeneration = expectedGeneration;
394
402
  const sequence = ++sequenceRef.current;
395
403
  pendingCommitSequenceRef.current = sequence;
404
+ pendingServerMutationSequenceRef.current = null;
396
405
  invalidatedDuringPendingCommitRef.current = false;
397
406
  pendingRef.current = pending;
398
407
  setPhase("committing");
399
408
  setError(null);
400
409
  const changesActor = pending.changesActor(current);
401
- if (changesActor) {
402
- // Peers must hide and abort the old actor before this request can be
403
- // accepted. Yielding one task after the secret-free hold lets queued
404
- // BroadcastChannel delivery run before the network mutation starts;
405
- // a peer that was already dispatching remains an ordinary pre-commit
406
- // in-flight request and is still fenced by the server epoch.
407
- publishActorHold(pending.operationId, current);
408
- await yieldToCrossTabActorHold();
409
- }
410
+ let initiatingActorFenced = false;
410
411
  try {
412
+ if (changesActor) {
413
+ // Every tab, including the initiator, must hide and abort the old actor
414
+ // before this request can be accepted. Otherwise a response-lost
415
+ // logout can leave the initiating route alive long enough for its
416
+ // event stream to reconnect with the newly revoked cookie.
417
+ publishActorHold(pending.operationId, current);
418
+ transitionAbortRef.current?.abort();
419
+ const controller = new AbortController();
420
+ transitionAbortRef.current = controller;
421
+ projectionRef.current = null;
422
+ setProjection(null);
423
+ setPhase("loading");
424
+ // The host can synchronously clear its actor surface before its
425
+ // transition promise later rejects. From this point on, every
426
+ // failure must reconcile and restore authority even though the
427
+ // account mutation has not started yet.
428
+ initiatingActorFenced = true;
429
+ await onActorTransition({
430
+ kind: pending.kind,
431
+ from: current,
432
+ to: null,
433
+ signal: controller.signal,
434
+ });
435
+ if (controller.signal.aborted || sequenceRef.current !== sequence) return false;
436
+ // Yield one task after the secret-free hold so queued BroadcastChannel
437
+ // delivery can fence peers before the network mutation starts.
438
+ await yieldToCrossTabActorHold();
439
+ if (controller.signal.aborted || sequenceRef.current !== sequence) return false;
440
+ }
441
+ pendingServerMutationSequenceRef.current = sequence;
411
442
  let accepted: ManagedAuthSessionSetProjection;
412
443
  try {
413
444
  accepted = await pending.execute(expectedGeneration);
@@ -429,7 +460,22 @@ export function BrowserAccountsProvider({
429
460
  if (sequenceRef.current !== sequence) return false;
430
461
  }
431
462
  pendingRef.current = null;
432
- if (!authorityResetConfirmed && sameSelectedActor(current, accepted)) {
463
+ // The server response and any invalidation reconciliation are now
464
+ // authoritative. Release commit ownership before host settlement so a
465
+ // later invalidation starts its own reconciliation instead of relying
466
+ // on the already-consumed check above.
467
+ if (pendingServerMutationSequenceRef.current === sequence) {
468
+ pendingServerMutationSequenceRef.current = null;
469
+ }
470
+ if (pendingCommitSequenceRef.current === sequence) {
471
+ pendingCommitSequenceRef.current = null;
472
+ invalidatedDuringPendingCommitRef.current = false;
473
+ }
474
+ if (
475
+ !initiatingActorFenced &&
476
+ !authorityResetConfirmed &&
477
+ sameSelectedActor(current, accepted)
478
+ ) {
433
479
  setProjection(accepted);
434
480
  setPhase("ready");
435
481
  setError(null);
@@ -441,20 +487,62 @@ export function BrowserAccountsProvider({
441
487
  sequence,
442
488
  true,
443
489
  authorityResetConfirmed,
490
+ initiatingActorFenced,
444
491
  );
445
492
  }
446
493
  return true;
447
494
  } catch (caught) {
495
+ if (initiatingActorFenced) {
496
+ let authorityRestored = false;
497
+ try {
498
+ const restored = await client.reconcileSessionSetAuthority();
499
+ if (sequenceRef.current !== sequence) return false;
500
+ await settleActorTransition(
501
+ pending.kind,
502
+ current,
503
+ restored,
504
+ sequence,
505
+ false,
506
+ true,
507
+ true,
508
+ );
509
+ if (sequenceRef.current !== sequence) return false;
510
+ authorityRestored = true;
511
+ } catch {
512
+ // If authority itself cannot be reconciled, retain the existing
513
+ // fail-closed provider error path and its manual retry surface.
514
+ }
515
+ if (authorityRestored) {
516
+ // Restore transport provenance before retaining the existing
517
+ // fail-closed retry surface. The retry screen keeps tenant data
518
+ // hidden, while refresh can safely reveal this reconciled actor.
519
+ const restoredError = accountError(caught);
520
+ setError(restoredError);
521
+ setPhase("recoverable_error");
522
+ throw restoredError;
523
+ }
524
+ }
448
525
  return await fail(caught, pending.kind);
449
526
  } finally {
450
527
  if (changesActor) publishActorRelease(pending.operationId);
528
+ if (pendingServerMutationSequenceRef.current === sequence) {
529
+ pendingServerMutationSequenceRef.current = null;
530
+ }
451
531
  if (pendingCommitSequenceRef.current === sequence) {
452
532
  pendingCommitSequenceRef.current = null;
453
533
  invalidatedDuringPendingCommitRef.current = false;
454
534
  }
455
535
  }
456
536
  },
457
- [client, fail, inspectBlockers, publishActorHold, publishActorRelease, settleActorTransition],
537
+ [
538
+ client,
539
+ fail,
540
+ inspectBlockers,
541
+ onActorTransition,
542
+ publishActorHold,
543
+ publishActorRelease,
544
+ settleActorTransition,
545
+ ],
458
546
  );
459
547
 
460
548
  const refresh = useCallback(async () => {
@@ -493,11 +581,25 @@ export function BrowserAccountsProvider({
493
581
  );
494
582
 
495
583
  const invalidateActor = useCallback(async () => {
496
- if (pendingCommitSequenceRef.current !== null) {
584
+ const pendingCommitSequence = pendingCommitSequenceRef.current;
585
+ if (
586
+ pendingCommitSequence !== null &&
587
+ pendingServerMutationSequenceRef.current === pendingCommitSequence
588
+ ) {
497
589
  invalidatedDuringPendingCommitRef.current = true;
498
590
  await beginNeutralActorInvalidation(projectionRef.current);
499
591
  return null;
500
592
  }
593
+ // An authority hint that arrives while the initiating host fence is still
594
+ // pending supersedes the not-yet-dispatched mutation. Reconcile it here;
595
+ // otherwise aborting that fence makes executePending return early while no
596
+ // operation remains to restore the neutral actor surface.
597
+ if (pendingCommitSequence !== null) {
598
+ pendingCommitSequenceRef.current = null;
599
+ pendingServerMutationSequenceRef.current = null;
600
+ invalidatedDuringPendingCommitRef.current = false;
601
+ pendingRef.current = null;
602
+ }
501
603
  const sequence = ++sequenceRef.current;
502
604
  const before = projectionRef.current;
503
605
  try {
@@ -1,13 +1,13 @@
1
- import type { Session } from "@opengeni/sdk";
1
+ import { deriveSessionDisplayTitle, type Session } from "@opengeni/sdk";
2
2
  import { cn } from "../lib/cn";
3
3
  import { formatRelativeTime } from "../lib/format";
4
4
  import { SessionStatus } from "./session-status";
5
5
 
6
- const AUTOMATIC_SESSION_TITLE_FALLBACK = "New conversation";
6
+ const SESSION_METADATA_TITLE_KEYS = ["title", "name"] as const;
7
7
 
8
8
  export type FleetTileProps = {
9
9
  session: Session;
10
- /** Overrides the derived title (session title, metadata title/name, or safe fallback). */
10
+ /** Overrides the derived title (durable title, metadata, prompt preview, or safe fallback). */
11
11
  title?: string | undefined;
12
12
  /** Extra line under the title — e.g. "drift check" or the worker's task. */
13
13
  subtitle?: string | undefined;
@@ -17,18 +17,7 @@ export type FleetTileProps = {
17
17
 
18
18
  /** Best-effort display title for a session. */
19
19
  export function sessionDisplayTitle(session: Session): string {
20
- // A set title (agent-generated or user-renamed) wins over metadata and the
21
- // prompt-free fallback.
22
- if (typeof session.title === "string" && session.title.trim().length > 0) {
23
- return session.title;
24
- }
25
- for (const key of ["title", "name"] as const) {
26
- const value = session.metadata[key];
27
- if (typeof value === "string" && value.trim().length > 0) {
28
- return value;
29
- }
30
- }
31
- return AUTOMATIC_SESSION_TITLE_FALLBACK;
20
+ return deriveSessionDisplayTitle(session, { metadataKeys: SESSION_METADATA_TITLE_KEYS });
32
21
  }
33
22
 
34
23
  /**