@@ -55,7 +55,7 @@ export function toggleAllObjects() {
5555
5656export function clearAllRequestsUI ( ) {
5757 const requestList = document . getElementById ( 'request-list' ) ;
58-
58+
5959 // First, manually remove all groups and items from DOM
6060 if ( requestList ) {
6161 // Remove all page groups, domain groups, path groups, and request items
@@ -70,7 +70,7 @@ export function clearAllRequestsUI() {
7070 }
7171 }
7272 } ) ;
73-
73+
7474 // Forcefully remove all remaining child nodes
7575 while ( requestList . firstChild ) {
7676 requestList . removeChild ( requestList . firstChild ) ;
@@ -82,7 +82,7 @@ export function clearAllRequestsUI() {
8282 emptyState . textContent = 'Listening for requests...' ;
8383 requestList . appendChild ( emptyState ) ;
8484 }
85-
85+
8686 // Then clear state (this will emit events)
8787 actions . request . clearAll ( ) ;
8888 actions . blocking . clearBlockedQueue ( ) ;
@@ -141,38 +141,38 @@ export function setupResizeHandle() {
141141 } else {
142142 const offsetX = e . clientX - containerRect . left ;
143143 const containerWidth = containerRect . width ;
144-
144+
145145 // Check if chat pane is open
146146 const chatPane = document . getElementById ( 'llm-chat-pane' ) ;
147147 const isChatOpen = chatPane && chatPane . style . display !== 'none' && window . getComputedStyle ( chatPane ) . display !== 'none' ;
148-
148+
149149 if ( isChatOpen ) {
150150 // When chat is open, only resize request and response, keep chat fixed
151151 const chatRect = chatPane . getBoundingClientRect ( ) ;
152152 const chatWidth = chatRect . width ;
153153 const chatResizeHandle = document . querySelector ( '.chat-resize-handle' ) ;
154154 const chatResizeHandleWidth = chatResizeHandle ? ( chatResizeHandle . offsetWidth || 5 ) : 5 ;
155-
155+
156156 // Available width is container minus chat pane and its resize handle
157157 const availableWidth = containerWidth - chatWidth - chatResizeHandleWidth ;
158-
158+
159159 // Enforce minimum pixel widths
160160 const minLeftPx = 200 ;
161161 const minRightPx = 200 ;
162162 const clampedOffsetX = Math . min (
163163 Math . max ( offsetX , minLeftPx ) ,
164164 Math . max ( availableWidth - minRightPx , minLeftPx )
165165 ) ;
166-
166+
167167 // Calculate percentages of available width (not full container)
168168 let requestPercentage = ( clampedOffsetX / availableWidth ) * 100 ;
169169 let responsePercentage = 100 - requestPercentage ;
170-
170+
171171 // Convert to container percentages
172172 const availablePercentage = ( availableWidth / containerWidth ) * 100 ;
173173 requestPercentage = ( requestPercentage / 100 ) * availablePercentage ;
174174 responsePercentage = ( responsePercentage / 100 ) * availablePercentage ;
175-
175+
176176 // Keep chat pane fixed, only adjust request and response
177177 requestPane . style . flex = `0 0 ${ requestPercentage } %` ;
178178 responsePane . style . flex = `0 0 ${ responsePercentage } %` ;
@@ -259,7 +259,7 @@ export function setupUndoRedo() {
259259 elements . rawRequestInput . addEventListener ( 'blur' , ( ) => {
260260 const content = elements . rawRequestInput . innerText ;
261261 elements . rawRequestInput . innerHTML = highlightHTTP ( content ) ;
262-
262+
263263 // Auto-save editor state when user leaves the editor (switching requests, etc.)
264264 if ( state . selectedRequest ) {
265265 const requestIndex = state . requests . indexOf ( state . selectedRequest ) ;
@@ -371,15 +371,15 @@ export function setupContextMenu() {
371371 // Store selected text and range in context menu dataset for later use
372372 elements . contextMenu . dataset . selectedText = selectedText ;
373373 currentSelection = selection ; // Store the selection object
374-
374+
375375 if ( selection . rangeCount > 0 ) {
376376 const range = selection . getRangeAt ( 0 ) ;
377377 currentRange = range . cloneRange ( ) ; // Clone the range to preserve it
378-
378+
379379 // Calculate character offset from start of editor for reliable positioning
380380 // Get plain text first (this strips HTML)
381381 const editorText = editor . textContent || editor . innerText || '' ;
382-
382+
383383 // Create a range from start of editor to selection start to count characters
384384 // This method works even when editor has HTML content
385385 try {
@@ -393,7 +393,7 @@ export function setupContextMenu() {
393393 null
394394 ) ;
395395 const firstTextNode = walker . nextNode ( ) ;
396-
396+
397397 if ( firstTextNode ) {
398398 range . setStart ( firstTextNode , 0 ) ;
399399 } else {
@@ -403,10 +403,10 @@ export function setupContextMenu() {
403403 range . setEnd ( container , offset ) ;
404404 return range . toString ( ) . length ;
405405 }
406-
406+
407407 const startOffset = getCharacterOffset ( range . startContainer , range . startOffset ) ;
408408 const endOffset = getCharacterOffset ( range . endContainer , range . endOffset ) ;
409-
409+
410410 // Verify the offsets make sense and match the selected text
411411 if ( startOffset >= 0 && endOffset >= startOffset && endOffset <= editorText . length ) {
412412 const selectedTextFromRange = editorText . substring ( startOffset , endOffset ) ;
@@ -423,7 +423,7 @@ export function setupContextMenu() {
423423 contextBefore : editorText . substring ( Math . max ( 0 , startOffset - 20 ) , startOffset ) , // Context for verification
424424 contextAfter : editorText . substring ( endOffset , Math . min ( editorText . length , endOffset + 20 ) )
425425 } ;
426-
426+
427427 // Store character offsets in context menu dataset for bulk replay
428428 // This allows marking the exact selected text even if it appears multiple times
429429 elements . contextMenu . dataset . charStart = startOffset . toString ( ) ;
@@ -467,7 +467,7 @@ export function setupContextMenu() {
467467 }
468468
469469 elements . contextMenu . dataset . fullSelection = isFullSelection ? 'true' : 'false' ;
470-
470+
471471 showContextMenu ( e . clientX , e . clientY , editor ) ;
472472 } ) ;
473473 } ) ;
@@ -493,13 +493,13 @@ export function setupContextMenu() {
493493 return ;
494494 }
495495
496- e . stopPropagation ( ) ;
497- const action = item . dataset . action ;
496+ e . stopPropagation ( ) ;
497+ const action = item . dataset . action ;
498498 if ( ! action ) return ;
499499
500500 // "Mark Payload (§)" is handled elsewhere
501501 if ( action === 'mark-payload' ) {
502- hideContextMenu ( ) ;
502+ hideContextMenu ( ) ;
503503 return ;
504504 }
505505
@@ -694,11 +694,11 @@ function handleEncodeDecode(action) {
694694 // Strategy: Try to use the range directly first (fastest and most accurate)
695695 // If that fails, use stored character offsets
696696 // Last resort: text search
697-
697+
698698 const editorText = editor . textContent || editor . innerText || '' ;
699699 let replacementDone = false ;
700700 let startIndex = - 1 ;
701-
701+
702702 // First, try to use the stored range directly (most reliable if still valid)
703703 if ( editor . contentEditable === 'true' && rangeToUse ) {
704704 try {
@@ -726,11 +726,11 @@ function handleEncodeDecode(action) {
726726 console . warn ( 'Range invalid, using fallback:' , e ) ;
727727 }
728728 }
729-
729+
730730 // If range didn't work, use stored character offset
731731 if ( ! replacementDone && storedRangeInfo && storedRangeInfo . editor === editor && storedRangeInfo . charStart !== undefined ) {
732732 startIndex = storedRangeInfo . charStart ;
733-
733+
734734 // Verify the text at this position matches
735735 if ( startIndex >= 0 && startIndex < editorText . length ) {
736736 const textAtPosition = editorText . substring ( startIndex , startIndex + selectedText . length ) ;
@@ -770,7 +770,7 @@ function handleEncodeDecode(action) {
770770 startIndex = - 1 ; // Invalid offset
771771 }
772772 }
773-
773+
774774 // Last resort: try to recreate range from stored info, or use indexOf
775775 if ( ! replacementDone && startIndex === - 1 ) {
776776 if ( storedRangeInfo && storedRangeInfo . editor === editor ) {
@@ -779,7 +779,7 @@ function handleEncodeDecode(action) {
779779 const range = document . createRange ( ) ;
780780 range . setStart ( storedRangeInfo . startContainer , storedRangeInfo . startOffset ) ;
781781 range . setEnd ( storedRangeInfo . endContainer , storedRangeInfo . endOffset ) ;
782-
782+
783783 if ( editor . contains ( range . commonAncestorContainer ) || range . commonAncestorContainer === editor ) {
784784 const rangeText = range . toString ( ) . trim ( ) ;
785785 if ( rangeText === selectedText . trim ( ) ) {
@@ -800,7 +800,7 @@ function handleEncodeDecode(action) {
800800 // Failed to recreate range
801801 }
802802 }
803-
803+
804804 // Final fallback: use indexOf (but warn if text appears multiple times)
805805 if ( ! replacementDone ) {
806806 startIndex = editorText . indexOf ( selectedText ) ;
@@ -810,7 +810,7 @@ function handleEncodeDecode(action) {
810810 }
811811 }
812812 }
813-
813+
814814 // Perform the replacement using text-based method if range didn't work
815815 if ( ! replacementDone && startIndex !== - 1 && startIndex >= 0 && startIndex < editorText . length ) {
816816 // Verify the text at this position matches what we expect
@@ -837,7 +837,7 @@ function handleEncodeDecode(action) {
837837 return ;
838838 }
839839 }
840-
840+
841841 // Extra validation: if startIndex is 0, make sure we have context or stored info
842842 if ( startIndex === 0 && ( ! storedRangeInfo || storedRangeInfo . charStart !== 0 ) ) {
843843 // Position 0 without stored confirmation - this might be wrong
@@ -866,11 +866,11 @@ function handleEncodeDecode(action) {
866866 }
867867 }
868868 }
869-
869+
870870 const before = editorText . substring ( 0 , startIndex ) ;
871871 const after = editorText . substring ( startIndex + selectedText . length ) ;
872872 const newText = before + transformedText + after ;
873-
873+
874874 // Replace the text content (this removes HTML, which is fine - we'll re-apply highlighting)
875875 editor . textContent = newText ;
876876 } else if ( ! replacementDone ) {
@@ -1057,6 +1057,55 @@ function handleCopyAs(action) {
10571057 jsLines . push ( ' .then(console.log)' ) ;
10581058 jsLines . push ( ' .catch(console.error);' ) ;
10591059 textToCopy = jsLines . join ( '\n' ) ;
1060+ } else if ( action === 'copy-as-fetch-poc' ) {
1061+ // POC-ready JavaScript fetch (clean, no cookies in headers, relative URL)
1062+ const urlObj = new URL ( req . url ) ;
1063+ const relativeUrl = urlObj . pathname + urlObj . search ;
1064+
1065+ // Filter out sensitive/browser-managed headers
1066+ const ignoreHeaders = [ 'host' , 'connection' , 'content-length' , 'cookie' , 'referer' ,
1067+ 'sec-fetch-dest' , 'sec-fetch-mode' , 'sec-fetch-site' , 'te' ] ;
1068+ const filteredHeaders = headers . filter ( h => ! ignoreHeaders . includes ( h . name . toLowerCase ( ) ) ) ;
1069+ const hasBody = body && ( method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE' ) ;
1070+
1071+ const pocLines = [ ] ;
1072+ pocLines . push ( `fetch("${ relativeUrl } ", {` ) ;
1073+ pocLines . push ( ` method: "${ method } ",` ) ;
1074+
1075+ // Only include important headers (exclude cookie and browser stuff)
1076+ if ( filteredHeaders . length > 0 ) {
1077+ pocLines . push ( ' headers: {' ) ;
1078+ filteredHeaders . forEach ( ( h , idx ) => {
1079+ const key = h . name . toLowerCase ( ) ;
1080+ const val = String ( h . value ) . replace ( / " / g, '\\"' ) ;
1081+ const comma = idx < filteredHeaders . length - 1 ? ',' : '' ;
1082+ pocLines . push ( ` "${ key } ": "${ val } "${ comma } ` ) ;
1083+ } ) ;
1084+ pocLines . push ( ' },' ) ;
1085+ }
1086+
1087+ // Body
1088+ if ( hasBody ) {
1089+ const ct = headers . find ( h => h . name . toLowerCase ( ) === 'content-type' ) ?. value || '' ;
1090+ if ( ct . toLowerCase ( ) . includes ( 'application/json' ) ) {
1091+ try {
1092+ const parsed = JSON . parse ( body ) ;
1093+ pocLines . push ( ` body: JSON.stringify(${ JSON . stringify ( parsed ) } ),` ) ;
1094+ } catch {
1095+ pocLines . push ( ` body: ${ JSON . stringify ( body ) } ,` ) ;
1096+ }
1097+ } else {
1098+ pocLines . push ( ` body: ${ JSON . stringify ( body ) } ,` ) ;
1099+ }
1100+ }
1101+
1102+ // credentials: "include" to let browser handle cookies
1103+ pocLines . push ( ' credentials: "include"' ) ;
1104+ pocLines . push ( '})' ) ;
1105+ pocLines . push ( '.then(r => r.json())' ) ;
1106+ pocLines . push ( '.then(data => console.log(data))' ) ;
1107+ pocLines . push ( '.catch(err => console.error(err));' ) ;
1108+ textToCopy = pocLines . join ( '\n' ) ;
10601109 } else {
10611110 return ;
10621111 }
@@ -1093,10 +1142,10 @@ export async function captureScreenshot() {
10931142 // Capture only the full request and response content (no headers/search bars),
10941143 // and make sure the entire text is visible in the image.
10951144 try {
1096- if ( typeof html2canvas === 'undefined' ) {
1097- alert ( 'html2canvas library not loaded' ) ;
1098- return ;
1099- }
1145+ if ( typeof html2canvas === 'undefined' ) {
1146+ alert ( 'html2canvas library not loaded' ) ;
1147+ return ;
1148+ }
11001149
11011150 const requestEditor = document . querySelector ( '#raw-request-input' ) ;
11021151 const responseActiveView = document . querySelector ( '.response-pane .view-content.active' ) ;
0 commit comments