@opengeni/react 6.0.0-canary.0 → 6.1.0-canary.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 +17 -0
- package/dist/capability-catalog-row.d.ts +20 -0
- package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
- package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
- package/dist/{chunk-IHQII5WO.js → chunk-G5JFM6ZE.js} +29 -4
- package/dist/chunk-G5JFM6ZE.js.map +1 -0
- package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
- package/dist/chunk-HJ5VJ4JB.js.map +1 -0
- package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
- package/dist/{chunk-CC7AVRIJ.js → chunk-KZ4QDBGM.js} +87 -14
- package/dist/chunk-KZ4QDBGM.js.map +1 -0
- package/dist/{chunk-PZ27MCZP.js → chunk-L5VJYZIW.js} +34 -4
- package/dist/chunk-L5VJYZIW.js.map +1 -0
- package/dist/{chunk-45XEUVTK.js → chunk-UWVY3OXF.js} +12 -5
- package/dist/chunk-UWVY3OXF.js.map +1 -0
- package/dist/components/message-timeline.d.ts +2 -2
- package/dist/composer.js +4 -4
- package/dist/connect-panel.d.ts +2 -1
- package/dist/connect-setup.d.ts +3 -1
- package/dist/connect.d.ts +1 -0
- package/dist/connect.js +620 -424
- package/dist/connect.js.map +1 -1
- package/dist/connection-catalog.d.ts +6 -1
- package/dist/connection-installed.d.ts +1 -0
- package/dist/connection-logo.d.ts +4 -2
- package/dist/discovery-cache.d.ts +5 -0
- package/dist/index.js +7 -7
- package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
- package/dist/model-policy.d.ts +6 -2
- package/dist/model-policy.js +1 -1
- package/dist/plugin-discovery.d.ts +4 -0
- package/dist/realtime.js +2 -2
- package/dist/session-ui.js +6 -6
- package/dist/session.js +1 -1
- package/dist/skill-discovery.d.ts +11 -0
- package/dist/timeline/projection.d.ts +3 -1
- package/dist/timeline/turn-summary.d.ts +2 -2
- package/package.json +3 -3
- package/src/capability-catalog-row.tsx +94 -0
- package/src/components/markdown.tsx +32 -3
- package/src/components/message-timeline.tsx +88 -4
- package/src/components/model-picker.tsx +11 -3
- package/src/components/model-policy-picker-menu.tsx +10 -3
- package/src/components/session-conversation.tsx +1 -3
- package/src/components/session-status.tsx +2 -2
- package/src/connect-panel.tsx +19 -8
- package/src/connect-setup.tsx +49 -14
- package/src/connect.ts +6 -0
- package/src/connection-catalog.tsx +61 -39
- package/src/connection-installed.tsx +2 -0
- package/src/connection-logo.tsx +14 -10
- package/src/discovery-cache.ts +42 -0
- package/src/hooks/use-session-events.ts +24 -2
- package/src/model-policy.ts +21 -0
- package/src/plugin-details.tsx +8 -5
- package/src/plugin-discovery.tsx +87 -71
- package/src/skill-discovery.tsx +101 -56
- package/src/timeline/projection.ts +31 -1
- package/src/timeline/turn-summary.tsx +3 -3
- package/styles/compiled.css +27 -0
- package/styles/connect.css +56 -15
- package/dist/chunk-45XEUVTK.js.map +0 -1
- package/dist/chunk-CC7AVRIJ.js.map +0 -1
- package/dist/chunk-IHQII5WO.js.map +0 -1
- package/dist/chunk-PZ27MCZP.js.map +0 -1
- package/dist/chunk-UMECRJRV.js.map +0 -1
- /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
|
@@ -84,9 +84,7 @@ function Conversation({
|
|
|
84
84
|
onLoadOlder={feed.loadOlder}
|
|
85
85
|
hasNewer={feed.hasNewer}
|
|
86
86
|
loadingNewer={feed.loadingNewer}
|
|
87
|
-
onLoadNewer={
|
|
88
|
-
void feed.loadNewer();
|
|
89
|
-
}}
|
|
87
|
+
onLoadNewer={feed.loadNewer}
|
|
90
88
|
onJumpToStart={async () => {
|
|
91
89
|
await feed.loadOldest();
|
|
92
90
|
}}
|
|
@@ -12,10 +12,10 @@ export type SessionStatusMeta = {
|
|
|
12
12
|
|
|
13
13
|
export const SESSION_STATUS_META: Record<SessionStatusValue, SessionStatusMeta> = {
|
|
14
14
|
queued: {
|
|
15
|
-
label: "
|
|
15
|
+
label: "Queued",
|
|
16
16
|
dotClassName: "bg-og-status-queued",
|
|
17
17
|
badgeClassName: "text-og-fg-muted border-og-border bg-og-status-queued/10",
|
|
18
|
-
pulse:
|
|
18
|
+
pulse: false,
|
|
19
19
|
},
|
|
20
20
|
running: {
|
|
21
21
|
label: "Running",
|
package/src/connect-panel.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ConnectAccounts } from "./connect-accounts";
|
|
2
2
|
import { ConnectChooser } from "./connect-chooser";
|
|
3
3
|
import { ConnectSetup, type ConnectSetupProps } from "./connect-setup";
|
|
4
|
+
import { useConnect } from "./connect";
|
|
4
5
|
|
|
5
6
|
export type ConnectPanelProps = ConnectSetupProps & {
|
|
6
7
|
returnUrl: string;
|
|
7
|
-
|
|
8
|
+
/** Null omits the heading when the host already supplies one. */
|
|
9
|
+
title?: string | null;
|
|
8
10
|
showAccounts?: boolean;
|
|
9
11
|
presentation?: "select" | "catalog";
|
|
10
12
|
};
|
|
@@ -20,16 +22,25 @@ export function ConnectPanel({
|
|
|
20
22
|
className,
|
|
21
23
|
...setup
|
|
22
24
|
}: ConnectPanelProps) {
|
|
25
|
+
const { attempt } = useConnect(setup.controller);
|
|
26
|
+
// Unknown/failed outcomes still need reconciliation or cancellation, not a
|
|
27
|
+
// competing new acquisition. Unmounting observers never cancels controller work.
|
|
28
|
+
const activeSetup =
|
|
29
|
+
attempt !== null && !["complete", "cancelled", "expired"].includes(attempt.state);
|
|
23
30
|
return (
|
|
24
31
|
<div className={["og-connect", className].filter(Boolean).join(" ")}>
|
|
25
|
-
<h2>{title}</h2>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
{title !== null && <h2>{title}</h2>}
|
|
33
|
+
{!activeSetup && (
|
|
34
|
+
<ConnectChooser
|
|
35
|
+
presentation={presentation}
|
|
36
|
+
controller={setup.controller}
|
|
37
|
+
returnUrl={returnUrl}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
31
40
|
<ConnectSetup {...setup} />
|
|
32
|
-
{showAccounts &&
|
|
41
|
+
{showAccounts && !activeSetup && (
|
|
42
|
+
<ConnectAccounts controller={setup.controller} returnUrl={returnUrl} />
|
|
43
|
+
)}
|
|
33
44
|
</div>
|
|
34
45
|
);
|
|
35
46
|
}
|
package/src/connect-setup.tsx
CHANGED
|
@@ -38,9 +38,11 @@ export type ConnectSetupProps = {
|
|
|
38
38
|
/** Host pagination must retain selections across pages and submit the final set. */
|
|
39
39
|
onBrowseResources?: (attempt: ConnectAttempt) => void | Promise<unknown>;
|
|
40
40
|
className?: string;
|
|
41
|
+
/** Provider-specific label supplied by the embedding host. */
|
|
42
|
+
authorizeLabel?: string | undefined;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
/**
|
|
45
|
+
/** Controller-backed setup surface, styled by opt-in connect.css. No provider secrets enter React state. Host owns
|
|
44
46
|
* chooser, controller lifetime, navigation and recovery after full redirect. */
|
|
45
47
|
export function ConnectSetup(props: ConnectSetupProps) {
|
|
46
48
|
// Attempt IDs/revisions are not a substitute for actor scope. A host can
|
|
@@ -55,7 +57,13 @@ export function ConnectSetup(props: ConnectSetupProps) {
|
|
|
55
57
|
return <ScopedSetup key={generation} {...props} />;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function ScopedSetup({
|
|
60
|
+
function ScopedSetup({
|
|
61
|
+
controller,
|
|
62
|
+
onAuthorize,
|
|
63
|
+
onBrowseResources,
|
|
64
|
+
className,
|
|
65
|
+
authorizeLabel = "Authorize connection",
|
|
66
|
+
}: ConnectSetupProps) {
|
|
59
67
|
const view = useConnect(controller);
|
|
60
68
|
const [localError, setLocalError] = useState(false);
|
|
61
69
|
const invoke = (operation: () => unknown | Promise<unknown>) => {
|
|
@@ -69,7 +77,10 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
69
77
|
const attempt = view.attempt;
|
|
70
78
|
if (!attempt)
|
|
71
79
|
return (
|
|
72
|
-
<section
|
|
80
|
+
<section
|
|
81
|
+
className={["og-connect-setup", className].filter(Boolean).join(" ")}
|
|
82
|
+
aria-label="Connection setup"
|
|
83
|
+
>
|
|
73
84
|
<p role="status">Choose a connection to begin.</p>
|
|
74
85
|
</section>
|
|
75
86
|
);
|
|
@@ -115,9 +126,24 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
115
126
|
}
|
|
116
127
|
};
|
|
117
128
|
return (
|
|
118
|
-
<section
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
<section
|
|
130
|
+
className={["og-connect-setup", className].filter(Boolean).join(" ")}
|
|
131
|
+
aria-label="Connection setup"
|
|
132
|
+
aria-busy={view.busy}
|
|
133
|
+
>
|
|
134
|
+
<div className="og-connect-setup-summary">
|
|
135
|
+
<p className="og-connect-setup-scope">
|
|
136
|
+
{attempt.ownership === "personal" ? "Personal connection" : "Workspace connection"}
|
|
137
|
+
</p>
|
|
138
|
+
<p role="status" className="og-connect-setup-status">
|
|
139
|
+
{setupStatus[attempt.state]}
|
|
140
|
+
</p>
|
|
141
|
+
<p className="og-connect-setup-description">
|
|
142
|
+
{attempt.ownership === "personal"
|
|
143
|
+
? "Only your work can use this connection."
|
|
144
|
+
: "Work in this workspace can use this connection."}
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
121
147
|
{attempt.account && <p>Account: {attempt.account.label}</p>}
|
|
122
148
|
{(localError || view.error || attempt.error) && (
|
|
123
149
|
<p role="alert">
|
|
@@ -129,10 +155,15 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
129
155
|
{!terminal && (
|
|
130
156
|
<form key={`${attempt.id}:${attempt.revision}`} onSubmit={submit} autoComplete="off">
|
|
131
157
|
<fieldset disabled={view.busy}>
|
|
132
|
-
|
|
158
|
+
{["credentials", "select_account", "select_resources", "preview"].includes(
|
|
159
|
+
action.type,
|
|
160
|
+
) ? (
|
|
161
|
+
<legend>Connection details</legend>
|
|
162
|
+
) : null}
|
|
133
163
|
{attempt.state === "connected_but_incomplete" && action.type === "none" && (
|
|
134
164
|
<button
|
|
135
165
|
type="button"
|
|
166
|
+
className="og-connect-setup-primary"
|
|
136
167
|
onClick={() => invoke(() => view.advance({ type: "retry" }, crypto.randomUUID()))}
|
|
137
168
|
>
|
|
138
169
|
Review integration operations
|
|
@@ -218,6 +249,7 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
218
249
|
) && (
|
|
219
250
|
<button
|
|
220
251
|
type="submit"
|
|
252
|
+
className="og-connect-setup-primary"
|
|
221
253
|
disabled={action.type === "select_resources" && Boolean(action.cursor)}
|
|
222
254
|
>
|
|
223
255
|
{action.type === "preview" ? "Install selected operations" : "Continue"}
|
|
@@ -226,9 +258,10 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
226
258
|
{action.type === "authorize" && (
|
|
227
259
|
<button
|
|
228
260
|
type="button"
|
|
261
|
+
className="og-connect-setup-primary"
|
|
229
262
|
onClick={() => invoke(() => onAuthorize(structuredClone(attempt)))}
|
|
230
263
|
>
|
|
231
|
-
|
|
264
|
+
{authorizeLabel}
|
|
232
265
|
</button>
|
|
233
266
|
)}
|
|
234
267
|
{action.type === "wait" && (
|
|
@@ -246,12 +279,14 @@ function ScopedSetup({ controller, onAuthorize, onBrowseResources, className }:
|
|
|
246
279
|
)}
|
|
247
280
|
</>
|
|
248
281
|
)}
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
<div className="og-connect-setup-secondary">
|
|
283
|
+
<button type="button" onClick={() => invoke(() => view.refresh())}>
|
|
284
|
+
Check status
|
|
285
|
+
</button>
|
|
286
|
+
<button type="button" onClick={() => invoke(() => view.cancel(crypto.randomUUID()))}>
|
|
287
|
+
Cancel setup
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
255
290
|
</fieldset>
|
|
256
291
|
</form>
|
|
257
292
|
)}
|
package/src/connect.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// The host owns controller lifetime; unmounting one observer does not cancel an
|
|
3
3
|
// attempt another observer is displaying or a durable backend operation.
|
|
4
4
|
import { useMemo, useSyncExternalStore } from "react";
|
|
5
|
+
export {
|
|
6
|
+
CapabilityCatalogRow,
|
|
7
|
+
CapabilityCatalogIndicator,
|
|
8
|
+
type CapabilityCatalogRowProps,
|
|
9
|
+
type CapabilityCatalogStatus,
|
|
10
|
+
} from "./capability-catalog-row";
|
|
5
11
|
export {
|
|
6
12
|
SkillDiscovery,
|
|
7
13
|
type SkillDiscoveryProps,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CapabilityCatalogIndicator,
|
|
4
|
+
CapabilityCatalogRow,
|
|
5
|
+
type CapabilityCatalogStatus,
|
|
6
|
+
} from "./capability-catalog-row";
|
|
2
7
|
|
|
3
8
|
/** Status comes from the host/controller, never inferred from a display label. */
|
|
4
9
|
export type ConnectionCatalogOption = {
|
|
@@ -7,6 +12,8 @@ export type ConnectionCatalogOption = {
|
|
|
7
12
|
description?: string | undefined;
|
|
8
13
|
status: string;
|
|
9
14
|
connected: boolean;
|
|
15
|
+
/** Explicit health/availability. Omit to retain visible legacy status text. */
|
|
16
|
+
state?: CapabilityCatalogStatus;
|
|
10
17
|
onOpen: () => void;
|
|
11
18
|
action?: ReactNode;
|
|
12
19
|
};
|
|
@@ -20,31 +27,25 @@ export type ConnectionCatalogProps = {
|
|
|
20
27
|
services: ConnectionCatalogService[];
|
|
21
28
|
query?: string;
|
|
22
29
|
columns?: 1 | 2;
|
|
30
|
+
resultLimit?: number;
|
|
31
|
+
onShowMore?: () => void;
|
|
23
32
|
/** Group capabilities by service, or show every capability independently. */
|
|
24
33
|
grouped?: boolean;
|
|
25
34
|
className?: string;
|
|
26
35
|
emptyMessage?: string;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
|
-
function ConnectionDetailsIndicator() {
|
|
30
|
-
return (
|
|
31
|
-
<span aria-hidden className="og-connection-group-chevron">
|
|
32
|
-
›
|
|
33
|
-
</span>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
38
|
export function ConnectionOptionRow({ option }: { option: ConnectionCatalogOption }) {
|
|
38
39
|
return (
|
|
39
40
|
<div className="og-connection-catalog-action-row og-connection-option">
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
|
|
41
|
+
<CapabilityCatalogRow
|
|
42
|
+
name={option.name}
|
|
43
|
+
description={option.description}
|
|
44
|
+
status={option.state ?? (option.connected ? "added" : "available")}
|
|
45
|
+
statusLabel={option.status}
|
|
46
|
+
showStatusLabel={option.state === undefined}
|
|
47
|
+
onOpen={option.onOpen}
|
|
48
|
+
/>
|
|
48
49
|
{option.action ? <span className="og-connection-option-action">{option.action}</span> : null}
|
|
49
50
|
</div>
|
|
50
51
|
);
|
|
@@ -53,10 +54,20 @@ export function ConnectionOptionRow({ option }: { option: ConnectionCatalogOptio
|
|
|
53
54
|
export function ConnectionServiceRow({ service }: { service: ConnectionCatalogService }) {
|
|
54
55
|
if (!service.options.length) return null;
|
|
55
56
|
const connected = service.options.filter((option) => option.connected).length;
|
|
57
|
+
const attention = service.options.find(
|
|
58
|
+
(option) => option.state === "attention" || option.state === "unavailable",
|
|
59
|
+
);
|
|
60
|
+
const groupState =
|
|
61
|
+
attention?.state ??
|
|
62
|
+
(service.options.some((option) => option.state === "loading")
|
|
63
|
+
? "loading"
|
|
64
|
+
: connected === service.options.length
|
|
65
|
+
? "added"
|
|
66
|
+
: "available");
|
|
56
67
|
const identity = (
|
|
57
68
|
<>
|
|
58
|
-
<span className="og-
|
|
59
|
-
<span className="og-
|
|
69
|
+
<span className="og-capability-catalog-icon">{service.logo}</span>
|
|
70
|
+
<span className="og-capability-catalog-copy">
|
|
60
71
|
<strong>{service.name}</strong>
|
|
61
72
|
<span>
|
|
62
73
|
{service.options.length === 1
|
|
@@ -64,28 +75,28 @@ export function ConnectionServiceRow({ service }: { service: ConnectionCatalogSe
|
|
|
64
75
|
: service.options.map((option) => option.name).join(" · ")}
|
|
65
76
|
</span>
|
|
66
77
|
</span>
|
|
67
|
-
<
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</span>
|
|
78
|
+
<CapabilityCatalogIndicator
|
|
79
|
+
status={groupState}
|
|
80
|
+
label={attention?.status ?? `${connected} of ${service.options.length} added`}
|
|
81
|
+
/>
|
|
72
82
|
</>
|
|
73
83
|
);
|
|
74
84
|
return service.options.length === 1 ? (
|
|
75
85
|
<div className="og-connection-catalog-action-row">
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
{
|
|
83
|
-
|
|
86
|
+
<CapabilityCatalogRow
|
|
87
|
+
name={service.name}
|
|
88
|
+
description={service.options[0]!.description}
|
|
89
|
+
icon={service.logo}
|
|
90
|
+
status={service.options[0]!.state ?? (connected ? "added" : "available")}
|
|
91
|
+
statusLabel={service.options[0]!.status}
|
|
92
|
+
showStatusLabel={service.options[0]!.state === undefined}
|
|
93
|
+
onOpen={service.options[0]!.onOpen}
|
|
94
|
+
/>
|
|
84
95
|
{service.options[0]?.action}
|
|
85
96
|
</div>
|
|
86
97
|
) : (
|
|
87
98
|
<details className="og-connection-catalog-service">
|
|
88
|
-
<summary className="og-
|
|
99
|
+
<summary className="og-capability-catalog-row">
|
|
89
100
|
{identity}
|
|
90
101
|
<span aria-hidden className="og-connection-group-chevron">
|
|
91
102
|
⌄
|
|
@@ -105,6 +116,8 @@ export function ConnectionCatalog({
|
|
|
105
116
|
query = "",
|
|
106
117
|
columns = 1,
|
|
107
118
|
grouped = true,
|
|
119
|
+
resultLimit,
|
|
120
|
+
onShowMore,
|
|
108
121
|
className = "",
|
|
109
122
|
emptyMessage = "No connections match your search.",
|
|
110
123
|
}: ConnectionCatalogProps) {
|
|
@@ -129,12 +142,21 @@ export function ConnectionCatalog({
|
|
|
129
142
|
.includes(search),
|
|
130
143
|
);
|
|
131
144
|
return (
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
visible.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
<>
|
|
146
|
+
<div className={`og-connection-catalog ${className}`} data-columns={columns}>
|
|
147
|
+
{visible.length ? (
|
|
148
|
+
visible
|
|
149
|
+
.slice(0, resultLimit)
|
|
150
|
+
.map((service) => <ConnectionServiceRow key={service.id} service={service} />)
|
|
151
|
+
) : (
|
|
152
|
+
<p role="status">{emptyMessage}</p>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{resultLimit && visible.length > resultLimit ? (
|
|
156
|
+
<button className="og-catalog-more" type="button" onClick={onShowMore}>
|
|
157
|
+
View all connections
|
|
158
|
+
</button>
|
|
159
|
+
) : null}
|
|
160
|
+
</>
|
|
139
161
|
);
|
|
140
162
|
}
|
|
@@ -6,6 +6,7 @@ export type ConnectionInstalledItem = {
|
|
|
6
6
|
status: string;
|
|
7
7
|
icon?: ReactNode;
|
|
8
8
|
needsAttention?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
9
10
|
onOpen: () => void;
|
|
10
11
|
};
|
|
11
12
|
|
|
@@ -31,6 +32,7 @@ export function ConnectionInstalled({
|
|
|
31
32
|
key={item.id}
|
|
32
33
|
type="button"
|
|
33
34
|
onClick={item.onOpen}
|
|
35
|
+
disabled={item.disabled}
|
|
34
36
|
title={item.status}
|
|
35
37
|
aria-label={`${item.name} · ${item.status}`}
|
|
36
38
|
>
|
package/src/connection-logo.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
/** Brand identifier; failed/missing assets use
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
/** Brand identifier; failed/missing assets use the supplied fallback or stable initials. */
|
|
3
3
|
export function ConnectionLogo({
|
|
4
4
|
src,
|
|
5
5
|
name,
|
|
6
6
|
size = 40,
|
|
7
|
+
fallback,
|
|
7
8
|
}: {
|
|
8
9
|
src: string | null;
|
|
9
10
|
name: string;
|
|
10
11
|
size?: number;
|
|
12
|
+
fallback?: ReactNode;
|
|
11
13
|
}) {
|
|
12
14
|
const [failedSrc, setFailedSrc] = useState<string | null>(null);
|
|
13
15
|
return (
|
|
@@ -15,14 +17,16 @@ export function ConnectionLogo({
|
|
|
15
17
|
{src && src !== failedSrc ? (
|
|
16
18
|
<img src={src} alt="" loading="lazy" decoding="async" onError={() => setFailedSrc(src)} />
|
|
17
19
|
) : (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
(fallback ?? (
|
|
21
|
+
<span>
|
|
22
|
+
{name
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.map((part) => part[0])
|
|
25
|
+
.join("")
|
|
26
|
+
.slice(0, 2)
|
|
27
|
+
.toUpperCase()}
|
|
28
|
+
</span>
|
|
29
|
+
))
|
|
26
30
|
)}
|
|
27
31
|
</span>
|
|
28
32
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Short-lived presentation data only. Never cache installation or authorization state. */
|
|
2
|
+
export function createDiscoveryCache<T>(ttlMs = 60_000, capacity = 64) {
|
|
3
|
+
const clients = new WeakMap<
|
|
4
|
+
object,
|
|
5
|
+
Map<string, { expires: number; value: T | undefined; pending: Promise<T> }>
|
|
6
|
+
>();
|
|
7
|
+
return {
|
|
8
|
+
peek(client: object, key: string): T | undefined {
|
|
9
|
+
const entry = clients.get(client)?.get(key);
|
|
10
|
+
return entry && entry.expires > Date.now() ? entry.value : undefined;
|
|
11
|
+
},
|
|
12
|
+
read(client: object, key: string, fetch: () => Promise<T>): Promise<T> {
|
|
13
|
+
let entries = clients.get(client);
|
|
14
|
+
if (!entries) {
|
|
15
|
+
entries = new Map();
|
|
16
|
+
clients.set(client, entries);
|
|
17
|
+
}
|
|
18
|
+
const existing = entries.get(key);
|
|
19
|
+
if (existing && existing.expires > Date.now()) return existing.pending;
|
|
20
|
+
entries.delete(key);
|
|
21
|
+
while (entries.size >= capacity) entries.delete(entries.keys().next().value!);
|
|
22
|
+
const entry = {
|
|
23
|
+
expires: Date.now() + ttlMs,
|
|
24
|
+
value: undefined as T | undefined,
|
|
25
|
+
pending: Promise.resolve().then(fetch),
|
|
26
|
+
};
|
|
27
|
+
entries.set(key, entry);
|
|
28
|
+
entry.pending = entry.pending.then(
|
|
29
|
+
(value) => {
|
|
30
|
+
entry.value = value;
|
|
31
|
+
entry.expires = Date.now() + ttlMs;
|
|
32
|
+
return value;
|
|
33
|
+
},
|
|
34
|
+
(error) => {
|
|
35
|
+
if (entries.get(key) === entry) entries.delete(key);
|
|
36
|
+
throw error;
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
return entry.pending;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -140,6 +140,7 @@ export function useSessionEvents(
|
|
|
140
140
|
const [eventWindow, setEventWindow] = useState<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
|
|
141
141
|
const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
|
|
142
142
|
const [error, setError] = useState<Error | null>(null);
|
|
143
|
+
const [newerError, setNewerError] = useState<Error | null>(null);
|
|
143
144
|
const [hasOlder, setHasOlder] = useState(false);
|
|
144
145
|
const [hasNewer, setHasNewer] = useState(false);
|
|
145
146
|
const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
|
|
@@ -186,6 +187,7 @@ export function useSessionEvents(
|
|
|
186
187
|
// Navigation belongs to the session/client lifetime, not the transport.
|
|
187
188
|
useEffect(() => {
|
|
188
189
|
navigationGenerationRef.current += 1;
|
|
190
|
+
setNewerError(null);
|
|
189
191
|
loadingOlderRef.current = false;
|
|
190
192
|
loadingNewerRef.current = false;
|
|
191
193
|
loadingOldestRef.current = false;
|
|
@@ -691,6 +693,7 @@ export function useSessionEvents(
|
|
|
691
693
|
initialWindowLoadedRef.current = true;
|
|
692
694
|
viewModeRef.current = "history";
|
|
693
695
|
loadingOldestRef.current = false;
|
|
696
|
+
setNewerError(null);
|
|
694
697
|
startTransition(() => {
|
|
695
698
|
if (status !== undefined) {
|
|
696
699
|
setSessionStatusProjection(status);
|
|
@@ -736,6 +739,7 @@ export function useSessionEvents(
|
|
|
736
739
|
return false;
|
|
737
740
|
}
|
|
738
741
|
if (window.events.length === 0) {
|
|
742
|
+
setNewerError(null);
|
|
739
743
|
hasNewerRef.current = false;
|
|
740
744
|
setHasNewer(false);
|
|
741
745
|
// Caught up with durable history — resume the live tip stream.
|
|
@@ -788,6 +792,7 @@ export function useSessionEvents(
|
|
|
788
792
|
resumeLive = true;
|
|
789
793
|
}
|
|
790
794
|
loadingNewerRef.current = false;
|
|
795
|
+
setNewerError(null);
|
|
791
796
|
startTransition(() => {
|
|
792
797
|
if (status !== undefined) {
|
|
793
798
|
setSessionStatusProjection(status);
|
|
@@ -803,6 +808,16 @@ export function useSessionEvents(
|
|
|
803
808
|
});
|
|
804
809
|
published = true;
|
|
805
810
|
return newer;
|
|
811
|
+
} catch (reason) {
|
|
812
|
+
// A settled request from an old navigation lifetime is not a failure of
|
|
813
|
+
// the current session. Preserve current loading/error state as well.
|
|
814
|
+
if (navigationGenerationRef.current !== generation) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
setNewerError(reason instanceof Error ? reason : new Error(String(reason)));
|
|
818
|
+
// Keep authorization and integrity failures actionable for callers;
|
|
819
|
+
// timeline-owned invocations attach their own explicit recovery UI.
|
|
820
|
+
throw reason;
|
|
806
821
|
} finally {
|
|
807
822
|
if (!published && navigationGenerationRef.current === generation) {
|
|
808
823
|
loadingNewerRef.current = false;
|
|
@@ -817,6 +832,7 @@ export function useSessionEvents(
|
|
|
817
832
|
}
|
|
818
833
|
loadingLatestRef.current = true;
|
|
819
834
|
setLoadingLatest(true);
|
|
835
|
+
setNewerError(null);
|
|
820
836
|
let published = false;
|
|
821
837
|
try {
|
|
822
838
|
streamAbortRef.current?.abort();
|
|
@@ -851,7 +867,13 @@ export function useSessionEvents(
|
|
|
851
867
|
|
|
852
868
|
const identityMatches = stateStreamKey === streamKey;
|
|
853
869
|
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
854
|
-
const timeline = useMemo(
|
|
870
|
+
const timeline = useMemo(
|
|
871
|
+
() =>
|
|
872
|
+
buildTimeline(visibleEvents, {
|
|
873
|
+
partialStart: hasOlder || eventWindow.truncated || after > 0,
|
|
874
|
+
}),
|
|
875
|
+
[visibleEvents, hasOlder, eventWindow.truncated, after],
|
|
876
|
+
);
|
|
855
877
|
|
|
856
878
|
return {
|
|
857
879
|
events: visibleEvents,
|
|
@@ -873,7 +895,7 @@ export function useSessionEvents(
|
|
|
873
895
|
loadOldest,
|
|
874
896
|
loadingLatest: !identityMatches ? false : loadingLatest,
|
|
875
897
|
jumpToLatest,
|
|
876
|
-
error: identityMatches ? error : null,
|
|
898
|
+
error: identityMatches ? (newerError ?? error) : null,
|
|
877
899
|
};
|
|
878
900
|
}
|
|
879
901
|
|
package/src/model-policy.ts
CHANGED
|
@@ -274,9 +274,11 @@ export function findPickerRow<TCatalog extends ClientModel>(
|
|
|
274
274
|
|
|
275
275
|
export function groupPickerRowsByBillingClass(
|
|
276
276
|
rows: PickerModelRow[],
|
|
277
|
+
options?: { codexOnly?: boolean },
|
|
277
278
|
): Array<{ billingClass: PickerBillingClass; label: string; rows: PickerModelRow[] }>;
|
|
278
279
|
export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
|
|
279
280
|
rows: PickerModelRow<TCatalog>[],
|
|
281
|
+
options?: { codexOnly?: boolean },
|
|
280
282
|
): Array<{
|
|
281
283
|
billingClass: PickerBillingClass;
|
|
282
284
|
label: string;
|
|
@@ -284,6 +286,7 @@ export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
|
|
|
284
286
|
}>;
|
|
285
287
|
export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
|
|
286
288
|
rows: PickerModelRow<TCatalog>[],
|
|
289
|
+
options?: { codexOnly?: boolean },
|
|
287
290
|
): Array<{
|
|
288
291
|
billingClass: PickerBillingClass;
|
|
289
292
|
label: string;
|
|
@@ -307,5 +310,23 @@ export function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(
|
|
|
307
310
|
rows: [row],
|
|
308
311
|
});
|
|
309
312
|
}
|
|
313
|
+
const codexIndex = groups.findIndex((group) => group.billingClass === "codex_subscription");
|
|
314
|
+
const codex = groups[codexIndex];
|
|
315
|
+
const selectableOpenGeni = groups
|
|
316
|
+
.find((group) => group.billingClass === "opengeni_credits")
|
|
317
|
+
?.rows.filter((row) => row.selectable);
|
|
318
|
+
// Presentation only: never change the catalog/default model ordering or selection.
|
|
319
|
+
const onlyFreeOpenGeni =
|
|
320
|
+
selectableOpenGeni !== undefined &&
|
|
321
|
+
selectableOpenGeni.length > 0 &&
|
|
322
|
+
selectableOpenGeni.every((row) => row.catalog.cost === "free");
|
|
323
|
+
if (
|
|
324
|
+
codex &&
|
|
325
|
+
codex.rows.some((row) => row.selectable) &&
|
|
326
|
+
(options?.codexOnly || onlyFreeOpenGeni)
|
|
327
|
+
) {
|
|
328
|
+
groups.splice(codexIndex, 1);
|
|
329
|
+
groups.unshift(codex);
|
|
330
|
+
}
|
|
310
331
|
return groups;
|
|
311
332
|
}
|
package/src/plugin-details.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { pluginMcpUnavailableReason, type PluginDiscoveryItem } from "@opengeni/sdk";
|
|
3
|
+
import { BoxesIcon } from "lucide-react";
|
|
3
4
|
import { Markdown } from "./components/markdown";
|
|
5
|
+
import { ConnectionLogo } from "./connection-logo";
|
|
4
6
|
|
|
5
7
|
const componentLabels: Record<string, string> = {
|
|
6
8
|
skills: "Skills",
|
|
@@ -38,9 +40,8 @@ export function PluginDetails({
|
|
|
38
40
|
const installableCount =
|
|
39
41
|
(item.skills?.length ?? 0) + (item.mcpServers?.filter(supported).length ?? 0);
|
|
40
42
|
const exceedsInstallLimit = installableCount > 64;
|
|
41
|
-
const installable = installableCount > 0 && !exceedsInstallLimit;
|
|
43
|
+
const installable = !contentsUnknown && installableCount > 0 && !exceedsInstallLimit;
|
|
42
44
|
const [expanded, setExpanded] = useState(false);
|
|
43
|
-
const [logoFailed, setLogoFailed] = useState(false);
|
|
44
45
|
const description = item.longDescription || item.description;
|
|
45
46
|
const lengthy = description.length > 700;
|
|
46
47
|
const registryLabel =
|
|
@@ -52,9 +53,11 @@ export function PluginDetails({
|
|
|
52
53
|
return (
|
|
53
54
|
<article className="og-plugin-details">
|
|
54
55
|
<header className="og-plugin-details-header">
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
<ConnectionLogo
|
|
57
|
+
src={item.logoUrl}
|
|
58
|
+
name={item.displayName}
|
|
59
|
+
fallback={<BoxesIcon aria-hidden="true" />}
|
|
60
|
+
/>
|
|
58
61
|
<div>
|
|
59
62
|
<h2>{item.displayName.replace(/-/g, " ")}</h2>
|
|
60
63
|
{item.author?.name ? <p>By {item.author.name}</p> : null}
|