@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.
Files changed (206) hide show
  1. package/README.md +83 -2
  2. package/dist/artifacts-document.js +1 -1
  3. package/dist/artifacts-presentation.js +1 -1
  4. package/dist/artifacts-spreadsheet.js +1 -1
  5. package/dist/artifacts.js +3 -3
  6. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  7. package/dist/capability-catalog-row.d.ts +20 -0
  8. package/dist/capability-logo-fallback.d.ts +3 -0
  9. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  10. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  11. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  12. package/dist/chunk-5UONOQ3Y.js +635 -0
  13. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  14. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  15. package/dist/chunk-AKOMC3VO.js.map +1 -0
  16. package/dist/chunk-BRM6FVZA.js +302 -0
  17. package/dist/chunk-BRM6FVZA.js.map +1 -0
  18. package/dist/{chunk-TDSEILUH.js → chunk-D5NOQPLR.js} +1289 -1530
  19. package/dist/chunk-D5NOQPLR.js.map +1 -0
  20. package/dist/chunk-D7GAPJ3V.js +1558 -0
  21. package/dist/chunk-D7GAPJ3V.js.map +1 -0
  22. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  23. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  24. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  25. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  26. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  27. package/dist/{chunk-KANFU34B.js → chunk-IYUPN32E.js} +6 -2
  28. package/dist/chunk-IYUPN32E.js.map +1 -0
  29. package/dist/{chunk-L2NBTKRX.js → chunk-JKLGJQO5.js} +193 -12
  30. package/dist/chunk-JKLGJQO5.js.map +1 -0
  31. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  32. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  33. package/dist/chunk-OWHPKAOV.js.map +1 -0
  34. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  35. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  36. package/dist/chunk-UXE5SSNN.js.map +1 -0
  37. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  38. package/dist/chunk-VR4Z73EU.js.map +1 -0
  39. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  40. package/dist/chunk-XMXRI5JK.js.map +1 -0
  41. package/dist/client.d.ts +1 -1
  42. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  43. package/dist/components/chat-composer.d.ts +13 -3
  44. package/dist/components/markdown.d.ts +4 -3
  45. package/dist/components/message-timeline.d.ts +7 -2
  46. package/dist/components/preview-loading.d.ts +2 -0
  47. package/dist/components/session-capability-frame.d.ts +24 -0
  48. package/dist/components/session-chrome.d.ts +1 -2
  49. package/dist/components/session-commands.d.ts +6 -0
  50. package/dist/components/session-connection-request.d.ts +8 -0
  51. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  52. package/dist/components/timeline-search.d.ts +34 -0
  53. package/dist/composer.js +7 -7
  54. package/dist/connect-account-identity.d.ts +8 -0
  55. package/dist/connect-accounts.d.ts +5 -0
  56. package/dist/connect-chooser.d.ts +6 -0
  57. package/dist/connect-panel.d.ts +9 -2
  58. package/dist/connect-setup.d.ts +5 -1
  59. package/dist/connect.d.ts +5 -21
  60. package/dist/connect.js +1268 -584
  61. package/dist/connect.js.map +1 -1
  62. package/dist/connection-catalog.d.ts +6 -1
  63. package/dist/connection-discovery.d.ts +12 -0
  64. package/dist/connection-installed.d.ts +1 -0
  65. package/dist/connection-logo.d.ts +5 -2
  66. package/dist/connection-service-logo.d.ts +8 -0
  67. package/dist/connection-service-presentation.d.ts +13 -0
  68. package/dist/connection-skeleton.d.ts +5 -0
  69. package/dist/discovery-cache.d.ts +5 -0
  70. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  71. package/dist/hooks/use-connect.d.ts +22 -0
  72. package/dist/hooks/use-session-events.d.ts +5 -0
  73. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  74. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  75. package/dist/index.d.ts +8 -2
  76. package/dist/index.js +771 -793
  77. package/dist/index.js.map +1 -1
  78. package/dist/interaction.js +2 -2
  79. package/dist/machines.js +2 -2
  80. package/dist/mcp-connection-status.d.ts +14 -0
  81. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  82. package/dist/model-policy.d.ts +6 -2
  83. package/dist/model-policy.js +1 -1
  84. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  85. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  86. package/dist/plugin-discovery.d.ts +6 -0
  87. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  88. package/dist/realtime.js +22 -22
  89. package/dist/realtime.js.map +1 -1
  90. package/dist/session-auth-recommendation.d.ts +5 -0
  91. package/dist/session-capability-policy.d.ts +12 -0
  92. package/dist/session-ui.d.ts +2 -0
  93. package/dist/session-ui.js +14 -12
  94. package/dist/session.d.ts +1 -0
  95. package/dist/session.js +6 -4
  96. package/dist/skill-discovery.d.ts +11 -0
  97. package/dist/timeline/compaction-copy.d.ts +10 -0
  98. package/dist/timeline/projection.d.ts +3 -1
  99. package/dist/timeline/turn-summary.d.ts +2 -2
  100. package/dist/timeline/types.d.ts +22 -1
  101. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  102. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  103. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  104. package/package.json +3 -3
  105. package/src/assets/README.md +8 -0
  106. package/src/assets/gmail.ico +0 -0
  107. package/src/assets/outlook.ico +0 -0
  108. package/src/capability-catalog-row.tsx +94 -0
  109. package/src/capability-logo-fallback.ts +22 -0
  110. package/src/client.ts +0 -9
  111. package/src/components/artifacts/document-editor.tsx +63 -6
  112. package/src/components/artifacts/presentation-editor.tsx +214 -9
  113. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  114. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  115. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  116. package/src/components/chat-composer.tsx +26 -3
  117. package/src/components/composer.tsx +10 -4
  118. package/src/components/human-input-form.tsx +12 -2
  119. package/src/components/markdown.tsx +86 -4
  120. package/src/components/message-timeline.tsx +104 -36
  121. package/src/components/model-picker.tsx +11 -3
  122. package/src/components/model-policy-picker-menu.tsx +10 -3
  123. package/src/components/preview-loading.tsx +154 -0
  124. package/src/components/sandbox-workspace.tsx +20 -36
  125. package/src/components/session-capability-frame.tsx +163 -0
  126. package/src/components/session-chrome.tsx +46 -77
  127. package/src/components/session-commands.tsx +18 -0
  128. package/src/components/session-connection-request.tsx +97 -0
  129. package/src/components/session-conversation.tsx +5 -4
  130. package/src/components/session-mcp-capability-card.tsx +532 -0
  131. package/src/components/session-status.tsx +2 -2
  132. package/src/components/timeline-anchor.tsx +9 -8
  133. package/src/components/timeline-search.tsx +168 -0
  134. package/src/components/user-message-body.tsx +7 -0
  135. package/src/connect-account-identity.tsx +34 -0
  136. package/src/connect-accounts.tsx +114 -52
  137. package/src/connect-chooser.tsx +171 -39
  138. package/src/connect-panel.tsx +73 -9
  139. package/src/connect-setup.tsx +72 -25
  140. package/src/connect.ts +13 -22
  141. package/src/connection-assets.d.ts +4 -0
  142. package/src/connection-catalog.tsx +61 -39
  143. package/src/connection-discovery.tsx +235 -0
  144. package/src/connection-installed.tsx +2 -0
  145. package/src/connection-logo.tsx +18 -10
  146. package/src/connection-service-logo.tsx +55 -0
  147. package/src/connection-service-presentation.ts +46 -0
  148. package/src/connection-skeleton.tsx +16 -0
  149. package/src/conversation-timeline.ts +1 -0
  150. package/src/discovery-cache.ts +42 -0
  151. package/src/hooks/use-codex-accounts.ts +33 -14
  152. package/src/hooks/use-composer.ts +4 -5
  153. package/src/hooks/use-connect.ts +23 -0
  154. package/src/hooks/use-session-events.ts +114 -1
  155. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  156. package/src/hooks/use-workspace-sessions.ts +10 -0
  157. package/src/index.ts +22 -2
  158. package/src/mcp-connection-status.ts +35 -0
  159. package/src/model-policy.ts +21 -0
  160. package/src/plugin-details.tsx +8 -5
  161. package/src/plugin-discovery.tsx +92 -72
  162. package/src/realtime/realtime-control.tsx +6 -8
  163. package/src/session-auth-recommendation.ts +48 -0
  164. package/src/session-capability-policy.ts +83 -0
  165. package/src/session-ui.ts +2 -0
  166. package/src/session.ts +1 -0
  167. package/src/skill-discovery.tsx +101 -56
  168. package/src/timeline/compaction-copy.ts +40 -0
  169. package/src/timeline/platform-activity-row.tsx +8 -3
  170. package/src/timeline/projection.ts +74 -2
  171. package/src/timeline/shared.tsx +6 -1
  172. package/src/timeline/startup-timings.tsx +1 -1
  173. package/src/timeline/turn-summary.tsx +17 -3
  174. package/src/timeline/types.ts +22 -2
  175. package/styles/compiled.css +325 -27
  176. package/styles/connect.css +90 -15
  177. package/styles/index.css +46 -0
  178. package/styles/session-capability.css +42 -0
  179. package/dist/chunk-45XEUVTK.js.map +0 -1
  180. package/dist/chunk-HQV2I5HV.js +0 -124
  181. package/dist/chunk-HQV2I5HV.js.map +0 -1
  182. package/dist/chunk-HUIKIDM5.js.map +0 -1
  183. package/dist/chunk-ICA6R6TW.js.map +0 -1
  184. package/dist/chunk-IHQII5WO.js +0 -886
  185. package/dist/chunk-IHQII5WO.js.map +0 -1
  186. package/dist/chunk-KANFU34B.js.map +0 -1
  187. package/dist/chunk-L2NBTKRX.js.map +0 -1
  188. package/dist/chunk-NYESIADM.js.map +0 -1
  189. package/dist/chunk-PEI6RANI.js.map +0 -1
  190. package/dist/chunk-TDSEILUH.js.map +0 -1
  191. package/dist/chunk-UMECRJRV.js.map +0 -1
  192. package/dist/hooks/use-packs.d.ts +0 -32
  193. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  194. package/src/hooks/use-packs.ts +0 -157
  195. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  196. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  197. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  198. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  199. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  200. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  201. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  202. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  203. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  204. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  205. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  206. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -0,0 +1,4 @@
