@opengeni/react 6.1.0 → 7.0.0-canary.35625901699001

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 (214) hide show
  1. package/README.md +83 -2
  2. package/dist/approvals.d.ts +2 -1
  3. package/dist/artifacts-document.js +1 -1
  4. package/dist/artifacts-presentation.js +1 -1
  5. package/dist/artifacts-spreadsheet.js +1 -1
  6. package/dist/artifacts.js +3 -3
  7. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  8. package/dist/capability-catalog-row.d.ts +20 -0
  9. package/dist/capability-logo-fallback.d.ts +3 -0
  10. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  11. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  12. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  13. package/dist/chunk-5UONOQ3Y.js +635 -0
  14. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  15. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  16. package/dist/chunk-AKOMC3VO.js.map +1 -0
  17. package/dist/chunk-BRM6FVZA.js +302 -0
  18. package/dist/chunk-BRM6FVZA.js.map +1 -0
  19. package/dist/{chunk-KANFU34B.js → chunk-DBPVUGQT.js} +11 -2
  20. package/dist/chunk-DBPVUGQT.js.map +1 -0
  21. package/dist/chunk-FHIGTPME.js +1589 -0
  22. package/dist/chunk-FHIGTPME.js.map +1 -0
  23. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  24. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  25. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  26. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  27. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  28. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  29. package/dist/{chunk-TDSEILUH.js → chunk-MI6ALLLD.js} +1297 -1535
  30. package/dist/chunk-MI6ALLLD.js.map +1 -0
  31. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  32. package/dist/chunk-OWHPKAOV.js.map +1 -0
  33. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  34. package/dist/{chunk-L2NBTKRX.js → chunk-TXCBE32L.js} +203 -19
  35. package/dist/chunk-TXCBE32L.js.map +1 -0
  36. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  37. package/dist/chunk-UXE5SSNN.js.map +1 -0
  38. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  39. package/dist/chunk-VR4Z73EU.js.map +1 -0
  40. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  41. package/dist/chunk-XMXRI5JK.js.map +1 -0
  42. package/dist/client.d.ts +1 -1
  43. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  44. package/dist/components/chat-composer.d.ts +13 -3
  45. package/dist/components/markdown.d.ts +6 -3
  46. package/dist/components/message-timeline.d.ts +7 -2
  47. package/dist/components/preview-loading.d.ts +2 -0
  48. package/dist/components/session-capability-frame.d.ts +24 -0
  49. package/dist/components/session-chrome.d.ts +1 -2
  50. package/dist/components/session-commands.d.ts +6 -0
  51. package/dist/components/session-connection-request.d.ts +8 -0
  52. package/dist/components/session-conversation.d.ts +3 -0
  53. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  54. package/dist/components/timeline-search.d.ts +34 -0
  55. package/dist/composer.js +7 -7
  56. package/dist/connect-account-identity.d.ts +8 -0
  57. package/dist/connect-accounts.d.ts +5 -0
  58. package/dist/connect-chooser.d.ts +6 -0
  59. package/dist/connect-panel.d.ts +9 -2
  60. package/dist/connect-setup.d.ts +5 -1
  61. package/dist/connect.d.ts +5 -21
  62. package/dist/connect.js +1268 -584
  63. package/dist/connect.js.map +1 -1
  64. package/dist/connection-catalog.d.ts +6 -1
  65. package/dist/connection-discovery.d.ts +12 -0
  66. package/dist/connection-installed.d.ts +1 -0
  67. package/dist/connection-logo.d.ts +5 -2
  68. package/dist/connection-service-logo.d.ts +8 -0
  69. package/dist/connection-service-presentation.d.ts +13 -0
  70. package/dist/connection-skeleton.d.ts +5 -0
  71. package/dist/discovery-cache.d.ts +5 -0
  72. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  73. package/dist/hooks/use-connect.d.ts +22 -0
  74. package/dist/hooks/use-session-events.d.ts +5 -0
  75. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  76. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  77. package/dist/index.d.ts +8 -2
  78. package/dist/index.js +771 -793
  79. package/dist/index.js.map +1 -1
  80. package/dist/interaction.js +2 -2
  81. package/dist/machines.js +2 -2
  82. package/dist/mcp-connection-status.d.ts +14 -0
  83. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  84. package/dist/model-policy.d.ts +6 -2
  85. package/dist/model-policy.js +1 -1
  86. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  87. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  88. package/dist/plugin-discovery.d.ts +6 -0
  89. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  90. package/dist/realtime.js +22 -22
  91. package/dist/realtime.js.map +1 -1
  92. package/dist/session-auth-recommendation.d.ts +5 -0
  93. package/dist/session-capability-policy.d.ts +12 -0
  94. package/dist/session-ui.d.ts +2 -0
  95. package/dist/session-ui.js +14 -12
  96. package/dist/session.d.ts +1 -0
  97. package/dist/session.js +6 -4
  98. package/dist/skill-discovery.d.ts +11 -0
  99. package/dist/timeline/compaction-copy.d.ts +10 -0
  100. package/dist/timeline/projection.d.ts +3 -1
  101. package/dist/timeline/tool-display-name.d.ts +2 -1
  102. package/dist/timeline/turn-summary.d.ts +2 -2
  103. package/dist/timeline/types.d.ts +24 -2
  104. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  105. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  106. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  107. package/package.json +3 -3
  108. package/src/approvals.ts +8 -1
  109. package/src/assets/README.md +8 -0
  110. package/src/assets/gmail.ico +0 -0
  111. package/src/assets/outlook.ico +0 -0
  112. package/src/capability-catalog-row.tsx +94 -0
  113. package/src/capability-logo-fallback.ts +22 -0
  114. package/src/client.ts +0 -9
  115. package/src/components/approval-surface.tsx +12 -2
  116. package/src/components/artifacts/document-editor.tsx +63 -6
  117. package/src/components/artifacts/presentation-editor.tsx +214 -9
  118. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  119. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  120. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  121. package/src/components/chat-composer.tsx +26 -3
  122. package/src/components/composer.tsx +10 -4
  123. package/src/components/human-input-form.tsx +12 -2
  124. package/src/components/markdown.tsx +133 -9
  125. package/src/components/message-timeline.tsx +104 -36
  126. package/src/components/model-picker.tsx +11 -3
  127. package/src/components/model-policy-picker-menu.tsx +10 -3
  128. package/src/components/preview-loading.tsx +154 -0
  129. package/src/components/sandbox-workspace.tsx +20 -36
  130. package/src/components/session-capability-frame.tsx +163 -0
  131. package/src/components/session-chrome.tsx +46 -77
  132. package/src/components/session-commands.tsx +18 -0
  133. package/src/components/session-connection-request.tsx +97 -0
  134. package/src/components/session-conversation.tsx +10 -5
  135. package/src/components/session-mcp-capability-card.tsx +532 -0
  136. package/src/components/session-status.tsx +2 -2
  137. package/src/components/timeline-anchor.tsx +9 -8
  138. package/src/components/timeline-search.tsx +168 -0
  139. package/src/components/user-message-body.tsx +7 -0
  140. package/src/connect-account-identity.tsx +34 -0
  141. package/src/connect-accounts.tsx +114 -52
  142. package/src/connect-chooser.tsx +171 -39
  143. package/src/connect-panel.tsx +73 -9
  144. package/src/connect-setup.tsx +72 -25
  145. package/src/connect.ts +13 -22
  146. package/src/connection-assets.d.ts +4 -0
  147. package/src/connection-catalog.tsx +61 -39
  148. package/src/connection-discovery.tsx +235 -0
  149. package/src/connection-installed.tsx +2 -0
  150. package/src/connection-logo.tsx +18 -10
  151. package/src/connection-service-logo.tsx +55 -0
  152. package/src/connection-service-presentation.ts +46 -0
  153. package/src/connection-skeleton.tsx +16 -0
  154. package/src/conversation-timeline.ts +1 -0
  155. package/src/discovery-cache.ts +42 -0
  156. package/src/hooks/use-codex-accounts.ts +33 -14
  157. package/src/hooks/use-composer.ts +4 -5
  158. package/src/hooks/use-connect.ts +23 -0
  159. package/src/hooks/use-session-events.ts +114 -1
  160. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  161. package/src/hooks/use-workspace-sessions.ts +10 -0
  162. package/src/index.ts +22 -2
  163. package/src/mcp-connection-status.ts +35 -0
  164. package/src/model-policy.ts +21 -0
  165. package/src/plugin-details.tsx +8 -5
  166. package/src/plugin-discovery.tsx +92 -72
  167. package/src/realtime/realtime-control.tsx +6 -8
  168. package/src/session-auth-recommendation.ts +48 -0
  169. package/src/session-capability-policy.ts +83 -0
  170. package/src/session-ui.ts +2 -0
  171. package/src/session.ts +1 -0
  172. package/src/skill-discovery.tsx +101 -56
  173. package/src/timeline/compaction-copy.ts +40 -0
  174. package/src/timeline/platform-activity-row.tsx +8 -3
  175. package/src/timeline/projection.ts +78 -2
  176. package/src/timeline/rolling-activity.tsx +4 -2
  177. package/src/timeline/shared.tsx +6 -1
  178. package/src/timeline/startup-timings.tsx +1 -1
  179. package/src/timeline/tool-display-name.ts +8 -6
  180. package/src/timeline/tool-renderers.tsx +5 -3
  181. package/src/timeline/turn-summary.tsx +17 -3
  182. package/src/timeline/types.ts +24 -2
  183. package/styles/compiled.css +325 -27
  184. package/styles/connect.css +90 -15
  185. package/styles/index.css +46 -0
  186. package/styles/session-capability.css +42 -0
  187. package/dist/chunk-45XEUVTK.js.map +0 -1
  188. package/dist/chunk-HQV2I5HV.js +0 -124
  189. package/dist/chunk-HQV2I5HV.js.map +0 -1
  190. package/dist/chunk-HUIKIDM5.js.map +0 -1
  191. package/dist/chunk-ICA6R6TW.js.map +0 -1
  192. package/dist/chunk-IHQII5WO.js +0 -886
  193. package/dist/chunk-IHQII5WO.js.map +0 -1
  194. package/dist/chunk-KANFU34B.js.map +0 -1
  195. package/dist/chunk-L2NBTKRX.js.map +0 -1
  196. package/dist/chunk-NYESIADM.js.map +0 -1
  197. package/dist/chunk-PEI6RANI.js.map +0 -1
  198. package/dist/chunk-TDSEILUH.js.map +0 -1
  199. package/dist/chunk-UMECRJRV.js.map +0 -1
  200. package/dist/hooks/use-packs.d.ts +0 -32
  201. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  202. package/src/hooks/use-packs.ts +0 -157
  203. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  204. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  205. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  206. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  207. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  208. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  209. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  210. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  211. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  212. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  213. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  214. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -0,0 +1,532 @@
