Skip to content

Commit 12690d6

Browse files
chelojimenezclaude
andcommitted
fix(probe): make host-probe copy buttons work in sandboxed iframe
The async Clipboard API silently rejects when the host doesn't grant `clipboard-write` permission policy on the iframe, and errors were swallowed so the button looked broken either way. Add an execCommand textarea fallback and a brief "Copied" / "Copy failed" label so the action is visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d40e08d commit 12690d6

1 file changed

Lines changed: 38 additions & 4 deletions

File tree

src/host-probe.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,49 @@ function render(key: SectionKey, value: unknown) {
3737
}
3838
}
3939

40+
async function copyText(text: string): Promise<boolean> {
41+
// Async Clipboard API needs `clipboard-write` permission policy on the
42+
// iframe. Hosts that don't honor _meta.ui.permissions.clipboardWrite
43+
// will reject it — fall back to execCommand via a hidden textarea.
44+
try {
45+
if (navigator.clipboard?.writeText) {
46+
await navigator.clipboard.writeText(text);
47+
return true;
48+
}
49+
} catch {
50+
// fall through
51+
}
52+
try {
53+
const ta = document.createElement("textarea");
54+
ta.value = text;
55+
ta.setAttribute("readonly", "");
56+
ta.style.position = "fixed";
57+
ta.style.top = "-1000px";
58+
ta.style.opacity = "0";
59+
document.body.appendChild(ta);
60+
ta.select();
61+
const ok = document.execCommand("copy");
62+
document.body.removeChild(ta);
63+
return ok;
64+
} catch {
65+
return false;
66+
}
67+
}
68+
4069
for (const btn of Array.from(
4170
document.querySelectorAll<HTMLButtonElement>("button[data-copy]"),
4271
)) {
43-
btn.addEventListener("click", () => {
72+
btn.addEventListener("click", async () => {
4473
const key = btn.dataset.copy as SectionKey;
4574
const pre = sections[key]?.querySelector("pre");
46-
if (pre?.textContent) {
47-
navigator.clipboard.writeText(pre.textContent).catch(() => {});
48-
}
75+
const text = pre?.textContent;
76+
if (!text) return;
77+
const original = btn.textContent;
78+
const ok = await copyText(text);
79+
btn.textContent = ok ? "Copied" : "Copy failed";
80+
setTimeout(() => {
81+
btn.textContent = original;
82+
}, 1200);
4983
});
5084
}
5185

0 commit comments

Comments
 (0)