@opengeni/react 0.45.0 → 0.48.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 (141) hide show
  1. package/README.md +68 -12
  2. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  3. package/dist/chunk-FK6VG4LL.js.map +1 -0
  4. package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
  5. package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
  6. package/dist/chunk-I4MPFDRP.js.map +1 -0
  7. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  8. package/dist/chunk-KZ73RL76.js.map +1 -0
  9. package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
  10. package/dist/chunk-LHLYWG5R.js.map +1 -0
  11. package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
  12. package/dist/chunk-NJ7M2B6Z.js.map +1 -0
  13. package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
  14. package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
  15. package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
  16. package/dist/chunk-OL6QVXGF.js.map +1 -0
  17. package/dist/chunk-VZTQ73XT.js +86 -0
  18. package/dist/chunk-VZTQ73XT.js.map +1 -0
  19. package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
  20. package/dist/chunk-WYA65Y3F.js.map +1 -0
  21. package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
  22. package/dist/chunk-XCNTXWBB.js.map +1 -0
  23. package/dist/client.d.ts +1 -1
  24. package/dist/components/chat-composer.d.ts +7 -2
  25. package/dist/components/composer-transcription-control.d.ts +2 -0
  26. package/dist/components/composer.d.ts +14 -0
  27. package/dist/components/machines/machine-detail.d.ts +3 -1
  28. package/dist/components/machines-dashboard.d.ts +3 -3
  29. package/dist/components/message-timeline.d.ts +12 -2
  30. package/dist/components/sandbox-files.d.ts +2 -2
  31. package/dist/components/sandbox-terminal.d.ts +29 -0
  32. package/dist/components/sandbox-workspace.d.ts +5 -1
  33. package/dist/components/workspace-dock.d.ts +3 -1
  34. package/dist/composer.d.ts +2 -2
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/internal.d.ts +6 -0
  37. package/dist/hooks/use-codex-accounts.d.ts +1 -1
  38. package/dist/hooks/use-environments.d.ts +10 -4
  39. package/dist/hooks/use-machines.d.ts +7 -0
  40. package/dist/hooks/use-sandbox-files.d.ts +19 -0
  41. package/dist/hooks/use-sandbox-git.d.ts +7 -0
  42. package/dist/hooks/use-transcription.d.ts +2 -2
  43. package/dist/hooks/use-voice-input.d.ts +13 -4
  44. package/dist/index.d.ts +6 -5
  45. package/dist/index.js +904 -538
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/composer-responsive-context.d.ts +9 -0
  48. package/dist/lib/use-portal-token-style.d.ts +14 -3
  49. package/dist/machines.js +2 -2
  50. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
  51. package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
  52. package/dist/realtime/realtime-control.d.ts +9 -0
  53. package/dist/realtime.js +29 -32
  54. package/dist/realtime.js.map +1 -1
  55. package/dist/session-ui.js +5 -5
  56. package/dist/session.js +4 -4
  57. package/dist/timeline/activity-rail.d.ts +3 -2
  58. package/dist/timeline/index.d.ts +2 -2
  59. package/dist/timeline/parsers.d.ts +3 -3
  60. package/dist/timeline/registry.d.ts +3 -0
  61. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  62. package/dist/timeline/types.d.ts +4 -0
  63. package/dist/voice-recording-store.d.ts +8 -2
  64. package/package.json +20 -4
  65. package/src/client.ts +3 -0
  66. package/src/components/chat-composer.tsx +12 -2
  67. package/src/components/code-editor.tsx +1 -1
  68. package/src/components/command-palette.tsx +1 -1
  69. package/src/components/composer-transcription-control.tsx +41 -16
  70. package/src/components/composer.tsx +52 -26
  71. package/src/components/copy-button.tsx +2 -2
  72. package/src/components/desktop-viewer.tsx +2 -2
  73. package/src/components/file-browser.tsx +39 -17
  74. package/src/components/human-input-form.tsx +2 -2
  75. package/src/components/machine-card.tsx +1 -1
  76. package/src/components/machines/machine-detail.tsx +39 -15
  77. package/src/components/machines-dashboard.tsx +7 -6
  78. package/src/components/message-timeline.tsx +44 -11
  79. package/src/components/model-picker.tsx +2 -2
  80. package/src/components/model-policy-picker.tsx +16 -15
  81. package/src/components/queue-surface-implementation.tsx +20 -16
  82. package/src/components/queue-surface-state.tsx +2 -2
  83. package/src/components/sandbox-files.tsx +140 -60
  84. package/src/components/sandbox-terminal.tsx +24 -5
  85. package/src/components/sandbox-workspace.tsx +180 -142
  86. package/src/components/session-chrome.tsx +14 -14
  87. package/src/components/tooltip.tsx +13 -16
  88. package/src/components/user-message-body.tsx +1 -1
  89. package/src/components/workbench-changes.tsx +4 -4
  90. package/src/components/workspace-dock.tsx +13 -5
  91. package/src/composer.ts +3 -0
  92. package/src/hooks/internal.ts +132 -38
  93. package/src/hooks/use-codex-accounts.ts +4 -4
  94. package/src/hooks/use-environments.ts +24 -5
  95. package/src/hooks/use-file-attachments.ts +70 -32
  96. package/src/hooks/use-goal.ts +17 -5
  97. package/src/hooks/use-machines.ts +47 -1
  98. package/src/hooks/use-sandbox-files.ts +310 -59
  99. package/src/hooks/use-sandbox-git.ts +148 -41
  100. package/src/hooks/use-session-capabilities.ts +35 -24
  101. package/src/hooks/use-session-events.ts +18 -4
  102. package/src/hooks/use-transcription.ts +4 -14
  103. package/src/hooks/use-turn-queue.ts +17 -5
  104. package/src/hooks/use-voice-input.ts +481 -36
  105. package/src/hooks/use-windowed-sections.ts +9 -7
  106. package/src/index.ts +11 -6
  107. package/src/lib/composer-responsive-context.ts +15 -0
  108. package/src/lib/use-portal-token-style.ts +60 -15
  109. package/src/provider.tsx +52 -7
  110. package/src/realtime/dropdown-menu.tsx +15 -19
  111. package/src/realtime/realtime-control.tsx +18 -7
  112. package/src/timeline/activity-rail.tsx +15 -6
  113. package/src/timeline/index.ts +1 -1
  114. package/src/timeline/parsers.ts +33 -20
  115. package/src/timeline/projection.ts +23 -0
  116. package/src/timeline/registry.ts +7 -0
  117. package/src/timeline/screenshot-lightbox.tsx +15 -4
  118. package/src/timeline/shared.tsx +8 -8
  119. package/src/timeline/tool-renderers.tsx +166 -22
  120. package/src/timeline/turn-summary.tsx +6 -6
  121. package/src/timeline/types.ts +4 -0
  122. package/src/voice-recording-store.ts +15 -0
  123. package/styles/compiled.css +5065 -0
  124. package/styles/compiled.d.ts +2 -0
  125. package/styles/effective-tokens.css +27 -0
  126. package/styles/index.css +3 -0
  127. package/styles/responsive.css +196 -0
  128. package/styles/responsive.d.ts +2 -0
  129. package/styles/tokens.css +50 -17
  130. package/dist/chunk-6EDV6YFE.js.map +0 -1
  131. package/dist/chunk-EB67J347.js.map +0 -1
  132. package/dist/chunk-EHSYZ4KP.js.map +0 -1
  133. package/dist/chunk-FD72GJMK.js.map +0 -1
  134. package/dist/chunk-JB5725BI.js +0 -48
  135. package/dist/chunk-JB5725BI.js.map +0 -1
  136. package/dist/chunk-L3EWO3UK.js.map +0 -1
  137. package/dist/chunk-LWR4MXSS.js.map +0 -1
  138. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  139. package/dist/chunk-U3CSCMWA.js.map +0 -1
  140. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
  141. /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
