@opengeni/react 5.0.5-canary.0 → 5.0.5-canary.1
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/dist/{chunk-SCZ6SUBW.js → chunk-H63SQTGI.js} +1258 -1837
- package/dist/chunk-H63SQTGI.js.map +1 -0
- package/dist/chunk-HQV2I5HV.js +124 -0
- package/dist/chunk-HQV2I5HV.js.map +1 -0
- package/dist/chunk-M2HNWZYR.js +835 -0
- package/dist/chunk-M2HNWZYR.js.map +1 -0
- package/dist/{chunk-Z7WBDB4E.js → chunk-V3WX3TQG.js} +283 -391
- package/dist/chunk-V3WX3TQG.js.map +1 -0
- package/dist/components/message-timeline.d.ts +3 -1
- package/dist/composer.js +2 -1
- package/dist/connect-chooser.d.ts +1 -0
- package/dist/connect-panel.d.ts +2 -1
- package/dist/connect.d.ts +7 -0
- package/dist/connect.js +791 -183
- package/dist/connect.js.map +1 -1
- package/dist/connection-catalog.d.ts +33 -0
- package/dist/connection-installed.d.ts +15 -0
- package/dist/connection-logo.d.ts +6 -0
- package/dist/connection-type-picker.d.ts +6 -0
- package/dist/index.js +17 -12
- package/dist/index.js.map +1 -1
- package/dist/plugin-details.d.ts +14 -0
- package/dist/plugin-discovery.d.ts +14 -0
- package/dist/session-ui.d.ts +3 -0
- package/dist/session-ui.js +113 -2
- package/dist/session-ui.js.map +1 -1
- package/dist/skill-discovery.d.ts +29 -0
- package/dist/timeline/activity-rail.d.ts +3 -1
- package/dist/timeline/genie-loading.d.ts +24 -0
- package/dist/timeline/startup-preference.d.ts +3 -0
- package/dist/timeline/startup-timings.d.ts +4 -0
- package/package.json +4 -3
- package/src/components/message-timeline.tsx +127 -20
- package/src/connect-chooser.tsx +81 -21
- package/src/connect-panel.tsx +7 -1
- package/src/connect.ts +21 -0
- package/src/connection-catalog.tsx +153 -0
- package/src/connection-installed.tsx +45 -0
- package/src/connection-logo.tsx +29 -0
- package/src/connection-type-picker.tsx +36 -0
- package/src/hooks/use-codex-accounts.ts +1 -0
- package/src/plugin-details.tsx +182 -0
- package/src/plugin-discovery.tsx +161 -0
- package/src/session-ui.ts +3 -0
- package/src/skill-discovery.tsx +152 -0
- package/src/timeline/activity-rail.tsx +75 -4
- package/src/timeline/genie-loading.tsx +112 -0
- package/src/timeline/startup-preference.ts +33 -0
- package/src/timeline/startup-timings.tsx +145 -0
- package/src/timeline/turn-summary.tsx +8 -3
- package/styles/compiled.css +73 -0
- package/styles/connect.css +149 -1
- package/styles/index.css +15 -0
- package/dist/chunk-SCZ6SUBW.js.map +0 -1
- package/dist/chunk-Z7WBDB4E.js.map +0 -1
package/src/connect-chooser.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectionCatalog } from "./connection-catalog";
|
|
1
2
|
import { useEffect, useState, type FormEvent } from "react";
|
|
2
3
|
import type { ConnectController, ConnectProvider } from "@opengeni/connect";
|
|
3
4
|
import { useConnect } from "./connect";
|
|
@@ -7,6 +8,7 @@ export type ConnectChooserProps = {
|
|
|
7
8
|
/** Exact host return string; no completion parameters are added. */
|
|
8
9
|
returnUrl: string;
|
|
9
10
|
className?: string;
|
|
11
|
+
presentation?: "select" | "catalog";
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
/** Catalog readiness is supplied by the authenticated backend, never inferred
|
|
@@ -24,11 +26,17 @@ export function ConnectChooser(props: ConnectChooserProps) {
|
|
|
24
26
|
return <ScopedChooser key={generation} {...props} />;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function ScopedChooser({
|
|
29
|
+
function ScopedChooser({
|
|
30
|
+
controller,
|
|
31
|
+
returnUrl,
|
|
32
|
+
className,
|
|
33
|
+
presentation = "select",
|
|
34
|
+
}: ConnectChooserProps) {
|
|
28
35
|
const view = useConnect(controller);
|
|
29
36
|
const [catalog, setCatalog] = useState<ConnectProvider[] | null>(null);
|
|
30
37
|
const [failed, setFailed] = useState(false);
|
|
31
38
|
const [load, setLoad] = useState(0);
|
|
39
|
+
const [query, setQuery] = useState("");
|
|
32
40
|
const [providerId, setProviderId] = useState("");
|
|
33
41
|
const [ownership, setOwnership] = useState("");
|
|
34
42
|
useEffect(() => {
|
|
@@ -85,7 +93,52 @@ function ScopedChooser({ controller, returnUrl, className }: ConnectChooserProps
|
|
|
85
93
|
<form onSubmit={submit}>
|
|
86
94
|
<fieldset disabled={view.busy}>
|
|
87
95
|
<legend>Provider and ownership</legend>
|
|
88
|
-
|
|
96
|
+
{presentation === "catalog" && !provider ? (
|
|
97
|
+
<>
|
|
98
|
+
<label>
|
|
99
|
+
Search connections
|
|
100
|
+
<input
|
|
101
|
+
type="search"
|
|
102
|
+
value={query}
|
|
103
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
104
|
+
/>
|
|
105
|
+
</label>
|
|
106
|
+
<ConnectionCatalog
|
|
107
|
+
query={query}
|
|
108
|
+
services={catalog!.map((entry) => ({
|
|
109
|
+
id: entry.id,
|
|
110
|
+
name: entry.label,
|
|
111
|
+
options: [
|
|
112
|
+
{
|
|
113
|
+
id: entry.id,
|
|
114
|
+
name: entry.label,
|
|
115
|
+
description: entry.reason,
|
|
116
|
+
status: entry.readiness.replaceAll("_", " "),
|
|
117
|
+
connected: false,
|
|
118
|
+
onOpen: () => {
|
|
119
|
+
if (!view.busy) {
|
|
120
|
+
setProviderId(entry.id);
|
|
121
|
+
setOwnership(entry.ownership.length === 1 ? entry.ownership[0]! : "");
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}))}
|
|
127
|
+
/>
|
|
128
|
+
</>
|
|
129
|
+
) : null}
|
|
130
|
+
{presentation === "catalog" && provider ? (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => {
|
|
134
|
+
setProviderId("");
|
|
135
|
+
setOwnership("");
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
Back to connections
|
|
139
|
+
</button>
|
|
140
|
+
) : null}
|
|
141
|
+
<label hidden={presentation === "catalog"}>
|
|
89
142
|
Provider
|
|
90
143
|
<select
|
|
91
144
|
required
|
|
@@ -110,27 +163,34 @@ function ScopedChooser({ controller, returnUrl, className }: ConnectChooserProps
|
|
|
110
163
|
</select>
|
|
111
164
|
</label>
|
|
112
165
|
{provider && (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<option key={choice} value={choice}>
|
|
125
|
-
{choice === "personal"
|
|
126
|
-
? "Personal — owned by you"
|
|
127
|
-
: "Workspace — shared connection"}
|
|
166
|
+
<>
|
|
167
|
+
<strong>{presentation === "catalog" ? provider.label : null}</strong>
|
|
168
|
+
<label>
|
|
169
|
+
Ownership
|
|
170
|
+
<select
|
|
171
|
+
required
|
|
172
|
+
value={ownership}
|
|
173
|
+
onChange={(event) => setOwnership(event.target.value)}
|
|
174
|
+
>
|
|
175
|
+
<option value="" disabled>
|
|
176
|
+
Choose ownership
|
|
128
177
|
</option>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
{provider.ownership.map((choice) => (
|
|
179
|
+
<option key={choice} value={choice}>
|
|
180
|
+
{choice === "personal"
|
|
181
|
+
? "Personal — owned by you"
|
|
182
|
+
: "Workspace — shared connection"}
|
|
183
|
+
</option>
|
|
184
|
+
))}
|
|
185
|
+
</select>
|
|
186
|
+
</label>
|
|
187
|
+
</>
|
|
132
188
|
)}
|
|
133
|
-
<button
|
|
189
|
+
<button
|
|
190
|
+
hidden={presentation === "catalog" && !provider}
|
|
191
|
+
type="submit"
|
|
192
|
+
disabled={!canBegin}
|
|
193
|
+
>
|
|
134
194
|
Start setup
|
|
135
195
|
</button>
|
|
136
196
|
</fieldset>
|
package/src/connect-panel.tsx
CHANGED
|
@@ -6,6 +6,7 @@ export type ConnectPanelProps = ConnectSetupProps & {
|
|
|
6
6
|
returnUrl: string;
|
|
7
7
|
title?: string;
|
|
8
8
|
showAccounts?: boolean;
|
|
9
|
+
presentation?: "select" | "catalog";
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
/** Optional composition over the same unstyled surfaces. Import
|
|
@@ -15,13 +16,18 @@ export function ConnectPanel({
|
|
|
15
16
|
returnUrl,
|
|
16
17
|
title = "Connections",
|
|
17
18
|
showAccounts = true,
|
|
19
|
+
presentation = "select",
|
|
18
20
|
className,
|
|
19
21
|
...setup
|
|
20
22
|
}: ConnectPanelProps) {
|
|
21
23
|
return (
|
|
22
24
|
<div className={["og-connect", className].filter(Boolean).join(" ")}>
|
|
23
25
|
<h2>{title}</h2>
|
|
24
|
-
<ConnectChooser
|
|
26
|
+
<ConnectChooser
|
|
27
|
+
presentation={presentation}
|
|
28
|
+
controller={setup.controller}
|
|
29
|
+
returnUrl={returnUrl}
|
|
30
|
+
/>
|
|
25
31
|
<ConnectSetup {...setup} />
|
|
26
32
|
{showAccounts && <ConnectAccounts controller={setup.controller} returnUrl={returnUrl} />}
|
|
27
33
|
</div>
|
package/src/connect.ts
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
SkillDiscovery,
|
|
7
|
+
type SkillDiscoveryProps,
|
|
8
|
+
type SkillDiscoveryClient,
|
|
9
|
+
type SkillDiscoveryItem,
|
|
10
|
+
type SkillDiscoveryPage,
|
|
11
|
+
} from "./skill-discovery";
|
|
5
12
|
import type { ConnectController } from "@opengeni/connect";
|
|
6
13
|
export { ConnectSetup, type ConnectSetupProps } from "./connect-setup";
|
|
7
14
|
export { ConnectChooser, type ConnectChooserProps } from "./connect-chooser";
|
|
@@ -46,3 +53,17 @@ export function useConnect(controller: ConnectController) {
|
|
|
46
53
|
);
|
|
47
54
|
return useMemo(() => ({ ...snapshot, ...actions }), [snapshot, actions]);
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
ConnectionCatalog,
|
|
59
|
+
ConnectionServiceRow,
|
|
60
|
+
ConnectionOptionRow,
|
|
61
|
+
type ConnectionCatalogProps,
|
|
62
|
+
type ConnectionCatalogService,
|
|
63
|
+
type ConnectionCatalogOption,
|
|
64
|
+
} from "./connection-catalog";
|
|
65
|
+
export { ConnectionInstalled, type ConnectionInstalledItem } from "./connection-installed";
|
|
66
|
+
export { ConnectionTypePicker, type ConnectionType } from "./connection-type-picker";
|
|
67
|
+
export { ConnectionLogo } from "./connection-logo";
|
|
68
|
+
export { PluginDiscovery, type PluginDiscoveryProps } from "./plugin-discovery";
|
|
69
|
+
export { PluginDetails } from "./plugin-details";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** Status comes from the host/controller, never inferred from a display label. */
|
|
4
|
+
export type ConnectionCatalogOption = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string | undefined;
|
|
8
|
+
status: string;
|
|
9
|
+
connected: boolean;
|
|
10
|
+
onOpen: () => void;
|
|
11
|
+
action?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
export type ConnectionCatalogService = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
logo?: ReactNode;
|
|
17
|
+
options: ConnectionCatalogOption[];
|
|
18
|
+
};
|
|
19
|
+
export type ConnectionCatalogProps = {
|
|
20
|
+
services: ConnectionCatalogService[];
|
|
21
|
+
query?: string;
|
|
22
|
+
columns?: 1 | 2;
|
|
23
|
+
/** Group capabilities by service, or show every capability independently. */
|
|
24
|
+
grouped?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
emptyMessage?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function ConnectionDetailsAction({
|
|
30
|
+
option,
|
|
31
|
+
name = option.name,
|
|
32
|
+
}: {
|
|
33
|
+
option: ConnectionCatalogOption;
|
|
34
|
+
name?: string;
|
|
35
|
+
}) {
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="og-connection-details-action"
|
|
40
|
+
aria-label={`View ${name} details`}
|
|
41
|
+
onClick={option.onOpen}
|
|
42
|
+
>
|
|
43
|
+
<span aria-hidden>{option.connected ? "›" : "+"}</span>
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ConnectionOptionRow({ option }: { option: ConnectionCatalogOption }) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="og-connection-catalog-action-row og-connection-option">
|
|
51
|
+
<button type="button" className="og-connection-catalog-row" onClick={option.onOpen}>
|
|
52
|
+
<span className="og-connection-catalog-copy">
|
|
53
|
+
<strong>{option.name}</strong>
|
|
54
|
+
<span>{option.description}</span>
|
|
55
|
+
</span>
|
|
56
|
+
<span className="og-connection-catalog-status">{option.status}</span>
|
|
57
|
+
</button>
|
|
58
|
+
<span className="og-connection-option-action">
|
|
59
|
+
{option.action ?? <ConnectionDetailsAction option={option} />}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ConnectionServiceRow({ service }: { service: ConnectionCatalogService }) {
|
|
66
|
+
if (!service.options.length) return null;
|
|
67
|
+
const connected = service.options.filter((option) => option.connected).length;
|
|
68
|
+
const identity = (
|
|
69
|
+
<>
|
|
70
|
+
<span className="og-connection-catalog-logo">{service.logo}</span>
|
|
71
|
+
<span className="og-connection-catalog-copy">
|
|
72
|
+
<strong>{service.name}</strong>
|
|
73
|
+
<span>
|
|
74
|
+
{service.options.length === 1
|
|
75
|
+
? service.options[0]?.description
|
|
76
|
+
: service.options.map((option) => option.name).join(" · ")}
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
<span className="og-connection-catalog-status">
|
|
80
|
+
{service.options.length === 1
|
|
81
|
+
? service.options[0]?.status
|
|
82
|
+
: `${connected} of ${service.options.length} connected`}
|
|
83
|
+
</span>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
return service.options.length === 1 ? (
|
|
87
|
+
<div className="og-connection-catalog-action-row">
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="og-connection-catalog-row"
|
|
91
|
+
onClick={service.options[0]!.onOpen}
|
|
92
|
+
>
|
|
93
|
+
{identity}
|
|
94
|
+
</button>
|
|
95
|
+
{service.options[0]?.action ?? (
|
|
96
|
+
<ConnectionDetailsAction option={service.options[0]!} name={service.name} />
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
<details className="og-connection-catalog-service">
|
|
101
|
+
<summary className="og-connection-catalog-row">
|
|
102
|
+
{identity}
|
|
103
|
+
<span aria-hidden className="og-connection-group-chevron">
|
|
104
|
+
⌄
|
|
105
|
+
</span>
|
|
106
|
+
</summary>
|
|
107
|
+
<div className="og-connection-catalog-options">
|
|
108
|
+
{service.options.map((option) => (
|
|
109
|
+
<ConnectionOptionRow key={option.id} option={option} />
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</details>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function ConnectionCatalog({
|
|
117
|
+
services,
|
|
118
|
+
query = "",
|
|
119
|
+
columns = 1,
|
|
120
|
+
grouped = true,
|
|
121
|
+
className = "",
|
|
122
|
+
emptyMessage = "No connections match your search.",
|
|
123
|
+
}: ConnectionCatalogProps) {
|
|
124
|
+
const search = query.trim().toLowerCase();
|
|
125
|
+
const entries = grouped
|
|
126
|
+
? services
|
|
127
|
+
: services.flatMap((service) =>
|
|
128
|
+
service.options.map((option) => ({
|
|
129
|
+
...service,
|
|
130
|
+
id: `${service.id}/${option.id}`,
|
|
131
|
+
name:
|
|
132
|
+
service.options.length > 1 && option.name !== service.name
|
|
133
|
+
? `${service.name} · ${option.name}`
|
|
134
|
+
: service.name,
|
|
135
|
+
options: [option],
|
|
136
|
+
})),
|
|
137
|
+
);
|
|
138
|
+
const visible = entries.filter((service) =>
|
|
139
|
+
[service.name, ...service.options.flatMap((option) => [option.name, option.description ?? ""])]
|
|
140
|
+
.join(" ")
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.includes(search),
|
|
143
|
+
);
|
|
144
|
+
return (
|
|
145
|
+
<div className={`og-connection-catalog ${className}`} data-columns={columns}>
|
|
146
|
+
{visible.length ? (
|
|
147
|
+
visible.map((service) => <ConnectionServiceRow key={service.id} service={service} />)
|
|
148
|
+
) : (
|
|
149
|
+
<p role="status">{emptyMessage}</p>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ConnectionInstalledItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
needsAttention?: boolean;
|
|
9
|
+
onOpen: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Compact account/service shortcuts. The host supplies authoritative status. */
|
|
13
|
+
export function ConnectionInstalled({
|
|
14
|
+
items,
|
|
15
|
+
title = "Connected",
|
|
16
|
+
className = "",
|
|
17
|
+
}: {
|
|
18
|
+
items: ConnectionInstalledItem[];
|
|
19
|
+
title?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}) {
|
|
22
|
+
if (!items.length) return null;
|
|
23
|
+
return (
|
|
24
|
+
<section aria-label={title} className={`og-connection-installed ${className}`}>
|
|
25
|
+
<h2>
|
|
26
|
+
{title} <span>{items.length}</span>
|
|
27
|
+
</h2>
|
|
28
|
+
<div>
|
|
29
|
+
{items.map((item) => (
|
|
30
|
+
<button
|
|
31
|
+
key={item.id}
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={item.onOpen}
|
|
34
|
+
title={item.status}
|
|
35
|
+
aria-label={`${item.name} · ${item.status}`}
|
|
36
|
+
>
|
|
37
|
+
{item.icon}
|
|
38
|
+
<span>{item.name}</span>
|
|
39
|
+
{item.needsAttention ? <span aria-label="Needs attention">!</span> : null}
|
|
40
|
+
</button>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
/** Brand identifier; failed/missing assets use a stable text fallback. */
|
|
3
|
+
export function ConnectionLogo({
|
|
4
|
+
src,
|
|
5
|
+
name,
|
|
6
|
+
size = 40,
|
|
7
|
+
}: {
|
|
8
|
+
src: string | null;
|
|
9
|
+
name: string;
|
|
10
|
+
size?: number;
|
|
11
|
+
}) {
|
|
12
|
+
const [failedSrc, setFailedSrc] = useState<string | null>(null);
|
|
13
|
+
return (
|
|
14
|
+
<span className="og-connection-logo" aria-hidden="true" style={{ width: size, height: size }}>
|
|
15
|
+
{src && src !== failedSrc ? (
|
|
16
|
+
<img src={src} alt="" loading="lazy" decoding="async" onError={() => setFailedSrc(src)} />
|
|
17
|
+
) : (
|
|
18
|
+
<span>
|
|
19
|
+
{name
|
|
20
|
+
.split(/\s+/)
|
|
21
|
+
.map((part) => part[0])
|
|
22
|
+
.join("")
|
|
23
|
+
.slice(0, 2)
|
|
24
|
+
.toUpperCase()}
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
export type ConnectionType = "mcp" | "openapi" | "graphql";
|
|
3
|
+
export function ConnectionTypePicker({
|
|
4
|
+
value,
|
|
5
|
+
onChange,
|
|
6
|
+
disabled = false,
|
|
7
|
+
}: {
|
|
8
|
+
value: ConnectionType;
|
|
9
|
+
onChange: (value: ConnectionType) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}) {
|
|
12
|
+
const name = useId();
|
|
13
|
+
return (
|
|
14
|
+
<fieldset className="og-connection-type-picker" disabled={disabled}>
|
|
15
|
+
<legend>Connection type</legend>
|
|
16
|
+
{(
|
|
17
|
+
[
|
|
18
|
+
["mcp", "MCP server"],
|
|
19
|
+
["openapi", "OpenAPI"],
|
|
20
|
+
["graphql", "GraphQL"],
|
|
21
|
+
] as const
|
|
22
|
+
).map(([type, label]) => (
|
|
23
|
+
<label key={type} data-selected={value === type}>
|
|
24
|
+
<input
|
|
25
|
+
type="radio"
|
|
26
|
+
name={name}
|
|
27
|
+
value={type}
|
|
28
|
+
checked={value === type}
|
|
29
|
+
onChange={() => onChange(type)}
|
|
30
|
+
/>
|
|
31
|
+
{label}
|
|
32
|
+
</label>
|
|
33
|
+
))}
|
|
34
|
+
</fieldset>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
/** Events that change which Codex account a session runs on (or just ran). */
|
|
17
17
|
export function isCodexAccountEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
18
18
|
return [
|
|
19
|
+
"codex.credential.selected",
|
|
19
20
|
"codex.account.switched",
|
|
20
21
|
"codex.account.selection.changed",
|
|
21
22
|
"codex.capacity.waiting",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { pluginMcpUnavailableReason, type PluginDiscoveryItem } from "@opengeni/sdk";
|
|
3
|
+
import { Markdown } from "./components/markdown";
|
|
4
|
+
|
|
5
|
+
const componentLabels: Record<string, string> = {
|
|
6
|
+
skills: "Skills",
|
|
7
|
+
mcp: "MCP connections",
|
|
8
|
+
apps: "Hosted apps",
|
|
9
|
+
hooks: "Hooks",
|
|
10
|
+
agents: "Agents",
|
|
11
|
+
commands: "Commands",
|
|
12
|
+
lsp: "Language servers",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const EMPTY_CONNECTIONS: Record<string, boolean> = {};
|
|
16
|
+
|
|
17
|
+
/** Embeddable plugin overview. Hosts own the dialog and installation actions. */
|
|
18
|
+
export function PluginDetails({
|
|
19
|
+
item,
|
|
20
|
+
onInstall,
|
|
21
|
+
busy = false,
|
|
22
|
+
installed = false,
|
|
23
|
+
error,
|
|
24
|
+
connections = EMPTY_CONNECTIONS,
|
|
25
|
+
onConnect,
|
|
26
|
+
}: {
|
|
27
|
+
item: PluginDiscoveryItem;
|
|
28
|
+
onInstall?: () => void;
|
|
29
|
+
busy?: boolean;
|
|
30
|
+
installed?: boolean;
|
|
31
|
+
error?: string | null;
|
|
32
|
+
connections?: Record<string, boolean>;
|
|
33
|
+
onConnect?: (server: { name: string; endpoint: string | null }) => void;
|
|
34
|
+
}) {
|
|
35
|
+
const supported = (server: NonNullable<PluginDiscoveryItem["mcpServers"]>[number]) =>
|
|
36
|
+
pluginMcpUnavailableReason(server) === null;
|
|
37
|
+
const contentsUnknown = item.skills == null || item.mcpServers == null;
|
|
38
|
+
const installableCount =
|
|
39
|
+
(item.skills?.length ?? 0) + (item.mcpServers?.filter(supported).length ?? 0);
|
|
40
|
+
const exceedsInstallLimit = installableCount > 64;
|
|
41
|
+
const installable = installableCount > 0 && !exceedsInstallLimit;
|
|
42
|
+
const [expanded, setExpanded] = useState(false);
|
|
43
|
+
const [logoFailed, setLogoFailed] = useState(false);
|
|
44
|
+
const description = item.longDescription || item.description;
|
|
45
|
+
const lengthy = description.length > 700;
|
|
46
|
+
const registryLabel =
|
|
47
|
+
item.provider === "openai"
|
|
48
|
+
? "OpenAI plugin registry"
|
|
49
|
+
: item.provider === "anthropic"
|
|
50
|
+
? "Anthropic plugin registry"
|
|
51
|
+
: "Custom source";
|
|
52
|
+
return (
|
|
53
|
+
<article className="og-plugin-details">
|
|
54
|
+
<header className="og-plugin-details-header">
|
|
55
|
+
{item.logoUrl && !logoFailed ? (
|
|
56
|
+
<img src={item.logoUrl} alt="" onError={() => setLogoFailed(true)} />
|
|
57
|
+
) : null}
|
|
58
|
+
<div>
|
|
59
|
+
<h2>{item.displayName.replace(/-/g, " ")}</h2>
|
|
60
|
+
{item.author?.name ? <p>By {item.author.name}</p> : null}
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
<div className="og-plugin-details-meta">
|
|
64
|
+
<span>{registryLabel}</span>
|
|
65
|
+
{item.category ? <span>{item.category}</span> : null}
|
|
66
|
+
{item.version ? <span>v{item.version}</span> : null}
|
|
67
|
+
</div>
|
|
68
|
+
<section className="og-plugin-details-members">
|
|
69
|
+
<h3>
|
|
70
|
+
Skills <span>{item.skills?.length ?? "—"}</span>
|
|
71
|
+
</h3>
|
|
72
|
+
{item.skills?.length ? (
|
|
73
|
+
<ul>
|
|
74
|
+
{item.skills.map((skill) => (
|
|
75
|
+
<li key={skill.sourceUrl}>
|
|
76
|
+
<a href={skill.sourceUrl} target="_blank" rel="noopener noreferrer">
|
|
77
|
+
<strong>{skill.name}</strong>
|
|
78
|
+
<span aria-hidden="true">↗</span>
|
|
79
|
+
</a>
|
|
80
|
+
</li>
|
|
81
|
+
))}
|
|
82
|
+
</ul>
|
|
83
|
+
) : (
|
|
84
|
+
<p>
|
|
85
|
+
{item.skills
|
|
86
|
+
? "No bundled skills."
|
|
87
|
+
: "Contents haven’t been indexed for this repository."}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
<h3>
|
|
91
|
+
MCP servers <span>{item.mcpServers?.length ?? "—"}</span>
|
|
92
|
+
</h3>
|
|
93
|
+
{item.mcpServers?.length ? (
|
|
94
|
+
<ul>
|
|
95
|
+
{item.mcpServers.map((server) => {
|
|
96
|
+
const connected = Boolean(server.endpoint && connections[server.endpoint]);
|
|
97
|
+
// Existing connections remain manageable regardless of the source manifest's setup requirements.
|
|
98
|
+
const unavailableReason = connected ? null : pluginMcpUnavailableReason(server);
|
|
99
|
+
return (
|
|
100
|
+
<li
|
|
101
|
+
key={server.name}
|
|
102
|
+
className={unavailableReason ? "og-plugin-member-disabled" : undefined}
|
|
103
|
+
>
|
|
104
|
+
<div className="og-plugin-server-heading">
|
|
105
|
+
<strong>{server.name}</strong>
|
|
106
|
+
{!unavailableReason && onConnect ? (
|
|
107
|
+
<button type="button" disabled={busy} onClick={() => onConnect(server)}>
|
|
108
|
+
{connected ? "Manage" : "Connect"}
|
|
109
|
+
</button>
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
<small>
|
|
113
|
+
{connected ? "Connected · " : ""}
|
|
114
|
+
{unavailableReason ?? server.endpoint}
|
|
115
|
+
</small>
|
|
116
|
+
</li>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</ul>
|
|
120
|
+
) : (
|
|
121
|
+
<p>
|
|
122
|
+
{item.mcpServers ? "No bundled MCP servers." : "Server details haven’t been indexed."}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
{item.components?.some((component) => !["skills", "mcp"].includes(component)) ? (
|
|
126
|
+
<div className="og-plugin-details-other">
|
|
127
|
+
Not installed here:{" "}
|
|
128
|
+
{item.components
|
|
129
|
+
.filter((component) => !["skills", "mcp"].includes(component))
|
|
130
|
+
.map((component) => componentLabels[component] ?? component)
|
|
131
|
+
.join(", ")}
|
|
132
|
+
</div>
|
|
133
|
+
) : null}
|
|
134
|
+
</section>
|
|
135
|
+
<section className="og-plugin-details-overview" aria-label="Overview">
|
|
136
|
+
<div className={lengthy && !expanded ? "og-plugin-details-excerpt" : undefined}>
|
|
137
|
+
<Markdown>{description}</Markdown>
|
|
138
|
+
</div>
|
|
139
|
+
{lengthy ? (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="og-plugin-details-more"
|
|
143
|
+
aria-expanded={expanded}
|
|
144
|
+
onClick={() => setExpanded((value) => !value)}
|
|
145
|
+
>
|
|
146
|
+
{expanded ? "Show less" : "Read more"}
|
|
147
|
+
</button>
|
|
148
|
+
) : null}
|
|
149
|
+
</section>
|
|
150
|
+
<footer>
|
|
151
|
+
{item.sourceUrl ? (
|
|
152
|
+
<a href={item.sourceUrl} target="_blank" rel="noopener noreferrer">
|
|
153
|
+
View repository <span aria-hidden="true">↗</span>
|
|
154
|
+
</a>
|
|
155
|
+
) : null}
|
|
156
|
+
{onInstall ? (
|
|
157
|
+
<button
|
|
158
|
+
className="og-plugin-install"
|
|
159
|
+
type="button"
|
|
160
|
+
disabled={!installable || busy || installed}
|
|
161
|
+
onClick={onInstall}
|
|
162
|
+
>
|
|
163
|
+
{installed ? "Installed" : busy ? "Installing…" : "Install plugin"}
|
|
164
|
+
</button>
|
|
165
|
+
) : null}
|
|
166
|
+
{error ? <p role="alert">{error}</p> : null}
|
|
167
|
+
{exceedsInstallLimit ? (
|
|
168
|
+
<p>This plugin exceeds the current 64-component installation limit.</p>
|
|
169
|
+
) : contentsUnknown ? (
|
|
170
|
+
<p>
|
|
171
|
+
Plugin contents are unavailable. Installation is disabled until the repository can be
|
|
172
|
+
inspected.
|
|
173
|
+
</p>
|
|
174
|
+
) : !installable ? (
|
|
175
|
+
<p>No installable skills or remote MCP servers.</p>
|
|
176
|
+
) : (
|
|
177
|
+
<p>Installs skills and adds connection references. Connect each server separately.</p>
|
|
178
|
+
)}
|
|
179
|
+
</footer>
|
|
180
|
+
</article>
|
|
181
|
+
);
|
|
182
|
+
}
|