@stigmer/react 0.0.84 → 0.0.86

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 (87) hide show
  1. package/demo/fixtures.d.ts +4 -0
  2. package/demo/fixtures.d.ts.map +1 -1
  3. package/demo/fixtures.js +4 -0
  4. package/demo/fixtures.js.map +1 -1
  5. package/demo/samples.d.ts +1 -0
  6. package/demo/samples.d.ts.map +1 -1
  7. package/demo/samples.js +1 -0
  8. package/demo/samples.js.map +1 -1
  9. package/execution/ArtifactPreviewModal.d.ts +78 -18
  10. package/execution/ArtifactPreviewModal.d.ts.map +1 -1
  11. package/execution/ArtifactPreviewModal.js +82 -60
  12. package/execution/ArtifactPreviewModal.js.map +1 -1
  13. package/execution/index.d.ts +2 -2
  14. package/execution/index.d.ts.map +1 -1
  15. package/execution/index.js +1 -1
  16. package/execution/index.js.map +1 -1
  17. package/index.d.ts +6 -4
  18. package/index.d.ts.map +1 -1
  19. package/index.js +4 -2
  20. package/index.js.map +1 -1
  21. package/library/ResourceListView.js +1 -1
  22. package/library/ResourceListView.js.map +1 -1
  23. package/mcp-server/McpServerConnectDialog.d.ts +51 -0
  24. package/mcp-server/McpServerConnectDialog.d.ts.map +1 -0
  25. package/mcp-server/McpServerConnectDialog.js +164 -0
  26. package/mcp-server/McpServerConnectDialog.js.map +1 -0
  27. package/mcp-server/McpServerDetailView.js +2 -2
  28. package/mcp-server/McpServerDetailView.js.map +1 -1
  29. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  30. package/mcp-server/McpServerPicker.js +7 -1
  31. package/mcp-server/McpServerPicker.js.map +1 -1
  32. package/mcp-server/index.d.ts +2 -0
  33. package/mcp-server/index.d.ts.map +1 -1
  34. package/mcp-server/index.js +1 -0
  35. package/mcp-server/index.js.map +1 -1
  36. package/oauth-app/CreateOAuthAppForm.d.ts +41 -0
  37. package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -0
  38. package/oauth-app/CreateOAuthAppForm.js +140 -0
  39. package/oauth-app/CreateOAuthAppForm.js.map +1 -0
  40. package/oauth-app/OAuthAppDetailPanel.d.ts +43 -0
  41. package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -0
  42. package/oauth-app/OAuthAppDetailPanel.js +202 -0
  43. package/oauth-app/OAuthAppDetailPanel.js.map +1 -0
  44. package/oauth-app/OAuthAppListPanel.d.ts +43 -0
  45. package/oauth-app/OAuthAppListPanel.d.ts.map +1 -0
  46. package/oauth-app/OAuthAppListPanel.js +79 -0
  47. package/oauth-app/OAuthAppListPanel.js.map +1 -0
  48. package/oauth-app/index.d.ts +15 -0
  49. package/oauth-app/index.d.ts.map +1 -0
  50. package/oauth-app/index.js +8 -0
  51. package/oauth-app/index.js.map +1 -0
  52. package/oauth-app/useCreateOAuthApp.d.ts +39 -0
  53. package/oauth-app/useCreateOAuthApp.d.ts.map +1 -0
  54. package/oauth-app/useCreateOAuthApp.js +50 -0
  55. package/oauth-app/useCreateOAuthApp.js.map +1 -0
  56. package/oauth-app/useDeleteOAuthApp.d.ts +31 -0
  57. package/oauth-app/useDeleteOAuthApp.d.ts.map +1 -0
  58. package/oauth-app/useDeleteOAuthApp.js +43 -0
  59. package/oauth-app/useDeleteOAuthApp.js.map +1 -0
  60. package/oauth-app/useOAuthAppList.d.ts +32 -0
  61. package/oauth-app/useOAuthAppList.d.ts.map +1 -0
  62. package/oauth-app/useOAuthAppList.js +61 -0
  63. package/oauth-app/useOAuthAppList.js.map +1 -0
  64. package/oauth-app/useUpdateOAuthApp.d.ts +38 -0
  65. package/oauth-app/useUpdateOAuthApp.d.ts.map +1 -0
  66. package/oauth-app/useUpdateOAuthApp.js +49 -0
  67. package/oauth-app/useUpdateOAuthApp.js.map +1 -0
  68. package/package.json +4 -4
  69. package/src/demo/fixtures.ts +8 -0
  70. package/src/demo/samples.ts +2 -0
  71. package/src/execution/ArtifactPreviewModal.tsx +206 -128
  72. package/src/execution/index.ts +2 -2
  73. package/src/index.ts +24 -0
  74. package/src/library/ResourceListView.tsx +8 -8
  75. package/src/mcp-server/McpServerConnectDialog.tsx +527 -0
  76. package/src/mcp-server/McpServerDetailView.tsx +2 -1
  77. package/src/mcp-server/McpServerPicker.tsx +8 -1
  78. package/src/mcp-server/index.ts +3 -0
  79. package/src/oauth-app/CreateOAuthAppForm.tsx +449 -0
  80. package/src/oauth-app/OAuthAppDetailPanel.tsx +671 -0
  81. package/src/oauth-app/OAuthAppListPanel.tsx +237 -0
  82. package/src/oauth-app/index.ts +14 -0
  83. package/src/oauth-app/useCreateOAuthApp.ts +70 -0
  84. package/src/oauth-app/useDeleteOAuthApp.ts +62 -0
  85. package/src/oauth-app/useOAuthAppList.ts +84 -0
  86. package/src/oauth-app/useUpdateOAuthApp.ts +69 -0
  87. package/styles.css +1 -1