@@ -8,19 +8,17 @@
8
8
  * tips and look like a blank white bubble.
9
9
  */
10
10
  import { Tooltip as TooltipPrimitive } from "radix-ui";
11
- import {
12
- createContext,
13
- useContext,
14
- useRef,
15
- type ComponentProps,
16
- type CSSProperties,
17
- type RefObject,
18
- } from "react";
11
+ import { createContext, useContext, type ComponentProps, type CSSProperties } from "react";
19
12
 
20
13
  import { cn } from "../lib/cn";
21
- import { usePortalTokenStyle } from "../lib/use-portal-token-style";
14
+ import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
22
15
 
23
- const TooltipSourceContext = createContext<RefObject<HTMLElement | null> | null>(null);
16
+ type TooltipSourceContextValue = {
17
+ source: HTMLElement | null;
18
+ ref: (node: HTMLElement | null) => void;
19
+ };
20
+
21
+ const TooltipSourceContext = createContext<TooltipSourceContextValue | null>(null);
24
22
 
25
23
  function TooltipProvider({
26
24
  delayDuration = 300,
@@ -30,9 +28,9 @@ function TooltipProvider({
30
28
  }
31
29
 
32
30
  function Tooltip({ children, ...props }: ComponentProps<typeof TooltipPrimitive.Root>) {
33
- const sourceRef = useRef<HTMLElement | null>(null);
31
+ const source = usePortalTokenSource<HTMLElement>();
34
32
  return (
35
- <TooltipSourceContext.Provider value={sourceRef}>
33
+ <TooltipSourceContext.Provider value={source}>
36
34
  <TooltipPrimitive.Root data-slot="tooltip" {...props}>
37
35
  {children}
38
36
  </TooltipPrimitive.Root>
@@ -50,7 +48,7 @@ function TooltipTrigger({
50
48
  data-slot="tooltip-trigger"
51
49
  asChild={asChild}
52
50
  ref={(node) => {
53
- if (sourceRef) sourceRef.current = node;
51
+ sourceRef?.ref(node);
54
52
  }}
55
53
  {...props}
56
54
  />
@@ -69,9 +67,8 @@ function TooltipContent({
69
67
  style,
70
68
  ...props
71
69
  }: ComponentProps<typeof TooltipPrimitive.Content>) {
72
- const sourceRef = useContext(TooltipSourceContext);
73
- const fallbackRef = useRef<HTMLElement | null>(null);
74
- const portalStyle = usePortalTokenStyle(sourceRef ?? fallbackRef);
70
+ const source = useContext(TooltipSourceContext);
71
+ const portalStyle = usePortalTokenStyle(source?.source ?? null);
75
72
 
76
73
  return (
77
74
  <TooltipPrimitive.Portal>
@@ -330,7 +330,7 @@ export function UserMessageBody({ messageId, text, children, className }: UserMe
330
330
  aria-controls={contentId}
331
331
  aria-expanded={expanded}
332
332
  data-og-user-message-disclosure=""
333
- className="mt-1.5 inline-flex min-h-7 items-center rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-none transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:min-h-11"
333
+ className="mt-1.5 inline-flex min-h-7 items-center rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-hidden transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:min-h-11"
334
334
  onClick={(event) => toggle(event.currentTarget)}
335
335
  >
336
336
  {expanded ? "Show less" : "Show more"}
@@ -321,7 +321,7 @@ export function WorkbenchChanges({
321
321
  }}
322
322
  title={activeFile?.path}
323
323
  data-compact-file-picker
324
- className="h-11 w-full rounded-og-md border border-og-border bg-og-bg px-3 font-og-mono text-og-sm text-og-fg outline-none transition-colors focus:border-og-accent focus:ring-2 focus:ring-og-accent-soft"
324
+ className="h-11 w-full rounded-og-md border border-og-border bg-og-bg px-3 font-og-mono text-og-sm text-og-fg outline-hidden transition-colors focus:border-og-accent focus:ring-2 focus:ring-og-accent-soft"
325
325
  >
326
326
  {orderedFiles.map((file, index) => (
327
327
  <option key={file.path} value={index}>
@@ -480,7 +480,7 @@ function RailFileRow({
480
480
  title={file.path}
481
481
  data-rail-file
482
482
  className={cn(
483
- "relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
483
+ "relative flex min-h-7 w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm transition-colors hover:bg-og-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent pointer-coarse:min-h-11",
484
484
  grouped && "pl-3",
485
485
  active &&
486
486
  "bg-og-accent-soft text-og-fg before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-r-full before:bg-og-accent",
@@ -597,7 +597,7 @@ function GuardBody({
597
597
  <button
598
598
  type="button"
599
599
  onClick={() => onOpenFile(file.path)}
600
- className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11"
600
+ className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-11"
601
601
  >
602
602
  Open in Files
603
603
  <ArrowUpRightIcon className="size-3.5" aria-hidden />
@@ -622,7 +622,7 @@ function LayoutToggle({
622
622
  type="button"
623
623
  onClick={() => onChange(value)}
624
624
  className={cn(
625
- "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
625
+ "min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs capitalize focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
626
626
  layout === value
627
627
  ? "bg-og-accent-soft text-og-fg"
628
628
  : "text-og-fg-subtle hover:text-og-fg",
@@ -44,6 +44,8 @@ export type WorkspaceDockProps = {
44
44
  /** Controlled collapsed state for hosts that expose their own dock toggle. */
45
45
  collapsed?: boolean | undefined;
46
46
  onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
47
+ /** Keep the built-in collapse control when the host controls collapsed state. */
48
+ showCollapseControl?: boolean | undefined;
47
49
  /** A status accessory pinned to the right of the tab strip, left of the
48
50
  * maximize/collapse controls (e.g. the machine-state chip). Renders in both
49
51
  * the docked header and the full-screen overlay header. */
@@ -129,6 +131,7 @@ export function WorkspaceDock({
129
131
  onActiveTabChange,
130
132
  collapsed: collapsedProp,
131
133
  onCollapsedChange,
134
+ showCollapseControl = false,
132
135
  headerAccessory,
133
136
  mobileLeadingControl,
134
137
  autoSaveId = "og.session.dock",
@@ -343,6 +346,11 @@ export function WorkspaceDock({
343
346
  className="fixed inset-0 z-40 flex flex-col bg-og-bg"
344
347
  hidden={collapsed}
345
348
  style={{
349
+ // Author utility classes can override the user-agent `[hidden]`
350
+ // rule (for example this surface's `flex` class). Keep an inline
351
+ // display guard so a collapsed mobile workspace is actually gone
352
+ // visually as well as from the accessibility tree.
353
+ display: collapsed ? "none" : undefined,
346
354
  paddingTop: "env(safe-area-inset-top)",
347
355
  paddingBottom: "env(safe-area-inset-bottom)",
348
356
  }}
@@ -367,7 +375,7 @@ export function WorkspaceDock({
367
375
  onClick={expand}
368
376
  title="Open workspace"
369
377
  aria-label="Open workspace"
370
- className="absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent"
378
+ className="absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
371
379
  style={{ marginTop: "env(safe-area-inset-top)" }}
372
380
  >
373
381
  <ChevronsLeftRightIcon className="size-4" aria-hidden />
@@ -396,7 +404,7 @@ export function WorkspaceDock({
396
404
  <Maximize2Icon className="size-3.5" />
397
405
  )}
398
406
  </ChromeButton>
399
- {hostControlled ? null : (
407
+ {hostControlled && !showCollapseControl ? null : (
400
408
  <ChromeButton onClick={collapse} title="Collapse" label="Collapse dock">
401
409
  <PanelRightCloseIcon className="size-3.5" />
402
410
  </ChromeButton>
@@ -426,7 +434,7 @@ export function WorkspaceDock({
426
434
  </Panel>
427
435
 
428
436
  {!collapsed && (
429
- <Separator className="group relative z-10 w-1.5 shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11">
437
+ <Separator className="group relative z-10 w-1.5 shrink-0 outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent max-[1023px]:-mx-[19px] max-[1023px]:w-11 pointer-coarse:-mx-[19px] pointer-coarse:w-11">
430
438
  <span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-og-border transition-colors group-hover:bg-og-accent group-focus-visible:bg-og-accent group-data-[separator-state=dragging]:bg-og-accent" />
431
439
  </Separator>
432
440
  )}
@@ -503,7 +511,7 @@ function ChromeButton({
503
511
  onClick={onClick}
504
512
  title={title}
505
513
  aria-label={label}
506
- className="inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11"
514
+ className="inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11"
507
515
  >
508
516
  {children}
509
517
  </button>
@@ -664,7 +672,7 @@ function DockChrome({
664
672
  onClick={() => onTab(tab.id)}
665
673
  onKeyDown={(event) => onTabKeyDown(event, index)}
666
674
  className={cn(
667
- "flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
675
+ "flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
668
676
  tab.id === current
669
677
  ? "bg-og-accent-soft text-og-fg"
670
678
  : "text-og-fg-subtle hover:text-og-fg",
package/src/composer.ts CHANGED
@@ -64,6 +64,7 @@ export type {
64
64
  ComposerSubmitBlocker,
65
65
  ComposerSubmitMode,
66
66
  ComposerSurfaceProps,
67
+ ResponsiveBasis,
67
68
  UseChatComposerControllerOptions,
68
69
  } from "./components/composer";
69
70
  export { ComposerTranscriptionControl } from "./components/composer-transcription-control";
@@ -101,7 +102,9 @@ export type {
101
102
  VoiceRecordingChunk,
102
103
  VoiceRecordingChunkUploadState,
103
104
  VoiceRecordingFinalizationState,
105
+ VoiceRecordingHandoffMode,
104
106
  VoiceRecordingManifest,
107
+ VoiceRecordingRecoveryMode,
105
108
  VoiceRecordingStore,
106
109
  VoiceRecordingTranscriptionState,
107
110
  VoiceRecordingUploadState,
@@ -2,6 +2,51 @@ import type { SessionEvent } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import type { EmbeddedSessionEventClientLike } from "../client";
4
4
 
5
+ const HIDDEN_STREAM_GRACE_MS = 2_000;
6
+
7
+ /**
8
+ * Keep live browser work active while the page is visible, then suspend it
9
+ * after a short hidden-tab grace period. Returning to the page re-enables the
10
+ * caller, which must reconcile durable state before resuming live delivery.
11
+ */
12
+ export function usePageLiveActivity(hiddenGraceMs = HIDDEN_STREAM_GRACE_MS): boolean {
13
+ // Keep the server and initial browser render identical; the effect applies
14
+ // visibility after mount and preserves the intentional hidden-tab grace.
15
+ const [active, setActive] = useState(true);
16
+
17
+ useEffect(() => {
18
+ if (typeof document === "undefined") return;
19
+ let hiddenTimer: ReturnType<typeof setTimeout> | null = null;
20
+ const cancelHiddenTimer = () => {
21
+ if (hiddenTimer !== null) {
22
+ clearTimeout(hiddenTimer);
23
+ hiddenTimer = null;
24
+ }
25
+ };
26
+ const sync = () => {
27
+ cancelHiddenTimer();
28
+ if (document.visibilityState !== "hidden") {
29
+ setActive(true);
30
+ return;
31
+ }
32
+ hiddenTimer = setTimeout(() => {
33
+ hiddenTimer = null;
34
+ if (document.visibilityState === "hidden") setActive(false);
35
+ }, hiddenGraceMs);
36
+ };
37
+ document.addEventListener("visibilitychange", sync);
38
+ window.addEventListener("pageshow", sync);
39
+ sync();
40
+ return () => {
41
+ cancelHiddenTimer();
42
+ document.removeEventListener("visibilitychange", sync);
43
+ window.removeEventListener("pageshow", sync);
44
+ };
45
+ }, [hiddenGraceMs]);
46
+
47
+ return active;
48
+ }
49
+
5
50
  export type AsyncListState<T> = {
6
51
  data: T | null;
7
52
  loading: boolean;
@@ -19,6 +64,7 @@ export function usePolledValue<T>(
19
64
  ): AsyncListState<T> {
20
65
  const enabled = options.enabled ?? true;
21
66
  const pollIntervalMs = options.pollIntervalMs;
67
+ const pageLive = usePageLiveActivity();
22
68
  const [data, setData] = useState<T | null>(null);
23
69
  const [loading, setLoading] = useState(enabled);
24
70
  const [error, setError] = useState<Error | null>(null);
@@ -28,6 +74,8 @@ export function usePolledValue<T>(
28
74
  activeLoadRef.current = load;
29
75
  }, [load]);
30
76
  const requestAbortRef = useRef<AbortController | null>(null);
77
+ const inFlightRef = useRef<{ load: typeof load; promise: Promise<void> } | null>(null);
78
+ const trailingRefreshRef = useRef<{ load: typeof load; promise: Promise<void> } | null>(null);
31
79
  const [stateIdentity, setStateIdentity] = useState<{ load: typeof load }>(() => ({ load }));
32
80
 
33
81
  // A new loader identity means a new query (different session/workspace/...):
@@ -40,68 +88,107 @@ export function usePolledValue<T>(
40
88
  }
41
89
  }, [load, stateIdentity.load]);
42
90
 
43
- const run = useCallback(async () => {
91
+ const run = useCallback((): Promise<void> => {
44
92
  // A callback retained by a completed mutation from the previous query must
45
93
  // not supersede or settle the current query's request.
46
- if (activeLoadRef.current !== load) return;
94
+ if (activeLoadRef.current !== load) return Promise.resolve();
95
+ const existing = inFlightRef.current;
96
+ if (existing?.load === load) return existing.promise;
47
97
  const ticket = ++generation.current;
48
- requestAbortRef.current?.abort();
49
98
  const requestAbort = new AbortController();
50
99
  requestAbortRef.current = requestAbort;
51
- try {
52
- const result = await load(requestAbort.signal);
53
- if (
54
- ticket === generation.current &&
55
- activeLoadRef.current === load &&
56
- !requestAbort.signal.aborted
57
- ) {
58
- setData(result);
59
- setError(null);
60
- setLoading(false);
61
- }
62
- } catch (cause) {
63
- if (
64
- ticket === generation.current &&
65
- activeLoadRef.current === load &&
66
- !requestAbort.signal.aborted
67
- ) {
68
- setError(cause instanceof Error ? cause : new Error(String(cause)));
69
- setLoading(false);
70
- }
71
- } finally {
72
- if (requestAbortRef.current === requestAbort) {
73
- requestAbortRef.current = null;
100
+ let promise!: Promise<void>;
101
+ promise = (async () => {
102
+ try {
103
+ const result = await load(requestAbort.signal);
104
+ if (
105
+ ticket === generation.current &&
106
+ activeLoadRef.current === load &&
107
+ !requestAbort.signal.aborted
108
+ ) {
109
+ setData(result);
110
+ setError(null);
111
+ setLoading(false);
112
+ }
113
+ } catch (cause) {
114
+ if (
115
+ ticket === generation.current &&
116
+ activeLoadRef.current === load &&
117
+ !requestAbort.signal.aborted
118
+ ) {
119
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
120
+ setLoading(false);
121
+ }
122
+ } finally {
123
+ if (requestAbortRef.current === requestAbort) requestAbortRef.current = null;
124
+ if (inFlightRef.current?.promise === promise) inFlightRef.current = null;
74
125
  }
75
- }
126
+ })();
127
+ inFlightRef.current = { load, promise };
128
+ return promise;
76
129
  }, [load]);
77
130
 
131
+ const refresh = useCallback((): Promise<void> => {
132
+ const existing = inFlightRef.current;
133
+ if (existing?.load !== load) return run();
134
+ const trailing = trailingRefreshRef.current;
135
+ if (trailing?.load === load) return trailing.promise;
136
+ let promise!: Promise<void>;
137
+ promise = existing.promise.then(async () => {
138
+ if (trailingRefreshRef.current?.promise === promise) {
139
+ trailingRefreshRef.current = null;
140
+ }
141
+ if (activeLoadRef.current === load) await run();
142
+ });
143
+ trailingRefreshRef.current = { load, promise };
144
+ return promise;
145
+ }, [load, run]);
146
+
78
147
  useEffect(() => {
79
- if (!enabled) {
148
+ if (!enabled || !pageLive) {
80
149
  setLoading(false);
150
+ generation.current += 1;
151
+ requestAbortRef.current?.abort();
152
+ inFlightRef.current = null;
153
+ trailingRefreshRef.current = null;
81
154
  return;
82
155
  }
83
156
  setLoading(true);
84
- void run();
157
+ let cancelled = false;
158
+ let timer: ReturnType<typeof setTimeout> | null = null;
159
+ const schedule = () => {
160
+ if (cancelled || pollIntervalMs === undefined || pollIntervalMs <= 0) return;
161
+ timer = setTimeout(() => {
162
+ timer = null;
163
+ void run().finally(schedule);
164
+ }, pollIntervalMs);
165
+ };
166
+ void run().finally(schedule);
85
167
  if (pollIntervalMs === undefined || pollIntervalMs <= 0) {
86
168
  return () => {
169
+ cancelled = true;
87
170
  generation.current += 1;
88
171
  requestAbortRef.current?.abort();
172
+ inFlightRef.current = null;
173
+ trailingRefreshRef.current = null;
89
174
  };
90
175
  }
91
- const timer = setInterval(() => void run(), pollIntervalMs);
92
176
  return () => {
93
- clearInterval(timer);
177
+ cancelled = true;
178
+ if (timer !== null) clearTimeout(timer);
94
179
  generation.current += 1;
95
180
  requestAbortRef.current?.abort();
181
+ inFlightRef.current = null;
182
+ trailingRefreshRef.current = null;
96
183
  };
97
- }, [run, enabled, pollIntervalMs]);
184
+ }, [run, enabled, pageLive, pollIntervalMs]);
98
185
 
99
186
  const identityMatches = stateIdentity.load === load;
100
187
  return {
101
188
  data: identityMatches ? data : null,
102
189
  loading: identityMatches ? loading : enabled,
103
190
  error: identityMatches ? error : null,
104
- refresh: run,
191
+ refresh,
105
192
  };
106
193
  }
107
194
 
@@ -224,6 +311,8 @@ export function useSessionEventTrigger(
224
311
  reconcileBeforeLive?: (() => void | Promise<void>) | undefined,
225
312
  ): void {
226
313
  const enabled = options.enabled ?? true;
314
+ const pageLive = usePageLiveActivity();
315
+ const liveEnabled = enabled && pageLive;
227
316
  const events = options.events;
228
317
  const sharedFeed = events !== undefined;
229
318
  const matchRef = useRef(match);
@@ -240,7 +329,7 @@ export function useSessionEventTrigger(
240
329
 
241
330
  // Shared-log mode: scan only the unseen tail on every append.
242
331
  useEffect(() => {
243
- if (!sharedFeed || !enabled || !sessionId) {
332
+ if (!sharedFeed || !liveEnabled || !sessionId) {
244
333
  return;
245
334
  }
246
335
  const feedKey = `${workspaceId}\u0000${sessionId}`;
@@ -285,11 +374,11 @@ export function useSessionEventTrigger(
285
374
  onEventRef.current(event);
286
375
  }
287
376
  }
288
- }, [sharedFeed, enabled, events, workspaceId, sessionId]);
377
+ }, [sharedFeed, liveEnabled, events, workspaceId, sessionId]);
289
378
 
290
379
  // Self-stream mode: tail from the session's current lastSequence.
291
380
  useEffect(() => {
292
- if (sharedFeed || !enabled || !sessionId) {
381
+ if (sharedFeed || !liveEnabled || !sessionId) {
293
382
  return;
294
383
  }
295
384
  const controller = new AbortController();
@@ -302,7 +391,12 @@ export function useSessionEventTrigger(
302
391
  const stream = client.streamEvents(workspaceId, sessionId, {
303
392
  after: session.lastSequence,
304
393
  signal: controller.signal,
305
- beforeLive: async () => await reconcileBeforeLiveRef.current?.(),
394
+ onOpen: () => {
395
+ // Start reconciliation without delaying consumption of live SSE.
396
+ void Promise.resolve()
397
+ .then(() => reconcileBeforeLiveRef.current?.())
398
+ .catch(() => undefined);
399
+ },
306
400
  });
307
401
  for await (const event of stream) {
308
402
  if (matchRef.current(event)) {
@@ -317,7 +411,7 @@ export function useSessionEventTrigger(
317
411
  return () => {
318
412
  controller.abort();
319
413
  };
320
- }, [sharedFeed, enabled, client, workspaceId, sessionId]);
414
+ }, [sharedFeed, liveEnabled, client, workspaceId, sessionId]);
321
415
  }
322
416
 
323
417
  /** Debounce rapid event bursts into one trailing call (default 150ms). */
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  /** Events that change which Codex account a session runs on (or just ran). */
17
17
  export function isCodexAccountEvent(event: Pick<SessionEvent, "type">): boolean {
18
- return event.type === "codex.account.switched" || event.type === "turn.started";
18
+ return event.type === "codex.account.switched";
19
19
  }
20
20
 
21
21
  /**
@@ -104,7 +104,7 @@ const EMPTY_STATE: CodexAccountsState = {
104
104
  * The workspace's Codex accounts + the per-workspace active pointer + (when
105
105
  * session-scoped) the session pin and last-ran-on account. Composed like
106
106
  * `useMachines`: slow polling (the realtime work is done by the
107
- * `codex.account.switched` / `turn.started` event trigger) + a `pin` mutation.
107
+ * `codex.account.switched` event trigger) + a `pin` mutation.
108
108
  * Dual-consumer safe via the structural `CodexAccountsClientLike` surface.
109
109
  */
110
110
  export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCodexAccountsResult {
@@ -141,8 +141,8 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
141
141
  const { run: runUsageMutation, mutating: refreshingUsage } = useMutationRunner();
142
142
  const [pinningTarget, setPinningTarget] = useState<string | null>(null);
143
143
 
144
- // Live flip: a manual switch (P1) or a failover (P3) emits codex.account.switched;
145
- // turn.started covers the case where the worker recorded the actual account.
144
+ // Refresh only after the durable post-selection event. `turn.started` is
145
+ // emitted before account selection settles and races this authoritative read.
146
146
  useSessionEventTrigger(
147
147
  client,
148
148
  workspaceId,
@@ -2,6 +2,7 @@ import type {
2
2
  CreateVariableSetRequest,
3
3
  UpdateVariableSetRequest,
4
4
  VariableSet,
5
+ VariableSetSecret,
5
6
  VariableSetVariableMetadata,
6
7
  } from "@opengeni/sdk";
7
8
  import { useCallback, useMemo } from "react";
@@ -21,7 +22,12 @@ export type UseVariableSetsResult = {
21
22
  create: (request: CreateVariableSetRequest) => Promise<VariableSet | null>;
22
23
  update: (variableSetId: string, request: UpdateVariableSetRequest) => Promise<VariableSet | null>;
23
24
  remove: (variableSetId: string) => Promise<boolean>;
24
- /** Set/rotate a variable. Values are write-only — reads expose metadata only. */
25
+ /**
26
+ * Read one exact value through the dedicated permissioned API. The hook never
27
+ * puts plaintext in its polled variable-set cache.
28
+ */
29
+ readVariable: (variableSetId: string, name: string) => Promise<VariableSetSecret | null>;
30
+ /** Set/rotate a variable. Generic reads expose metadata only. */
25
31
  setVariable: (
26
32
  variableSetId: string,
27
33
  name: string,
@@ -35,8 +41,9 @@ export type UseVariableSetsResult = {
35
41
 
36
42
  /**
37
43
  * Variable sets (named, encrypted variable sets attached to sessions
38
- * and scheduled tasks). Variable values are write-only end to end: this hook
39
- * never sees a value after it is sent.
44
+ * and scheduled tasks). Generic reads stay metadata-only. A dedicated,
45
+ * permissioned method can retrieve one value and returns it directly without
46
+ * adding plaintext to hook state or polling caches.
40
47
  */
41
48
  export function useVariableSets(options: UseVariableSetsOptions = {}): UseVariableSetsResult {
42
49
  const { client, workspaceId } = useOpenGeni(options);
@@ -89,6 +96,12 @@ export function useVariableSets(options: UseVariableSetsOptions = {}): UseVariab
89
96
  [client, workspaceId, run, refresh],
90
97
  );
91
98
 
99
+ const readVariable = useCallback(
100
+ async (variableSetId: string, name: string): Promise<VariableSetSecret | null> =>
101
+ await run(() => client.getVariableSetVariable(workspaceId, variableSetId, name)),
102
+ [client, workspaceId, run],
103
+ );
104
+
92
105
  const setVariable = useCallback(
93
106
  async (
94
107
  variableSetId: string,
@@ -128,6 +141,7 @@ export function useVariableSets(options: UseVariableSetsOptions = {}): UseVariab
128
141
  create,
129
142
  update,
130
143
  remove,
144
+ readVariable,
131
145
  setVariable,
132
146
  deleteVariable,
133
147
  mutating,
@@ -139,7 +153,9 @@ export function useVariableSets(options: UseVariableSetsOptions = {}): UseVariab
139
153
  /** @deprecated use UseVariableSetsOptions */
140
154
  export type UseEnvironmentsOptions = UseVariableSetsOptions;
141
155
  /** @deprecated use UseVariableSetsResult */
142
- export type UseEnvironmentsResult = UseVariableSetsResult & { environments: VariableSet[] };
156
+ export type UseEnvironmentsResult = UseVariableSetsResult & {
157
+ environments: VariableSet[];
158
+ };
143
159
  /** @deprecated use useVariableSets */
144
160
  export function useEnvironments(options: UseEnvironmentsOptions = {}): UseEnvironmentsResult {
145
161
  const legacyClient = useMemo(() => {
@@ -158,6 +174,9 @@ export function useEnvironments(options: UseEnvironmentsOptions = {}): UseEnviro
158
174
  options.client.deleteEnvironmentVariable ?? options.client.deleteVariableSetVariable,
159
175
  };
160
176
  }, [options.client]);
161
- const result = useVariableSets({ ...options, ...(legacyClient ? { client: legacyClient } : {}) });
177
+ const result = useVariableSets({
178
+ ...options,
179
+ ...(legacyClient ? { client: legacyClient } : {}),
180
+ });
162
181
  return { ...result, environments: result.variableSets };
163
182
  }