@@ -23,6 +23,180 @@ const INDEX_HTML_PATH = path.join(
2323// Placeholder CSS - will be replaced by dynamic theme injection at runtime
2424const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
2525 <script>
26+ // === Clipboard paste bridge ===
27+ // Cross-origin iframes inside VS Code webviews cannot use the Clipboard API
28+ // (microsoft/vscode#129178, #182642). We intercept Cmd/Ctrl+V over paste-capable
29+ // fields and ask the parent webview for the clipboard text, then insert it ourselves.
30+ (function () {
31+ var pasteRequestId = 0;
32+ var pendingPasteRequests = new Map();
33+
34+ function isPasteableInput(el) {
35+ if (!el) return false;
36+ if (el.isContentEditable) return true;
37+ if (el.tagName === 'TEXTAREA') return !el.disabled && !el.readOnly;
38+ if (el.tagName === 'INPUT') {
39+ var t = (el.type || 'text').toLowerCase();
40+ var pasteableTypes = ['text', 'search', 'url', 'email', 'password', 'tel', 'number'];
41+ return pasteableTypes.indexOf(t) !== -1 && !el.disabled && !el.readOnly;
42+ }
43+ return false;
44+ }
45+
46+ function insertTextAt(el, text) {
47+ if (!el || !text) return;
48+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
49+ // input[type="number"] (and a few others) don't support selection APIs;
50+ // setSelectionRange throws InvalidStateError, which would skip the input event.
51+ var canSelect = el.tagName === 'TEXTAREA' ||
52+ ['text', 'search', 'url', 'email', 'password', 'tel'].indexOf((el.type || 'text').toLowerCase()) !== -1;
53+ var start = canSelect && el.selectionStart != null ? el.selectionStart : el.value.length;
54+ var end = canSelect && el.selectionEnd != null ? el.selectionEnd : el.value.length;
55+ var nativeSetter = Object.getOwnPropertyDescriptor(
56+ el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
57+ 'value'
58+ ).set;
59+ // Use the native setter so React's onChange fires (React tracks the prior value)
60+ nativeSetter.call(el, el.value.slice(0, start) + text + el.value.slice(end));
61+ if (canSelect) {
62+ var caret = start + text.length;
63+ el.setSelectionRange(caret, caret);
64+ }
65+ el.dispatchEvent(new Event('input', { bubbles: true }));
66+ } else if (el.isContentEditable) {
67+ document.execCommand('insertText', false, text);
68+ }
69+ }
70+
71+ function selectAll(el) {
72+ if (!el) return;
73+ if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
74+ try { el.select(); } catch (_) { /* ignore */ }
75+ } else if (el.isContentEditable) {
76+ var range = document.createRange();
77+ range.selectNodeContents(el);
78+ var sel = window.getSelection();
79+ sel.removeAllRanges();
80+ sel.addRange(range);
81+ }
82+ }
83+
84+ function getSelectionContext() {
85+ var ae = document.activeElement;
86+ if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA')) {
87+ var s = ae.selectionStart;
88+ var en = ae.selectionEnd;
89+ if (s != null && en != null && s !== en) {
90+ return { text: ae.value.slice(s, en), inInput: true, el: ae, start: s, end: en };
91+ }
92+ }
93+ var sel = window.getSelection();
94+ var selText = sel ? sel.toString() : '';
95+ if (selText) return { text: selText, inInput: false };
96+ return null;
97+ }
98+
99+ function deleteSelection(ctx) {
100+ if (!ctx) return;
101+ if (ctx.inInput && ctx.el && !ctx.el.disabled && !ctx.el.readOnly) {
102+ var nativeSetter = Object.getOwnPropertyDescriptor(
103+ ctx.el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
104+ 'value'
105+ ).set;
106+ nativeSetter.call(ctx.el, ctx.el.value.slice(0, ctx.start) + ctx.el.value.slice(ctx.end));
107+ ctx.el.setSelectionRange(ctx.start, ctx.start);
108+ ctx.el.dispatchEvent(new Event('input', { bubbles: true }));
109+ } else {
110+ try { document.execCommand('delete'); } catch (_) { /* ignore */ }
111+ }
112+ }
113+
114+ document.addEventListener('keydown', function (e) {
115+ if (!(e.metaKey || e.ctrlKey)) return;
116+ if (e.shiftKey || e.altKey) return;
117+ var k = e.key && e.key.toLowerCase();
118+
119+ // Copy / Cut: work on any selection in the document (form field or UI text).
120+ if (k === 'c' || k === 'x') {
121+ var ctx = getSelectionContext();
122+ if (!ctx || !ctx.text) return;
123+ e.preventDefault();
124+ e.stopPropagation();
125+ if (typeof window.__mcpInspectorWriteClipboard === 'function') {
126+ var write = window.__mcpInspectorWriteClipboard(ctx.text);
127+ if (k === 'x') {
128+ // Defer the delete until after the write resolves to avoid silent data loss.
129+ write.then(function () { deleteSelection(ctx); }, function () { /* keep selection on failure */ });
130+ } else {
131+ write.catch(function () { /* swallow */ });
132+ }
133+ }
134+ return;
135+ }
136+
137+ // Paste / Select-all: only meaningful when a paste-capable field is focused.
138+ var ae = document.activeElement;
139+ if (!isPasteableInput(ae)) return;
140+ if (k === 'v') {
141+ // Prevent the (broken) native paste path; do our own via the parent webview.
142+ e.preventDefault();
143+ e.stopPropagation();
144+ var id = ++pasteRequestId;
145+ pendingPasteRequests.set(id, ae);
146+ setTimeout(function () {
147+ pendingPasteRequests.delete(id);
148+ }, 10000);
149+ window.parent.postMessage({ type: 'mcp-inspector-request-paste', requestId: id }, '*');
150+ } else if (k === 'a') {
151+ // VS Code's webview swallows the native Select All; do it manually.
152+ e.preventDefault();
153+ e.stopPropagation();
154+ selectAll(ae);
155+ }
156+ }, true);
157+
158+ // Bridge for writing to the system clipboard (copy direction).
159+ // Cross-origin iframes can't use navigator.clipboard.writeText() either,
160+ // so we ask the extension host to do it via vscode.env.clipboard.writeText().
161+ var copyRequestId = 0;
162+ var pendingCopyRequests = new Map();
163+ window.__mcpInspectorWriteClipboard = function (text) {
164+ return new Promise(function (resolve, reject) {
165+ var id = ++copyRequestId;
166+ pendingCopyRequests.set(id, { resolve: resolve, reject: reject });
167+ window.parent.postMessage({
168+ type: 'mcp-inspector-request-clipboard-write',
169+ requestId: id,
170+ text: text == null ? '' : String(text)
171+ }, '*');
172+ setTimeout(function () {
173+ if (pendingCopyRequests.has(id)) {
174+ pendingCopyRequests.delete(id);
175+ reject(new Error('Clipboard write timed out'));
176+ }
177+ }, 5000);
178+ });
179+ };
180+
181+ window.addEventListener('message', function (e) {
182+ // Only honour clipboard responses from the parent webview shell.
183+ if (e.source !== window.parent) return;
184+ var msg = e.data;
185+ if (!msg) return;
186+ if (msg.type === 'mcp-inspector-paste-response') {
187+ var target = pendingPasteRequests.get(msg.requestId);
188+ pendingPasteRequests.delete(msg.requestId);
189+ if (target && msg.text) insertTextAt(target, msg.text);
190+ } else if (msg.type === 'mcp-inspector-clipboard-write-result') {
191+ var pending = pendingCopyRequests.get(msg.requestId);
192+ if (!pending) return;
193+ pendingCopyRequests.delete(msg.requestId);
194+ if (msg.ok) pending.resolve();
195+ else pending.reject(new Error(msg.error || 'Clipboard write failed'));
196+ }
197+ });
198+ })();
199+
26200 // Function to convert color (hex or rgb/rgba) to HSL format
27201 function colorToHsl(color) {
28202 let r, g, b;
@@ -69,7 +243,44 @@ const CUSTOM_CSS = ` <!-- Dynamic Theme Injection Placeholder -->
69243 return Math.round(h * 360) + ' ' + Math.round(s * 100) + '% ' + Math.round(l * 100) + '%';
70244 }
71245
72- // Remove existing theme styles when updating
246+ // Fallback to execCommand when webview blocks navigator.clipboard.writeText.
247+ (function patchClipboard() {
248+ const execCopy = (text) => {
249+ const ta = document.createElement('textarea');
250+ ta.value = text == null ? '' : String(text);
251+ ta.setAttribute('readonly', '');
252+ ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
253+ document.body.appendChild(ta);
254+ const sel = document.getSelection();
255+ const prevRange = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
256+ ta.select();
257+ ta.setSelectionRange(0, ta.value.length);
258+ let ok = false;
259+ try { ok = document.execCommand('copy'); } catch (_) { ok = false; }
260+ document.body.removeChild(ta);
261+ if (prevRange && sel) { sel.removeAllRanges(); sel.addRange(prevRange); }
262+ return ok;
263+ };
264+ const native = navigator.clipboard && navigator.clipboard.writeText
265+ ? navigator.clipboard.writeText.bind(navigator.clipboard)
266+ : null;
267+ const writeText = async (text) => {
268+ if (native) {
269+ try { return await native(text); } catch (_) {}
270+ }
271+ // VS Code webview path: ask the extension host to write the clipboard.
272+ if (typeof window.__mcpInspectorWriteClipboard === 'function') {
273+ try { await window.__mcpInspectorWriteClipboard(text); return; } catch (_) {}
274+ }
275+ if (execCopy(text)) return;
276+ throw new Error('Clipboard copy failed');
277+ };
278+ if (!navigator.clipboard) {
279+ Object.defineProperty(navigator, 'clipboard', { value: {}, configurable: true });
280+ }
281+ try { navigator.clipboard.writeText = writeText; } catch (_) {}
282+ })();
283+
73284 let themeStyleElement = null;
74285
75286 // Listen for theme color messages from parent
0 commit comments