@opengeni/react 0.46.1 → 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-3PB7MIT6.js → chunk-NJ7M2B6Z.js} +207 -67
- 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-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-LGVMUIRV.js → chunk-XCNTXWBB.js} +264 -115
- 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 +5 -3
- 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 +27 -30
- 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 +49 -23
- package/src/components/copy-button.tsx +1 -1
- 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 +39 -6
- 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 +163 -19
- 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 +16 -5
- package/src/timeline/activity-rail.tsx +14 -5
- 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 +5 -5
- package/src/timeline/tool-renderers.tsx +166 -22
- package/src/timeline/turn-summary.tsx +2 -2
- 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-3PB7MIT6.js.map +0 -1
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-FGZUCXZF.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-LGVMUIRV.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
package/src/provider.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@opengeni/sdk";
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
8
8
|
import type { SessionClientLike } from "./client";
|
|
9
|
+
import { usePageLiveActivity } from "./hooks/internal";
|
|
9
10
|
import { OpenGeniContext } from "./session-context";
|
|
10
11
|
|
|
11
12
|
export { useOpenGeni, useOpenGeniClient } from "./session-context";
|
|
@@ -40,6 +41,9 @@ export function OpenGeniProvider({
|
|
|
40
41
|
);
|
|
41
42
|
const callbackRef = useRef(onWorkspaceControlEvent);
|
|
42
43
|
const reconcilersRef = useRef(new Map<string, Map<string, () => Promise<void>>>());
|
|
44
|
+
const reconcileInFlightRef = useRef(new Map<string, Promise<void>>());
|
|
45
|
+
const workspaceControlSequencesRef = useRef(new Map<string, number>());
|
|
46
|
+
const pageLive = usePageLiveActivity();
|
|
43
47
|
callbackRef.current = onWorkspaceControlEvent;
|
|
44
48
|
|
|
45
49
|
const verifyApiContract = useCallback(async (): Promise<void> => {
|
|
@@ -76,31 +80,57 @@ export function OpenGeniProvider({
|
|
|
76
80
|
);
|
|
77
81
|
const reconcileSession = useMemo(
|
|
78
82
|
() =>
|
|
79
|
-
|
|
83
|
+
(sessionId: string): Promise<void> => {
|
|
84
|
+
const existing = reconcileInFlightRef.current.get(sessionId);
|
|
85
|
+
if (existing) return existing;
|
|
80
86
|
// This read also crosses the exact API-contract handshake before stale
|
|
81
87
|
// state can be presented as live after a deployment.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
const promise = (async () => {
|
|
89
|
+
await withTimeout(verifyApiContract(), 10_000, "API contract reconciliation");
|
|
90
|
+
const callbacks = [...(reconcilersRef.current.get(sessionId)?.values() ?? [])];
|
|
91
|
+
await withTimeout(
|
|
92
|
+
Promise.all(callbacks.map((reconcile) => reconcile())).then(() => undefined),
|
|
93
|
+
15_000,
|
|
94
|
+
"session reconciliation",
|
|
95
|
+
);
|
|
96
|
+
})().finally(() => {
|
|
97
|
+
if (reconcileInFlightRef.current.get(sessionId) === promise) {
|
|
98
|
+
reconcileInFlightRef.current.delete(sessionId);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
reconcileInFlightRef.current.set(sessionId, promise);
|
|
102
|
+
return promise;
|
|
85
103
|
},
|
|
86
104
|
[verifyApiContract],
|
|
87
105
|
);
|
|
88
106
|
|
|
89
107
|
useEffect(() => {
|
|
108
|
+
if (!pageLive) {
|
|
109
|
+
setWorkspaceControlConnectionState("idle");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
90
112
|
const controller = new AbortController();
|
|
91
|
-
setWorkspaceControlEvent(null);
|
|
92
113
|
setWorkspaceControlConnectionState("connecting");
|
|
93
114
|
void (async () => {
|
|
94
115
|
try {
|
|
95
116
|
await verifyApiContract();
|
|
96
117
|
const workspace = await client.getWorkspace(workspaceId);
|
|
118
|
+
const resumeAfter = Math.max(
|
|
119
|
+
workspaceControlSequencesRef.current.get(workspaceId) ?? 0,
|
|
120
|
+
workspace.inferenceControl.revision,
|
|
121
|
+
);
|
|
122
|
+
workspaceControlSequencesRef.current.set(workspaceId, resumeAfter);
|
|
97
123
|
const stream = client.streamWorkspaceControlEvents(workspaceId, {
|
|
98
|
-
after:
|
|
124
|
+
after: resumeAfter,
|
|
99
125
|
signal: controller.signal,
|
|
100
126
|
onStateChange: setWorkspaceControlConnectionState,
|
|
101
127
|
});
|
|
102
128
|
for await (const event of stream) {
|
|
103
129
|
if (controller.signal.aborted) return;
|
|
130
|
+
workspaceControlSequencesRef.current.set(
|
|
131
|
+
workspaceId,
|
|
132
|
+
Math.max(workspaceControlSequencesRef.current.get(workspaceId) ?? 0, event.sequence),
|
|
133
|
+
);
|
|
104
134
|
setWorkspaceControlEvent((current) =>
|
|
105
135
|
!current || event.sequence > current.sequence ? event : current,
|
|
106
136
|
);
|
|
@@ -115,7 +145,7 @@ export function OpenGeniProvider({
|
|
|
115
145
|
}
|
|
116
146
|
})();
|
|
117
147
|
return () => controller.abort();
|
|
118
|
-
}, [client, verifyApiContract, workspaceId]);
|
|
148
|
+
}, [client, pageLive, verifyApiContract, workspaceId]);
|
|
119
149
|
|
|
120
150
|
const value = useMemo(
|
|
121
151
|
() => ({
|
|
@@ -143,6 +173,21 @@ export function OpenGeniProvider({
|
|
|
143
173
|
);
|
|
144
174
|
}
|
|
145
175
|
|
|
176
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
177
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
178
|
+
return Promise.race([
|
|
179
|
+
promise,
|
|
180
|
+
new Promise<T>((_resolve, reject) => {
|
|
181
|
+
timer = setTimeout(
|
|
182
|
+
() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
|
|
183
|
+
timeoutMs,
|
|
184
|
+
);
|
|
185
|
+
}),
|
|
186
|
+
]).finally(() => {
|
|
187
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
146
191
|
function ApiContractMismatchScreen({ mismatch }: { mismatch: OpenGeniApiContractMismatchError }) {
|
|
147
192
|
return (
|
|
148
193
|
<div
|
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { ChevronRightIcon } from "lucide-react";
|
|
4
4
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
5
|
-
import {
|
|
6
|
-
createContext,
|
|
7
|
-
useContext,
|
|
8
|
-
useRef,
|
|
9
|
-
type ComponentProps,
|
|
10
|
-
type Ref,
|
|
11
|
-
type RefObject,
|
|
12
|
-
} from "react";
|
|
5
|
+
import { createContext, useContext, type ComponentProps, type Ref } from "react";
|
|
13
6
|
import { cn } from "../lib/cn";
|
|
14
|
-
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
7
|
+
import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
type DropdownMenuSourceContextValue = {
|
|
10
|
+
source: HTMLElement | null;
|
|
11
|
+
ref: (node: HTMLElement | null) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DropdownMenuSourceContext = createContext<DropdownMenuSourceContextValue | null>(null);
|
|
17
15
|
|
|
18
16
|
function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
|
|
19
17
|
if (typeof ref === "function") ref(value);
|
|
@@ -21,9 +19,9 @@ function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
function DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
24
|
-
const
|
|
22
|
+
const source = usePortalTokenSource<HTMLElement>();
|
|
25
23
|
return (
|
|
26
|
-
<DropdownMenuSourceContext.Provider value={
|
|
24
|
+
<DropdownMenuSourceContext.Provider value={source}>
|
|
27
25
|
<DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
28
26
|
</DropdownMenuSourceContext.Provider>
|
|
29
27
|
);
|
|
@@ -38,7 +36,7 @@ function DropdownMenuTrigger({
|
|
|
38
36
|
<DropdownMenuPrimitive.Trigger
|
|
39
37
|
data-slot="dropdown-menu-trigger"
|
|
40
38
|
ref={(node) => {
|
|
41
|
-
|
|
39
|
+
sourceRef?.ref(node);
|
|
42
40
|
assignRef(ref, node);
|
|
43
41
|
}}
|
|
44
42
|
{...props}
|
|
@@ -52,9 +50,8 @@ function DropdownMenuContent({
|
|
|
52
50
|
style,
|
|
53
51
|
...props
|
|
54
52
|
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const portalStyle = usePortalTokenStyle(sourceRef ?? fallbackRef);
|
|
53
|
+
const source = useContext(DropdownMenuSourceContext);
|
|
54
|
+
const portalStyle = usePortalTokenStyle(source?.source ?? null);
|
|
58
55
|
return (
|
|
59
56
|
<DropdownMenuPrimitive.Portal>
|
|
60
57
|
<DropdownMenuPrimitive.Content
|
|
@@ -138,9 +135,8 @@ function DropdownMenuSubContent({
|
|
|
138
135
|
style,
|
|
139
136
|
...props
|
|
140
137
|
}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const portalStyle = usePortalTokenStyle(sourceRef ?? fallbackRef);
|
|
138
|
+
const source = useContext(DropdownMenuSourceContext);
|
|
139
|
+
const portalStyle = usePortalTokenStyle(source?.source ?? null);
|
|
144
140
|
return (
|
|
145
141
|
<DropdownMenuPrimitive.SubContent
|
|
146
142
|
data-slot="dropdown-menu-sub-content"
|
|
@@ -297,6 +297,13 @@ export function SessionRealtimeControl(props: {
|
|
|
297
297
|
onRealtimeAutostartConsumed?: (() => void) | undefined;
|
|
298
298
|
/** Host hides dictate (and can choreograph layout) while realtime voice is live. */
|
|
299
299
|
onVoiceActiveChange?: ((active: boolean) => void) | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* `split` attaches the model chevron at every breakpoint (public default).
|
|
302
|
+
* `split-desktop` hides it below `sm` when the host owns mobile selection.
|
|
303
|
+
* Inside a container-responsive Composer.Root, `sm` follows that composer.
|
|
304
|
+
* `none` never attaches a model menu to the bar button.
|
|
305
|
+
*/
|
|
306
|
+
modelMenu?: "split" | "split-desktop" | "none" | undefined;
|
|
300
307
|
/** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
|
|
301
308
|
controllerFactory?: SessionRealtimeControllerFactory | undefined;
|
|
302
309
|
}) {
|
|
@@ -365,6 +372,7 @@ export function SessionRealtimeControl(props: {
|
|
|
365
372
|
admissionBlocker={realtime.admissionBlocker}
|
|
366
373
|
modelAvailable={selectedModel.available}
|
|
367
374
|
menuSide="top"
|
|
375
|
+
modelMenu={props.modelMenu ?? "split"}
|
|
368
376
|
audioRef={realtime.audioRef}
|
|
369
377
|
selectedModel={selectedModel}
|
|
370
378
|
models={selection.models}
|
|
@@ -475,6 +483,7 @@ export function NewSessionRealtimeControl(props: {
|
|
|
475
483
|
/**
|
|
476
484
|
* `split` attaches the model chevron at every breakpoint (public default).
|
|
477
485
|
* `split-desktop` hides it below `sm` when the host owns mobile selection (e.g. “+”).
|
|
486
|
+
* Inside a container-responsive Composer.Root, `sm` follows that composer.
|
|
478
487
|
* `none` never attaches a model menu to the bar button.
|
|
479
488
|
*/
|
|
480
489
|
modelMenu?: "split" | "split-desktop" | "none" | undefined;
|
|
@@ -556,6 +565,7 @@ export function RealtimeVoiceControl(props: {
|
|
|
556
565
|
/**
|
|
557
566
|
* `split` = start + model chevron at every breakpoint (public SDK default).
|
|
558
567
|
* `split-desktop` = chevron from `sm` up; below that the host owns selection.
|
|
568
|
+
* Inside a container-responsive Composer.Root, `sm` follows that composer.
|
|
559
569
|
* `none` = start button only.
|
|
560
570
|
*/
|
|
561
571
|
modelMenu?: "split" | "split-desktop" | "none" | undefined;
|
|
@@ -632,7 +642,8 @@ export function RealtimeVoiceControl(props: {
|
|
|
632
642
|
role="group"
|
|
633
643
|
aria-label="Realtime voice"
|
|
634
644
|
data-picker-side={props.menuSide ?? "top"}
|
|
635
|
-
|
|
645
|
+
data-model-menu={modelMenu}
|
|
646
|
+
className="og-realtime-control inline-flex shrink-0 items-center"
|
|
636
647
|
>
|
|
637
648
|
<audio ref={props.audioRef} autoPlay className="hidden" aria-hidden="true" />
|
|
638
649
|
<RealtimeComposerGlow phase={status.phase} reduceMotion={reduceMotion === true} />
|
|
@@ -719,7 +730,7 @@ export function RealtimeVoiceControl(props: {
|
|
|
719
730
|
// Match transcription mic: ghost icon when idle; filled only when live.
|
|
720
731
|
// Public contract: coarse pointers keep a 44px target; split menu uses
|
|
721
732
|
// left-only rounding at `sm+` where the chevron attaches.
|
|
722
|
-
"relative inline-flex size-8 items-center justify-center outline-
|
|
733
|
+
"og-realtime-primary relative inline-flex size-8 items-center justify-center outline-hidden",
|
|
723
734
|
"rounded-og-md transition-[background-color,border-color,color,box-shadow] duration-200 ease-og-out",
|
|
724
735
|
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
725
736
|
"disabled:cursor-not-allowed disabled:opacity-45 pointer-coarse:size-11",
|
|
@@ -748,7 +759,7 @@ export function RealtimeVoiceControl(props: {
|
|
|
748
759
|
className={cn(
|
|
749
760
|
// Keep the split trigger compact on desktop while preserving a
|
|
750
761
|
// full 44px mobile target independent of pointer-media support.
|
|
751
|
-
"inline-flex h-11 w-11 items-center justify-center rounded-r-og-md outline-
|
|
762
|
+
"og-realtime-model-trigger inline-flex h-11 w-11 items-center justify-center rounded-r-og-md outline-hidden sm:h-8 sm:w-6",
|
|
752
763
|
"transition-[background-color,border-color,color] duration-200 ease-og-out",
|
|
753
764
|
"focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-og-accent/45",
|
|
754
765
|
desktopOnlyModelMenu && "hidden sm:inline-flex",
|
|
@@ -763,7 +774,7 @@ export function RealtimeVoiceControl(props: {
|
|
|
763
774
|
align="end"
|
|
764
775
|
sideOffset={8}
|
|
765
776
|
collisionPadding={12}
|
|
766
|
-
className="flex w-72 max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] flex-col overflow-hidden rounded-xl border-border bg-surface p-1.5 shadow-xl"
|
|
777
|
+
className="og-realtime-menu flex w-72 max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] flex-col overflow-hidden rounded-xl border-border bg-surface p-1.5 shadow-xl"
|
|
767
778
|
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
768
779
|
>
|
|
769
780
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
@@ -931,7 +942,7 @@ function RealtimeMuteButton(props: {
|
|
|
931
942
|
{...(props.reduceMotion ? {} : { whileTap: { scale: 0.92 } })}
|
|
932
943
|
onClick={props.onToggle}
|
|
933
944
|
className={cn(
|
|
934
|
-
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md outline-
|
|
945
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-og-md outline-hidden",
|
|
935
946
|
"transition-[background-color,border-color,color] duration-150 ease-og-out",
|
|
936
947
|
"focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:size-11",
|
|
937
948
|
props.muted
|
|
@@ -12,7 +12,7 @@ import { cn } from "../lib/cn";
|
|
|
12
12
|
import { truncate } from "../lib/format";
|
|
13
13
|
import { defaultToolRegistry } from "./tool-renderers";
|
|
14
14
|
import { useEntranceAnimation, useEntranceAnimationLive } from "./entrance";
|
|
15
|
-
import type { ToolRegistry } from "./registry";
|
|
15
|
+
import type { RetainedScreenshotLoader, ToolRegistry } from "./registry";
|
|
16
16
|
import { useSeenActivityIds } from "./seen-activity-ids";
|
|
17
17
|
import { BodyNote, PayloadBlock, ActivityDisclosure } from "./shared";
|
|
18
18
|
import { toolDisplayName } from "./tool-display-name";
|
|
@@ -43,6 +43,7 @@ export type ActivityRailProps = {
|
|
|
43
43
|
* row is then non-interactive rich content). See {@link MessageTimelineProps}.
|
|
44
44
|
*/
|
|
45
45
|
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
46
|
+
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
46
47
|
/** Drop the left rule + indent (used inside a folded turn summary). */
|
|
47
48
|
bare?: boolean | undefined;
|
|
48
49
|
className?: string | undefined;
|
|
@@ -65,6 +66,7 @@ export function ActivityRail({
|
|
|
65
66
|
toolRegistry = defaultToolRegistry,
|
|
66
67
|
onOpenSession,
|
|
67
68
|
onMemoryClick,
|
|
69
|
+
loadRetainedScreenshot,
|
|
68
70
|
bare,
|
|
69
71
|
className,
|
|
70
72
|
}: ActivityRailProps) {
|
|
@@ -112,7 +114,13 @@ export function ActivityRail({
|
|
|
112
114
|
>
|
|
113
115
|
{items.map((item, index) => {
|
|
114
116
|
const newFamily = index > 0 && familyOf(item) !== familyOf(items[index - 1]!);
|
|
115
|
-
const row = renderActivity(
|
|
117
|
+
const row = renderActivity(
|
|
118
|
+
item,
|
|
119
|
+
toolRegistry,
|
|
120
|
+
onOpenSession,
|
|
121
|
+
onMemoryClick,
|
|
122
|
+
loadRetainedScreenshot,
|
|
123
|
+
);
|
|
116
124
|
return (
|
|
117
125
|
<div
|
|
118
126
|
key={item.id}
|
|
@@ -137,13 +145,14 @@ function renderActivity(
|
|
|
137
145
|
toolRegistry: ToolRegistry,
|
|
138
146
|
onOpenSession: ((sessionId: string) => void) | undefined,
|
|
139
147
|
onMemoryClick: ((memoryId: string) => void) | undefined,
|
|
148
|
+
loadRetainedScreenshot: RetainedScreenshotLoader | undefined,
|
|
140
149
|
) {
|
|
141
150
|
switch (item.kind) {
|
|
142
151
|
case "reasoning":
|
|
143
152
|
return <ReasoningRow item={item} />;
|
|
144
153
|
case "tool-call": {
|
|
145
154
|
const Renderer = toolRegistry.resolve(item);
|
|
146
|
-
return <Renderer item={item} />;
|
|
155
|
+
return <Renderer item={item} loadRetainedScreenshot={loadRetainedScreenshot} />;
|
|
147
156
|
}
|
|
148
157
|
case "worker":
|
|
149
158
|
return <WorkerRow item={item} onOpenSession={onOpenSession} />;
|
|
@@ -281,7 +290,7 @@ function MemoryRow({
|
|
|
281
290
|
onClick={() => onMemoryClick?.(targetId)}
|
|
282
291
|
className={cn(
|
|
283
292
|
"group/memlink -mx-1 inline-flex w-fit items-center gap-1 rounded-og-sm px-1 py-0.5 text-left text-og-sm text-og-fg-subtle",
|
|
284
|
-
"outline-
|
|
293
|
+
"outline-hidden transition-colors duration-150 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
285
294
|
)}
|
|
286
295
|
>
|
|
287
296
|
View in memory
|
|
@@ -418,7 +427,7 @@ function WorkerRow({
|
|
|
418
427
|
onClick={() => onOpenSession(sessionId)}
|
|
419
428
|
className={cn(
|
|
420
429
|
"group/worker -mx-1.5 flex w-full items-start gap-2 rounded-og-sm px-1.5 py-1.5 text-left",
|
|
421
|
-
"outline-
|
|
430
|
+
"outline-hidden transition-colors duration-150 hover:bg-og-surface-1 focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
422
431
|
"pointer-coarse:min-h-11 pointer-coarse:py-2.5",
|
|
423
432
|
)}
|
|
424
433
|
>
|
package/src/timeline/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export type {
|
|
|
56
56
|
export { createToolRegistry, rawTypeOf } from "./registry";
|
|
57
57
|
export type {
|
|
58
58
|
CreateToolRegistryOptions,
|
|
59
|
+
RetainedScreenshotLoader,
|
|
59
60
|
ToolRegistry,
|
|
60
61
|
ToolRegistryEntry,
|
|
61
62
|
ToolRenderer,
|
|
@@ -123,7 +124,6 @@ export {
|
|
|
123
124
|
looksBinary,
|
|
124
125
|
parseExecBannerSessionId,
|
|
125
126
|
parseToolArgs,
|
|
126
|
-
redactSecrets,
|
|
127
127
|
sandboxCommandExitCode,
|
|
128
128
|
stripExecBanner,
|
|
129
129
|
tailPeek,
|
package/src/timeline/parsers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeMcpOutput, type GitFileDiff } from "@opengeni/sdk";
|
|
1
|
+
import { normalizeMcpOutput, type GitFileDiff, type RetainedArtifactMetadata } from "@opengeni/sdk";
|
|
2
2
|
import { tryParseJson } from "../lib/format";
|
|
3
3
|
|
|
4
4
|
/* ----------------------------------------------------------------------------
|
|
@@ -437,25 +437,6 @@ export function isApplyPatch(item: { name: string; raw: unknown }): boolean {
|
|
|
437
437
|
return name === "apply_patch_call" || name === "apply_patch" || name.endsWith("__apply_patch");
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
/* --- secret redaction ------------------------------------------------------- */
|
|
441
|
-
|
|
442
|
-
const SECRET_KEY = /^(value|secret|token|password|api[_-]?key|signing[_-]?key)$/i;
|
|
443
|
-
|
|
444
|
-
/** Deep-redact secret-looking values so arguments never leak a key into the UI. */
|
|
445
|
-
export function redactSecrets(value: unknown): unknown {
|
|
446
|
-
if (Array.isArray(value)) {
|
|
447
|
-
return value.map(redactSecrets);
|
|
448
|
-
}
|
|
449
|
-
if (value && typeof value === "object") {
|
|
450
|
-
const out: Record<string, unknown> = {};
|
|
451
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
452
|
-
out[k] = SECRET_KEY.test(k) ? "••••" : redactSecrets(v);
|
|
453
|
-
}
|
|
454
|
-
return out;
|
|
455
|
-
}
|
|
456
|
-
return value;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
440
|
/** Parse tool arguments that may arrive as a JSON string or an object. */
|
|
460
441
|
export function parseToolArgs(args: unknown): Record<string, unknown> {
|
|
461
442
|
if (args == null) {
|
|
@@ -558,6 +539,38 @@ export function screenshotDataUrl(out: unknown): string | null {
|
|
|
558
539
|
return null;
|
|
559
540
|
}
|
|
560
541
|
|
|
542
|
+
/** Parse the closed retained-screenshot receipt carried by new tool events. */
|
|
543
|
+
export function retainedScreenshotMetadata(out: unknown): RetainedArtifactMetadata | null {
|
|
544
|
+
if (typeof out === "string" && (out.startsWith("{") || out.startsWith("["))) {
|
|
545
|
+
const parsed = tryParseJson(out);
|
|
546
|
+
if (parsed !== undefined && parsed !== out) return retainedScreenshotMetadata(parsed);
|
|
547
|
+
}
|
|
548
|
+
if (Array.isArray(out)) {
|
|
549
|
+
for (const entry of out) {
|
|
550
|
+
const metadata = retainedScreenshotMetadata(entry);
|
|
551
|
+
if (metadata) return metadata;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if (!out || typeof out !== "object") return null;
|
|
556
|
+
const value = out as Record<string, unknown>;
|
|
557
|
+
if (typeof value.artifactId !== "string" || typeof value.available !== "boolean") return null;
|
|
558
|
+
if (!value.available) {
|
|
559
|
+
return typeof value.reason === "string" ? (value as unknown as RetainedArtifactMetadata) : null;
|
|
560
|
+
}
|
|
561
|
+
return value.kind === "computer_screenshot" &&
|
|
562
|
+
value.contentType === "image/png" &&
|
|
563
|
+
typeof value.originalBytes === "number" &&
|
|
564
|
+
typeof value.sha256 === "string" &&
|
|
565
|
+
value.dimensions !== null &&
|
|
566
|
+
typeof value.dimensions === "object" &&
|
|
567
|
+
value.retention !== null &&
|
|
568
|
+
typeof value.retention === "object" &&
|
|
569
|
+
(value.retention as Record<string, unknown>).policy === "session_screenshot"
|
|
570
|
+
? (value as unknown as RetainedArtifactMetadata)
|
|
571
|
+
: null;
|
|
572
|
+
}
|
|
573
|
+
|
|
561
574
|
export type TimelineMediaPreview = {
|
|
562
575
|
type: "media_preview";
|
|
563
576
|
mediaType: string;
|
|
@@ -540,6 +540,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
540
540
|
|
|
541
541
|
case "tool.auth_needed":
|
|
542
542
|
case "credential.auth_needed": {
|
|
543
|
+
if (
|
|
544
|
+
event.type === "tool.auth_needed" &&
|
|
545
|
+
!stringValue(payload.toolName) &&
|
|
546
|
+
stringValue(payload.serverId) !== "codex_apps"
|
|
547
|
+
) {
|
|
548
|
+
// Historical optional-MCP initialize/tools-list credential misses are
|
|
549
|
+
// setup availability, not evidence of a concrete conversational tool
|
|
550
|
+
// call. Keep the raw event in Debug/audit but do not manufacture an
|
|
551
|
+
// actionable reconnect card in the transcript. Actual tools/call auth
|
|
552
|
+
// failures carry a concrete toolName and remain visible.
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
543
555
|
// Keep the whole structured payload — the renderer turns it into a clean
|
|
544
556
|
// inline reconnect card, and the app starts the recovery flow off the
|
|
545
557
|
// connectionId/resource. Losing it to a plain-text notice was the ugly
|
|
@@ -549,6 +561,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
549
561
|
kind: "auth-needed",
|
|
550
562
|
id: event.id,
|
|
551
563
|
turnId,
|
|
564
|
+
serverId: typeof payload.serverId === "string" ? payload.serverId : null,
|
|
565
|
+
source: event.type === "tool.auth_needed" ? "tool" : "credential",
|
|
552
566
|
providerDomain: stringValue(payload.providerDomain),
|
|
553
567
|
connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
|
|
554
568
|
reason: authNeededReason(payload.reason),
|
|
@@ -1609,6 +1623,15 @@ export function extractSessionRef(value: unknown, depth = 0): string | null {
|
|
|
1609
1623
|
return null;
|
|
1610
1624
|
}
|
|
1611
1625
|
const record = value as Record<string, unknown>;
|
|
1626
|
+
const receiptResource = asRecord(record.resource);
|
|
1627
|
+
if (
|
|
1628
|
+
record.receiptVersion === "mcp-mutation-receipt.v1" &&
|
|
1629
|
+
receiptResource.type === "session" &&
|
|
1630
|
+
typeof receiptResource.id === "string" &&
|
|
1631
|
+
looksLikeId(receiptResource.id)
|
|
1632
|
+
) {
|
|
1633
|
+
return receiptResource.id;
|
|
1634
|
+
}
|
|
1612
1635
|
if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) {
|
|
1613
1636
|
return record.sessionId;
|
|
1614
1637
|
}
|
package/src/timeline/registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RetainedArtifactReference } from "@opengeni/sdk";
|
|
1
2
|
import type { ComponentType } from "react";
|
|
2
3
|
import { mcpToolLeaf } from "./tool-display-name";
|
|
3
4
|
import type { ToolCallItem } from "./types";
|
|
@@ -22,8 +23,14 @@ import type { ToolCallItem } from "./types";
|
|
|
22
23
|
|
|
23
24
|
export type ToolRendererProps = {
|
|
24
25
|
item: ToolCallItem;
|
|
26
|
+
loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
export type RetainedScreenshotLoader = (
|
|
30
|
+
artifact: RetainedArtifactReference,
|
|
31
|
+
signal: AbortSignal,
|
|
32
|
+
) => Promise<Uint8Array | null>;
|
|
33
|
+
|
|
27
34
|
export type ToolRenderer = ComponentType<ToolRendererProps>;
|
|
28
35
|
|
|
29
36
|
/** A registry entry: which key it matches and the component that renders it. */
|
|
@@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react";
|
|
|
3
3
|
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
|
|
4
4
|
import { Dialog } from "radix-ui";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
|
+
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
6
7
|
|
|
7
8
|
/* ----------------------------------------------------------------------------
|
|
8
9
|
Screenshot lightbox
|
|
@@ -18,7 +19,7 @@ import { cn } from "../lib/cn";
|
|
|
18
19
|
-------------------------------------------------------------------------- */
|
|
19
20
|
|
|
20
21
|
type LightboxController = {
|
|
21
|
-
open: (src: string, caption?: string) => void;
|
|
22
|
+
open: (src: string, caption?: string, source?: HTMLElement | null) => void;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const LightboxContext = createContext<LightboxController | null>(null);
|
|
@@ -59,17 +60,25 @@ export function LightboxProvider({ children }: { children: ReactNode }) {
|
|
|
59
60
|
|
|
60
61
|
function LightboxRoot({ children }: { children: ReactNode }) {
|
|
61
62
|
const [state, setState] = useState<{ src: string; caption?: string } | null>(null);
|
|
63
|
+
const [tokenSource, setTokenSource] = useState<HTMLElement | null>(null);
|
|
64
|
+
const portalStyle = usePortalTokenStyle(tokenSource);
|
|
62
65
|
|
|
63
|
-
const open = useCallback((src: string, caption?: string) => {
|
|
66
|
+
const open = useCallback((src: string, caption?: string, source?: HTMLElement | null) => {
|
|
67
|
+
setTokenSource(source ?? null);
|
|
64
68
|
setState(caption ? { src, caption } : { src });
|
|
65
69
|
}, []);
|
|
66
70
|
|
|
71
|
+
const close = useCallback(() => {
|
|
72
|
+
setState(null);
|
|
73
|
+
setTokenSource(null);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
67
76
|
const controller = useMemo<LightboxController>(() => ({ open }), [open]);
|
|
68
77
|
|
|
69
78
|
return (
|
|
70
79
|
<LightboxContext.Provider value={controller}>
|
|
71
80
|
{children}
|
|
72
|
-
<Dialog.Root open={state !== null} onOpenChange={(next) => !next &&
|
|
81
|
+
<Dialog.Root open={state !== null} onOpenChange={(next) => !next && close()}>
|
|
73
82
|
<AnimatePresence>
|
|
74
83
|
{state !== null ? (
|
|
75
84
|
<Dialog.Portal forceMount>
|
|
@@ -80,6 +89,7 @@ function LightboxRoot({ children }: { children: ReactNode }) {
|
|
|
80
89
|
exit={{ opacity: 0 }}
|
|
81
90
|
transition={{ duration: 0.15 }}
|
|
82
91
|
className="og-root fixed inset-0 z-50 bg-black/90 backdrop-blur-md"
|
|
92
|
+
style={portalStyle}
|
|
83
93
|
/>
|
|
84
94
|
</Dialog.Overlay>
|
|
85
95
|
<Dialog.Content
|
|
@@ -101,10 +111,11 @@ function LightboxRoot({ children }: { children: ReactNode }) {
|
|
|
101
111
|
// figcaption's "click outside to close" affordance is real.
|
|
102
112
|
onClick={(event) => {
|
|
103
113
|
if (event.target === event.currentTarget) {
|
|
104
|
-
|
|
114
|
+
close();
|
|
105
115
|
}
|
|
106
116
|
}}
|
|
107
117
|
className="og-root fixed inset-0 z-50 flex items-center justify-center p-6 sm:p-12"
|
|
118
|
+
style={portalStyle}
|
|
108
119
|
>
|
|
109
120
|
<Dialog.Title className="sr-only">Screenshot</Dialog.Title>
|
|
110
121
|
{/* The figure is one self-contained object: image, caption, and
|
package/src/timeline/shared.tsx
CHANGED
|
@@ -163,7 +163,7 @@ export function ActivityDisclosure({
|
|
|
163
163
|
const inner = (
|
|
164
164
|
<>
|
|
165
165
|
{hasBody ? (
|
|
166
|
-
<ChevronRightIcon className="size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-[var(--
|
|
166
|
+
<ChevronRightIcon className="size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-[var(--_og-duration-disclose)] ease-og-in-out group-data-[state=open]/disclosure:rotate-90" />
|
|
167
167
|
) : (
|
|
168
168
|
<span className="size-3.5 shrink-0" />
|
|
169
169
|
)}
|
|
@@ -226,7 +226,7 @@ export function ActivityDisclosure({
|
|
|
226
226
|
data-status={dataStatus}
|
|
227
227
|
className={cn(
|
|
228
228
|
rowClass,
|
|
229
|
-
"cursor-pointer outline-
|
|
229
|
+
"cursor-pointer outline-hidden hover:bg-og-surface-1 hover:text-og-fg",
|
|
230
230
|
"focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-0",
|
|
231
231
|
)}
|
|
232
232
|
>
|
|
@@ -498,7 +498,7 @@ export function Thumbnail({
|
|
|
498
498
|
type="button"
|
|
499
499
|
onClick={(event) => {
|
|
500
500
|
event.stopPropagation();
|
|
501
|
-
lightbox.open(src, caption);
|
|
501
|
+
lightbox.open(src, caption, event.currentTarget);
|
|
502
502
|
}}
|
|
503
503
|
onKeyDown={(event) => {
|
|
504
504
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -507,7 +507,7 @@ export function Thumbnail({
|
|
|
507
507
|
}}
|
|
508
508
|
className={cn(
|
|
509
509
|
MEDIA_BOX,
|
|
510
|
-
"group/thumb relative inline-flex overflow-hidden bg-og-bg outline-
|
|
510
|
+
"group/thumb relative inline-flex overflow-hidden bg-og-bg outline-hidden",
|
|
511
511
|
"focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
512
512
|
)}
|
|
513
513
|
aria-label="Expand screenshot"
|
|
@@ -554,7 +554,7 @@ export function ScreenshotFigure({
|
|
|
554
554
|
) : lightbox ? (
|
|
555
555
|
<button
|
|
556
556
|
type="button"
|
|
557
|
-
onClick={() => lightbox.open(src, caption)}
|
|
557
|
+
onClick={(event) => lightbox.open(src, caption, event.currentTarget)}
|
|
558
558
|
className={surface}
|
|
559
559
|
aria-label="Expand screenshot"
|
|
560
560
|
>
|