@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.
- package/README.md +83 -2
- package/dist/approvals.d.ts +2 -1
- 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-KANFU34B.js → chunk-DBPVUGQT.js} +11 -2
- package/dist/chunk-DBPVUGQT.js.map +1 -0
- package/dist/chunk-FHIGTPME.js +1589 -0
- package/dist/chunk-FHIGTPME.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-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
- package/dist/{chunk-TDSEILUH.js → chunk-MI6ALLLD.js} +1297 -1535
- package/dist/chunk-MI6ALLLD.js.map +1 -0
- 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-L2NBTKRX.js → chunk-TXCBE32L.js} +203 -19
- package/dist/chunk-TXCBE32L.js.map +1 -0
- 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 +6 -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-conversation.d.ts +3 -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/tool-display-name.d.ts +2 -1
- package/dist/timeline/turn-summary.d.ts +2 -2
- package/dist/timeline/types.d.ts +24 -2
- 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/approvals.ts +8 -1
- 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/approval-surface.tsx +12 -2
- 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 +133 -9
- 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 +10 -5
- 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 +78 -2
- package/src/timeline/rolling-activity.tsx +4 -2
- package/src/timeline/shared.tsx +6 -1
- package/src/timeline/startup-timings.tsx +1 -1
- package/src/timeline/tool-display-name.ts +8 -6
- package/src/timeline/tool-renderers.tsx +5 -3
- package/src/timeline/turn-summary.tsx +17 -3
- package/src/timeline/types.ts +24 -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
|
@@ -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: "
|
|
15
|
+
label: "Queued",
|
|
16
16
|
dotClassName: "bg-og-status-queued",
|
|
17
17
|
badgeClassName: "text-og-fg-muted border-og-border bg-og-status-queued/10",
|
|
18
|
-
pulse:
|
|
18
|
+
pulse: false,
|
|
19
19
|
},
|
|
20
20
|
running: {
|
|
21
21
|
label: "Running",
|
|
@@ -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
|
-
|
|
25
|
-
|
|
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:
|
|
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
|
-
|
|
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"));
|