1
+ import {
2
+ authorizeConnectAttempt,
3
+ ConnectController,
4
+ createBrowserConnectNavigation,
5
+ reserveBrowserConnectNavigation,
6
+ type ConnectAttempt,
7
+ type ConnectNavigation,
8
+ type ConnectOwnership,
9
+ } from "@opengeni/connect";
10
+ import type { CapabilityCatalogItem } from "@opengeni/sdk";
11
+ import type { OpenGeniBrowserClient as OpenGeniClient } from "@opengeni/sdk/browser";
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { useConnect } from "../hooks/use-connect";
14
+ import { attachSessionCapability, sessionCapabilityTools } from "../session-capability-policy";
15
+ import { SessionCapabilityFrame } from "./session-capability-frame";
16
+ import { matchingActiveMcpConnections } from "../mcp-connection-status";
17
+
18
+ export type SessionMcpCapabilityCardProps = {
19
+ client: OpenGeniClient;
20
+ workspaceId: string;
21
+ sessionId: string;
22
+ capabilityId: string;
23
+ name: string;
24
+ rationale?: string;
25
+ /** Exact host route; no console-specific routing is assumed. */
26
+ returnUrl: string;
27
+ onConfigured?: (() => void | Promise<void>) | undefined;
28
+ };
29
+
30
+ export type McpConnectionCardProps = Omit<SessionMcpCapabilityCardProps, "sessionId"> & {
31
+ /** Omit for connection management: no session tool selection is written. */
32
+ sessionId?: string;
33
+ dialogOnly?: boolean;
34
+ onClose?: (() => void) | undefined;
35
+ };
36
+
37
+ /** Native OAuth recommendation flow. Identity and endpoint come from the live
38
+ * catalog, never from model-authored recommendation text. */
39
+ export function SessionMcpCapabilityCard(props: SessionMcpCapabilityCardProps) {
40
+ return <McpConnectionCard {...props} />;
41
+ }
42
+
43
+ export function McpConnectionCard(props: McpConnectionCardProps) {
44
+ const [client, setClient] = useState(props.client);
45
+ const [generation, setGeneration] = useState(0);
46
+ if (client !== props.client) {
47
+ setClient(props.client);
48
+ setGeneration(generation + 1);
49
+ }
50
+ return (
51
+ <ScopedCard
52
+ key={`${generation}:${props.workspaceId}:${props.sessionId}:${props.capabilityId}`}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function ScopedCard({
59
+ client,
60
+ workspaceId,
61
+ sessionId,
62
+ capabilityId,
63
+ name,
64
+ rationale = "",
65
+ returnUrl,
66
+ onConfigured,
67
+ dialogOnly = false,
68
+ onClose,
69
+ }: McpConnectionCardProps) {
70
+ const [controller] = useState(
71
+ () => new ConnectController(client.connectTransport(), workspaceId),
72
+ );
73
+ const view = useConnect(controller);
74
+ const [item, setItem] = useState<CapabilityCatalogItem | null>(null);
75
+ const [logo, setLogo] = useState<string | null>(null);
76
+ const [expanded, setExpanded] = useState(dialogOnly);
77
+ const [complete, setComplete] = useState(false);
78
+ const [connected, setConnected] = useState(false);
79
+ const [waiting, setWaiting] = useState(false);
80
+ const [busy, setBusy] = useState(false);
81
+ const [error, setError] = useState<string | null>(null);
82
+ const [ownership, setOwnership] = useState<ConnectOwnership>("workspace");
83
+ const lifetime = useRef<AbortController | null>(null);
84
+ const operation = useRef(false);
85
+ const startKey = useRef(crypto.randomUUID());
86
+ const advanceKey = useRef(crypto.randomUUID());
87
+ const storageKey = sessionId
88
+ ? `opengeni:session-connect:${workspaceId}:${sessionId}:${capabilityId}`
89
+ : `opengeni:workspace-connect:${workspaceId}:${capabilityId}`;
90
+ const current = () => !!lifetime.current && !lifetime.current.signal.aborted;
91
+
92
+ async function load() {
93
+ const invocation = lifetime.current;
94
+ const [catalog, session] = await Promise.all([
95
+ client.listCapabilities(workspaceId),
96
+ sessionId ? client.getSession(workspaceId, sessionId) : Promise.resolve(null),
97
+ ]);
98
+ if (!invocation || invocation.signal.aborted || lifetime.current !== invocation) return null;
99
+ const resolved = catalog.items.find((entry) => entry.id === capabilityId);
100
+ if (!resolved || resolved.kind !== "mcp" || resolved.authKind !== "oauth2")
101
+ throw new Error(
102
+ "This recommendation is not an available OAuth MCP integration. Review the current connection catalog.",
103
+ );
104
+ setItem(resolved);
105
+ if (resolved.connectionRef)
106
+ setOwnership(resolved.connectionRef.subjectScope === "subject" ? "personal" : "workspace");
107
+ else if (!item && resolved.metadata?.defaultConnectionOwnership === "personal")
108
+ setOwnership("personal");
109
+ let accountReady = false;
110
+ if (resolved.enabled && resolved.connectionRef) {
111
+ const connections =
112
+ resolved.connectionRef.subjectScope === "subject"
113
+ ? await client.listOwnConnectionAccounts(workspaceId)
114
+ : await client.listConnections(workspaceId);
115
+ if (invocation.signal.aborted || lifetime.current !== invocation) return null;
116
+ const matches = matchingActiveMcpConnections(resolved, connections);
117
+ accountReady = matches.length === 1;
118
+ }
119
+ if (invocation.signal.aborted || lifetime.current !== invocation) return null;
120
+ const selected =
121
+ session?.toolPolicy.mode === "workspace_default"
122
+ ? session.effectiveToolPolicy?.selectedIds
123
+ : session?.tools.filter((tool) => tool.kind === "mcp").map((tool) => tool.id);
124
+ setConnected(accountReady);
125
+ setComplete(
126
+ accountReady &&
127
+ !!selected &&
128
+ sessionCapabilityTools(resolved).every((tool) => selected.includes(tool.id)),
129
+ );
130
+ return resolved;
131
+ }
132
+
133
+ useEffect(() => {
134
+ const abort = new AbortController();
135
+ lifetime.current = abort;
136
+ // Mount reads are scoped to this effect, not to the user-action lock.
137
+ // Strict Mode may cancel the first mount while its promise is still in
138
+ // flight; that must not suppress the replacement mount's initialization.
139
+ if (dialogOnly) setBusy(true);
140
+ void (async () => {
141
+ await load();
142
+ if (abort.signal.aborted || !dialogOnly) return;
143
+ await recoverSavedAttempt();
144
+ })()
145
+ .catch((failure) => {
146
+ if (!abort.signal.aborted)
147
+ setError(
148
+ failure instanceof Error ? failure.message : "Couldn't load connection details.",
149
+ );
150
+ })
151
+ .finally(() => {
152
+ if (!abort.signal.aborted && dialogOnly) setBusy(false);
153
+ });
154
+ return () => {
155
+ abort.abort();
156
+ };
157
+ // ScopedCard remounts for client/workspace/session/capability changes.
158
+ // These reads initialize that scope; rerunning on action closures would
159
+ // cancel initialization whenever its own state updates render the card.
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, [client, workspaceId, sessionId, capabilityId, dialogOnly]);
162
+
163
+ useEffect(() => {
164
+ const path = item?.logoAssetPath;
165
+ setLogo(null);
166
+ if (!path?.startsWith("catalog-assets/")) return;
167
+ const abort = new AbortController();
168
+ let objectUrl: string | null = null;
169
+ // Embeds can require authentication even for the API's public catalog
170
+ // asset route. Use the client's transport instead of an unauthenticated img.
171
+ void client
172
+ .downloadCatalogAsset(path, { signal: abort.signal })
173
+ .then((blob) => {
174
+ if (abort.signal.aborted || !blob.type.startsWith("image/") || blob.size > 2_000_000)
175
+ return;
176
+ objectUrl = URL.createObjectURL(blob);
177
+ setLogo(objectUrl);
178
+ })
179
+ .catch(() => {
180
+ /* Keep the provider's initials if its passive mark is unavailable. */
181
+ });
182
+ return () => {
183
+ abort.abort();
184
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
185
+ };
186
+ }, [client, item?.logoAssetPath]);
187
+
188
+ async function run(action: () => Promise<void>) {
189
+ if (operation.current || !current()) return;
190
+ operation.current = true;
191
+ setBusy(true);
192
+ setError(null);
193
+ try {
194
+ await action();
195
+ } catch (failure) {
196
+ if (current())
197
+ setError(
198
+ failure instanceof Error
199
+ ? failure.message
200
+ : "Connection setup could not finish. Try again.",
201
+ );
202
+ } finally {
203
+ operation.current = false;
204
+ if (current()) setBusy(false);
205
+ }
206
+ }
207
+
208
+ function remember(attemptId: string) {
209
+ // An opaque recovery hint only. The backend rechecks the authenticated
210
+ // owner on recover; this value never grants access or proves completion.
211
+ try {
212
+ sessionStorage.setItem(storageKey, attemptId);
213
+ } catch {
214
+ /* Storage may be disabled. */
215
+ }
216
+ }
217
+
218
+ async function reconcile(attempt: ConnectAttempt) {
219
+ if (!current()) return;
220
+ if (attempt.state !== "complete" || !attempt.account || !attempt.credentialsCommitted) return;
221
+ const resolved = await load();
222
+ if (!resolved || !current()) return;
223
+ const connection = (await client.listConnections(workspaceId)).find(
224
+ (entry) => entry.id === attempt.account!.id,
225
+ );
226
+ if (!current()) return;
227
+ if (
228
+ !connection ||
229
+ connection.status !== "active" ||
230
+ connection.kind !== "oauth2" ||
231
+ connection.metadata.mcpUrl !== (resolved.mcpUrl ?? resolved.endpointUrl) ||
232
+ (connection.subjectId !== null) !== (attempt.ownership === "personal")
233
+ )
234
+ throw new Error(
235
+ "The authorized account does not match this integration. Review its connection settings.",
236
+ );
237
+ const previous = resolved.connectionRef;
238
+ if (
239
+ resolved.enabled &&
240
+ previous?.subjectScope !== "subject" &&
241
+ previous?.connectionId &&
242
+ previous.connectionId !== connection.id
243
+ )
244
+ throw new Error(
245
+ "This integration's selected account changed during setup. Review it before continuing.",
246
+ );
247
+ await client.enableCapability(workspaceId, capabilityId, {
248
+ connectionRef:
249
+ attempt.ownership === "personal"
250
+ ? { providerDomain: connection.providerDomain, kind: "oauth2", subjectScope: "subject" }
251
+ : {
252
+ providerDomain: connection.providerDomain,
253
+ kind: "oauth2",
254
+ subjectScope: "workspace",
255
+ connectionId: connection.id,
256
+ },
257
+ });
258
+ if (!current()) return;
259
+ const enabled = await load();
260
+ if (enabled && current()) {
261
+ await finishConnection(enabled);
262
+ }
263
+ try {
264
+ sessionStorage.removeItem(storageKey);
265
+ } catch {
266
+ /* Recovery hint only. */
267
+ }
268
+ }
269
+
270
+ async function open() {
271
+ setExpanded(true);
272
+ await run(async () => {
273
+ await load();
274
+ if (!current()) return;
275
+ await recoverSavedAttempt();
276
+ });
277
+ }
278
+
279
+ async function recoverSavedAttempt() {
280
+ let saved: string | null = null;
281
+ try {
282
+ saved = sessionStorage.getItem(storageKey);
283
+ } catch {
284
+ /* No persisted attempt. */
285
+ }
286
+ if (saved) {
287
+ const attempt = await controller.recover(saved);
288
+ await reconcile(attempt);
289
+ }
290
+ }
291
+
292
+ async function begin() {
293
+ await run(async () => {
294
+ if (view.attempt && ["failed", "expired", "cancelled"].includes(view.attempt.state)) {
295
+ startKey.current = crypto.randomUUID();
296
+ advanceKey.current = crypto.randomUUID();
297
+ }
298
+ const reserved = reserveBrowserConnectNavigation(window);
299
+ try {
300
+ const resolved = await load();
301
+ if (!resolved || !current()) return;
302
+ if (
303
+ resolved.connectionRef &&
304
+ ownership !==
305
+ (resolved.connectionRef.subjectScope === "subject" ? "personal" : "workspace")
306
+ )
307
+ throw new Error(
308
+ "The connection ownership changed. Review the current account before continuing.",
309
+ );
310
+ const mcpUrl = resolved.mcpUrl ?? resolved.endpointUrl;
311
+ if (!mcpUrl)
312
+ throw new Error(
313
+ "The current catalog has no authorization endpoint for this integration.",
314
+ );
315
+ // Never silently replace an already selected account. Reauthorization
316
+ // stays on its exact native connection ID.
317
+ let reconnectAccountId = resolved.connectionRef?.connectionId ?? undefined;
318
+ if (resolved.connectionRef?.subjectScope === "subject") {
319
+ const connections = (await client.listConnections(workspaceId)).filter(
320
+ (entry) =>
321
+ entry.subjectId !== null &&
322
+ entry.kind === "oauth2" &&
323
+ entry.metadata.mcpUrl === mcpUrl,
324
+ );
325
+ if (!current()) return;
326
+ if (connections.length > 1)
327
+ throw new Error(
328
+ "More than one personal account matches. Choose the account in connection settings before reconnecting.",
329
+ );
330
+ reconnectAccountId = connections[0]?.id;
331
+ }
332
+ let attempt = await controller.begin({
333
+ providerId: "mcp-oauth",
334
+ ownership,
335
+ returnUrl,
336
+ idempotencyKey: startKey.current,
337
+ ...(reconnectAccountId ? { reconnectAccountId } : {}),
338
+ });
339
+ if (!current()) return;
340
+ remember(attempt.id);
341
+ if (attempt.state === "credential_input")
342
+ attempt = await controller.advance(
343
+ { type: "credentials", values: { mcpUrl } },
344
+ advanceKey.current,
345
+ );
346
+ if (attempt.nextAction.type === "authorize")
347
+ await performAuthorization(attempt, reserved.navigation);
348
+ else await reconcile(attempt);
349
+ } finally {
350
+ reserved.close();
351
+ }
352
+ });
353
+ }
354
+
355
+ async function performAuthorization(attempt: ConnectAttempt, navigation: ConnectNavigation) {
356
+ setWaiting(true);
357
+ try {
358
+ const result = await authorizeConnectAttempt(controller.transport, attempt, navigation, {
359
+ mode: "popup",
360
+ signal: lifetime.current!.signal,
361
+ });
362
+ if (!current() || !result) return;
363
+ await controller.refresh();
364
+ await reconcile(result);
365
+ if (result.state !== "complete")
366
+ throw new Error("Sign-in did not finish. You can try connecting again.");
367
+ } finally {
368
+ if (current()) setWaiting(false);
369
+ }
370
+ }
371
+
372
+ function authorize(attempt: ConnectAttempt) {
373
+ // run invokes action synchronously, retaining the browser click gesture.
374
+ return run(() => performAuthorization(attempt, createBrowserConnectNavigation(window)));
375
+ }
376
+
377
+ async function finishConnection(capability: CapabilityCatalogItem) {
378
+ if (!sessionId) {
379
+ // Connection management does not select session tools.
380
+ await onConfigured?.();
381
+ if (!current()) return;
382
+ setComplete(true);
383
+ setExpanded(false);
384
+ onClose?.();
385
+ return;
386
+ }
387
+ if (capability.connectionRef && capability.connectionRef.subjectScope !== "subject") {
388
+ const selected = (await client.listConnections(workspaceId)).find(
389
+ (entry) => entry.id === capability.connectionRef?.connectionId,
390
+ );
391
+ if (!current()) return;
392
+ if (!selected || selected.status !== "active")
393
+ throw new Error("This account needs reconnection before it can be used here.");
394
+ }
395
+ if (capability.connectionRef?.subjectScope === "subject") {
396
+ const accounts = await client.listOwnConnectionAccounts(workspaceId);
397
+ if (!current()) return;
398
+ if (matchingActiveMcpConnections(capability, accounts).length === 0)
399
+ throw new Error("Your account needs reconnection before it can be used here.");
400
+ }
401
+ if (!current()) return;
402
+ await attachSessionCapability(client, workspaceId, sessionId, capability, current);
403
+ if (!current()) return;
404
+ await onConfigured?.();
405
+ if (!current()) return;
406
+ setComplete(true);
407
+ setExpanded(false);
408
+ }
409
+
410
+ async function useHere() {
411
+ await run(async () => {
412
+ if (item) await finishConnection(item);
413
+ });
414
+ }
415
+
416
+ return (
417
+ <SessionCapabilityFrame
418
+ name={item?.name ?? name}
419
+ subtitle={item?.providerDomain ?? ""}
420
+ logo={logo}
421
+ typeLabel="MCP server"
422
+ description={item?.description || rationale}
423
+ skill={false}
424
+ expanded={expanded}
425
+ complete={complete}
426
+ actionLabel={
427
+ connected
428
+ ? sessionId
429
+ ? "Use in this conversation"
430
+ : "Connected"
431
+ : `Connect ${item?.name ?? name}`
432
+ }
433
+ note={
434
+ sessionId
435
+ ? "Review access before signing in. You'll return to this conversation after authorization."
436
+ : "Review access before signing in. You'll return here after authorization."
437
+ }
438
+ onOpen={() => void open()}
439
+ onClose={() => {
440
+ setExpanded(false);
441
+ onClose?.();
442
+ }}
443
+ busy={busy || view.busy}
444
+ dialogOnly={dialogOnly}
445
+ >
446
+ <div className="og-session-capability-setup">
447
+ {error ? <p role="alert">{error}</p> : null}
448
+ {!item ? (
449
+ <>
450
+ <p role="status">
451
+ {busy ? "Loading connection details…" : "Connection details are unavailable."}
452
+ </p>
453
+ <button disabled={busy} onClick={() => void open()}>
454
+ Retry
455
+ </button>
456
+ </>
457
+ ) : (
458
+ <>
459
+ <p>{item.description || rationale}</p>
460
+ {busy ? (
461
+ <p role="status" className="og-session-capability-progress">
462
+ {waiting
463
+ ? `Finish signing in with ${item.name} in the opened window. This will close automatically when you’re connected.`
464
+ : "Preparing your connection…"}
465
+ </p>
466
+ ) : !connected ? (
467
+ <>
468
+ {!item.connectionRef ? (
469
+ <fieldset disabled={busy}>
470
+ <legend>Who can use this connection?</legend>
471
+ <label>
472
+ <input
473
+ type="radio"
474
+ name={`${capabilityId}-ownership`}
475
+ checked={ownership === "workspace"}
476
+ onChange={() => setOwnership("workspace")}
477
+ />
478
+ Everyone in this workspace
479
+ </label>
480
+ <label>
481
+ <input
482
+ type="radio"
483
+ name={`${capabilityId}-ownership`}
484
+ checked={ownership === "personal"}
485
+ onChange={() => setOwnership("personal")}
486
+ />
487
+ Only me
488
+ </label>
489
+ </fieldset>
490
+ ) : null}
491
+ <p className="og-session-capability-scope">
492
+ {ownership === "workspace"
493
+ ? sessionId
494
+ ? "This connection will be available to your workspace and used in this conversation."
495
+ : "This connection will be available to your workspace."
496
+ : "This connection belongs to you. Your messages can use it; other participants use their own accounts."}
497
+ </p>
498
+ <button
499
+ className="og-session-capability-primary"
500
+ onClick={() => {
501
+ if (view.attempt?.nextAction.type === "authorize") void authorize(view.attempt);
502
+ else void begin();
503
+ }}
504
+ >
505
+ {error && view.attempt?.nextAction.type === "authorize"
506
+ ? "Try signing in again"
507
+ : `Continue to ${item.name}`}
508
+ </button>
509
+ </>
510
+ ) : null}
511
+ {connected && !busy ? (
512
+ <>
513
+ <p className="og-session-capability-scope">
514
+ {sessionId
515
+ ? "Your account is connected. Enable it for this conversation to finish."
516
+ : "Your account is connected."}
517
+ </p>
518
+ <button
519
+ className="og-session-capability-primary"
520
+ disabled={busy || view.busy}
521
+ onClick={() => void useHere()}
522
+ >
523
+ {sessionId ? "Use in this conversation" : "Done"}
524
+ </button>
525
+ </>
526
+ ) : null}
527
+ </>
528
+ )}
529
+ </div>
530
+ </SessionCapabilityFrame>
531
+ );
532
+ }
@@ -12,10 +12,10 @@ export type SessionStatusMeta = {
12
12
 
13
13
  export const SESSION_STATUS_META: Record<SessionStatusValue, SessionStatusMeta> = {
14
14
  queued: {
15
- label: "Starting",
15
+ label: "Queued",
16
16
  dotClassName: "bg-og-status-queued",
17
17
  badgeClassName: "text-og-fg-muted border-og-border bg-og-status-queued/10",
18
- pulse: true,
18
+ pulse: false,
19
19
  },
20
20
  running: {
21
21
  label: "Running",
@@ -21,9 +21,10 @@ export class TimelineBeforeLayout extends Component<{
21
21
  export function captureTimelineAnchor(scroller: HTMLElement): TimelineAnchor | null {
22
22
  const viewport = scroller.getBoundingClientRect();
23
23
  if (viewport.height <= 0) return null;
24
- const groups = Array.from(scroller.querySelectorAll<HTMLElement>("[data-og-group-key]")).filter(
25
- (group) => group.getBoundingClientRect().height > 0,
26
- );
24
+ // Capture each box once: these measurements share one pre-commit DOM snapshot.
25
+ const groups = Array.from(scroller.querySelectorAll<HTMLElement>("[data-og-group-key]"))
26
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
27
+ .filter(({ rect }) => rect.height > 0);
27
28
  const anchors: TimelineAnchor = [];
28
29
  // A disclosure is the reader's explicit point of interaction. In particular,
29
30
  // anchoring a paragraph below an expanding disclosure would move its button.
@@ -39,8 +40,7 @@ export function captureTimelineAnchor(scroller: HTMLElement): TimelineAnchor | n
39
40
  }
40
41
  }
41
42
  // A paragraph survives even when earlier deltas reconstruct its containing message.
42
- for (const group of groups) {
43
- const rect = group.getBoundingClientRect();
43
+ for (const { element: group, rect } of groups) {
44
44
  if (rect.bottom <= viewport.top || rect.top >= viewport.bottom) continue;
45
45
  for (const element of group.querySelectorAll<HTMLElement>("p, li, pre, h1, h2, h3, h4")) {
46
46
  const box = element.getBoundingClientRect();
@@ -52,11 +52,11 @@ export function captureTimelineAnchor(scroller: HTMLElement): TimelineAnchor | n
52
52
  }
53
53
  // Prefer a retained visible row, then a following row. A following row also
54
54
  // anchors the unchanged suffix of a partially loaded message above it.
55
- const rows = groups.map((element) => ({
55
+ const rows = groups.map(({ element, rect }) => ({
56
56
  element,
57
57
  key: element.getAttribute("data-og-group-key"),
58
58
  text: null,
59
- top: element.getBoundingClientRect().top,
59
+ top: rect.top,
60
60
  }));
61
61
  anchors.push(...rows.filter((row) => row.top >= viewport.top));
62
62
  anchors.push(...rows.filter((row) => row.top < viewport.top).reverse());
@@ -69,7 +69,7 @@ export function timelineAnchorCorrection(
69
69
  anchors: TimelineAnchor,
70
70
  ): number | null {
71
71
  let blocks: HTMLElement[] | undefined;
72
- const groups = Array.from(scroller.querySelectorAll<HTMLElement>("[data-og-group-key]"));
72
+ let groups: HTMLElement[] | undefined;
73
73
  for (const anchor of anchors) {
74
74
  let element: HTMLElement | undefined;
75
75
  if (
@@ -78,6 +78,7 @@ export function timelineAnchorCorrection(
78
78
  ) {
79
79
  element = anchor.element;
80
80
  } else if (anchor.key) {
81
+ groups ??= Array.from(scroller.querySelectorAll<HTMLElement>("[data-og-group-key]"));
81
82
  element = groups.find((group) => group.getAttribute("data-og-group-key") === anchor.key);
82
83
  } else if (anchor.text) {
83
84
  blocks ??= Array.from(scroller.querySelectorAll<HTMLElement>("p, li, pre, h1, h2, h3, h4"));