@opengeni/react 1.2.1-canary.37222965366001 → 1.4.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 +52 -1
- package/dist/artifacts.js +2 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ERGYJIBB.js → chunk-AWZLIMKX.js} +2 -2
- package/dist/{chunk-ERGYJIBB.js.map → chunk-AWZLIMKX.js.map} +1 -1
- package/dist/{chunk-FZCP527Y.js → chunk-CJUO7J5Z.js} +2 -2
- package/dist/{chunk-742CZ3L3.js → chunk-H5X55XH7.js} +16 -200
- package/dist/chunk-H5X55XH7.js.map +1 -0
- package/dist/{chunk-4OTO7KBK.js → chunk-LJ2UTJJE.js} +2737 -1580
- package/dist/chunk-LJ2UTJJE.js.map +1 -0
- package/dist/{chunk-CY7WK5FD.js → chunk-LN6TCXD3.js} +22 -1
- package/dist/chunk-LN6TCXD3.js.map +1 -0
- package/dist/{chunk-TMYGL6X5.js → chunk-MBMPHOMA.js} +71 -6
- package/dist/chunk-MBMPHOMA.js.map +1 -0
- package/dist/{chunk-RC6BZ44Q.js → chunk-OA6HFRQP.js} +2 -2
- package/dist/{chunk-X34BSY2R.js → chunk-WZMYFJD3.js} +242 -63
- package/dist/chunk-WZMYFJD3.js.map +1 -0
- package/dist/components/approval-surface.d.ts +11 -13
- package/dist/components/composer-transcription-control.d.ts +3 -0
- package/dist/components/open-geni-chat.d.ts +26 -7
- package/dist/components/session-conversation.d.ts +26 -3
- package/dist/components/session-list.d.ts +4 -1
- package/dist/components/session-proxy-scope.d.ts +16 -2
- package/dist/components/tool-action-review.d.ts +47 -0
- package/dist/components/tool-review-history.d.ts +25 -0
- package/dist/composer.js +1 -1
- package/dist/connect.js +1 -1
- package/dist/hooks/use-client-config-flags.d.ts +37 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +27 -11
- package/dist/index.js.map +1 -1
- package/dist/{latest-question-YTOBHCWR.js → latest-question-GPYM2JNU.js} +3 -3
- package/dist/realtime/realtime-control.d.ts +2 -0
- package/dist/realtime.js +25 -4
- package/dist/realtime.js.map +1 -1
- package/dist/session-capability-policy.d.ts +20 -1
- package/dist/session-ui.d.ts +4 -1
- package/dist/session-ui.js +12 -6
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +6 -6
- package/dist/timeline/index.d.ts +2 -0
- package/dist/timeline/retained-loaders.d.ts +27 -0
- package/package.json +3 -3
- package/src/components/approval-surface.tsx +368 -239
- package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
- package/src/components/composer-transcription-control.tsx +14 -0
- package/src/components/composer.tsx +1 -0
- package/src/components/open-geni-chat.tsx +245 -72
- package/src/components/session-conversation.tsx +243 -58
- package/src/components/session-list.tsx +9 -2
- package/src/components/session-proxy-scope.tsx +47 -7
- package/src/components/tool-action-review.tsx +649 -0
- package/src/components/tool-review-history.tsx +88 -0
- package/src/hooks/use-client-config-flags.ts +102 -0
- package/src/hooks/use-session-events.ts +19 -16
- package/src/hooks/use-transcription.ts +6 -0
- package/src/hooks/use-voice-input.ts +6 -0
- package/src/index.ts +18 -1
- package/src/realtime/realtime-control.tsx +28 -1
- package/src/session-capability-policy.ts +119 -6
- package/src/session-ui.ts +9 -1
- package/src/timeline/index.ts +10 -0
- package/src/timeline/retained-loaders.ts +86 -0
- package/src/timeline/tool-display-name.ts +5 -1
- package/src/timeline/tool-renderers.tsx +148 -8
- package/styles/compiled.css +79 -15
- package/styles/tokens.css +1 -1
- package/dist/chunk-4OTO7KBK.js.map +0 -1
- package/dist/chunk-742CZ3L3.js.map +0 -1
- package/dist/chunk-CY7WK5FD.js.map +0 -1
- package/dist/chunk-TMYGL6X5.js.map +0 -1
- package/dist/chunk-X34BSY2R.js.map +0 -1
- /package/dist/{chunk-FZCP527Y.js.map → chunk-CJUO7J5Z.js.map} +0 -0
- /package/dist/{chunk-RC6BZ44Q.js.map → chunk-OA6HFRQP.js.map} +0 -0
- /package/dist/{latest-question-YTOBHCWR.js.map → latest-question-GPYM2JNU.js.map} +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import type { SessionEvent, ToolActionReview } from "@opengeni/sdk";
|
|
3
|
+
import { approvalsFromRequiresAction } from "../approvals";
|
|
4
|
+
|
|
5
|
+
type HistoryContext = {
|
|
6
|
+
approvalIds: ReadonlySet<string>;
|
|
7
|
+
revision: number;
|
|
8
|
+
load: (approvalId: string) => Promise<ToolActionReview>;
|
|
9
|
+
onViewDetails: (review: ToolActionReview, path: string) => void;
|
|
10
|
+
};
|
|
11
|
+
const History = createContext<HistoryContext | null>(null);
|
|
12
|
+
|
|
13
|
+
/** Opt-in, authenticated history for the exact reviewed calls in the visible event window. */
|
|
14
|
+
export function ToolReviewHistoryProvider({
|
|
15
|
+
events,
|
|
16
|
+
load,
|
|
17
|
+
onViewDetails,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
events: readonly SessionEvent[];
|
|
21
|
+
load: HistoryContext["load"];
|
|
22
|
+
onViewDetails: HistoryContext["onViewDetails"];
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
const value = useMemo<HistoryContext>(
|
|
26
|
+
() => ({
|
|
27
|
+
approvalIds: new Set(
|
|
28
|
+
events.flatMap((event) =>
|
|
29
|
+
event.type === "session.requiresAction"
|
|
30
|
+
? approvalsFromRequiresAction(event.payload).map((approval) => approval.id)
|
|
31
|
+
: [],
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
revision:
|
|
35
|
+
[...events]
|
|
36
|
+
.reverse()
|
|
37
|
+
.find(
|
|
38
|
+
(event) =>
|
|
39
|
+
event.type === "session.requiresAction" ||
|
|
40
|
+
event.type === "agent.toolCall.output" ||
|
|
41
|
+
event.type.startsWith("turn.") ||
|
|
42
|
+
event.type === "user.approvalDecision" ||
|
|
43
|
+
event.type.startsWith("session.control."),
|
|
44
|
+
)?.sequence ?? 0,
|
|
45
|
+
load,
|
|
46
|
+
onViewDetails,
|
|
47
|
+
}),
|
|
48
|
+
[events, load, onViewDetails],
|
|
49
|
+
);
|
|
50
|
+
return <History.Provider value={value}>{children}</History.Provider>;
|
|
51
|
+
}
|
|
52
|
+
export function useHasToolReview(approvalId: string | null): boolean {
|
|
53
|
+
const history = useContext(History);
|
|
54
|
+
return Boolean(approvalId && history?.approvalIds.has(approvalId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The saved review for one recorded call, or null while it loads or when it is
|
|
59
|
+
* unavailable (callers then keep their ordinary tool row).
|
|
60
|
+
*/
|
|
61
|
+
export function useRecordedToolReview(approvalId: string | null): {
|
|
62
|
+
review: ToolActionReview | null;
|
|
63
|
+
onViewDetails: ((path: string) => void) | undefined;
|
|
64
|
+
} {
|
|
65
|
+
const history = useContext(History);
|
|
66
|
+
const [review, setReview] = useState<ToolActionReview | null>(null);
|
|
67
|
+
const load = history?.load;
|
|
68
|
+
const revision = history?.revision;
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!load || !approvalId) return;
|
|
71
|
+
let active = true;
|
|
72
|
+
void load(approvalId).then(
|
|
73
|
+
(value) => {
|
|
74
|
+
if (active && value.id === approvalId) setReview(value);
|
|
75
|
+
},
|
|
76
|
+
() => undefined,
|
|
77
|
+
);
|
|
78
|
+
return () => {
|
|
79
|
+
active = false;
|
|
80
|
+
};
|
|
81
|
+
}, [approvalId, load, revision]);
|
|
82
|
+
const onViewDetails = history?.onViewDetails;
|
|
83
|
+
return {
|
|
84
|
+
review: review && review.id === approvalId ? review : null,
|
|
85
|
+
onViewDetails:
|
|
86
|
+
review && onViewDetails ? (path: string) => onViewDetails(review, path) : undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ClientVoiceInputConfig, ReasoningEffort } from "@opengeni/sdk";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/** The client-config fields the stock conversation and chat read. */
|
|
5
|
+
export type ClientConfigFlagsSource = {
|
|
6
|
+
getClientConfig: () => Promise<{
|
|
7
|
+
fileUploads?: { enabled?: boolean };
|
|
8
|
+
modelSelection?: boolean | undefined;
|
|
9
|
+
sandboxFiles?: boolean | undefined;
|
|
10
|
+
defaultReasoningEffort?: ReasoningEffort | undefined;
|
|
11
|
+
artifacts?: unknown;
|
|
12
|
+
sessionCreation?: boolean | undefined;
|
|
13
|
+
archive?: boolean | undefined;
|
|
14
|
+
voiceInput?: ClientVoiceInputConfig | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ClientConfigFlags = {
|
|
19
|
+
/** Whether the config has loaded. Proxy capability flags stay `true` until it says otherwise. */
|
|
20
|
+
loaded: boolean;
|
|
21
|
+
uploads: boolean;
|
|
22
|
+
modelSelection: boolean;
|
|
23
|
+
sandboxFiles: boolean;
|
|
24
|
+
defaultReasoningEffort: ReasoningEffort | null;
|
|
25
|
+
/**
|
|
26
|
+
* Inline Site previews and the artifact viewer. `false` only when a session
|
|
27
|
+
* proxy says it does not serve them; native deployments omit the flag.
|
|
28
|
+
*/
|
|
29
|
+
artifacts: boolean;
|
|
30
|
+
/** Browser chat creation. `false` only when a session proxy has no `createSession` hook. */
|
|
31
|
+
sessionCreation: boolean;
|
|
32
|
+
/** Chat archive/restore. `false` only when a session proxy disables it. */
|
|
33
|
+
archive: boolean;
|
|
34
|
+
/** Native voice input when the deployment can transcribe, else null. */
|
|
35
|
+
voiceInput: ClientVoiceInputConfig | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const INITIAL_FLAGS: ClientConfigFlags = {
|
|
39
|
+
loaded: false,
|
|
40
|
+
uploads: false,
|
|
41
|
+
modelSelection: false,
|
|
42
|
+
sandboxFiles: false,
|
|
43
|
+
defaultReasoningEffort: null,
|
|
44
|
+
artifacts: true,
|
|
45
|
+
sessionCreation: true,
|
|
46
|
+
archive: true,
|
|
47
|
+
voiceInput: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ClientConfigFlagsInput = Awaited<ReturnType<ClientConfigFlagsSource["getClientConfig"]>>;
|
|
51
|
+
|
|
52
|
+
// One read per client: the chat, its list, the new-chat composer and the
|
|
53
|
+
// conversation all ask, and behind a session proxy each read is a round trip.
|
|
54
|
+
const configReads = new WeakMap<object, Promise<ClientConfigFlagsInput>>();
|
|
55
|
+
|
|
56
|
+
function readClientConfig(client: ClientConfigFlagsSource): Promise<ClientConfigFlagsInput> {
|
|
57
|
+
let read = configReads.get(client);
|
|
58
|
+
if (!read) {
|
|
59
|
+
read = client.getClientConfig();
|
|
60
|
+
configReads.set(client, read);
|
|
61
|
+
// A failed read is retried by the next mount rather than cached.
|
|
62
|
+
read.catch(() => {
|
|
63
|
+
if (configReads.get(client) === read) configReads.delete(client);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return read;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Deployment and session-proxy capabilities from the client config. */
|
|
70
|
+
export function useClientConfigFlags(
|
|
71
|
+
client: Partial<ClientConfigFlagsSource> | ClientConfigFlagsSource,
|
|
72
|
+
): ClientConfigFlags {
|
|
73
|
+
const [flags, setFlags] = useState<ClientConfigFlags>(INITIAL_FLAGS);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// Custom clients without a config read keep the defaults.
|
|
76
|
+
if (typeof client.getClientConfig !== "function") return;
|
|
77
|
+
let live = true;
|
|
78
|
+
readClientConfig(client as ClientConfigFlagsSource).then(
|
|
79
|
+
(config) => {
|
|
80
|
+
if (live) {
|
|
81
|
+
setFlags({
|
|
82
|
+
loaded: true,
|
|
83
|
+
uploads: config.fileUploads?.enabled === true,
|
|
84
|
+
// Only an explicit offer shows end users the picker.
|
|
85
|
+
modelSelection: config.modelSelection === true,
|
|
86
|
+
sandboxFiles: config.sandboxFiles !== false,
|
|
87
|
+
defaultReasoningEffort: config.defaultReasoningEffort ?? null,
|
|
88
|
+
artifacts: config.artifacts !== false,
|
|
89
|
+
sessionCreation: config.sessionCreation !== false,
|
|
90
|
+
archive: config.archive !== false,
|
|
91
|
+
voiceInput: config.voiceInput?.available === true ? config.voiceInput : null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
() => undefined,
|
|
96
|
+
);
|
|
97
|
+
return () => {
|
|
98
|
+
live = false;
|
|
99
|
+
};
|
|
100
|
+
}, [client]);
|
|
101
|
+
return flags;
|
|
102
|
+
}
|
|
@@ -225,8 +225,8 @@ export function useSessionEvents(
|
|
|
225
225
|
// new stream identity cannot expose the previous session's event log.
|
|
226
226
|
const [stateStreamKey, setStateStreamKey] = useState(streamKey);
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
228
|
+
// Navigation belongs to the session/client lifetime, not the transport: an
|
|
229
|
+
// SSE reconnect must never cancel an in-flight history page.
|
|
230
230
|
useEffect(() => {
|
|
231
231
|
navigationGenerationRef.current += 1;
|
|
232
232
|
setNewerError(null);
|
|
@@ -621,9 +621,6 @@ export function useSessionEvents(
|
|
|
621
621
|
}
|
|
622
622
|
const current = eventWindowRef.current;
|
|
623
623
|
assertPrependOrder(current.events, window.events);
|
|
624
|
-
// Freeze the live iterator before replacing its in-memory window. Rows
|
|
625
|
-
// pending in the aborted iterator were never cursor-committed and will
|
|
626
|
-
// be replayed from the retained high-water mark below.
|
|
627
624
|
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
628
625
|
direction: "oldest",
|
|
629
626
|
});
|
|
@@ -633,7 +630,6 @@ export function useSessionEvents(
|
|
|
633
630
|
markTailPreserved();
|
|
634
631
|
return false;
|
|
635
632
|
}
|
|
636
|
-
streamAbortRef.current?.abort();
|
|
637
633
|
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
638
634
|
const retained = {
|
|
639
635
|
...next,
|
|
@@ -645,21 +641,28 @@ export function useSessionEvents(
|
|
|
645
641
|
}
|
|
646
642
|
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
647
643
|
const previousNewest = maxResumeSequenceOrNull(current.events);
|
|
644
|
+
const evictedLiveTail =
|
|
645
|
+
previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
646
|
+
if (evictedLiveTail && viewModeRef.current === "live") {
|
|
647
|
+
// The prepend leaves the live tip. Freeze the iterator before the
|
|
648
|
+
// window is replaced so a late flush cannot append live rows onto
|
|
649
|
+
// (and newest-bound away) the history the reader just requested.
|
|
650
|
+
streamAbortRef.current?.abort();
|
|
651
|
+
}
|
|
648
652
|
eventWindowRef.current = retained;
|
|
649
653
|
markCommitted();
|
|
650
654
|
oldestSequenceRef.current = retainedOldest;
|
|
651
655
|
newestSequenceRef.current = retainedNewest;
|
|
652
|
-
|
|
656
|
+
if (evictedLiveTail) {
|
|
657
|
+
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
658
|
+
}
|
|
653
659
|
// Oldest-directed eviction can discard newer in-memory rows. That fact
|
|
654
660
|
// keeps windowTruncated true, but it does not imply older durable rows
|
|
655
661
|
// exist; only the backward DB page can answer hasOlder truthfully.
|
|
656
662
|
const olderStillAvailable = window.hasOlder;
|
|
657
663
|
hasOlderRef.current = olderStillAvailable;
|
|
658
|
-
const evictedLiveTail =
|
|
659
|
-
previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
660
664
|
let newer: boolean | null = null;
|
|
661
665
|
let enterHistory = false;
|
|
662
|
-
let reconnectLive = false;
|
|
663
666
|
if (viewModeRef.current === "history" || evictedLiveTail) {
|
|
664
667
|
const highWater = lastSequenceRef.current;
|
|
665
668
|
newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
@@ -672,11 +675,13 @@ export function useSessionEvents(
|
|
|
672
675
|
viewModeRef.current = "history";
|
|
673
676
|
enterHistory = true;
|
|
674
677
|
}
|
|
675
|
-
} else {
|
|
676
|
-
// The retained merge is still one contiguous live suffix. Restart
|
|
677
|
-
// SSE from its cursor so events queued during the fetch are replayed.
|
|
678
|
-
reconnectLive = true;
|
|
679
678
|
}
|
|
679
|
+
// Otherwise the merge is still one contiguous live suffix and the open
|
|
680
|
+
// SSE iterator keeps appending to it: every flush reads
|
|
681
|
+
// eventWindowRef, so rows delivered during or after this fetch land
|
|
682
|
+
// once, in order, on the prepended window. Reopening the stream here
|
|
683
|
+
// would flash a connecting state and re-run session reconciliation on
|
|
684
|
+
// every scroll-up for no data benefit.
|
|
680
685
|
loadingOlderRef.current = false;
|
|
681
686
|
startTransition(() => {
|
|
682
687
|
if (status !== undefined) {
|
|
@@ -689,8 +694,6 @@ export function useSessionEvents(
|
|
|
689
694
|
}
|
|
690
695
|
if (enterHistory) {
|
|
691
696
|
setViewMode("history");
|
|
692
|
-
} else if (reconnectLive) {
|
|
693
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
694
697
|
}
|
|
695
698
|
setLoadingOlder(false);
|
|
696
699
|
});
|
|
@@ -611,6 +611,12 @@ function classifyStartError(error: unknown): TranscriptionErrorCode {
|
|
|
611
611
|
|
|
612
612
|
export function normalizeTranscriptionErrorCode(code: unknown): TranscriptionErrorCode {
|
|
613
613
|
switch (code) {
|
|
614
|
+
case "insufficient_credits":
|
|
615
|
+
case "allowance_exhausted":
|
|
616
|
+
case "monthly_model_cost_limit":
|
|
617
|
+
case "unavailable":
|
|
618
|
+
case "too_large":
|
|
619
|
+
case "invalid_audio":
|
|
614
620
|
case "permission_denied":
|
|
615
621
|
case "not_supported":
|
|
616
622
|
case "network":
|
|
@@ -1503,6 +1503,12 @@ function errorCode(error: unknown): string {
|
|
|
1503
1503
|
}
|
|
1504
1504
|
|
|
1505
1505
|
function isRetryableVoiceInputError(error: unknown): boolean {
|
|
1506
|
+
if (
|
|
1507
|
+
["insufficient_credits", "allowance_exhausted", "monthly_model_cost_limit"].includes(
|
|
1508
|
+
errorCode(error),
|
|
1509
|
+
)
|
|
1510
|
+
)
|
|
1511
|
+
return false;
|
|
1506
1512
|
if (error instanceof OpenGeniApiError) {
|
|
1507
1513
|
return error.retryable && error.status !== 409;
|
|
1508
1514
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,9 @@ export {
|
|
|
16
16
|
export {
|
|
17
17
|
attachSessionCapability,
|
|
18
18
|
completeSessionCapabilityOAuth,
|
|
19
|
+
prepareSessionCapabilityAccess,
|
|
20
|
+
applySessionCapabilityAccess,
|
|
21
|
+
type SessionCapabilityAccessPlan,
|
|
19
22
|
} from "./session-capability-policy";
|
|
20
23
|
export { retainedImageId } from "./components/markdown";
|
|
21
24
|
// oxlint-disable-next-line typescript/triple-slash-reference -- package consumers must load the ambient type for this optional untyped peer without emitting a runtime import.
|
|
@@ -328,6 +331,8 @@ export type {
|
|
|
328
331
|
export { approvalsFromRequiresAction, projectPendingApprovals } from "./approvals";
|
|
329
332
|
export type { PendingApproval } from "./approvals";
|
|
330
333
|
export { ApprovalSurface, defaultApprovalSurfaceMessages } from "./components/approval-surface";
|
|
334
|
+
export { ToolActionReviewCard, ToolActionReviewDetails } from "./components/tool-action-review";
|
|
335
|
+
export type { ToolReviewDetailsLoader } from "./components/tool-action-review";
|
|
331
336
|
export type { ApprovalSurfaceMessages, ApprovalSurfaceProps } from "./components/approval-surface";
|
|
332
337
|
|
|
333
338
|
// Structured human input: event projection, authoritative hook, and styled form.
|
|
@@ -386,14 +391,20 @@ export type {
|
|
|
386
391
|
// Tool-renderer registry + the per-tool renderers (the timeline's extension API)
|
|
387
392
|
export {
|
|
388
393
|
createDefaultToolRegistry,
|
|
394
|
+
createSessionRetainedScreenshotLoader,
|
|
389
395
|
createToolRegistry,
|
|
396
|
+
createWorkspaceRetainedArtifactLoader,
|
|
397
|
+
createWorkspaceRetainedVideoLoader,
|
|
390
398
|
defaultToolRegistry,
|
|
391
399
|
rawTypeOf,
|
|
392
400
|
} from "./timeline";
|
|
393
401
|
export type {
|
|
394
402
|
CreateToolRegistryOptions,
|
|
395
403
|
RetainedArtifactLoader,
|
|
404
|
+
RetainedArtifactLoaderClient,
|
|
396
405
|
RetainedScreenshotLoader,
|
|
406
|
+
RetainedScreenshotLoaderClient,
|
|
407
|
+
RetainedVideoLoaderClient,
|
|
397
408
|
VideoArtifactPlaybackLoader,
|
|
398
409
|
ToolRegistry,
|
|
399
410
|
ToolRegistryEntry,
|
|
@@ -491,7 +502,11 @@ export { SessionConversation } from "./components/session-conversation";
|
|
|
491
502
|
export { SessionList } from "./components/session-list";
|
|
492
503
|
export type { SessionListLabels, SessionListProps } from "./components/session-list";
|
|
493
504
|
export { OpenGeniChat } from "./components/open-geni-chat";
|
|
494
|
-
export type {
|
|
505
|
+
export type {
|
|
506
|
+
OpenGeniChatCreateOptions,
|
|
507
|
+
OpenGeniChatLabels,
|
|
508
|
+
OpenGeniChatProps,
|
|
509
|
+
} from "./components/open-geni-chat";
|
|
495
510
|
export type {
|
|
496
511
|
SessionConversationLabels,
|
|
497
512
|
SessionConversationProps,
|
|
@@ -714,3 +729,5 @@ export { ClaudeMark } from "./components/claude-mark";
|
|
|
714
729
|
export { AnthropicMark } from "./components/anthropic-mark";
|
|
715
730
|
export { OpenRouterMark } from "./components/openrouter-mark";
|
|
716
731
|
export { GrokMark } from "./components/grok-mark";
|
|
732
|
+
|
|
733
|
+
export { ToolReviewHistoryProvider } from "./components/tool-review-history";
|
|
@@ -63,9 +63,26 @@ export type RealtimeModelOption = {
|
|
|
63
63
|
description: string;
|
|
64
64
|
available: boolean;
|
|
65
65
|
unavailableReason: string | null;
|
|
66
|
+
/** Machine-readable reason, e.g. `insufficient_credits` (same codes as voice input). */
|
|
67
|
+
unavailableCode?: string | null | undefined;
|
|
66
68
|
recommended: boolean;
|
|
67
69
|
};
|
|
68
70
|
|
|
71
|
+
/** Short status label for a credit/availability refusal (shared with the model menu). */
|
|
72
|
+
function realtimeRefusalLabel(code: string | null | undefined): string | null {
|
|
73
|
+
switch (code) {
|
|
74
|
+
case "insufficient_credits":
|
|
75
|
+
return "Out of credits";
|
|
76
|
+
case "allowance_exhausted":
|
|
77
|
+
case "monthly_model_cost_limit":
|
|
78
|
+
return "Usage limit reached";
|
|
79
|
+
case "realtime_voice_unavailable":
|
|
80
|
+
return "Voice unavailable";
|
|
81
|
+
default:
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
const CODEX_LIVE_MODEL: RealtimeModelOption = {
|
|
70
87
|
id: "gpt-live-1-boulder-alpha",
|
|
71
88
|
label: "Codex Live",
|
|
@@ -822,6 +839,7 @@ export function RealtimeVoiceControl(props: {
|
|
|
822
839
|
props.canStart,
|
|
823
840
|
props.admissionBlocker ?? null,
|
|
824
841
|
selectedModel.label,
|
|
842
|
+
selectedModel.unavailableCode ?? null,
|
|
825
843
|
);
|
|
826
844
|
const modeOwned = props.snapshot.mode?.state === "active";
|
|
827
845
|
const retryConnection =
|
|
@@ -1333,6 +1351,7 @@ function statusContent(
|
|
|
1333
1351
|
canStart: boolean,
|
|
1334
1352
|
admissionBlocker: string | null,
|
|
1335
1353
|
modelLabel: string,
|
|
1354
|
+
unavailableCode: string | null = null,
|
|
1336
1355
|
): { phase: RealtimeVisualPhase; label: string; detail: string } {
|
|
1337
1356
|
if (snapshot.audibleOutput === "blocked" && !snapshot.outputMuted) {
|
|
1338
1357
|
return {
|
|
@@ -1379,6 +1398,13 @@ function statusContent(
|
|
|
1379
1398
|
detail: "Return to the browser that started it, or wait for that connection to expire.",
|
|
1380
1399
|
};
|
|
1381
1400
|
case "error":
|
|
1401
|
+
if (snapshot.refusal) {
|
|
1402
|
+
return {
|
|
1403
|
+
phase: "unavailable",
|
|
1404
|
+
label: realtimeRefusalLabel(snapshot.refusal.code) ?? "Voice unavailable",
|
|
1405
|
+
detail: snapshot.refusal.message,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1382
1408
|
return {
|
|
1383
1409
|
phase: "error",
|
|
1384
1410
|
label: "Voice unavailable",
|
|
@@ -1388,7 +1414,7 @@ function statusContent(
|
|
|
1388
1414
|
if (!modelAvailable) {
|
|
1389
1415
|
return {
|
|
1390
1416
|
phase: "unavailable",
|
|
1391
|
-
label: "Voice model unavailable",
|
|
1417
|
+
label: realtimeRefusalLabel(unavailableCode) ?? "Voice model unavailable",
|
|
1392
1418
|
detail: admissionBlocker ?? "Choose another voice model.",
|
|
1393
1419
|
};
|
|
1394
1420
|
}
|
|
@@ -1580,6 +1606,7 @@ function writeRealtimeModelPreference(workspaceId: string, model: SessionRealtim
|
|
|
1580
1606
|
|
|
1581
1607
|
function isRealtimeModel(value: string | null): value is SessionRealtimeModel {
|
|
1582
1608
|
return (
|
|
1609
|
+
value === "opengeni-azure/gpt-live-1" ||
|
|
1583
1610
|
value === "gpt-live-1-boulder-alpha" ||
|
|
1584
1611
|
value === "opengeni-gateway/openai/gpt-realtime-2.1" ||
|
|
1585
1612
|
value === "opengeni-gateway/openai/gpt-realtime-mini" ||
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FirstPartyMcpToolName,
|
|
3
|
+
CapabilityCatalogItem,
|
|
4
|
+
Session,
|
|
5
|
+
UpdateSessionToolPolicyRequest,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
2
7
|
import type { OpenGeniBrowserClient as OpenGeniClient } from "@opengeni/sdk/browser";
|
|
3
8
|
|
|
4
9
|
/** Catalog tool discovery may be lazy; selecting the installed server does not
|
|
@@ -25,6 +30,16 @@ export async function attachSessionCapability(
|
|
|
25
30
|
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
26
31
|
const session = await client.getSession(workspaceId, sessionId);
|
|
27
32
|
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
33
|
+
const request = capabilityToolPolicyUpdate(session, item);
|
|
34
|
+
if (request) await client.updateSessionToolPolicy(workspaceId, sessionId, request);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function capabilityToolPolicyUpdate(
|
|
38
|
+
session: Session,
|
|
39
|
+
item: CapabilityCatalogItem,
|
|
40
|
+
): UpdateSessionToolPolicyRequest | null {
|
|
41
|
+
const tools = sessionCapabilityTools(item);
|
|
42
|
+
if (item.kind === "skill" || tools.length === 0) return null;
|
|
28
43
|
// Session.tools includes mandatory runtime servers, which must never be
|
|
29
44
|
// echoed into the human's explicit selection.
|
|
30
45
|
let existing = session.tools.filter(
|
|
@@ -42,10 +57,19 @@ export async function attachSessionCapability(
|
|
|
42
57
|
// automatic selection. Compare against the resolved defaults instead.
|
|
43
58
|
existing = policy.effectiveIds
|
|
44
59
|
.filter((id) => !policy.mandatoryIds.includes(id))
|
|
45
|
-
.map(
|
|
60
|
+
.map(
|
|
61
|
+
(id) =>
|
|
62
|
+
session.tools.find((tool) => tool.kind === "mcp" && tool.id === id) ?? {
|
|
63
|
+
kind: "mcp" as const,
|
|
64
|
+
id,
|
|
65
|
+
optional: true,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
46
68
|
}
|
|
47
69
|
const additions = tools.filter(
|
|
48
|
-
(tool) =>
|
|
70
|
+
(tool) =>
|
|
71
|
+
!session.effectiveToolPolicy?.mandatoryIds.includes(tool.id) &&
|
|
72
|
+
!existing.some((current) => current.kind === tool.kind && current.id === tool.id),
|
|
49
73
|
);
|
|
50
74
|
const recommended = Array.isArray(item.metadata?.firstPartyMcpTools)
|
|
51
75
|
? (item.metadata.firstPartyMcpTools.filter(
|
|
@@ -53,13 +77,102 @@ export async function attachSessionCapability(
|
|
|
53
77
|
) as FirstPartyMcpToolName[])
|
|
54
78
|
: [];
|
|
55
79
|
const firstParty = [...new Set([...session.firstPartyMcpTools, ...recommended])];
|
|
56
|
-
if (additions.length === 0 && firstParty.length === session.firstPartyMcpTools.length)
|
|
57
|
-
|
|
80
|
+
if (additions.length === 0 && firstParty.length === session.firstPartyMcpTools.length)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
58
83
|
mode: "explicit",
|
|
59
84
|
tools: [...existing, ...additions],
|
|
60
85
|
firstPartyMcpTools: firstParty,
|
|
61
86
|
expectedVersion: session.toolPolicyVersion,
|
|
62
|
-
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read-only review. Hosts must show ancestor changes and obtain a separate
|
|
91
|
+
* explicit click before applying. This plan is never authorization. */
|
|
92
|
+
export type SessionCapabilityAccessPlan = {
|
|
93
|
+
workspaceId: string;
|
|
94
|
+
sessionId: string;
|
|
95
|
+
capabilityId: string;
|
|
96
|
+
sessions: Array<{
|
|
97
|
+
id: string;
|
|
98
|
+
title: string | null;
|
|
99
|
+
parentSessionId: string | null;
|
|
100
|
+
toolPolicyVersion: number;
|
|
101
|
+
request: UpdateSessionToolPolicyRequest | null;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export async function prepareSessionCapabilityAccess(
|
|
106
|
+
client: OpenGeniClient,
|
|
107
|
+
workspaceId: string,
|
|
108
|
+
sessionId: string,
|
|
109
|
+
item: CapabilityCatalogItem,
|
|
110
|
+
stillCurrent: () => boolean = () => true,
|
|
111
|
+
): Promise<SessionCapabilityAccessPlan> {
|
|
112
|
+
const sessions: SessionCapabilityAccessPlan["sessions"] = [];
|
|
113
|
+
if (item.kind === "skill" || sessionCapabilityTools(item).length === 0)
|
|
114
|
+
return { workspaceId, sessionId, capabilityId: item.id, sessions };
|
|
115
|
+
const visited = new Set<string>();
|
|
116
|
+
let id: string | null = sessionId;
|
|
117
|
+
while (id) {
|
|
118
|
+
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
119
|
+
if (visited.has(id)) throw new Error("The parent chat chain could not be verified.");
|
|
120
|
+
visited.add(id);
|
|
121
|
+
const session = await client.getSession(workspaceId, id);
|
|
122
|
+
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
123
|
+
const request = capabilityToolPolicyUpdate(session, item);
|
|
124
|
+
sessions.push({
|
|
125
|
+
id,
|
|
126
|
+
title: session.title ?? null,
|
|
127
|
+
parentSessionId: session.parentSessionId ?? null,
|
|
128
|
+
toolPolicyVersion: session.toolPolicyVersion,
|
|
129
|
+
request,
|
|
130
|
+
});
|
|
131
|
+
// An unchanged immediate parent already supplies this capability's ceiling.
|
|
132
|
+
// Do not read or broaden ancestors irrelevant to this addition.
|
|
133
|
+
if (!request) break;
|
|
134
|
+
id = session.parentSessionId ?? null;
|
|
135
|
+
}
|
|
136
|
+
return { workspaceId, sessionId, capabilityId: item.id, sessions: sessions.reverse() };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Preflight the entire review, then apply ordinary CAS-fenced API writes root
|
|
140
|
+
* first. A partial failure may leave approved ancestor changes committed; never
|
|
141
|
+
* claim success or roll back another person's edits. Prepare a new review. */
|
|
142
|
+
export async function applySessionCapabilityAccess(
|
|
143
|
+
client: OpenGeniClient,
|
|
144
|
+
plan: SessionCapabilityAccessPlan,
|
|
145
|
+
stillCurrent: () => boolean = () => true,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
for (const reviewed of plan.sessions) {
|
|
148
|
+
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
149
|
+
const current = await client.getSession(plan.workspaceId, reviewed.id);
|
|
150
|
+
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
151
|
+
if (
|
|
152
|
+
current.toolPolicyVersion !== reviewed.toolPolicyVersion ||
|
|
153
|
+
(current.parentSessionId ?? null) !== reviewed.parentSessionId
|
|
154
|
+
)
|
|
155
|
+
throw new Error("Chat access changed. Click Add tools to review the current choices again.");
|
|
156
|
+
}
|
|
157
|
+
for (const reviewed of plan.sessions) {
|
|
158
|
+
if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
|
|
159
|
+
if (reviewed.request) {
|
|
160
|
+
const updated = await client.updateSessionToolPolicy(
|
|
161
|
+
plan.workspaceId,
|
|
162
|
+
reviewed.id,
|
|
163
|
+
reviewed.request,
|
|
164
|
+
);
|
|
165
|
+
if (
|
|
166
|
+
reviewed.request.mode === "explicit" &&
|
|
167
|
+
reviewed.request.firstPartyMcpTools.some(
|
|
168
|
+
(tool) => !updated.firstPartyMcpTools.includes(tool),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
throw new Error(
|
|
172
|
+
"This chat's capabilities do not allow these tools. Review its capability settings before adding tools again.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
63
176
|
}
|
|
64
177
|
|
|
65
178
|
/** Native OAuth callbacks have already saved the provider connection. Finish
|
package/src/session-ui.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type {
|
|
|
9
9
|
} from "./components/human-input-form";
|
|
10
10
|
export { HumanInputSurface } from "./components/human-input-surface";
|
|
11
11
|
export { ApprovalSurface } from "./components/approval-surface";
|
|
12
|
+
export { ToolActionReviewCard, ToolActionReviewDetails } from "./components/tool-action-review";
|
|
13
|
+
export type { ToolReviewDetailsLoader } from "./components/tool-action-review";
|
|
12
14
|
export type { ApprovalSurfaceProps, ApprovalSurfaceMessages } from "./components/approval-surface";
|
|
13
15
|
export type { HumanInputSurfaceProps } from "./components/human-input-surface";
|
|
14
16
|
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
@@ -69,7 +71,11 @@ export { SessionConversation } from "./components/session-conversation";
|
|
|
69
71
|
export { SessionList } from "./components/session-list";
|
|
70
72
|
export type { SessionListLabels, SessionListProps } from "./components/session-list";
|
|
71
73
|
export { OpenGeniChat } from "./components/open-geni-chat";
|
|
72
|
-
export type {
|
|
74
|
+
export type {
|
|
75
|
+
OpenGeniChatCreateOptions,
|
|
76
|
+
OpenGeniChatLabels,
|
|
77
|
+
OpenGeniChatProps,
|
|
78
|
+
} from "./components/open-geni-chat";
|
|
73
79
|
export type {
|
|
74
80
|
SessionConversationLabels,
|
|
75
81
|
SessionConversationProps,
|
|
@@ -95,3 +101,5 @@ export type { KnowledgeActivityActions } from "./timeline/knowledge-receipt";
|
|
|
95
101
|
export { StartupTimings } from "./timeline/startup-timings";
|
|
96
102
|
export { useStartupDetails, setStartupDetails } from "./timeline/startup-preference";
|
|
97
103
|
export type { GenieLoadingOptions } from "./timeline/genie-loading";
|
|
104
|
+
|
|
105
|
+
export { ToolReviewHistoryProvider } from "./components/tool-review-history";
|
package/src/timeline/index.ts
CHANGED
|
@@ -61,6 +61,16 @@ export type {
|
|
|
61
61
|
|
|
62
62
|
// renderer registry
|
|
63
63
|
export { createToolRegistry, rawTypeOf } from "./registry";
|
|
64
|
+
export {
|
|
65
|
+
createSessionRetainedScreenshotLoader,
|
|
66
|
+
createWorkspaceRetainedArtifactLoader,
|
|
67
|
+
createWorkspaceRetainedVideoLoader,
|
|
68
|
+
} from "./retained-loaders";
|
|
69
|
+
export type {
|
|
70
|
+
RetainedArtifactLoaderClient,
|
|
71
|
+
RetainedScreenshotLoaderClient,
|
|
72
|
+
RetainedVideoLoaderClient,
|
|
73
|
+
} from "./retained-loaders";
|
|
64
74
|
export type {
|
|
65
75
|
CreateToolRegistryOptions,
|
|
66
76
|
RetainedArtifactLoader,
|