@@ -114,8 +114,8 @@ export type { ArtifactCardProps } from "./ArtifactCard";
114
114
  export { ArtifactContentRenderer } from "./ArtifactContentRenderer";
115
115
  export type { ArtifactContentRendererProps } from "./ArtifactContentRenderer";
116
116
 
117
- export { ArtifactPreviewModal } from "./ArtifactPreviewModal";
118
- export type { ArtifactPreviewModalProps } from "./ArtifactPreviewModal";
117
+ export { ArtifactPreviewContent, ArtifactPreviewModal } from "./ArtifactPreviewModal";
118
+ export type { ArtifactPreviewContentProps, ArtifactPreviewModalProps } from "./ArtifactPreviewModal";
119
119
 
120
120
  export { ArtifactsWidget } from "./ArtifactsWidget";
121
121
  export type { ArtifactsWidgetProps } from "./ArtifactsWidget";
package/src/index.ts CHANGED
@@ -112,6 +112,7 @@ export {
112
112
  ApprovalCard,
113
113
  ArtifactCard,
114
114
  ArtifactContentRenderer,
115
+ ArtifactPreviewContent,
115
116
  ArtifactPreviewModal,
116
117
  ArtifactsWidget,
117
118
  WriteBacksWidget,
@@ -162,6 +163,7 @@ export type {
162
163
  ArtifactCardProps,
163
164
  ArtifactContentRendererProps,
164
165
  ArtifactRenderMode,
166
+ ArtifactPreviewContentProps,
165
167
  ArtifactPreviewModalProps,
166
168
  ArtifactsWidgetProps,
167
169
  WriteBacksWidgetProps,
@@ -227,6 +229,7 @@ export {
227
229
  McpServerPicker,
228
230
  McpServerConfigPanel,
229
231
  McpServerDetailView,
232
+ McpServerConnectDialog,
230
233
  McpToolSelector,
231
234
  toServerKey,
232
235
  } from "./mcp-server";
@@ -249,6 +252,7 @@ export type {
249
252
  McpServerCredentialsProps,
250
253
  McpServerOAuthSignInProps,
251
254
  McpServerDetailViewProps,
255
+ McpServerConnectDialogProps,
252
256
  CapabilityTab,
253
257
  McpToolSelectorProps,
254
258
  UseMcpServerConnectReturn,
@@ -446,6 +450,26 @@ export type {
446
450
  ApiKeyCreatedAlertProps,
447
451
  } from "./api-key";
448
452
 
453
+ // OAuth App — data hooks, mutation hooks, and styled components for OAuth app management
454
+ export {
455
+ useOAuthAppList,
456
+ useCreateOAuthApp,
457
+ useUpdateOAuthApp,
458
+ useDeleteOAuthApp,
459
+ OAuthAppListPanel,
460
+ CreateOAuthAppForm,
461
+ OAuthAppDetailPanel,
462
+ } from "./oauth-app";
463
+ export type {
464
+ UseOAuthAppListReturn,
465
+ UseCreateOAuthAppReturn,
466
+ UseUpdateOAuthAppReturn,
467
+ UseDeleteOAuthAppReturn,
468
+ OAuthAppListPanelProps,
469
+ CreateOAuthAppFormProps,
470
+ OAuthAppDetailPanelProps,
471
+ } from "./oauth-app";
472
+
449
473
  // Identity Provider — data hooks, mutation hooks, styled components, presets, and guided wizard for IdP management and SSO discovery
450
474
  export {
451
475
  useIdentityProviderList,
@@ -537,14 +537,9 @@ function DefaultResourceCard({
537
537
  <div className="flex items-start gap-3">
538
538
  <ResourceIcon kind={item.kind} iconUrl={item.iconUrl} />
539
539
  <div className="min-w-0 flex-1">
540
- <div className="flex items-center gap-2">
541
- <span className="truncate text-sm font-semibold text-foreground">
542
- {displayName}
543
- </span>
544
- {item.visibility === ApiResourceVisibility.visibility_public && (
545
- <VisibilityBadge />
546
- )}
547
- </div>
540
+ <span className="line-clamp-2 text-sm font-semibold leading-snug text-foreground">
541
+ {displayName}
542
+ </span>
548
543
  <span className="mt-0.5 block text-xs text-muted-foreground">
549
544
  {item.org}
550
545
  </span>
@@ -556,6 +551,11 @@ function DefaultResourceCard({
556
551
  {item.description}
557
552
  </p>
558
553
  )}
554
+ {item.visibility === ApiResourceVisibility.visibility_public && (
555
+ <div className="mt-auto">
556
+ <VisibilityBadge />
557
+ </div>
558
+ )}
559
559
  </div>
560
560
  );
561
561
  }
@@ -0,0 +1,527 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import type { EnvVarInput } from "@stigmer/sdk";
6
+ import { useMcpServer } from "./useMcpServer";
7
+ import { useMcpServerCredentials } from "./useMcpServerCredentials";
8
+ import { useMcpServerOAuthConnect } from "./useMcpServerOAuthConnect";
9
+ import type { OAuthConnectPhase } from "./useMcpServerOAuthConnect";
10
+ import { useMcpServerConnect } from "./useMcpServerConnect";
11
+ import { useDisconnectOAuth } from "./useDisconnectOAuth";
12
+ import { EnvVarForm } from "../environment/EnvVarForm";
13
+ import { ErrorMessage } from "../error/ErrorMessage";
14
+
15
+ /** Props for {@link McpServerConnectDialog}. */
16
+ export interface McpServerConnectDialogProps {
17
+ /** Organization slug that owns the MCP server. */
18
+ readonly org: string;
19
+ /** MCP server slug. */
20
+ readonly slug: string;
21
+ /**
22
+ * The authenticated user's active organization slug.
23
+ * Used for credential storage — tokens are stored in the user's
24
+ * personal environment within this org.
25
+ * Falls back to `org` when omitted.
26
+ */
27
+ readonly activeOrg?: string;
28
+ /** Whether the dialog is open. */
29
+ readonly open: boolean;
30
+ /** Called when the dialog should close (backdrop click, cancel, or success). */
31
+ readonly onClose: () => void;
32
+ /** Called after a successful connect with the server name. */
33
+ readonly onConnected?: (serverName: string) => void;
34
+ /** Additional CSS classes for the dialog element. */
35
+ readonly className?: string;
36
+ }
37
+
38
+ type DialogPhase = "credentials" | "connecting" | "success" | "error";
39
+
40
+ /**
41
+ * Modal dialog for connecting to an MCP server.
42
+ *
43
+ * Fetches the server, determines whether credentials or OAuth are
44
+ * needed, and walks the user through the connect flow — all without
45
+ * navigating away from the current page.
46
+ *
47
+ * Uses the native `<dialog>` element for accessibility (focus trap,
48
+ * Escape to close, backdrop click).
49
+ *
50
+ * Built on existing hooks: {@link useMcpServer},
51
+ * {@link useMcpServerCredentials}, {@link useMcpServerOAuthConnect},
52
+ * and {@link useMcpServerConnect}.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const [connectTarget, setConnectTarget] = useState<{ org: string; slug: string } | null>(null);
57
+ *
58
+ * <McpServerConnectDialog
59
+ * org={connectTarget?.org ?? ""}
60
+ * slug={connectTarget?.slug ?? ""}
61
+ * open={connectTarget !== null}
62
+ * onClose={() => setConnectTarget(null)}
63
+ * onConnected={(name) => toast(`Connected to ${name}`)}
64
+ * />
65
+ * ```
66
+ */
67
+ export function McpServerConnectDialog({
68
+ org,
69
+ slug,
70
+ activeOrg,
71
+ open,
72
+ onClose,
73
+ onConnected,
74
+ className,
75
+ }: McpServerConnectDialogProps) {
76
+ const dialogRef = useRef<HTMLDialogElement>(null);
77
+ const resolvedOrg = activeOrg || org;
78
+
79
+ useEffect(() => {
80
+ const dialog = dialogRef.current;
81
+ if (!dialog) return;
82
+
83
+ if (open && !dialog.open) {
84
+ dialog.showModal();
85
+ } else if (!open && dialog.open) {
86
+ dialog.close();
87
+ }
88
+ }, [open]);
89
+
90
+ const handleDialogClose = useCallback(() => {
91
+ onClose();
92
+ }, [onClose]);
93
+
94
+ const handleBackdropClick = useCallback(
95
+ (e: React.MouseEvent<HTMLDialogElement>) => {
96
+ if (e.target === dialogRef.current) {
97
+ onClose();
98
+ }
99
+ },
100
+ [onClose],
101
+ );
102
+
103
+ if (!open) return null;
104
+
105
+ return (
106
+ <dialog
107
+ ref={dialogRef}
108
+ onClose={handleDialogClose}
109
+ onClick={handleBackdropClick}
110
+ className={cn(
111
+ "m-auto max-h-[85vh] w-full max-w-md overflow-visible rounded-lg border border-border bg-card p-0 text-foreground shadow-lg",
112
+ "backdrop:bg-black/50",
113
+ className,
114
+ )}
115
+ >
116
+ <div
117
+ className="flex max-h-[85vh] flex-col overflow-y-auto p-6"
118
+ onClick={(e) => e.stopPropagation()}
119
+ >
120
+ <ConnectDialogContent
121
+ org={org}
122
+ slug={slug}
123
+ activeOrg={resolvedOrg}
124
+ onClose={onClose}
125
+ onConnected={onConnected}
126
+ />
127
+ </div>
128
+ </dialog>
129
+ );
130
+ }
131
+
132
+ function ConnectDialogContent({
133
+ org,
134
+ slug,
135
+ activeOrg,
136
+ onClose,
137
+ onConnected,
138
+ }: {
139
+ readonly org: string;
140
+ readonly slug: string;
141
+ readonly activeOrg: string;
142
+ readonly onClose: () => void;
143
+ readonly onConnected?: (serverName: string) => void;
144
+ }) {
145
+ const { mcpServer, isLoading: isServerLoading, error: serverError } = useMcpServer(org, slug);
146
+ const creds = useMcpServerCredentials(activeOrg, mcpServer);
147
+ const { connect, isConnecting, error: connectError, clearError: clearConnectError } = useMcpServerConnect();
148
+ const oauth = useMcpServerOAuthConnect();
149
+ const disconnect = useDisconnectOAuth();
150
+
151
+ const [phase, setPhase] = useState<DialogPhase>("credentials");
152
+
153
+ const serverName = mcpServer?.metadata?.name ?? slug;
154
+ const serverId = mcpServer?.metadata?.id ?? "";
155
+ const declaredEnvKeys = Object.keys(mcpServer?.spec?.env ?? {});
156
+
157
+ const handleConnect = useCallback(async () => {
158
+ if (!serverId) return;
159
+
160
+ setPhase("connecting");
161
+ try {
162
+ await connect(serverId, activeOrg, undefined, declaredEnvKeys);
163
+ setPhase("success");
164
+ onConnected?.(serverName);
165
+ } catch {
166
+ setPhase("error");
167
+ }
168
+ }, [serverId, activeOrg, declaredEnvKeys, connect, onConnected, serverName]);
169
+
170
+ const handleCredentialSubmit = useCallback(
171
+ async (values: Record<string, EnvVarInput>) => {
172
+ await creds.saveCredentials(values);
173
+ creds.refetch();
174
+ },
175
+ [creds],
176
+ );
177
+
178
+ const handleOAuthSignIn = useCallback(() => {
179
+ if (!serverId) return;
180
+ oauth.startOAuth(serverId, activeOrg, declaredEnvKeys).then(
181
+ () => {
182
+ creds.refetch();
183
+ setPhase("success");
184
+ onConnected?.(serverName);
185
+ },
186
+ () => {
187
+ setPhase("error");
188
+ },
189
+ );
190
+ }, [serverId, activeOrg, declaredEnvKeys, oauth, creds, onConnected, serverName]);
191
+
192
+ // Auto-trigger connect when credentials become ready (manual-only servers)
193
+ useEffect(() => {
194
+ if (
195
+ phase === "credentials" &&
196
+ creds.isReady &&
197
+ !creds.isLoading &&
198
+ mcpServer &&
199
+ creds.authMode === "manual"
200
+ ) {
201
+ // Don't auto-connect; let user click the button
202
+ }
203
+ }, [phase, creds.isReady, creds.isLoading, mcpServer, creds.authMode]);
204
+
205
+ if (isServerLoading || creds.isLoading) {
206
+ return (
207
+ <>
208
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
209
+ <div className="flex flex-col items-center gap-3 py-8">
210
+ <LoadingSpinner />
211
+ <p className="text-sm text-muted-foreground">Loading server details...</p>
212
+ </div>
213
+ </>
214
+ );
215
+ }
216
+
217
+ if (serverError) {
218
+ return (
219
+ <>
220
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
221
+ <div className="py-4">
222
+ <ErrorMessage error={serverError} />
223
+ </div>
224
+ </>
225
+ );
226
+ }
227
+
228
+ if (!mcpServer) {
229
+ return (
230
+ <>
231
+ <DialogHeader title="Connect MCP Server" onClose={onClose} />
232
+ <p className="py-4 text-sm text-muted-foreground">MCP server not found.</p>
233
+ </>
234
+ );
235
+ }
236
+
237
+ if (phase === "success") {
238
+ return (
239
+ <>
240
+ <DialogHeader title="Connected" onClose={onClose} />
241
+ <div className="flex flex-col items-center gap-3 py-6">
242
+ <SuccessIcon />
243
+ <p className="text-sm font-medium text-foreground">
244
+ Successfully connected to {serverName}
245
+ </p>
246
+ <p className="text-xs text-muted-foreground">
247
+ Tools and capabilities have been discovered.
248
+ </p>
249
+ <button
250
+ type="button"
251
+ onClick={onClose}
252
+ className={cn(
253
+ "mt-2 inline-flex items-center rounded-md px-4 py-2 text-sm font-medium",
254
+ "bg-primary text-primary-foreground",
255
+ "hover:bg-primary/90",
256
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
257
+ )}
258
+ >
259
+ Done
260
+ </button>
261
+ </div>
262
+ </>
263
+ );
264
+ }
265
+
266
+ const isConnectingPhase = phase === "connecting" || isConnecting || oauth.isInProgress;
267
+ const activeError = connectError ?? oauth.error;
268
+
269
+ return (
270
+ <>
271
+ <DialogHeader title={serverName} onClose={onClose} />
272
+
273
+ {mcpServer.spec?.description && (
274
+ <p className="mb-4 text-xs leading-relaxed text-muted-foreground">
275
+ {mcpServer.spec.description}
276
+ </p>
277
+ )}
278
+
279
+ {activeError && (
280
+ <div className="mb-4">
281
+ <ErrorMessage
282
+ error={activeError}
283
+ retry={() => {
284
+ clearConnectError();
285
+ oauth.clearError();
286
+ setPhase("credentials");
287
+ }}
288
+ />
289
+ </div>
290
+ )}
291
+
292
+ {creds.authMode === "oauth" && !creds.manualOverride && (
293
+ <OAuthSection
294
+ isConnected={creds.isOAuthConnected}
295
+ phase={oauth.phase}
296
+ onSignIn={handleOAuthSignIn}
297
+ isVendorApprovalBlocked={creds.isVendorApprovalBlocked}
298
+ onSwitchToManual={() => creds.setManualOverride(true)}
299
+ disabled={isConnectingPhase}
300
+ />
301
+ )}
302
+
303
+ {creds.missingVariables.length > 0 && (
304
+ <div className="mt-2">
305
+ <EnvVarForm
306
+ variables={creds.missingVariables}
307
+ onSubmit={(values) => handleCredentialSubmit(values)}
308
+ isSubmitting={creds.isSaving}
309
+ disabled={isConnectingPhase}
310
+ hideSaveToggle
311
+ />
312
+ </div>
313
+ )}
314
+
315
+ {creds.authMode === "oauth" && creds.manualOverride && (
316
+ <button
317
+ type="button"
318
+ onClick={() => creds.setManualOverride(false)}
319
+ disabled={isConnectingPhase}
320
+ className="mt-2 text-xs text-muted-foreground underline hover:text-foreground"
321
+ >
322
+ Sign in with OAuth instead
323
+ </button>
324
+ )}
325
+
326
+ {creds.isReady && creds.authMode === "manual" && (
327
+ <button
328
+ type="button"
329
+ onClick={handleConnect}
330
+ disabled={isConnectingPhase}
331
+ className={cn(
332
+ "mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
333
+ "bg-primary text-primary-foreground",
334
+ "hover:bg-primary/90",
335
+ "disabled:pointer-events-none disabled:opacity-50",
336
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
337
+ )}
338
+ >
339
+ {isConnectingPhase && <LoadingSpinner size="sm" />}
340
+ {isConnectingPhase ? "Connecting..." : "Connect"}
341
+ </button>
342
+ )}
343
+
344
+ {creds.isReady && creds.authMode === "oauth" && creds.isOAuthConnected && (
345
+ <button
346
+ type="button"
347
+ onClick={handleConnect}
348
+ disabled={isConnectingPhase}
349
+ className={cn(
350
+ "mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
351
+ "bg-primary text-primary-foreground",
352
+ "hover:bg-primary/90",
353
+ "disabled:pointer-events-none disabled:opacity-50",
354
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
355
+ )}
356
+ >
357
+ {isConnectingPhase && <LoadingSpinner size="sm" />}
358
+ {isConnectingPhase ? "Discovering tools..." : "Discover Tools"}
359
+ </button>
360
+ )}
361
+
362
+ {!creds.isReady &&
363
+ creds.missingVariables.length === 0 &&
364
+ creds.authMode === "manual" && (
365
+ <p className="mt-4 text-center text-xs text-muted-foreground">
366
+ No credentials required — this server is ready to connect.
367
+ </p>
368
+ )}
369
+ </>
370
+ );
371
+ }
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Sub-components
375
+ // ---------------------------------------------------------------------------
376
+
377
+ function DialogHeader({
378
+ title,
379
+ onClose,
380
+ }: {
381
+ readonly title: string;
382
+ readonly onClose: () => void;
383
+ }) {
384
+ return (
385
+ <div className="mb-4 flex items-start justify-between gap-3">
386
+ <h2 className="text-base font-semibold text-foreground">{title}</h2>
387
+ <button
388
+ type="button"
389
+ onClick={onClose}
390
+ aria-label="Close"
391
+ className="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
392
+ >
393
+ <CloseIcon />
394
+ </button>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ function OAuthSection({
400
+ isConnected,
401
+ phase,
402
+ onSignIn,
403
+ isVendorApprovalBlocked,
404
+ onSwitchToManual,
405
+ disabled,
406
+ }: {
407
+ readonly isConnected: boolean;
408
+ readonly phase: OAuthConnectPhase;
409
+ readonly onSignIn: () => void;
410
+ readonly isVendorApprovalBlocked: boolean;
411
+ readonly onSwitchToManual: () => void;
412
+ readonly disabled?: boolean;
413
+ }) {
414
+ if (isConnected) {
415
+ return (
416
+ <div className="flex items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
417
+ <span className="size-2 shrink-0 rounded-full bg-green-500" />
418
+ <span className="text-sm text-foreground">OAuth connected</span>
419
+ </div>
420
+ );
421
+ }
422
+
423
+ const isInProgress = phase !== "idle" && phase !== "done";
424
+ const phaseLabel: Record<string, string> = {
425
+ initiating: "Starting OAuth...",
426
+ "awaiting-callback": "Waiting for authorization...",
427
+ completing: "Completing OAuth...",
428
+ connecting: "Discovering tools...",
429
+ };
430
+
431
+ return (
432
+ <div className="flex flex-col gap-2">
433
+ <button
434
+ type="button"
435
+ onClick={onSignIn}
436
+ disabled={disabled || isInProgress || isVendorApprovalBlocked}
437
+ className={cn(
438
+ "inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
439
+ "bg-primary text-primary-foreground",
440
+ "hover:bg-primary/90",
441
+ "disabled:pointer-events-none disabled:opacity-50",
442
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
443
+ )}
444
+ >
445
+ {isInProgress && <LoadingSpinner size="sm" />}
446
+ {isInProgress ? (phaseLabel[phase] ?? "Connecting...") : "Sign in with OAuth"}
447
+ </button>
448
+ {isVendorApprovalBlocked && (
449
+ <p className="text-xs text-muted-foreground">
450
+ OAuth sign-in is pending vendor approval. You can enter your token manually instead.
451
+ </p>
452
+ )}
453
+ <button
454
+ type="button"
455
+ onClick={onSwitchToManual}
456
+ disabled={disabled || isInProgress}
457
+ className="text-xs text-muted-foreground underline hover:text-foreground"
458
+ >
459
+ Enter token manually
460
+ </button>
461
+ </div>
462
+ );
463
+ }
464
+
465
+ function LoadingSpinner({ size = "md" }: { readonly size?: "sm" | "md" }) {
466
+ const cls = size === "sm" ? "size-3.5" : "size-5";
467
+ return (
468
+ <svg
469
+ className={cn(cls, "animate-spin text-current")}
470
+ viewBox="0 0 24 24"
471
+ fill="none"
472
+ aria-hidden="true"
473
+ >
474
+ <circle
475
+ cx="12"
476
+ cy="12"
477
+ r="10"
478
+ stroke="currentColor"
479
+ strokeWidth="3"
480
+ strokeLinecap="round"
481
+ className="opacity-25"
482
+ />
483
+ <path
484
+ d="M12 2a10 10 0 0 1 10 10"
485
+ stroke="currentColor"
486
+ strokeWidth="3"
487
+ strokeLinecap="round"
488
+ className="opacity-75"
489
+ />
490
+ </svg>
491
+ );
492
+ }
493
+
494
+ function SuccessIcon() {
495
+ return (
496
+ <svg
497
+ className="size-10 text-green-500"
498
+ viewBox="0 0 24 24"
499
+ fill="none"
500
+ stroke="currentColor"
501
+ strokeWidth="2"
502
+ strokeLinecap="round"
503
+ strokeLinejoin="round"
504
+ aria-hidden="true"
505
+ >
506
+ <circle cx="12" cy="12" r="10" />
507
+ <path d="m9 12 2 2 4-4" />
508
+ </svg>
509
+ );
510
+ }
511
+
512
+ function CloseIcon() {
513
+ return (
514
+ <svg
515
+ className="size-4"
516
+ viewBox="0 0 16 16"
517
+ fill="none"
518
+ stroke="currentColor"
519
+ strokeWidth="1.5"
520
+ strokeLinecap="round"
521
+ strokeLinejoin="round"
522
+ aria-hidden="true"
523
+ >
524
+ <path d="m4 4 8 8M12 4l-8 8" />
525
+ </svg>
526
+ );
527
+ }
@@ -409,7 +409,7 @@ export function McpServerDetailView({
409
409
  ref={byoaDialogRef}
410
410
  onCancel={handleByoaDialogCancel}
411
411
  className={cn(
412
- "w-full max-w-md rounded-lg border border-border bg-background p-6 shadow-lg",
412
+ "m-auto w-full max-w-md rounded-lg border border-border bg-background p-6 shadow-lg",
413
413
  "backdrop:bg-black/50",
414
414
  )}
415
415
  >
@@ -868,6 +868,7 @@ function ConnectBar({
868
868
  <button
869
869
  type="button"
870
870
  onClick={onBringOwnApp}
871
+ data-cursor-target="byoa-cta-button"
871
872
  className="mt-1.5 inline-flex items-center gap-1 rounded-md bg-amber-600 px-2.5 py-1 text-[11px] font-medium text-white hover:bg-amber-700 dark:bg-amber-500 dark:text-amber-950 dark:hover:bg-amber-400"
872
873
  >
873
874
  Use your own OAuth app
@@ -13,6 +13,7 @@ import { cn } from "@stigmer/theme";
13
13
  import type { EnvVarInput, McpServerUsageInput, ResourceRef } from "@stigmer/sdk";
14
14
  import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
15
15
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
16
+ import { VendorApprovalStatus } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
16
17
  import type { EnvVarFormSubmitOptions } from "../environment/EnvVarForm";
17
18
  import { useMcpServerSearch } from "./useMcpServerSearch";
18
19
  import { useScrollShadows } from "../internal/useScrollShadows";
@@ -418,7 +419,12 @@ export function McpServerPicker({
418
419
 
419
420
  const oauthStatus = entry.mcpServer.status?.oauthStatus;
420
421
  const isVendorApprovalPending =
421
- hasOAuth && oauthStatus?.vendorApprovalStatus === 1; // VendorApprovalStatus.PENDING
422
+ hasOAuth &&
423
+ oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.PENDING;
424
+ const isVendorApprovalBlocked =
425
+ hasOAuth &&
426
+ (oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.PENDING ||
427
+ oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.REJECTED);
422
428
 
423
429
  const oauthSignInProps =
424
430
  hasOAuth && !isManualOverride
@@ -440,6 +446,7 @@ export function McpServerPicker({
440
446
  error: oauth.error,
441
447
  onClearError: oauth.clearError,
442
448
  isVendorApprovalPending,
449
+ isVendorApprovalBlocked,
443
450
  vendorApprovalDocsUrl: oauthStatus?.vendorApprovalDocsUrl || null,
444
451
  }
445
452
  : undefined;
@@ -82,3 +82,6 @@ export type {
82
82
  UseMcpServerCredentialsReturn,
83
83
  McpServerAuthMode,
84
84
  } from "./useMcpServerCredentials";
85
+
86
+ export { McpServerConnectDialog } from "./McpServerConnectDialog";
87
+ export type { McpServerConnectDialogProps } from "./McpServerConnectDialog";