diff --git a/packages/ui/src/components/sections/mcp/McpPage.tsx b/packages/ui/src/components/sections/mcp/McpPage.tsx new file mode 100644 index 00000000..97d99aaa --- /dev/null +++ b/packages/ui/src/components/sections/mcp/McpPage.tsx @@ -0,0 +1,748 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { ButtonLarge } from '@/components/ui/button-large'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { toast } from '@/components/ui'; +import { + useMcpConfigStore, + envRecordToArray, + type McpDraft, + type McpScope, +} from '@/stores/useMcpConfigStore'; +import { useMcpStore } from '@/stores/useMcpStore'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { + RiAddLine, + RiClipboardLine, + RiDeleteBinLine, + RiEyeLine, + RiEyeOffLine, + RiFolderLine, + RiPlugLine, + RiSaveLine, + RiUser3Line, +} from '@remixicon/react'; +import { cn } from '@/lib/utils'; +import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { ButtonSmall } from '@/components/ui/button-small'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from '@/components/ui/select'; + +// ───────────────────────────────────────────────────────────── +// CommandTextarea — one arg per line, paste-friendly +// ───────────────────────────────────────────────────────────── +interface CommandTextareaProps { + value: string[]; + onChange: (v: string[]) => void; +} + +/** + * Splits a shell-like command string into argv array. + * Handles simple quoted args (single/double) and plain tokens. + */ +function parseShellCommand(raw: string): string[] { + const args: string[] = []; + let current = ''; + let inSingle = false; + let inDouble = false; + + for (let i = 0; i < raw.length; i++) { + const ch = raw[i]; + if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } + if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } + if ((ch === ' ' || ch === '\t') && !inSingle && !inDouble) { + if (current) { args.push(current); current = ''; } + continue; + } + current += ch; + } + if (current) args.push(current); + return args; +} + +const CommandTextarea: React.FC = ({ value, onChange }) => { + // Internal: one arg per line + const [text, setText] = React.useState(() => value.join('\n')); + + // Sync when external value changes (e.g. switching servers) + const prevValueRef = React.useRef(value); + React.useEffect(() => { + if (JSON.stringify(prevValueRef.current) !== JSON.stringify(value)) { + prevValueRef.current = value; + setText(value.join('\n')); + } + }, [value]); + + const commit = (raw: string) => { + const lines = raw.split('\n').filter((l) => l.trim().length > 0); + onChange(lines); + }; + + const handlePasteFromClipboard = async () => { + try { + const raw = await navigator.clipboard.readText(); + const trimmed = raw.trim(); + // If it looks like a multi-line list, keep as-is; otherwise parse as shell command + const lines = trimmed.includes('\n') + ? trimmed.split('\n').filter((l) => l.trim()) + : parseShellCommand(trimmed); + setText(lines.join('\n')); + onChange(lines); + toast.success(`Pasted ${lines.length} argument${lines.length !== 1 ? 's' : ''}`); + } catch { + toast.error('Cannot read clipboard'); + } + }; + + return ( +
+
+

+ One argument per line. Blank lines are ignored. +

+ +
+ +