Skip to content

Commit 513becc

Browse files
committed
feat: Add 'Copy as JavaScript fetch (POC)' option for clean POC-ready fetch code
1 parent 959aaf9 commit 513becc

File tree

2 files changed

+89
-37
lines changed

2 files changed

+89
-37
lines changed

js/ui/ui-utils.js

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function toggleAllObjects() {
5555

5656
export 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');

panel.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,9 @@ <h3>Bulk Replay Results</h3>
621621
<div class="context-menu-item" data-action="copy-as-fetch" data-requires-full-selection="true">
622622
JavaScript fetch
623623
</div>
624+
<div class="context-menu-item" data-action="copy-as-fetch-poc" data-requires-full-selection="true">
625+
JavaScript fetch (POC)
626+
</div>
624627
</div>
625628
</div>
626629
<div class="context-menu-separator"></div>

0 commit comments

Comments
 (0)