@opengeni/react 6.1.0 → 7.0.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 +83 -2
- package/dist/artifacts-document.js +1 -1
- package/dist/artifacts-presentation.js +1 -1
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +3 -3
- package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
- package/dist/capability-catalog-row.d.ts +20 -0
- package/dist/capability-logo-fallback.d.ts +3 -0
- package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
- 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-5UONOQ3Y.js +635 -0
- package/dist/chunk-5UONOQ3Y.js.map +1 -0
- package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
- package/dist/chunk-AKOMC3VO.js.map +1 -0
- package/dist/chunk-BRM6FVZA.js +302 -0
- package/dist/chunk-BRM6FVZA.js.map +1 -0
- package/dist/{chunk-TDSEILUH.js → chunk-D5NOQPLR.js} +1289 -1530
- package/dist/chunk-D5NOQPLR.js.map +1 -0
- package/dist/chunk-D7GAPJ3V.js +1558 -0
- package/dist/chunk-D7GAPJ3V.js.map +1 -0
- package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
- package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
- 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-KANFU34B.js → chunk-IYUPN32E.js} +6 -2
- package/dist/chunk-IYUPN32E.js.map +1 -0
- package/dist/{chunk-L2NBTKRX.js → chunk-JKLGJQO5.js} +193 -12
- package/dist/chunk-JKLGJQO5.js.map +1 -0
- package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
- package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
- package/dist/chunk-OWHPKAOV.js.map +1 -0
- package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
- package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
- package/dist/chunk-UXE5SSNN.js.map +1 -0
- package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
- package/dist/chunk-VR4Z73EU.js.map +1 -0
- package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
- package/dist/chunk-XMXRI5JK.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
- package/dist/components/chat-composer.d.ts +13 -3
- package/dist/components/markdown.d.ts +4 -3
- package/dist/components/message-timeline.d.ts +7 -2
- package/dist/components/preview-loading.d.ts +2 -0
- package/dist/components/session-capability-frame.d.ts +24 -0
- package/dist/components/session-chrome.d.ts +1 -2
- package/dist/components/session-commands.d.ts +6 -0
- package/dist/components/session-connection-request.d.ts +8 -0
- package/dist/components/session-mcp-capability-card.d.ts +22 -0
- package/dist/components/timeline-search.d.ts +34 -0
- package/dist/composer.js +7 -7
- package/dist/connect-account-identity.d.ts +8 -0
- package/dist/connect-accounts.d.ts +5 -0
- package/dist/connect-chooser.d.ts +6 -0
- package/dist/connect-panel.d.ts +9 -2
- package/dist/connect-setup.d.ts +5 -1
- package/dist/connect.d.ts +5 -21
- package/dist/connect.js +1268 -584
- package/dist/connect.js.map +1 -1
- package/dist/connection-catalog.d.ts +6 -1
- package/dist/connection-discovery.d.ts +12 -0
- package/dist/connection-installed.d.ts +1 -0
- package/dist/connection-logo.d.ts +5 -2
- package/dist/connection-service-logo.d.ts +8 -0
- package/dist/connection-service-presentation.d.ts +13 -0
- package/dist/connection-skeleton.d.ts +5 -0
- package/dist/discovery-cache.d.ts +5 -0
- package/dist/hooks/use-codex-accounts.d.ts +11 -5
- package/dist/hooks/use-connect.d.ts +22 -0
- package/dist/hooks/use-session-events.d.ts +5 -0
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-workspace-sessions.d.ts +2 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +771 -793
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/machines.js +2 -2
- package/dist/mcp-connection-status.d.ts +14 -0
- 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/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
- package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
- package/dist/plugin-discovery.d.ts +6 -0
- package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
- package/dist/realtime.js +22 -22
- package/dist/realtime.js.map +1 -1
- package/dist/session-auth-recommendation.d.ts +5 -0
- package/dist/session-capability-policy.d.ts +12 -0
- package/dist/session-ui.d.ts +2 -0
- package/dist/session-ui.js +14 -12
- package/dist/session.d.ts +1 -0
- package/dist/session.js +6 -4
- package/dist/skill-discovery.d.ts +11 -0
- package/dist/timeline/compaction-copy.d.ts +10 -0
- package/dist/timeline/projection.d.ts +3 -1
- package/dist/timeline/turn-summary.d.ts +2 -2
- package/dist/timeline/types.d.ts +22 -1
- package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
- package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
- package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
- package/package.json +3 -3
- package/src/assets/README.md +8 -0
- package/src/assets/gmail.ico +0 -0
- package/src/assets/outlook.ico +0 -0
- package/src/capability-catalog-row.tsx +94 -0
- package/src/capability-logo-fallback.ts +22 -0
- package/src/client.ts +0 -9
- package/src/components/artifacts/document-editor.tsx +63 -6
- package/src/components/artifacts/presentation-editor.tsx +214 -9
- package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
- package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
- package/src/components/chat-composer.tsx +26 -3
- package/src/components/composer.tsx +10 -4
- package/src/components/human-input-form.tsx +12 -2
- package/src/components/markdown.tsx +86 -4
- package/src/components/message-timeline.tsx +104 -36
- package/src/components/model-picker.tsx +11 -3
- package/src/components/model-policy-picker-menu.tsx +10 -3
- package/src/components/preview-loading.tsx +154 -0
- package/src/components/sandbox-workspace.tsx +20 -36
- package/src/components/session-capability-frame.tsx +163 -0
- package/src/components/session-chrome.tsx +46 -77
- package/src/components/session-commands.tsx +18 -0
- package/src/components/session-connection-request.tsx +97 -0
- package/src/components/session-conversation.tsx +5 -4
- package/src/components/session-mcp-capability-card.tsx +532 -0
- package/src/components/session-status.tsx +2 -2
- package/src/components/timeline-anchor.tsx +9 -8
- package/src/components/timeline-search.tsx +168 -0
- package/src/components/user-message-body.tsx +7 -0
- package/src/connect-account-identity.tsx +34 -0
- package/src/connect-accounts.tsx +114 -52
- package/src/connect-chooser.tsx +171 -39
- package/src/connect-panel.tsx +73 -9
- package/src/connect-setup.tsx +72 -25
- package/src/connect.ts +13 -22
- package/src/connection-assets.d.ts +4 -0
- package/src/connection-catalog.tsx +61 -39
- package/src/connection-discovery.tsx +235 -0
- package/src/connection-installed.tsx +2 -0
- package/src/connection-logo.tsx +18 -10
- package/src/connection-service-logo.tsx +55 -0
- package/src/connection-service-presentation.ts +46 -0
- package/src/connection-skeleton.tsx +16 -0
- package/src/conversation-timeline.ts +1 -0
- package/src/discovery-cache.ts +42 -0
- package/src/hooks/use-codex-accounts.ts +33 -14
- package/src/hooks/use-composer.ts +4 -5
- package/src/hooks/use-connect.ts +23 -0
- package/src/hooks/use-session-events.ts +114 -1
- package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
- package/src/hooks/use-workspace-sessions.ts +10 -0
- package/src/index.ts +22 -2
- package/src/mcp-connection-status.ts +35 -0
- package/src/model-policy.ts +21 -0
- package/src/plugin-details.tsx +8 -5
- package/src/plugin-discovery.tsx +92 -72
- package/src/realtime/realtime-control.tsx +6 -8
- package/src/session-auth-recommendation.ts +48 -0
- package/src/session-capability-policy.ts +83 -0
- package/src/session-ui.ts +2 -0
- package/src/session.ts +1 -0
- package/src/skill-discovery.tsx +101 -56
- package/src/timeline/compaction-copy.ts +40 -0
- package/src/timeline/platform-activity-row.tsx +8 -3
- package/src/timeline/projection.ts +74 -2
- package/src/timeline/shared.tsx +6 -1
- package/src/timeline/startup-timings.tsx +1 -1
- package/src/timeline/turn-summary.tsx +17 -3
- package/src/timeline/types.ts +22 -2
- package/styles/compiled.css +325 -27
- package/styles/connect.css +90 -15
- package/styles/index.css +46 -0
- package/styles/session-capability.css +42 -0
- package/dist/chunk-45XEUVTK.js.map +0 -1
- package/dist/chunk-HQV2I5HV.js +0 -124
- package/dist/chunk-HQV2I5HV.js.map +0 -1
- package/dist/chunk-HUIKIDM5.js.map +0 -1
- package/dist/chunk-ICA6R6TW.js.map +0 -1
- package/dist/chunk-IHQII5WO.js +0 -886
- package/dist/chunk-IHQII5WO.js.map +0 -1
- package/dist/chunk-KANFU34B.js.map +0 -1
- package/dist/chunk-L2NBTKRX.js.map +0 -1
- package/dist/chunk-NYESIADM.js.map +0 -1
- package/dist/chunk-PEI6RANI.js.map +0 -1
- package/dist/chunk-TDSEILUH.js.map +0 -1
- package/dist/chunk-UMECRJRV.js.map +0 -1
- package/dist/hooks/use-packs.d.ts +0 -32
- package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
- package/src/hooks/use-packs.ts +0 -157
- /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
- /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
- /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
- /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
- /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
- /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
- /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
- /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
- /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
- /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
- /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { ConnectAccount, ConnectController, ConnectProvider } from "@opengeni/connect";
|
|
2
|
+
import { ConnectChooser } from "./connect-chooser";
|
|
3
|
+
import {
|
|
4
|
+
capabilityConnectProviderId,
|
|
5
|
+
connectionServicePresentation,
|
|
6
|
+
isServiceConnectProvider,
|
|
7
|
+
} from "./connection-service-presentation";
|
|
8
|
+
import type { CapabilityCatalogItem, ConnectionMetadata, OpenGeniClient } from "@opengeni/sdk";
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
|
+
import { CapabilityCatalogRow } from "./capability-catalog-row";
|
|
11
|
+
import { ConnectionLogo } from "./connection-logo";
|
|
12
|
+
import { ConnectionSkeleton } from "./connection-skeleton";
|
|
13
|
+
import { ConnectionServiceLogo } from "./connection-service-logo";
|
|
14
|
+
import { McpConnectionCard } from "./components/session-mcp-capability-card";
|
|
15
|
+
import { mcpConnectionDiscoveryState } from "./mcp-connection-status";
|
|
16
|
+
|
|
17
|
+
export type ConnectionDiscoveryProps = {
|
|
18
|
+
client: OpenGeniClient;
|
|
19
|
+
controller?: ConnectController | undefined;
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
returnUrl: string;
|
|
22
|
+
onConfigured?: (() => void | Promise<void>) | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Service discovery over the native catalogue. No host-owned OAuth state or
|
|
26
|
+
* assumed account selection: details resolve current authority before acting. */
|
|
27
|
+
export function ConnectionDiscovery(props: ConnectionDiscoveryProps) {
|
|
28
|
+
const [scope, setScope] = useState({ client: props.client, controller: props.controller });
|
|
29
|
+
const [generation, setGeneration] = useState(0);
|
|
30
|
+
if (scope.client !== props.client || scope.controller !== props.controller) {
|
|
31
|
+
setScope({ client: props.client, controller: props.controller });
|
|
32
|
+
setGeneration(generation + 1);
|
|
33
|
+
}
|
|
34
|
+
return <ScopedDiscovery key={`${generation}:${props.workspaceId}`} {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ScopedDiscovery({
|
|
38
|
+
client,
|
|
39
|
+
controller,
|
|
40
|
+
workspaceId,
|
|
41
|
+
returnUrl,
|
|
42
|
+
onConfigured,
|
|
43
|
+
}: ConnectionDiscoveryProps) {
|
|
44
|
+
const [providers, setProviders] = useState<ConnectProvider[] | null>(null);
|
|
45
|
+
const [accounts, setAccounts] = useState<ConnectAccount[]>([]);
|
|
46
|
+
const [selectedProvider, setSelectedProvider] = useState<ConnectProvider | null>(null);
|
|
47
|
+
const [providerFailed, setProviderFailed] = useState(false);
|
|
48
|
+
const [items, setItems] = useState<CapabilityCatalogItem[] | null>(null);
|
|
49
|
+
const [failed, setFailed] = useState(false);
|
|
50
|
+
const [connections, setConnections] = useState<ConnectionMetadata[]>([]);
|
|
51
|
+
const [reload, setReload] = useState(0);
|
|
52
|
+
const [query, setQuery] = useState("");
|
|
53
|
+
const [limit, setLimit] = useState(12);
|
|
54
|
+
const [selected, setSelected] = useState<CapabilityCatalogItem | null>(null);
|
|
55
|
+
const opener = useRef<HTMLElement | null>(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const abort = new AbortController();
|
|
58
|
+
// Capture each read independently: even a synchronously throwing transport
|
|
59
|
+
// is captured by Promise.all, and cannot strand another read's rejection.
|
|
60
|
+
const read = async <T,>(operation: () => Promise<T>): Promise<T> => {
|
|
61
|
+
abort.signal.throwIfAborted();
|
|
62
|
+
return operation();
|
|
63
|
+
};
|
|
64
|
+
setFailed(false);
|
|
65
|
+
setSelectedProvider(null);
|
|
66
|
+
setProviders(null);
|
|
67
|
+
setItems(null);
|
|
68
|
+
setProviderFailed(false);
|
|
69
|
+
if (controller)
|
|
70
|
+
void Promise.all([
|
|
71
|
+
read(() => controller.transport.catalog(workspaceId, { signal: abort.signal })),
|
|
72
|
+
read(() => controller.transport.accounts(workspaceId, { signal: abort.signal })),
|
|
73
|
+
])
|
|
74
|
+
.then(([catalog, inventory]) => {
|
|
75
|
+
if (!abort.signal.aborted) {
|
|
76
|
+
setProviders(catalog.filter(isServiceConnectProvider));
|
|
77
|
+
setAccounts(inventory);
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {
|
|
81
|
+
if (!abort.signal.aborted) setProviderFailed(true);
|
|
82
|
+
});
|
|
83
|
+
void Promise.all([
|
|
84
|
+
read(() => client.listCapabilities(workspaceId)),
|
|
85
|
+
read(() => client.listConnections(workspaceId)),
|
|
86
|
+
])
|
|
87
|
+
.then(([catalog, inventory]) => {
|
|
88
|
+
if (!abort.signal.aborted) {
|
|
89
|
+
setConnections(inventory);
|
|
90
|
+
setItems(
|
|
91
|
+
catalog.items.filter(
|
|
92
|
+
(item) =>
|
|
93
|
+
item.kind === "mcp" &&
|
|
94
|
+
item.authKind === "oauth2" &&
|
|
95
|
+
Boolean(item.mcpUrl ?? item.endpointUrl),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {
|
|
101
|
+
if (!abort.signal.aborted) setFailed(true);
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
abort.abort();
|
|
105
|
+
};
|
|
106
|
+
}, [client, controller, workspaceId, reload]);
|
|
107
|
+
const matches =
|
|
108
|
+
items?.filter((item) =>
|
|
109
|
+
`${item.name} ${item.description ?? ""} ${item.providerDomain ?? ""}`
|
|
110
|
+
.toLocaleLowerCase()
|
|
111
|
+
.includes(query.trim().toLocaleLowerCase()),
|
|
112
|
+
) ?? [];
|
|
113
|
+
const providerMatches = (providers ?? []).filter(
|
|
114
|
+
(provider) =>
|
|
115
|
+
!items?.some((item) => capabilityConnectProviderId(item) === provider.id) &&
|
|
116
|
+
provider.label.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()),
|
|
117
|
+
);
|
|
118
|
+
const loading = (!items && !failed) || Boolean(controller && !providers && !providerFailed);
|
|
119
|
+
if (selectedProvider && controller)
|
|
120
|
+
return (
|
|
121
|
+
<section aria-label={`Connect ${selectedProvider.label}`}>
|
|
122
|
+
<button type="button" onClick={() => setSelectedProvider(null)}>
|
|
123
|
+
Back to connections
|
|
124
|
+
</button>
|
|
125
|
+
<ConnectionLogo
|
|
126
|
+
name={selectedProvider.label}
|
|
127
|
+
src={connectionServicePresentation(selectedProvider).logo}
|
|
128
|
+
/>
|
|
129
|
+
<ConnectChooser
|
|
130
|
+
controller={controller}
|
|
131
|
+
returnUrl={returnUrl}
|
|
132
|
+
presentation="catalog"
|
|
133
|
+
providerOnly
|
|
134
|
+
initialProviderId={selectedProvider.id}
|
|
135
|
+
/>
|
|
136
|
+
</section>
|
|
137
|
+
);
|
|
138
|
+
return (
|
|
139
|
+
<section className="og-connection-discovery" aria-label="Find a connection">
|
|
140
|
+
<h3>Add a connection</h3>
|
|
141
|
+
<label>
|
|
142
|
+
<span className="og-capability-catalog-sr-only">Search services</span>
|
|
143
|
+
<input
|
|
144
|
+
type="search"
|
|
145
|
+
placeholder="Search services…"
|
|
146
|
+
value={query}
|
|
147
|
+
onChange={(event) => {
|
|
148
|
+
setQuery(event.target.value);
|
|
149
|
+
setLimit(12);
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</label>
|
|
153
|
+
{(providerFailed || failed) && (
|
|
154
|
+
<p role="alert">
|
|
155
|
+
Some services could not be loaded.{" "}
|
|
156
|
+
<button type="button" onClick={() => setReload((value) => value + 1)}>
|
|
157
|
+
Retry
|
|
158
|
+
</button>
|
|
159
|
+
</p>
|
|
160
|
+
)}
|
|
161
|
+
<div className="og-connection-discovery-results">
|
|
162
|
+
{providerMatches.map((provider) => {
|
|
163
|
+
const connected = accounts.some(
|
|
164
|
+
(account) => account.providerId === provider.id && account.status === "connected",
|
|
165
|
+
);
|
|
166
|
+
return (
|
|
167
|
+
<CapabilityCatalogRow
|
|
168
|
+
key={provider.id}
|
|
169
|
+
name={provider.label}
|
|
170
|
+
icon={
|
|
171
|
+
<ConnectionLogo
|
|
172
|
+
name={provider.label}
|
|
173
|
+
src={connectionServicePresentation(provider).logo}
|
|
174
|
+
/>
|
|
175
|
+
}
|
|
176
|
+
status={connected ? "added" : "available"}
|
|
177
|
+
statusLabel={connected ? "Connected" : "Connect"}
|
|
178
|
+
showStatusLabel
|
|
179
|
+
onOpen={() => setSelectedProvider(provider)}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
{matches.slice(0, limit).map((item) => {
|
|
184
|
+
const state = mcpConnectionDiscoveryState(item, connections);
|
|
185
|
+
return (
|
|
186
|
+
<CapabilityCatalogRow
|
|
187
|
+
key={item.id}
|
|
188
|
+
name={item.name}
|
|
189
|
+
description={item.description ?? undefined}
|
|
190
|
+
icon={<ConnectionServiceLogo client={client} item={item} name={item.name} />}
|
|
191
|
+
status={state.status}
|
|
192
|
+
statusLabel={state.label}
|
|
193
|
+
showStatusLabel
|
|
194
|
+
onOpen={() => {
|
|
195
|
+
opener.current =
|
|
196
|
+
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
197
|
+
setSelected(item);
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
|
|
203
|
+
{loading && <ConnectionSkeleton rows={3} label="Loading services" />}
|
|
204
|
+
{!loading && !failed && !providerFailed && !matches.length && !providerMatches.length && (
|
|
205
|
+
<p role="status">No services match your search.</p>
|
|
206
|
+
)}
|
|
207
|
+
{matches.length > limit && (
|
|
208
|
+
<button type="button" onClick={() => setLimit((value) => value + 12)}>
|
|
209
|
+
Show more services
|
|
210
|
+
</button>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
{selected ? (
|
|
214
|
+
<McpConnectionCard
|
|
215
|
+
client={client}
|
|
216
|
+
workspaceId={workspaceId}
|
|
217
|
+
capabilityId={selected.id}
|
|
218
|
+
name={selected.name}
|
|
219
|
+
returnUrl={returnUrl}
|
|
220
|
+
dialogOnly
|
|
221
|
+
onClose={() => {
|
|
222
|
+
setSelected(null);
|
|
223
|
+
queueMicrotask(() => {
|
|
224
|
+
if (opener.current?.isConnected) opener.current.focus();
|
|
225
|
+
});
|
|
226
|
+
}}
|
|
227
|
+
onConfigured={async () => {
|
|
228
|
+
setReload((value) => value + 1);
|
|
229
|
+
await onConfigured?.();
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
) : null}
|
|
233
|
+
</section>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -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,28 +1,36 @@
|
|
|
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,
|
|
8
|
+
loading = false,
|
|
7
9
|
}: {
|
|
8
10
|
src: string | null;
|
|
9
11
|
name: string;
|
|
10
12
|
size?: number;
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
loading?: boolean;
|
|
11
15
|
}) {
|
|
12
16
|
const [failedSrc, setFailedSrc] = useState<string | null>(null);
|
|
13
17
|
return (
|
|
14
18
|
<span className="og-connection-logo" aria-hidden="true" style={{ width: size, height: size }}>
|
|
15
19
|
{src && src !== failedSrc ? (
|
|
16
20
|
<img src={src} alt="" loading="lazy" decoding="async" onError={() => setFailedSrc(src)} />
|
|
21
|
+
) : loading ? (
|
|
22
|
+
<span className="og-connection-skeleton-mark" />
|
|
17
23
|
) : (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
(fallback ?? (
|
|
25
|
+
<span>
|
|
26
|
+
{name
|
|
27
|
+
.split(/\s+/)
|
|
28
|
+
.map((part) => part[0])
|
|
29
|
+
.join("")
|
|
30
|
+
.slice(0, 2)
|
|
31
|
+
.toUpperCase()}
|
|
32
|
+
</span>
|
|
33
|
+
))
|
|
26
34
|
)}
|
|
27
35
|
</span>
|
|
28
36
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CapabilityCatalogItem, OpenGeniClient } from "@opengeni/sdk";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { ConnectionLogo } from "./connection-logo";
|
|
4
|
+
import { capabilityLogoFallback } from "./capability-logo-fallback";
|
|
5
|
+
|
|
6
|
+
/** The same passive logo loading path for discovery and connected accounts. */
|
|
7
|
+
export function ConnectionServiceLogo({
|
|
8
|
+
client,
|
|
9
|
+
item,
|
|
10
|
+
name,
|
|
11
|
+
fallback = null,
|
|
12
|
+
}: {
|
|
13
|
+
client?: OpenGeniClient | undefined;
|
|
14
|
+
item?: CapabilityCatalogItem | undefined;
|
|
15
|
+
name: string;
|
|
16
|
+
fallback?: string | null;
|
|
17
|
+
}) {
|
|
18
|
+
const path = item?.logoAssetPath;
|
|
19
|
+
const [image, setImage] = useState<{
|
|
20
|
+
client: OpenGeniClient;
|
|
21
|
+
path: string;
|
|
22
|
+
url: string | null;
|
|
23
|
+
} | null>(null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!client || !path?.startsWith("catalog-assets/")) return;
|
|
26
|
+
const abort = new AbortController();
|
|
27
|
+
let url: string | null = null;
|
|
28
|
+
void client
|
|
29
|
+
.downloadCatalogAsset(path, { signal: abort.signal })
|
|
30
|
+
.then((blob) => {
|
|
31
|
+
if (!abort.signal.aborted && blob.type.startsWith("image/") && blob.size <= 2_000_000)
|
|
32
|
+
url = URL.createObjectURL(blob);
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {})
|
|
35
|
+
.finally(() => {
|
|
36
|
+
if (!abort.signal.aborted) setImage({ client, path, url });
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
abort.abort();
|
|
40
|
+
if (url) URL.revokeObjectURL(url);
|
|
41
|
+
};
|
|
42
|
+
}, [client, path]);
|
|
43
|
+
const current = image && image.client === client && image.path === path ? image : null;
|
|
44
|
+
const loading = Boolean(client && path?.startsWith("catalog-assets/") && !current);
|
|
45
|
+
return (
|
|
46
|
+
<ConnectionLogo
|
|
47
|
+
name={name}
|
|
48
|
+
loading={loading}
|
|
49
|
+
src={
|
|
50
|
+
current?.url ??
|
|
51
|
+
(loading ? null : ((item ? capabilityLogoFallback(item) : null) ?? fallback))
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ConnectAccount, ConnectProvider } from "@opengeni/connect";
|
|
2
|
+
import type { CapabilityCatalogItem } from "@opengeni/sdk";
|
|
3
|
+
import gmailLogo from "./assets/gmail.ico";
|
|
4
|
+
import outlookLogo from "./assets/outlook.ico";
|
|
5
|
+
|
|
6
|
+
/** Passive service identity only. Never selects an account or grants tool access. */
|
|
7
|
+
const services: Record<string, { name: string; logo: string; mcpUrl?: string }> = {
|
|
8
|
+
gmail: { name: "Gmail", logo: gmailLogo, mcpUrl: "https://gmailmcp.googleapis.com/mcp/v1" },
|
|
9
|
+
"microsoft-outlook-mail": { name: "Outlook Mail", logo: outlookLogo },
|
|
10
|
+
"microsoft-outlook-calendar": { name: "Outlook Calendar", logo: outlookLogo },
|
|
11
|
+
"microsoft-outlook-contacts": { name: "Outlook Contacts", logo: outlookLogo },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function connectionServicePresentation(provider: Pick<ConnectProvider, "id" | "label">) {
|
|
15
|
+
return (
|
|
16
|
+
(Object.hasOwn(services, provider.id) ? services[provider.id] : undefined) ?? {
|
|
17
|
+
name: provider.label,
|
|
18
|
+
logo: null,
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function capabilityConnectProviderId(
|
|
24
|
+
item: Partial<Pick<CapabilityCatalogItem, "mcpUrl">>,
|
|
25
|
+
): string | undefined {
|
|
26
|
+
return Object.entries(services).find(
|
|
27
|
+
([, service]) => service.mcpUrl && service.mcpUrl === item.mcpUrl,
|
|
28
|
+
)?.[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function accountServiceCapability(account: ConnectAccount, items: CapabilityCatalogItem[]) {
|
|
32
|
+
const exact = items.filter((item) => item.connectionRef?.connectionId === account.id);
|
|
33
|
+
if (exact.length === 1) return exact[0];
|
|
34
|
+
// Presentation may use the service definition even when a personal credential
|
|
35
|
+
// has no workspace installation reference. The account id remains unchanged.
|
|
36
|
+
return items.find((item) => capabilityConnectProviderId(item) === account.providerId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isServiceConnectProvider(provider: ConnectProvider): boolean {
|
|
40
|
+
return (
|
|
41
|
+
provider.readiness === "available" &&
|
|
42
|
+
provider.ownership.length > 0 &&
|
|
43
|
+
provider.family !== "mcp" &&
|
|
44
|
+
!provider.setup.some((kind) => ["installation", "openapi", "graphql"].includes(kind))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Reserve the same row geometry as the loaded connection list. */
|
|
2
|
+
export function ConnectionSkeleton({ rows = 1, label }: { rows?: number; label: string }) {
|
|
3
|
+
return (
|
|
4
|
+
<div role="status" aria-label={label} className="og-connection-skeleton">
|
|
5
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
6
|
+
<div className="og-connection-skeleton-row" aria-hidden="true" key={index}>
|
|
7
|
+
<span className="og-connection-skeleton-mark" />
|
|
8
|
+
<span className="og-connection-skeleton-copy">
|
|
9
|
+
<span />
|
|
10
|
+
<span />
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -43,6 +43,7 @@ export function conversationTimeline(
|
|
|
43
43
|
const optimistic: UserMessageItem[] = pending
|
|
44
44
|
.filter(
|
|
45
45
|
(message) =>
|
|
46
|
+
(message.destination === "chat" || message.state === "failed") &&
|
|
46
47
|
!keys.has(`user-message:${message.clientEventId}`) &&
|
|
47
48
|
!queue.queue.some((turn) => turn.id === message.turnId),
|
|
48
49
|
)
|
|
@@ -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
|
+
}
|