1
+ declare module "*.ico" {
2
+ const source: string;
3
+ export default source;
4
+ }
@@ -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
- <button type="button" className="og-connection-catalog-row" onClick={option.onOpen}>
41
- <span className="og-connection-catalog-copy">
42
- <strong>{option.name}</strong>
43
- <span>{option.description}</span>
44
- </span>
45
- <span className="og-connection-catalog-status">{option.status}</span>
46
- {!option.action ? <ConnectionDetailsIndicator /> : null}
47
- </button>
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-connection-catalog-logo">{service.logo}</span>
59
- <span className="og-connection-catalog-copy">
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
- <span className="og-connection-catalog-status">
68
- {service.options.length === 1
69
- ? service.options[0]?.status
70
- : `${connected} of ${service.options.length} connected`}
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
- <button
77
- type="button"
78
- className="og-connection-catalog-row"
79
- onClick={service.options[0]!.onOpen}
80
- >
81
- {identity}
82
- {!service.options[0]?.action ? <ConnectionDetailsIndicator /> : null}
83
- </button>
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-connection-catalog-row">
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
- <div className={`og-connection-catalog ${className}`} data-columns={columns}>
133
- {visible.length ? (
134
- visible.map((service) => <ConnectionServiceRow key={service.id} service={service} />)
135
- ) : (
136
- <p role="status">{emptyMessage}</p>
137
- )}
138
- </div>
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
  >
@@ -1,28 +1,36 @@
1
- import { useState } from "react";
2
- /** Brand identifier; failed/missing assets use a stable text fallback. */
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
- <span>
19
- {name
20
- .split(/\s+/)
21
- .map((part) => part[0])
22
- .join("")
23
- .slice(0, 2)
24
- .toUpperCase()}
25
- </span>
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
+ }