@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.
- package/README.md +68 -12
- package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
- package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
- package/dist/chunk-I4MPFDRP.js.map +1 -0
- package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
- package/dist/chunk-LHLYWG5R.js.map +1 -0
- package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
- package/dist/chunk-NJ7M2B6Z.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
- package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
- package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
- package/dist/chunk-OL6QVXGF.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
- package/dist/chunk-XCNTXWBB.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/chat-composer.d.ts +7 -2
- package/dist/components/composer-transcription-control.d.ts +2 -0
- package/dist/components/composer.d.ts +14 -0
- package/dist/components/machines/machine-detail.d.ts +3 -1
- package/dist/components/machines-dashboard.d.ts +3 -3
- package/dist/components/message-timeline.d.ts +12 -2
- package/dist/components/sandbox-files.d.ts +2 -2
- package/dist/components/sandbox-terminal.d.ts +29 -0
- package/dist/components/sandbox-workspace.d.ts +5 -1
- package/dist/components/workspace-dock.d.ts +3 -1
- package/dist/composer.d.ts +2 -2
- package/dist/composer.js +6 -6
- package/dist/hooks/internal.d.ts +6 -0
- package/dist/hooks/use-codex-accounts.d.ts +1 -1
- package/dist/hooks/use-environments.d.ts +10 -4
- package/dist/hooks/use-machines.d.ts +7 -0
- package/dist/hooks/use-sandbox-files.d.ts +19 -0
- package/dist/hooks/use-sandbox-git.d.ts +7 -0
- package/dist/hooks/use-transcription.d.ts +2 -2
- package/dist/hooks/use-voice-input.d.ts +13 -4
- package/dist/index.d.ts +6 -5
- package/dist/index.js +904 -538
- package/dist/index.js.map +1 -1
- package/dist/lib/composer-responsive-context.d.ts +9 -0
- package/dist/lib/use-portal-token-style.d.ts +14 -3
- package/dist/machines.js +2 -2
- package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +9 -0
- package/dist/realtime.js +29 -32
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +5 -5
- package/dist/session.js +4 -4
- package/dist/timeline/activity-rail.d.ts +3 -2
- package/dist/timeline/index.d.ts +2 -2
- package/dist/timeline/parsers.d.ts +3 -3
- package/dist/timeline/registry.d.ts +3 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/types.d.ts +4 -0
- package/dist/voice-recording-store.d.ts +8 -2
- package/package.json +20 -4
- package/src/client.ts +3 -0
- package/src/components/chat-composer.tsx +12 -2
- package/src/components/code-editor.tsx +1 -1
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +41 -16
- package/src/components/composer.tsx +52 -26
- package/src/components/copy-button.tsx +2 -2
- package/src/components/desktop-viewer.tsx +2 -2
- package/src/components/file-browser.tsx +39 -17
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +44 -11
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +20 -16
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +140 -60
- package/src/components/sandbox-terminal.tsx +24 -5
- package/src/components/sandbox-workspace.tsx +180 -142
- package/src/components/session-chrome.tsx +14 -14
- package/src/components/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +4 -4
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- package/src/hooks/use-codex-accounts.ts +4 -4
- package/src/hooks/use-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machines.ts +47 -1
- package/src/hooks/use-sandbox-files.ts +310 -59
- package/src/hooks/use-sandbox-git.ts +148 -41
- package/src/hooks/use-session-capabilities.ts +35 -24
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-voice-input.ts +481 -36
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/index.ts +11 -6
- package/src/lib/composer-responsive-context.ts +15 -0
- package/src/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +18 -7
- package/src/timeline/activity-rail.tsx +15 -6
- package/src/timeline/index.ts +1 -1
- package/src/timeline/parsers.ts +33 -20
- package/src/timeline/projection.ts +23 -0
- package/src/timeline/registry.ts +7 -0
- package/src/timeline/screenshot-lightbox.tsx +15 -4
- package/src/timeline/shared.tsx +8 -8
- package/src/timeline/tool-renderers.tsx +166 -22
- package/src/timeline/turn-summary.tsx +6 -6
- package/src/timeline/types.ts +4 -0
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5065 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +3 -0
- package/styles/responsive.css +196 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EHSYZ4KP.js.map +0 -1
- package/dist/chunk-FD72GJMK.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-L3EWO3UK.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
- /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
|
-
|
|
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
|
|
31
|
+
const source = usePortalTokenSource<HTMLElement>();
|
|
34
32
|
return (
|
|
35
|
-
<TooltipSourceContext.Provider value={
|
|
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
|
-
|
|
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
|
|
73
|
-
const
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
package/src/hooks/internal.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 || !
|
|
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,
|
|
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 || !
|
|
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
|
-
|
|
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,
|
|
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"
|
|
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`
|
|
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
|
-
//
|
|
145
|
-
//
|
|
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
|
-
/**
|
|
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).
|
|
39
|
-
*
|
|
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 & {
|
|
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({
|
|
177
|
+
const result = useVariableSets({
|
|
178
|
+
...options,
|
|
179
|
+
...(legacyClient ? { client: legacyClient } : {}),
|
|
180
|
+
});
|
|
162
181
|
return { ...result, environments: result.variableSets };
|
|
163
182
|
}
|