import React from 'react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { toast } from '@/components/ui'; import { copyTextToClipboard } from '@/lib/clipboard'; import { openExternalUrl } from '@/lib/url'; import { isVSCodeRuntime } from '@/lib/desktop'; import { useMcpConfigStore, envRecordToArray, type McpDraft, type McpScope, } from '@/stores/useMcpConfigStore'; import { useShallow } from 'zustand/react/shallow'; import { parseImportedMcpSnippet, applyImportedMcpToDraft, } from './mcpImport'; import { useMcpStore } from '@/stores/useMcpStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; import { cn } from '@/lib/utils'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { MCP_OAUTH_CALLBACK_PATH, parseMcpOAuthCallbackContext, parseMcpOAuthCallbackStateKey } from '@/components/sections/mcp/mcpOAuth'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, } from '@/components/ui/select'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; // ───────────────────────────────────────────────────────────── // CommandTextarea — one arg per line, paste-friendly // ───────────────────────────────────────────────────────────── interface CommandTextareaProps { value: string[]; onChange: (v: string[]) => void; pasteCommandTitle: string; pasteCommandLabel: string; pasteSuccess: (count: number) => string; clipboardReadFailed: string; preview: (count: number) => string; } /** * 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; } function extractAuthorizationResponse(raw: string): { code: string | null; context: { name: string; directory: string | null } | null; stateKey: string | null; } { const trimmed = raw.trim(); if (!trimmed) { return { code: null, context: null, stateKey: null }; } try { const parsed = new URL(trimmed); const code = parsed.searchParams.get('code'); if (typeof code === 'string' && code.trim()) { return { code: code.trim(), context: parseMcpOAuthCallbackContext(parsed.searchParams), stateKey: parseMcpOAuthCallbackStateKey(parsed.searchParams), }; } } catch { // Fall through to treating the pasted value as a raw authorization code. } return { code: trimmed, context: null, stateKey: null, }; } const CommandTextarea: React.FC = ({ value, onChange, pasteCommandTitle, pasteCommandLabel, pasteSuccess, clipboardReadFailed, preview, }) => { // 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(pasteSuccess(lines.length)); } catch { toast.error(clipboardReadFailed); } }; return (