@ciridae/ui 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/blocks/copy-field.d.ts +18 -0
- package/dist/components/blocks/copy-field.js +61 -0
- package/dist/components/blocks/feedback-center-sheet.js +2 -2
- package/dist/components/blocks/feedback-sheet.js +2 -2
- package/dist/components/blocks/generated-connect-link.d.ts +22 -0
- package/dist/components/blocks/generated-connect-link.js +68 -0
- package/dist/components/blocks/kanban-board-parts.d.ts +1 -1
- package/dist/components/blocks/kanban-board-parts.js +0 -2
- package/dist/components/blocks/mcp-access-tool-picker.d.ts +35 -0
- package/dist/components/blocks/mcp-access-tool-picker.js +41 -0
- package/dist/components/blocks/mcp-access-types.d.ts +39 -0
- package/dist/components/blocks/mcp-access-types.js +7 -0
- package/dist/components/blocks/mcp-client-setup.d.ts +29 -0
- package/dist/components/blocks/mcp-client-setup.js +58 -0
- package/dist/components/blocks/mcp-connect-clients.d.ts +43 -0
- package/dist/components/blocks/mcp-connect-clients.js +66 -0
- package/dist/components/blocks/mcp-plan-choice.d.ts +17 -0
- package/dist/components/blocks/mcp-plan-choice.js +28 -0
- package/dist/components/ui/color-theme.js +0 -2
- package/dist/components/ui/form-sheet.d.ts +7 -0
- package/dist/components/ui/form-sheet.js +35 -69
- package/dist/entries/copy-field.d.ts +1 -0
- package/dist/entries/copy-field.js +1 -0
- package/dist/entries/generated-connect-link.d.ts +1 -0
- package/dist/entries/generated-connect-link.js +1 -0
- package/dist/entries/mcp-access-tool-picker.d.ts +1 -0
- package/dist/entries/mcp-access-tool-picker.js +1 -0
- package/dist/entries/mcp-access-types.d.ts +1 -0
- package/dist/entries/mcp-access-types.js +1 -0
- package/dist/entries/mcp-client-setup.d.ts +1 -0
- package/dist/entries/mcp-client-setup.js +1 -0
- package/dist/entries/mcp-connect-clients.d.ts +1 -0
- package/dist/entries/mcp-connect-clients.js +1 -0
- package/dist/entries/mcp-plan-choice.d.ts +1 -0
- package/dist/entries/mcp-plan-choice.js +1 -0
- package/dist/lib/browser-storage.d.ts +4 -5
- package/dist/lib/browser-storage.js +5 -6
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One value as a labelled read-only text field, so it reads like the field it
|
|
3
|
+
* is about to be pasted into. A real `input` rather than a styled block: it
|
|
4
|
+
* selects, tabs, and reads to assistive tech like its destination. The row
|
|
5
|
+
* stays when there is no value yet, because dropping it left the steps above
|
|
6
|
+
* pointing at nothing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function CopyField({ label, placeholder, value, }: {
|
|
9
|
+
label: string;
|
|
10
|
+
/**
|
|
11
|
+
* What an absent value means, which is not the same for every field. A
|
|
12
|
+
* minted secret is not generated yet; a deployment-wide URL that is
|
|
13
|
+
* missing will never arrive, and saying otherwise sends someone back to
|
|
14
|
+
* the mint button indefinitely.
|
|
15
|
+
*/
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
value: string | undefined;
|
|
18
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
3
|
+
import { useEffect, useId, useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Button } from "../ui/button";
|
|
6
|
+
import { Input } from "../ui/input";
|
|
7
|
+
import { Label } from "../ui/label";
|
|
8
|
+
/** PostHog's own no-capture vocabulary — a fixed class name, not app config. */
|
|
9
|
+
const POSTHOG_NO_CAPTURE_CLASS_NAME = "ph-no-capture";
|
|
10
|
+
const COPIED_RESET_DELAY = 2000;
|
|
11
|
+
/**
|
|
12
|
+
* One value as a labelled read-only text field, so it reads like the field it
|
|
13
|
+
* is about to be pasted into. A real `input` rather than a styled block: it
|
|
14
|
+
* selects, tabs, and reads to assistive tech like its destination. The row
|
|
15
|
+
* stays when there is no value yet, because dropping it left the steps above
|
|
16
|
+
* pointing at nothing.
|
|
17
|
+
*/
|
|
18
|
+
export function CopyField({ label, placeholder = "Not generated yet", value, }) {
|
|
19
|
+
const fieldId = useId();
|
|
20
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-1", children: [_jsx(Label, { className: "text-muted-foreground text-xs", htmlFor: fieldId, children: label }), _jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx(Input, { className: `${POSTHOG_NO_CAPTURE_CLASS_NAME} h-8 font-mono text-[11px]`, "data-sentry-mask": true, id: fieldId, onFocus: (event) => {
|
|
21
|
+
event.target.select();
|
|
22
|
+
}, placeholder: placeholder, readOnly: true, value: value ?? "" }), value === undefined ? null : _jsx(CopyButton, { label: label, text: value })] })] }));
|
|
23
|
+
}
|
|
24
|
+
function CopyButton({ label, text }) {
|
|
25
|
+
// What was copied, not that something was: this button survives a remint,
|
|
26
|
+
// so a boolean would keep claiming the checkmark over a value the clipboard
|
|
27
|
+
// has never held. Comparing makes a new `text` read as uncopied at once.
|
|
28
|
+
const [copiedText, setCopiedText] = useState(null);
|
|
29
|
+
const copied = copiedText === text;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!copied)
|
|
32
|
+
return;
|
|
33
|
+
const timer = window.setTimeout(() => {
|
|
34
|
+
setCopiedText(null);
|
|
35
|
+
}, COPIED_RESET_DELAY);
|
|
36
|
+
return () => {
|
|
37
|
+
window.clearTimeout(timer);
|
|
38
|
+
};
|
|
39
|
+
}, [copied]);
|
|
40
|
+
/**
|
|
41
|
+
* `await` rather than `.catch`, because the two failures arrive differently:
|
|
42
|
+
* a denied permission or an unfocused document rejects, while an insecure
|
|
43
|
+
* origin has no `navigator.clipboard` at all and throws before a promise
|
|
44
|
+
* exists. Either way the user is about to paste a stale clipboard into a
|
|
45
|
+
* credential setup, so neither can pass silently.
|
|
46
|
+
*/
|
|
47
|
+
const copy = async () => {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.clipboard.writeText(text);
|
|
50
|
+
setCopiedText(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
toast.error(`Could not copy the ${label.toLowerCase()}`, {
|
|
54
|
+
description: "Select the field and copy it by hand.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return (_jsx(Button, { "aria-label": copied ? `${label} copied` : `Copy ${label.toLowerCase()}`, onClick: () => {
|
|
59
|
+
void copy();
|
|
60
|
+
}, size: "icon-xs", variant: "ghost", children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) }));
|
|
61
|
+
}
|
|
@@ -10,8 +10,8 @@ function FeedbackCenterSheet(props) {
|
|
|
10
10
|
return (_jsx(FormSheet, { closeDisabled: props.isSubmitting ?? false, isDirty: state.isDirty, onOpenChange: props.onOpenChange, open: props.open, children: _jsx(FeedbackCenterSheetContent, { props: props, state: state }) }));
|
|
11
11
|
}
|
|
12
12
|
function FeedbackCenterSheetContent({ props, state, }) {
|
|
13
|
-
const { requestClose } = useFormSheetActions();
|
|
14
|
-
return (_jsx(SheetContent, { size: "md", children: _jsxs(Tabs, { className: "min-h-0 flex-1", onValueChange: setFeedbackCenterTab(state.handleTabChange), value: state.activeTab, children: [_jsx(FeedbackCenterSheetHeader, { unreadReplyCount: state.unreadReplyCount }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(FeedbackCenterTabs, { composerProps: getFeedbackComposerProps(props), commentsByFeedbackId: props.commentsByFeedbackId ?? {}, commentsError: props.commentsError, formId: state.formId, isCommentSubmitting: props.isCommentSubmitting ?? false, isCommentsLoading: props.isCommentsLoading ?? false, isLoading: props.isLoading ?? false, items: props.items, itemsError: props.itemsError, onBack: state.handleBack, onCancel: requestClose, onDirtyChange: state.setIsDirty, onRetryComments: props.onRetryComments, onRetryItems: props.onRetryItems, onSelectFeedback: state.handleSelectFeedback, onSubmitComment: props.onSubmitComment, onSubmittedChange: state.setIsSubmitted, onUrgencyChange: state.setUrgency, renderEvidence: props.renderEvidence, selectedFeedback: state.selectedFeedback, view: state.view }) }), _jsx(FeedbackCenterFooter, { activeTab: state.activeTab, formId: state.formId, isSubmitted: state.isSubmitted, isSubmitting: props.isSubmitting ?? false, onCancel: requestClose, urgency: state.urgency })] }) }));
|
|
13
|
+
const { contentProps, requestClose } = useFormSheetActions();
|
|
14
|
+
return (_jsx(SheetContent, { size: "md", ...contentProps, children: _jsxs(Tabs, { className: "min-h-0 flex-1", onValueChange: setFeedbackCenterTab(state.handleTabChange), value: state.activeTab, children: [_jsx(FeedbackCenterSheetHeader, { unreadReplyCount: state.unreadReplyCount }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(FeedbackCenterTabs, { composerProps: getFeedbackComposerProps(props), commentsByFeedbackId: props.commentsByFeedbackId ?? {}, commentsError: props.commentsError, formId: state.formId, isCommentSubmitting: props.isCommentSubmitting ?? false, isCommentsLoading: props.isCommentsLoading ?? false, isLoading: props.isLoading ?? false, items: props.items, itemsError: props.itemsError, onBack: state.handleBack, onCancel: requestClose, onDirtyChange: state.setIsDirty, onRetryComments: props.onRetryComments, onRetryItems: props.onRetryItems, onSelectFeedback: state.handleSelectFeedback, onSubmitComment: props.onSubmitComment, onSubmittedChange: state.setIsSubmitted, onUrgencyChange: state.setUrgency, renderEvidence: props.renderEvidence, selectedFeedback: state.selectedFeedback, view: state.view }) }), _jsx(FeedbackCenterFooter, { activeTab: state.activeTab, formId: state.formId, isSubmitted: state.isSubmitted, isSubmitting: props.isSubmitting ?? false, onCancel: requestClose, urgency: state.urgency })] }) }));
|
|
15
15
|
}
|
|
16
16
|
function useFeedbackCenterSheetState(props) {
|
|
17
17
|
const defaultTab = props.defaultTab ?? "submit";
|
|
@@ -14,7 +14,7 @@ function FeedbackSheet({ onOpenChange, open, ...composerProps }) {
|
|
|
14
14
|
return (_jsx(FormSheet, { closeDisabled: composerProps.isSubmitting ?? false, isDirty: isDirty, onOpenChange: onOpenChange, open: open, children: _jsx(FeedbackSheetContent, { composerProps: composerProps, onDirtyChange: setIsDirty, open: open }) }));
|
|
15
15
|
}
|
|
16
16
|
function FeedbackSheetContent({ composerProps, onDirtyChange, open, }) {
|
|
17
|
-
const { requestClose } = useFormSheetActions();
|
|
17
|
+
const { contentProps, requestClose } = useFormSheetActions();
|
|
18
18
|
const formId = React.useId();
|
|
19
19
|
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
|
20
20
|
const [urgency, setUrgency] = React.useState("normal");
|
|
@@ -24,6 +24,6 @@ function FeedbackSheetContent({ composerProps, onDirtyChange, open, }) {
|
|
|
24
24
|
setUrgency("normal");
|
|
25
25
|
}
|
|
26
26
|
}, [open]);
|
|
27
|
-
return (_jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Send feedback" }), _jsx(SheetDescription, { children: "Share a bug, feature request, or general note." })] }), _jsx(SheetBody, { children: _jsx(FeedbackComposer, { ...composerProps, actions: "external", formId: formId, onCancel: requestClose, onDirtyChange: onDirtyChange, onSubmittedChange: setIsSubmitted, onUrgencyChange: setUrgency }) }), isSubmitted ? null : (_jsxs(SheetFooter, { children: [_jsx(Button, { disabled: composerProps.isSubmitting, onClick: requestClose, type: "button", variant: "outline", children: "Cancel" }), _jsxs(Button, { form: formId, loading: composerProps.isSubmitting, type: "submit", variant: urgency === "emergency" ? "destructive" : "default", children: [urgency === "emergency" ? _jsx(AlertTriangleIcon, {}) : _jsx(SendIcon, {}), urgency === "emergency" ? "Send emergency" : "Submit feedback"] })] }))] }));
|
|
27
|
+
return (_jsxs(SheetContent, { size: "md", ...contentProps, children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: "Send feedback" }), _jsx(SheetDescription, { children: "Share a bug, feature request, or general note." })] }), _jsx(SheetBody, { children: _jsx(FeedbackComposer, { ...composerProps, actions: "external", formId: formId, onCancel: requestClose, onDirtyChange: onDirtyChange, onSubmittedChange: setIsSubmitted, onUrgencyChange: setUrgency }) }), isSubmitted ? null : (_jsxs(SheetFooter, { children: [_jsx(Button, { disabled: composerProps.isSubmitting, onClick: requestClose, type: "button", variant: "outline", children: "Cancel" }), _jsxs(Button, { form: formId, loading: composerProps.isSubmitting, type: "submit", variant: urgency === "emergency" ? "destructive" : "default", children: [urgency === "emergency" ? _jsx(AlertTriangleIcon, {}) : _jsx(SendIcon, {}), urgency === "emergency" ? "Send emergency" : "Submit feedback"] })] }))] }));
|
|
28
28
|
}
|
|
29
29
|
export { FeedbackSheet };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { McpAccessMode, McpAccessToolSelector, McpConnectLink } from "./mcp-access-types";
|
|
2
|
+
/**
|
|
3
|
+
* The slice of the app's mint mutation this control drives, shaped so a
|
|
4
|
+
* TanStack Query mutation result can be passed whole. `data` is the link the
|
|
5
|
+
* last mint produced, absent until one succeeds.
|
|
6
|
+
*/
|
|
7
|
+
export type CreateConnectLinkState = {
|
|
8
|
+
data: McpConnectLink | undefined;
|
|
9
|
+
isPending: boolean;
|
|
10
|
+
mutate: () => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The mint button and whatever link it has produced. Split from the sheet
|
|
14
|
+
* because this is the one control that acts on the chosen access, and what it
|
|
15
|
+
* is named and coloured for has to track that choice rather than the mode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function GeneratedConnectLink({ access, createConnectLink, currentTools, disabled, }: {
|
|
18
|
+
access: McpAccessMode | undefined;
|
|
19
|
+
createConnectLink: CreateConnectLinkState;
|
|
20
|
+
currentTools: McpAccessToolSelector[] | null;
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "../ui/button";
|
|
3
|
+
import { keyOf } from "./mcp-access-tool-picker";
|
|
4
|
+
import { Spinner } from "../ui/spinner";
|
|
5
|
+
const expiryFormatter = new Intl.DateTimeFormat(undefined, {
|
|
6
|
+
dateStyle: "medium",
|
|
7
|
+
timeStyle: "short",
|
|
8
|
+
});
|
|
9
|
+
const ACCESS_LABEL = { read_only: "read-only", all: "full", selected: "custom" };
|
|
10
|
+
const generateLabel = (hasLink, access, writes) => {
|
|
11
|
+
if (hasLink)
|
|
12
|
+
return "Generate new code";
|
|
13
|
+
if (access === "all")
|
|
14
|
+
return "Generate full-access code";
|
|
15
|
+
return writes ? "Generate write-access code" : "Generate code";
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The methods the tool compiler treats as read-only, mirroring its
|
|
19
|
+
* `_READ_ONLY_METHODS` — the `readOnly` an operation reports is that set
|
|
20
|
+
* membership and nothing else, so a selector's own method already answers this.
|
|
21
|
+
*/
|
|
22
|
+
const READ_ONLY_METHODS = new Set(["GET"]);
|
|
23
|
+
/**
|
|
24
|
+
* Whether the link this button mints can write. `all` always can, and so can a
|
|
25
|
+
* Custom set holding a non-`GET` operation. Read off the selectors the grant is
|
|
26
|
+
* already made of rather than the compiled inventory: the two agree by
|
|
27
|
+
* construction, and asking the server would put a network boundary — and a
|
|
28
|
+
* failure mode — under a pure predicate.
|
|
29
|
+
*/
|
|
30
|
+
function grantsWrite(access, currentTools) {
|
|
31
|
+
if (access === "all")
|
|
32
|
+
return true;
|
|
33
|
+
if (access !== "selected")
|
|
34
|
+
return false;
|
|
35
|
+
return (currentTools ?? []).some((tool) => !READ_ONLY_METHODS.has(tool.method));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The mint button and whatever link it has produced. Split from the sheet
|
|
39
|
+
* because this is the one control that acts on the chosen access, and what it
|
|
40
|
+
* is named and coloured for has to track that choice rather than the mode.
|
|
41
|
+
*/
|
|
42
|
+
export function GeneratedConnectLink({ access, createConnectLink, currentTools, disabled, }) {
|
|
43
|
+
const link = createConnectLink.data;
|
|
44
|
+
const writeGranting = grantsWrite(access, currentTools);
|
|
45
|
+
// The link reports the access it was minted with, not the current setting,
|
|
46
|
+
// so switching modes — or changing a custom selection — with a link on
|
|
47
|
+
// screen labels it honestly instead of silently re-describing a credential
|
|
48
|
+
// that already exists.
|
|
49
|
+
const changedSinceMint = link !== undefined &&
|
|
50
|
+
(link.access !== access ||
|
|
51
|
+
!sameSelection(link.access === "selected" ? link.tools : null, currentTools));
|
|
52
|
+
const staleLabel = changedSinceMint && access !== undefined ? ACCESS_LABEL[access] : null;
|
|
53
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx(Button, { disabled: createConnectLink.isPending || disabled, onClick: () => {
|
|
54
|
+
// What the button is labelled for, not what was last read.
|
|
55
|
+
createConnectLink.mutate();
|
|
56
|
+
}, size: "sm",
|
|
57
|
+
// Named and coloured for what it will mint: the access is chosen
|
|
58
|
+
// well above this button, and this is the click that acts on it.
|
|
59
|
+
// Keyed on whether the grant can write rather than on the mode, so
|
|
60
|
+
// a Custom set holding an action tool does not read as read-only.
|
|
61
|
+
variant: writeGranting ? "destructive" : "default", children: generateLabel(link !== undefined, access, writeGranting) }), createConnectLink.isPending ? (_jsx(Spinner, { "aria-label": "Generating connection code", className: "text-muted-foreground" })) : null, link === undefined ? null : (_jsxs("p", { className: "text-muted-foreground/70 text-xs", children: [_jsxs("span", { className: "font-medium", children: [ACCESS_LABEL[link.access], " access"] }), " \u00B7 single use \u00B7 expires ", expiryFormatter.format(new Date(link.expiresAt))] }))] }), staleLabel === null ? null : (_jsxs("p", { className: "text-destructive text-xs", children: ["This code was generated before you changed the access above. Generate a new one to share", " ", staleLabel, " access."] }))] }));
|
|
62
|
+
}
|
|
63
|
+
function sameSelection(left, right) {
|
|
64
|
+
if (left === null || right === null)
|
|
65
|
+
return left === right;
|
|
66
|
+
const keys = new Set(left.map((tool) => keyOf(tool)));
|
|
67
|
+
return right.length === left.length && right.every((tool) => keys.has(keyOf(tool)));
|
|
68
|
+
}
|
|
@@ -22,5 +22,5 @@ declare function KanbanBoardColumn({ activeCardId, activeCardRect, activeMove, b
|
|
|
22
22
|
declare function KanbanDragOverlay({ activeCard, activeCardRect, }: {
|
|
23
23
|
activeCard: KanbanCardData | undefined;
|
|
24
24
|
activeCardRect: KanbanOverlayRect | undefined;
|
|
25
|
-
}): React.
|
|
25
|
+
}): React.ReactPortal;
|
|
26
26
|
export { KanbanBoardColumn, KanbanDragOverlay };
|
|
@@ -45,8 +45,6 @@ function KanbanDropPlaceholder({ height }) {
|
|
|
45
45
|
}
|
|
46
46
|
function KanbanDragOverlay({ activeCard, activeCardRect, }) {
|
|
47
47
|
const overlay = (_jsx(DragOverlay, { adjustScale: false, children: activeCard ? (_jsx("div", { style: { width: activeCardRect?.width }, children: _jsx(KanbanCard, { card: activeCard, isDragging: true }) })) : null }));
|
|
48
|
-
if (typeof document === "undefined")
|
|
49
|
-
return overlay;
|
|
50
48
|
return createPortal(overlay, document.body);
|
|
51
49
|
}
|
|
52
50
|
function getSortableCardClickHandler({ card, onCardSelect, suppressSelectRef, }) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { McpAccessToolListing, McpAccessToolSelector } from "./mcp-access-types";
|
|
2
|
+
/** The coordinate a selector and a compiled operation are matched on. */
|
|
3
|
+
export declare const keyOf: (tool: {
|
|
4
|
+
method: string;
|
|
5
|
+
pathTemplate: string;
|
|
6
|
+
}) => string;
|
|
7
|
+
/**
|
|
8
|
+
* The slice of the app's inventory query the picker reads, shaped so a
|
|
9
|
+
* TanStack Query result can be passed whole. `data` is `undefined` for both
|
|
10
|
+
* "still loading" and "failed to load"; `isPending` says which.
|
|
11
|
+
*/
|
|
12
|
+
export type McpToolInventoryState = {
|
|
13
|
+
data: {
|
|
14
|
+
tools: McpAccessToolListing[];
|
|
15
|
+
} | undefined;
|
|
16
|
+
isPending: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* An exact tool selection, applied as it is clicked. There is nothing to
|
|
20
|
+
* confirm: the list on screen is always what a code will be minted with, and an
|
|
21
|
+
* empty selection is reported but never stored because it is not a grant.
|
|
22
|
+
*
|
|
23
|
+
* Controlled rather than seeded once: the sheet owns the selection because the
|
|
24
|
+
* mint gate has to agree with these boxes, and a copy here could only be kept
|
|
25
|
+
* in step by remounting — which loses the scroll position on every click and
|
|
26
|
+
* still leaves the gate reading the older value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function McpAccessToolPicker({ busy, inventory, selected, onChange, }: {
|
|
29
|
+
busy: boolean;
|
|
30
|
+
/** The compiled inventory a Custom selection chooses from. The app owns
|
|
31
|
+
* the fetch — and with it when the request is made at all. */
|
|
32
|
+
inventory: McpToolInventoryState;
|
|
33
|
+
selected: McpAccessToolSelector[];
|
|
34
|
+
onChange: (tools: McpAccessToolSelector[]) => void;
|
|
35
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { Checkbox } from "../ui/checkbox";
|
|
4
|
+
import { Spinner } from "../ui/spinner";
|
|
5
|
+
/** The coordinate a selector and a compiled operation are matched on. */
|
|
6
|
+
export const keyOf = (tool) => `${tool.method} ${tool.pathTemplate}`;
|
|
7
|
+
/**
|
|
8
|
+
* An exact tool selection, applied as it is clicked. There is nothing to
|
|
9
|
+
* confirm: the list on screen is always what a code will be minted with, and an
|
|
10
|
+
* empty selection is reported but never stored because it is not a grant.
|
|
11
|
+
*
|
|
12
|
+
* Controlled rather than seeded once: the sheet owns the selection because the
|
|
13
|
+
* mint gate has to agree with these boxes, and a copy here could only be kept
|
|
14
|
+
* in step by remounting — which loses the scroll position on every click and
|
|
15
|
+
* still leaves the gate reading the older value.
|
|
16
|
+
*/
|
|
17
|
+
export function McpAccessToolPicker({ busy, inventory, selected, onChange, }) {
|
|
18
|
+
const selectedKeys = new Set(selected.map((tool) => keyOf(tool)));
|
|
19
|
+
if (inventory.data === undefined) {
|
|
20
|
+
return (_jsx("p", { className: "text-muted-foreground text-xs", children: inventory.isPending ? "Loading the tool list…" : "Couldn't load the tool list." }));
|
|
21
|
+
}
|
|
22
|
+
const tools = inventory.data.tools;
|
|
23
|
+
const toggle = (key, checked) => {
|
|
24
|
+
const next = new Set(selectedKeys);
|
|
25
|
+
if (checked)
|
|
26
|
+
next.add(key);
|
|
27
|
+
else
|
|
28
|
+
next.delete(key);
|
|
29
|
+
onChange(tools
|
|
30
|
+
.filter((tool) => next.has(keyOf(tool)))
|
|
31
|
+
.map((tool) => ({ method: tool.method, pathTemplate: tool.pathTemplate })));
|
|
32
|
+
};
|
|
33
|
+
const chosen = tools.filter((tool) => selectedKeys.has(keyOf(tool)));
|
|
34
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-2", children: [_jsx("ul", { className: "max-h-64 divide-y divide-border/50 overflow-y-auto rounded-md border border-border/60", children: tools.map((tool) => (_jsx(PickerRow, { checked: selectedKeys.has(keyOf(tool)), disabled: busy, onToggle: toggle, tool: tool }, keyOf(tool)))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [busy ? _jsx(Spinner, { "aria-label": "Saving selection", className: "text-muted-foreground" }) : null, _jsxs("p", { "aria-live": "polite", className: "text-muted-foreground text-xs", children: [chosen.length, " of ", tools.length, " tools selected"] }), chosen.length === 0 ? (_jsx("p", { className: "text-destructive text-xs", children: "Select at least one tool to generate a connection code." })) : null] })] }));
|
|
35
|
+
}
|
|
36
|
+
function PickerRow({ checked, disabled, tool, onToggle, }) {
|
|
37
|
+
const id = useId();
|
|
38
|
+
return (_jsxs("li", { className: "flex min-h-11 items-start gap-3 px-3 py-2", children: [_jsx(Checkbox, { checked: checked, disabled: disabled, id: id, onCheckedChange: (next) => {
|
|
39
|
+
onToggle(keyOf(tool), next === true);
|
|
40
|
+
} }), _jsxs("label", { className: "min-w-0 flex-1 cursor-pointer", htmlFor: id, children: [_jsxs("span", { className: "flex flex-wrap items-center gap-x-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: tool.name }), _jsx("span", { className: "text-muted-foreground text-[10px] uppercase", children: tool.readOnly ? "read-only" : "action" })] }), _jsx("span", { className: "block truncate text-muted-foreground text-xs", children: tool.description })] })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary the mcp-access controls speak. The gateway owns these
|
|
3
|
+
* shapes; they travel unchanged from its database through an app's backend to
|
|
4
|
+
* here. Plain types rather than schemas: parsing belongs to the app's own
|
|
5
|
+
* data layer, and the components only need to name what they are handed.
|
|
6
|
+
*/
|
|
7
|
+
/** The gateway owns this vocabulary; it travels unchanged from its database to here. */
|
|
8
|
+
export type McpAccessMode = "read_only" | "all" | "selected";
|
|
9
|
+
/** Who adds the connector, which is the only thing the two setups differ on. */
|
|
10
|
+
export type ConnectPlan = "personal" | "workspace";
|
|
11
|
+
export type McpHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
12
|
+
/** The durable coordinate a Custom selection stores per operation. */
|
|
13
|
+
export type McpAccessToolSelector = {
|
|
14
|
+
method: McpHttpMethod;
|
|
15
|
+
pathTemplate: string;
|
|
16
|
+
};
|
|
17
|
+
/** A selector's coordinate plus its display labels. */
|
|
18
|
+
export type McpAccessToolListing = {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
readOnly: boolean;
|
|
22
|
+
method: McpHttpMethod;
|
|
23
|
+
pathTemplate: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* What a mint produced, as the fields these controls report: the access the
|
|
27
|
+
* code was stamped with, and when it stops working. The code itself is not
|
|
28
|
+
* here — the app shows it through its own `CopyField`, and a type that never
|
|
29
|
+
* carries the credential cannot leak it. Discriminated on `access`: only a
|
|
30
|
+
* selected link carries tools, and it always does.
|
|
31
|
+
*/
|
|
32
|
+
export type McpConnectLink = {
|
|
33
|
+
access: Exclude<McpAccessMode, "selected">;
|
|
34
|
+
expiresAt: string;
|
|
35
|
+
} | {
|
|
36
|
+
access: "selected";
|
|
37
|
+
tools: McpAccessToolSelector[];
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary the mcp-access controls speak. The gateway owns these
|
|
3
|
+
* shapes; they travel unchanged from its database through an app's backend to
|
|
4
|
+
* here. Plain types rather than schemas: parsing belongs to the app's own
|
|
5
|
+
* data layer, and the components only need to name what they are handed.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ConnectPlan } from "./mcp-access-types";
|
|
3
|
+
import type { McpConnectClient } from "./mcp-connect-clients";
|
|
4
|
+
/**
|
|
5
|
+
* Per-client setup for the connection.
|
|
6
|
+
*
|
|
7
|
+
* One client at a time, because every client wants the same values and only
|
|
8
|
+
* the steps differ — listing all of them stacked repeated the same URL five
|
|
9
|
+
* times and buried the one set of values the reader actually needs. The values
|
|
10
|
+
* sit above the tabs so switching clients never moves them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function McpClientSetup({ clientId, code, connectorName, footer, onClientChange, onPlanChange, plan, url, urlNote, }: {
|
|
13
|
+
clientId: McpConnectClient;
|
|
14
|
+
/** The minted secret, absent until the member generates one. */
|
|
15
|
+
code: string | undefined;
|
|
16
|
+
/** Interpolated unquoted into the copyable CLI commands, so it must be a
|
|
17
|
+
* plain slug (no spaces, no leading `-`): quoting would have to pick a
|
|
18
|
+
* shell, and a leading `-` reaches the CLIs as an option. A name that
|
|
19
|
+
* fails the check renders the placeholder instead of a broken command. */
|
|
20
|
+
connectorName: string;
|
|
21
|
+
/** Minting lives in the card, beside the field it fills. */
|
|
22
|
+
footer: ReactNode;
|
|
23
|
+
onClientChange: (client: McpConnectClient) => void;
|
|
24
|
+
onPlanChange: (plan: ConnectPlan) => void;
|
|
25
|
+
plan: ConnectPlan;
|
|
26
|
+
url: string | undefined;
|
|
27
|
+
/** What to show in place of `url`, given why it is absent. */
|
|
28
|
+
urlNote: string;
|
|
29
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
|
|
3
|
+
import { CopyField } from "./copy-field";
|
|
4
|
+
import { CONNECT_CLIENTS, clientSteps, planAffectsClient } from "./mcp-connect-clients";
|
|
5
|
+
import { McpPlanChoice } from "./mcp-plan-choice";
|
|
6
|
+
/**
|
|
7
|
+
* The shape a connector name must have to be interpolated unquoted into the
|
|
8
|
+
* CLI commands below. Stricter than any one CLI needs: the first character
|
|
9
|
+
* may not be `-` because `--help-readonly` is a legal name by looser rules
|
|
10
|
+
* yet reaches both CLIs as an option, and there is no quoting that works for
|
|
11
|
+
* POSIX shells and Windows `cmd` at once.
|
|
12
|
+
*/
|
|
13
|
+
const PLAIN_SLUG = /^[\w.][\w.-]*$/;
|
|
14
|
+
/**
|
|
15
|
+
* Per-client setup for the connection.
|
|
16
|
+
*
|
|
17
|
+
* One client at a time, because every client wants the same values and only
|
|
18
|
+
* the steps differ — listing all of them stacked repeated the same URL five
|
|
19
|
+
* times and buried the one set of values the reader actually needs. The values
|
|
20
|
+
* sit above the tabs so switching clients never moves them.
|
|
21
|
+
*/
|
|
22
|
+
export function McpClientSetup({ clientId, code, connectorName, footer, onClientChange, onPlanChange, plan, url, urlNote, }) {
|
|
23
|
+
const client = CONNECT_CLIENTS.find((candidate) => candidate.id === clientId);
|
|
24
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-3", children: [_jsx(ValueCard, { code: code, connectorName: connectorName, footer: footer, isCli: clientId === "cli", url: url, urlNote: urlNote }), _jsx(Tabs, { onValueChange: (value) => {
|
|
25
|
+
// Narrowed through the catalogue rather than asserted: the tab list is
|
|
26
|
+
// built from it, so a value that is not in it is not a client.
|
|
27
|
+
const next = CONNECT_CLIENTS.find((candidate) => candidate.id === value);
|
|
28
|
+
if (next)
|
|
29
|
+
onClientChange(next.id);
|
|
30
|
+
}, value: clientId, children: _jsx(TabsList, { className: "w-full justify-start gap-x-2 overflow-x-auto", variant: "line", children: CONNECT_CLIENTS.map((candidate) => (_jsxs(TabsTrigger, { className: "text-xs", value: candidate.id, children: [_jsx(candidate.Icon, { "aria-hidden": "true", className: "size-3" }), candidate.tab] }, candidate.id))) }) }), client && planAffectsClient(client) ? (_jsx(McpPlanChoice, { onChange: onPlanChange, plan: plan })) : null, _jsx("p", { className: "text-muted-foreground text-sm", children: client ? clientSteps(client, plan) : null })] }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The values, shown once. The CLIs take a whole command rather than a name and
|
|
34
|
+
* a URL, so that tab swaps the card's contents instead of adding a third field
|
|
35
|
+
* nobody else uses.
|
|
36
|
+
*
|
|
37
|
+
* The footer sits with the fields it fills, so minting no longer needs a
|
|
38
|
+
* sentence pointing at a control elsewhere on screen.
|
|
39
|
+
*/
|
|
40
|
+
function ValueCard({ code, connectorName, footer, isCli, url, urlNote, }) {
|
|
41
|
+
// The URL is deployment configuration now, not a mint result, so reusing
|
|
42
|
+
// the minted secret's "not generated yet" would point at a button that can
|
|
43
|
+
// never fill it. The caller owns which of the absent-URL states applies.
|
|
44
|
+
// The commands interpolate the name bare, so only a plain slug may reach
|
|
45
|
+
// them — `--help-readonly` would parse as an option, a space as two argv
|
|
46
|
+
// entries — and neither is visible in the rendered field. Each absence
|
|
47
|
+
// names its own cause: a bad name with a good URL must not read as a
|
|
48
|
+
// missing URL.
|
|
49
|
+
const cliName = PLAIN_SLUG.test(connectorName) ? connectorName : undefined;
|
|
50
|
+
const cliNote = url !== undefined && cliName === undefined
|
|
51
|
+
? "The connector name can't be placed in a command — it needs to be a plain slug."
|
|
52
|
+
: urlNote;
|
|
53
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-3 rounded-lg border border-border bg-muted/30 p-3", children: [isCli ? (_jsxs(_Fragment, { children: [_jsx(CopyField, { label: "Claude Code CLI", placeholder: cliNote, value: url === undefined || cliName === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: `claude mcp add --transport http ${cliName} ${url}` }), _jsx(CopyField, { label: "Codex CLI", placeholder: cliNote, value: url === undefined || cliName === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: `codex mcp add ${cliName} --url ${url}` })] })) : (_jsxs(_Fragment, { children: [_jsx(CopyField, { label: "Name", placeholder: urlNote, value: url === undefined ? undefined : connectorName }), _jsx(CopyField, { label: "URL", placeholder: urlNote, value: url })] })), _jsx(CopyField, { label: "Connection code", value: code }), _jsx("div", { className: "border-border border-t pt-3", children: footer })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import type { ConnectPlan } from "./mcp-access-types";
|
|
3
|
+
/**
|
|
4
|
+
* Closed: an unknown client is not a reason to show a broken dialog.
|
|
5
|
+
*
|
|
6
|
+
* One `cli` token, not one per CLI: both commands take the same two values, so
|
|
7
|
+
* a tab each would have shown the same link twice to say `--url` differently.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MCP_CONNECT_CLIENT_VALUES: readonly ["claude-web", "claude-desktop", "chatgpt-web", "chatgpt-desktop", "cli"];
|
|
10
|
+
export type McpConnectClient = (typeof MCP_CONNECT_CLIENT_VALUES)[number];
|
|
11
|
+
type ConnectClientMetadata = {
|
|
12
|
+
tab: string;
|
|
13
|
+
/** The label carries the brand, so the icon carries the surface — and the
|
|
14
|
+
* surface is what decides whether a local gateway works. */
|
|
15
|
+
Icon: LucideIcon;
|
|
16
|
+
steps: string;
|
|
17
|
+
/**
|
|
18
|
+
* Steps for a plan where only an owner may add a connector, or `null` for
|
|
19
|
+
* a client where the distinction does not exist.
|
|
20
|
+
*
|
|
21
|
+
* Null is the meaningful state rather than a missing key: a desktop app or
|
|
22
|
+
* CLI is installed per-machine, so everyone adds their own connector
|
|
23
|
+
* whatever plan the workspace is on, and the Plan control hides itself
|
|
24
|
+
* rather than offering a choice that changes nothing.
|
|
25
|
+
*/
|
|
26
|
+
workspaceSteps: string | null;
|
|
27
|
+
};
|
|
28
|
+
export type ConnectClient = ConnectClientMetadata & {
|
|
29
|
+
id: McpConnectClient;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The steps to show for one client on one plan.
|
|
33
|
+
*
|
|
34
|
+
* `workspaceSteps` is nullable, so this has to answer for a null; the sheet
|
|
35
|
+
* never asks, because it keys the plan by client and writes it only where the
|
|
36
|
+
* control renders. Answering with the personal steps rather than nothing keeps
|
|
37
|
+
* the worst case a wrong-but-followable instruction instead of a blank panel.
|
|
38
|
+
*/
|
|
39
|
+
export declare const clientSteps: (client: ConnectClient, plan: ConnectPlan) => string;
|
|
40
|
+
/** Whether the plan choice changes anything for this client. */
|
|
41
|
+
export declare const planAffectsClient: (client: ConnectClientMetadata) => boolean;
|
|
42
|
+
export declare const CONNECT_CLIENTS: ConnectClient[];
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { GlobeIcon, MonitorIcon, TerminalIcon } from "lucide-react";
|
|
2
|
+
/**
|
|
3
|
+
* Closed: an unknown client is not a reason to show a broken dialog.
|
|
4
|
+
*
|
|
5
|
+
* One `cli` token, not one per CLI: both commands take the same two values, so
|
|
6
|
+
* a tab each would have shown the same link twice to say `--url` differently.
|
|
7
|
+
*/
|
|
8
|
+
export const MCP_CONNECT_CLIENT_VALUES = [
|
|
9
|
+
"claude-web",
|
|
10
|
+
"claude-desktop",
|
|
11
|
+
"chatgpt-web",
|
|
12
|
+
"chatgpt-desktop",
|
|
13
|
+
"cli",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Keyed by the URL token rather than listed alongside it: a `Record` over the
|
|
17
|
+
* client union is exhaustive, so adding a token without metadata fails to
|
|
18
|
+
* compile instead of deep-linking to a sheet with no tab and blank steps.
|
|
19
|
+
*/
|
|
20
|
+
const CONNECT_CLIENT_METADATA = {
|
|
21
|
+
"claude-web": {
|
|
22
|
+
Icon: GlobeIcon,
|
|
23
|
+
steps: "Customize → Connectors → Add → Add custom connector. Paste the values, click Add, then paste your connection code when Claude asks for it.",
|
|
24
|
+
tab: "Claude",
|
|
25
|
+
workspaceSteps: "An owner adds the connector once using the name and URL above. After that, select Connect on it and paste your own connection code. On Team and Enterprise plans only owners can add connectors.",
|
|
26
|
+
},
|
|
27
|
+
"claude-desktop": {
|
|
28
|
+
Icon: MonitorIcon,
|
|
29
|
+
steps: "Settings → Connectors → Add custom connector. Paste the values, then paste your connection code when Claude asks for it.",
|
|
30
|
+
tab: "Claude Desktop",
|
|
31
|
+
workspaceSteps: null,
|
|
32
|
+
},
|
|
33
|
+
"chatgpt-web": {
|
|
34
|
+
Icon: GlobeIcon,
|
|
35
|
+
steps: "Settings → Connectors → Create. Paste the values, tick the risk acknowledgement, and Create, then paste your connection code when asked.",
|
|
36
|
+
tab: "ChatGPT",
|
|
37
|
+
workspaceSteps: "An owner adds the connector once using the name and URL above. After that, select Connect on it and paste your own connection code.",
|
|
38
|
+
},
|
|
39
|
+
"chatgpt-desktop": {
|
|
40
|
+
Icon: MonitorIcon,
|
|
41
|
+
steps: "Settings → Connectors → Create. Choose Streamable HTTP, paste the values, and Save, then paste your connection code when asked.",
|
|
42
|
+
tab: "ChatGPT Desktop",
|
|
43
|
+
workspaceSteps: null,
|
|
44
|
+
},
|
|
45
|
+
cli: {
|
|
46
|
+
Icon: TerminalIcon,
|
|
47
|
+
steps: "Run the command for your CLI, then paste your connection code when asked on first use.",
|
|
48
|
+
tab: "CLI",
|
|
49
|
+
workspaceSteps: null,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The steps to show for one client on one plan.
|
|
54
|
+
*
|
|
55
|
+
* `workspaceSteps` is nullable, so this has to answer for a null; the sheet
|
|
56
|
+
* never asks, because it keys the plan by client and writes it only where the
|
|
57
|
+
* control renders. Answering with the personal steps rather than nothing keeps
|
|
58
|
+
* the worst case a wrong-but-followable instruction instead of a blank panel.
|
|
59
|
+
*/
|
|
60
|
+
export const clientSteps = (client, plan) => plan === "workspace" ? (client.workspaceSteps ?? client.steps) : client.steps;
|
|
61
|
+
/** Whether the plan choice changes anything for this client. */
|
|
62
|
+
export const planAffectsClient = (client) => client.workspaceSteps !== null;
|
|
63
|
+
export const CONNECT_CLIENTS = MCP_CONNECT_CLIENT_VALUES.map((id) => ({
|
|
64
|
+
id,
|
|
65
|
+
...CONNECT_CLIENT_METADATA[id],
|
|
66
|
+
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectPlan } from "./mcp-access-types";
|
|
2
|
+
/**
|
|
3
|
+
* Who adds the connector, as a named pair matching the access control above it.
|
|
4
|
+
*
|
|
5
|
+
* This asks rather than infers. The alternative was gating on this app's own
|
|
6
|
+
* admin role, but a project admin here is not necessarily an owner of the
|
|
7
|
+
* Claude or ChatGPT workspace: the two are unrelated systems, so inferring one
|
|
8
|
+
* from the other would hide the URL from the person who needs it and show it
|
|
9
|
+
* to someone who cannot use it. Only the reader knows which plan they are on.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here changes what is minted or what it may reach. The plan selects
|
|
12
|
+
* which setup steps are true, and the values above stay identical either way.
|
|
13
|
+
*/
|
|
14
|
+
export declare function McpPlanChoice({ onChange, plan, }: {
|
|
15
|
+
onChange: (plan: ConnectPlan) => void;
|
|
16
|
+
plan: ConnectPlan;
|
|
17
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "../ui/button";
|
|
3
|
+
const PLAN_LABEL = {
|
|
4
|
+
personal: "Personal",
|
|
5
|
+
workspace: "Team or Enterprise",
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Who adds the connector, as a named pair matching the access control above it.
|
|
9
|
+
*
|
|
10
|
+
* This asks rather than infers. The alternative was gating on this app's own
|
|
11
|
+
* admin role, but a project admin here is not necessarily an owner of the
|
|
12
|
+
* Claude or ChatGPT workspace: the two are unrelated systems, so inferring one
|
|
13
|
+
* from the other would hide the URL from the person who needs it and show it
|
|
14
|
+
* to someone who cannot use it. Only the reader knows which plan they are on.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here changes what is minted or what it may reach. The plan selects
|
|
17
|
+
* which setup steps are true, and the values above stay identical either way.
|
|
18
|
+
*/
|
|
19
|
+
export function McpPlanChoice({ onChange, plan, }) {
|
|
20
|
+
return (_jsxs("div", { "aria-label": "Who adds the connector", className: "inline-flex w-fit rounded-md border border-border p-0.5", role: "group", children: [_jsx(PlanOption, { label: PLAN_LABEL.personal, onSelect: () => {
|
|
21
|
+
onChange("personal");
|
|
22
|
+
}, selected: plan === "personal" }), _jsx(PlanOption, { label: PLAN_LABEL.workspace, onSelect: () => {
|
|
23
|
+
onChange("workspace");
|
|
24
|
+
}, selected: plan === "workspace" })] }));
|
|
25
|
+
}
|
|
26
|
+
function PlanOption({ label, onSelect, selected, }) {
|
|
27
|
+
return (_jsx(Button, { "aria-pressed": selected, onClick: onSelect, size: "sm", variant: selected ? "secondary" : "ghost", children: label }));
|
|
28
|
+
}
|
|
@@ -139,8 +139,6 @@ function parseStoredPalette(value) {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
function getSystemTheme() {
|
|
142
|
-
if (typeof window === "undefined")
|
|
143
|
-
return "light";
|
|
144
142
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
145
143
|
}
|
|
146
144
|
export { ThemeProvider };
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
type FormSheetActions = Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* Spread onto the `SheetContent` this form sheet wraps. Carries the
|
|
5
|
+
* close-focus handler; without it, closing drops focus on `<body>`.
|
|
6
|
+
*/
|
|
7
|
+
contentProps: Readonly<{
|
|
8
|
+
onCloseAutoFocus: (event: Event) => void;
|
|
9
|
+
}>;
|
|
3
10
|
requestClose: () => void;
|
|
4
11
|
}>;
|
|
5
12
|
type FormSheetProps = {
|
|
@@ -11,13 +11,14 @@ function useFormSheetActions() {
|
|
|
11
11
|
}
|
|
12
12
|
function FormSheet({ children, closeDisabled = false, isDirty, onOpenChange, open, trigger, }) {
|
|
13
13
|
const [discardPromptOpen, setDiscardPromptOpen] = React.useState(false);
|
|
14
|
+
const sheetReturnFocusRef = React.useRef(null);
|
|
14
15
|
const discardReturnFocusRef = React.useRef(null);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}, [
|
|
16
|
+
// Layout effect: parent layout effects run before the sheet's focus scope
|
|
17
|
+
// captures on mount, so this still sees the element focused before opening.
|
|
18
|
+
React.useLayoutEffect(() => {
|
|
19
|
+
if (open)
|
|
20
|
+
sheetReturnFocusRef.current = getActiveElement();
|
|
21
|
+
}, [open]);
|
|
21
22
|
const requestClose = React.useCallback(() => {
|
|
22
23
|
if (closeDisabled)
|
|
23
24
|
return;
|
|
@@ -26,83 +27,48 @@ function FormSheet({ children, closeDisabled = false, isDirty, onOpenChange, ope
|
|
|
26
27
|
setDiscardPromptOpen(true);
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
}, [closeDisabled,
|
|
31
|
-
const
|
|
30
|
+
onOpenChange(false);
|
|
31
|
+
}, [closeDisabled, isDirty, onOpenChange]);
|
|
32
|
+
const handleSheetCloseAutoFocus = React.useCallback((event) => {
|
|
33
|
+
returnFocusTo(event, sheetReturnFocusRef.current);
|
|
34
|
+
}, []);
|
|
35
|
+
const handleDiscardCloseAutoFocus = React.useCallback((event) => {
|
|
36
|
+
returnFocusTo(event, discardReturnFocusRef.current);
|
|
37
|
+
}, []);
|
|
38
|
+
const actions = React.useMemo(() => ({
|
|
39
|
+
contentProps: { onCloseAutoFocus: handleSheetCloseAutoFocus },
|
|
40
|
+
requestClose,
|
|
41
|
+
}), [handleSheetCloseAutoFocus, requestClose]);
|
|
32
42
|
function handleOpenChange(nextOpen) {
|
|
33
43
|
if (nextOpen) {
|
|
34
|
-
returnFocus.capture();
|
|
35
44
|
onOpenChange(true);
|
|
36
45
|
return;
|
|
37
46
|
}
|
|
38
47
|
requestClose();
|
|
39
48
|
}
|
|
40
49
|
function discardChanges() {
|
|
41
|
-
discardingRef.current = true;
|
|
42
50
|
setDiscardPromptOpen(false);
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
function handleDiscardPromptOpenChange(nextOpen) {
|
|
46
|
-
setDiscardPromptOpen(nextOpen);
|
|
47
|
-
if (!nextOpen && !discardingRef.current) {
|
|
48
|
-
requestAnimationFrame(() => discardReturnFocusRef.current?.focus());
|
|
49
|
-
}
|
|
50
|
-
discardingRef.current = false;
|
|
51
|
+
onOpenChange(false);
|
|
51
52
|
}
|
|
52
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Sheet, { onOpenChange: handleOpenChange, open: open, children: [trigger ? _jsx(SheetTrigger, { asChild: true, children: trigger }) : null, _jsx(FormSheetContext.Provider, { value: actions, children: children })] }), _jsx(DiscardChangesPrompt, { onDiscard: discardChanges, onOpenChange:
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Sheet, { onOpenChange: handleOpenChange, open: open, children: [trigger ? _jsx(SheetTrigger, { asChild: true, children: trigger }) : null, _jsx(FormSheetContext.Provider, { value: actions, children: children })] }), _jsx(DiscardChangesPrompt, { onCloseAutoFocus: handleDiscardCloseAutoFocus, onDiscard: discardChanges, onOpenChange: setDiscardPromptOpen, open: discardPromptOpen })] }));
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
focusFrameRef.current = requestAnimationFrame(() => {
|
|
67
|
-
focusFrameRef.current = null;
|
|
68
|
-
if (returnFocus.isConnected)
|
|
69
|
-
returnFocus.focus();
|
|
70
|
-
});
|
|
71
|
-
}, []);
|
|
72
|
-
// Layout effect: a controlled open (no trigger, so capture() never ran) must
|
|
73
|
-
// read the pre-open element before the sheet's focus scope moves focus.
|
|
74
|
-
useIsomorphicLayoutEffect(() => {
|
|
75
|
-
if (open) {
|
|
76
|
-
if (!wasOpenRef.current)
|
|
77
|
-
returnFocusRef.current = getActiveElement();
|
|
78
|
-
wasOpenRef.current = true;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (!wasOpenRef.current)
|
|
82
|
-
return;
|
|
83
|
-
wasOpenRef.current = false;
|
|
84
|
-
schedule();
|
|
85
|
-
}, [open, schedule]);
|
|
86
|
-
React.useEffect(() => () => {
|
|
87
|
-
if (wasOpenRef.current)
|
|
88
|
-
schedule();
|
|
89
|
-
}, [schedule]);
|
|
90
|
-
const capture = React.useCallback(() => {
|
|
91
|
-
returnFocusRef.current = getActiveElement();
|
|
92
|
-
wasOpenRef.current = true;
|
|
93
|
-
}, []);
|
|
94
|
-
const prepareForClose = React.useCallback(() => {
|
|
95
|
-
schedule();
|
|
96
|
-
wasOpenRef.current = false;
|
|
97
|
-
}, [schedule]);
|
|
98
|
-
return React.useMemo(() => ({ capture, prepareForClose }), [capture, prepareForClose]);
|
|
55
|
+
/**
|
|
56
|
+
* Radix aims its close-focus at the overlay's trigger. A controlled sheet has
|
|
57
|
+
* no trigger, so Radix suppresses the focus scope's own fallback and then
|
|
58
|
+
* focuses nothing, leaving `<body>` active. Restoring here rather than on a
|
|
59
|
+
* timer is what keeps the timing right: Radix fires this after the content
|
|
60
|
+
* unmounts, which is the moment the element is safe to focus again.
|
|
61
|
+
*/
|
|
62
|
+
function returnFocusTo(event, returnFocus) {
|
|
63
|
+
if (!returnFocus?.isConnected)
|
|
64
|
+
return; // discarded: the sheet owns focus from here
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
returnFocus.focus();
|
|
99
67
|
}
|
|
100
|
-
function DiscardChangesPrompt({ onDiscard, onOpenChange, open, }) {
|
|
101
|
-
return (_jsx(AlertDialog, { onOpenChange: onOpenChange, open: open, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Discard unsaved changes?" }), _jsx(AlertDialogDescription, { children: "Your changes have not been saved. Keep editing or discard them and close this panel." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Keep editing" }), _jsx(AlertDialogAction, { onClick: onDiscard, variant: "destructive", children: "Discard changes" })] })] }) }));
|
|
68
|
+
function DiscardChangesPrompt({ onCloseAutoFocus, onDiscard, onOpenChange, open, }) {
|
|
69
|
+
return (_jsx(AlertDialog, { onOpenChange: onOpenChange, open: open, children: _jsxs(AlertDialogContent, { onCloseAutoFocus: onCloseAutoFocus, size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Discard unsaved changes?" }), _jsx(AlertDialogDescription, { children: "Your changes have not been saved. Keep editing or discard them and close this panel." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Keep editing" }), _jsx(AlertDialogAction, { onClick: onDiscard, variant: "destructive", children: "Discard changes" })] })] }) }));
|
|
102
70
|
}
|
|
103
71
|
function getActiveElement() {
|
|
104
|
-
if (typeof document === "undefined")
|
|
105
|
-
return null; // SSR-safe
|
|
106
72
|
return document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
107
73
|
}
|
|
108
74
|
export { FormSheet, useFormSheetActions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/copy-field";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/copy-field";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/generated-connect-link";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/generated-connect-link";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-tool-picker";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-tool-picker";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-types";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-types";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-client-setup";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-client-setup";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-connect-clients";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-connect-clients";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-plan-choice";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-plan-choice";
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* localStorage that cannot throw at its callers.
|
|
3
3
|
*
|
|
4
|
-
* Every access can raise: the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* in-memory state.
|
|
4
|
+
* Every access can raise: reading the property throws when storage is disabled
|
|
5
|
+
* by policy or by Safari's private mode, and `setItem` throws on quota. A
|
|
6
|
+
* preference is worth remembering and never worth failing over, so each call
|
|
7
|
+
* degrades to "no stored value" and the caller keeps its in-memory state.
|
|
9
8
|
*/
|
|
10
9
|
declare function readStoredValue(key: string): string | null;
|
|
11
10
|
declare function writeStoredValue(key: string, value: string): void;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* localStorage that cannot throw at its callers.
|
|
3
3
|
*
|
|
4
|
-
* Every access can raise: the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* in-memory state.
|
|
4
|
+
* Every access can raise: reading the property throws when storage is disabled
|
|
5
|
+
* by policy or by Safari's private mode, and `setItem` throws on quota. A
|
|
6
|
+
* preference is worth remembering and never worth failing over, so each call
|
|
7
|
+
* degrades to "no stored value" and the caller keeps its in-memory state.
|
|
9
8
|
*/
|
|
10
9
|
function storage() {
|
|
11
10
|
try {
|
|
12
|
-
return
|
|
11
|
+
return window.localStorage;
|
|
13
12
|
}
|
|
14
13
|
catch {
|
|
15
14
|
// Accessing the property itself throws when storage is blocked by policy.
|