Fix copy buttons on non-secure origins; move API keys into the sidebar - #1022
Conversation
The copy buttons (API key, MCP install command, tool IDs, …) called
navigator.clipboard.writeText with no fallback. navigator.clipboard only
exists in a secure context (HTTPS or localhost), so on a self-hosted console
served over plain HTTP on a LAN host/IP the property is undefined and the
click threw and was swallowed — copy silently did nothing.
Add a shared copyToClipboard helper (@executor-js/react lib/clipboard) that
tries the async Clipboard API and falls back to selecting a hidden node via a
document Range + execCommand("copy"). The Range path does NOT move focus, so
it still works inside a focus trap such as the dialog that shows a freshly
created API key (focusing a throwaway <textarea> there gets yanked straight
back, copying nothing). Route CopyButton, the code blocks, and the app update
card through it.
Move the API keys link out of the account dropdown and into the main sidebar
nav for the hosts that serve keys in-app (self-host + cloud). Cloudflare
manages API keys in Access, so it keeps the default nav without the item.
Add selfhost browser e2e guards for both: API keys is a first-class sidebar
item, and the copy button copies a new key on a simulated plain-HTTP origin.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | 5f56924 | Commit Preview URL Branch Preview URL |
Jun 14 2026, 10:54 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 5f56924 | Jun 14 2026, 10:54 PM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Greptile SummaryThis PR fixes two self-hosted user-facing regressions: copy buttons silently doing nothing on plain-HTTP origins (where
Confidence Score: 5/5Safe to merge — the changes are narrowly scoped to the clipboard helper, its four call sites, and the per-host sidebar nav arrays. The clipboard helper is well-designed: the Range-based fallback avoids focus-trap issues documented in the PR, the hidden span is always cleaned up in a finally block, and the boolean return surface is handled uniformly at every call site with an error toast. The nav refactor removes the apiKeysTo prop cleanly and each host's nav array is explicit about what it includes. Three e2e scenarios cover the primary regression paths and the error case, and all format/lint/type/unit gates are green. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User clicks Copy button] --> B{navigator.clipboard available?}
B -->|Yes| C[await clipboard.writeText]
C -->|Success| D[return true]
C -->|Rejects| E[fall through to legacyCopy]
B -->|No| E
E --> F[Create hidden span with text]
F --> G[Append span to document.body]
G --> H[Save current Selection range]
H --> I[Select span via Range - no focus move]
I --> J[execCommand copy]
J -->|true| K[return true]
J -->|false or throws| L[return false]
K --> M[finally: restore selection and remove span]
L --> M
D --> N{ok?}
M --> N
N -->|true| O[Show Copied confirmation]
N -->|false| P[toast.error: Failed to copy]
Reviews (2): Last reviewed commit: "Toast on copy failure instead of failing..." | Re-trigger Greptile |
| if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { | ||
| // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the Clipboard API rejects (permission denied, blur) and we recover via the legacy path | ||
| try { | ||
| await navigator.clipboard.writeText(text); | ||
| return true; | ||
| } catch { | ||
| // Permission denied / document not focused — fall through to the legacy | ||
| // path rather than failing the copy. | ||
| } | ||
| } | ||
|
|
||
| return legacyCopy(text); |
There was a problem hiding this comment.
Async gap may break
execCommand user-gesture tracking on the fallback path. When the Clipboard API is present but rejects (e.g. permission denied on HTTPS), the await inside the try block crosses a microtask boundary before legacyCopy is called. Some browsers enforce that document.execCommand("copy") must be called synchronously within the original user-gesture task, so on those browsers the fallback silently does nothing — the same outcome as before this fix. The target scenario (non-secure origin where navigator.clipboard is undefined) is unaffected since it never enters the if block and calls legacyCopy synchronously.
| if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { | |
| // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the Clipboard API rejects (permission denied, blur) and we recover via the legacy path | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| return true; | |
| } catch { | |
| // Permission denied / document not focused — fall through to the legacy | |
| // path rather than failing the copy. | |
| } | |
| } | |
| return legacyCopy(text); | |
| if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { | |
| // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the Clipboard API rejects (permission denied, blur); falling through to legacyCopy after an await crosses a microtask boundary so we just report failure instead | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| return true; | |
| } catch { | |
| // Permission denied / document not focused. The async gap means | |
| // legacyCopy may not have a live user-gesture token here. | |
| return false; | |
| } | |
| } | |
| return legacyCopy(text); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
When a copy genuinely can't happen (no navigator.clipboard AND execCommand refuses), copyToClipboard returns false. Show a "Failed to copy to clipboard" error toast on that path across every copy site (CopyButton, the code blocks, the app update card) rather than doing nothing. Add a selfhost e2e guard that forces both copy paths to fail and asserts the toast surfaces.
Feedback from a self-hosted user about API keys, plus a follow-up on copy failure handling.
1. The copy buttons didn't work
CopyButton(and the code blocks / the app update card) callednavigator.clipboard.writeTextwith no fallback.navigator.clipboardonlyexists in a secure context (HTTPS or
localhost), so on a self-hostedconsole served over plain HTTP on a LAN host/IP the property is
undefined—the click threw and was swallowed by
void, so copy silently did nothing.Fix: a shared
copyToClipboardhelper that tries the async Clipboard API andfalls back to selecting a hidden node via a document
Range+execCommand("copy"). The Range path deliberately does not move focus, soit survives the focus trap of the dialog that shows a freshly-created key —
focusing a throwaway
<textarea>there gets yanked straight back and copiesnothing (a real bug the e2e test caught). Every
CopyButton, the code blocks,and the app update card now route through the helper.
2. API keys wasn't in the sidebar
The link lived only in the account dropdown. It's now a first-class item in the
main sidebar nav for the hosts that serve keys in-app (self-host + cloud), and
removed from the dropdown (it's a move, not a duplicate). Cloudflare manages API
keys in Access, so it keeps the default nav without the item.
3. Failed copies now tell the user
If a copy genuinely can't happen (no
navigator.clipboardandexecCommandrefuses), the helper returnsfalseand the button shows aFailed to copy to clipboarderror toast instead of failing silently.Evidence
Three selfhost browser e2e scenarios (
e2e/selfhost/api-keys-feedback.test.ts)guard the behaviour — they fail on the old code and pass on this branch.
The copy button copies a new key on a (simulated) plain-HTTP origin:
API keys is reachable from the main sidebar:
A copy that can't reach the clipboard surfaces an error toast:
Gates green:
format:check,lint,typecheck(39/39),packages/reactunittests (154/154), and all three e2e scenarios.