diff --git a/src/components/detail/RequestSection.tsx b/src/components/detail/RequestSection.tsx index 48fb32c..ffea48a 100644 --- a/src/components/detail/RequestSection.tsx +++ b/src/components/detail/RequestSection.tsx @@ -1,3 +1,4 @@ +import { useState, useCallback } from 'react' import type { Flow } from '../../../shared/types' import type { EnhancerMatch } from '@/enhancers' import type { ViewMode } from '@/components/ViewModeToggle' @@ -6,6 +7,7 @@ import { MethodBadge } from '@/components/StatusBadge' import { FetchedRawHttpView } from '@/enhancers/claude-messages/components/FetchedRawHttpView' import { HttpBodyView } from './HttpBodyView' import { getRequestViewModes } from '@/lib/format' +import { flowToCurl } from '@/lib/curl' interface RequestSectionProps { flow: Flow @@ -21,6 +23,14 @@ export function RequestSection({ onViewModeChange, }: RequestSectionProps) { const modes = getRequestViewModes(flow, enhancer) + const [copied, setCopied] = useState(false) + + const copyAsCurl = useCallback(() => { + navigator.clipboard.writeText(flowToCurl(flow)).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }) + }, [flow]) const renderContent = () => { if (viewMode === 'raw' && flow.hasRawHttp) { @@ -65,6 +75,13 @@ export function RequestSection({ {flow.request.url}
+ {modes.length > 1 && ( )} diff --git a/src/lib/curl.ts b/src/lib/curl.ts new file mode 100644 index 0000000..db10979 --- /dev/null +++ b/src/lib/curl.ts @@ -0,0 +1,40 @@ +import type { Flow } from '../../shared/types' + +/** + * Convert a Flow object into a curl command string. + */ +export function flowToCurl(flow: Flow): string { + const parts: string[] = ['curl'] + + // Method (omit for GET since it's the default) + if (flow.request.method !== 'GET') { + parts.push(`-X ${flow.request.method}`) + } + + // URL + parts.push(`'${escapeShell(flow.request.url)}'`) + + // Headers + for (const [key, value] of Object.entries(flow.request.headers)) { + if (value === undefined) continue + // Skip pseudo-headers and host (curl sets it from URL) + const lowerKey = key.toLowerCase() + if (lowerKey === 'host' || lowerKey === 'content-length') continue + + const values = Array.isArray(value) ? value : [value] + for (const v of values) { + parts.push(`-H '${escapeShell(`${key}: ${v}`)}'`) + } + } + + // Body + if (flow.request.body) { + parts.push(`-d '${escapeShell(flow.request.body)}'`) + } + + return parts.join(' \\\n ') +} + +function escapeShell(s: string): string { + return s.replace(/'/g, "'\\''") +}