← back to Exo
Allow copying on dashboard even on HTTP (#1902)
058bb08261dc8b4eb8c3ee9761cf41fbe1d02eee · 2026-04-15 23:30:12 +0100 · rltakashige
## Motivation
<!-- Why is this change needed? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here -->
## Changes
<!-- Describe what you changed in detail -->
## Why It Works
<!-- Explain why your approach solves the problem -->
## Test Plan
### Manual Testing
<!-- Hardware: (e.g., MacBook Pro M1 Max 32GB, Mac Mini M2 16GB,
connected via Thunderbolt 4) -->
<!-- What you did: -->
<!-- - -->
### Automated Testing
<!-- Describe changes to automated tests, or how existing tests cover
this change -->
<!-- - -->
Files touched
M dashboard/src/lib/components/IntegrationCard.svelteA dashboard/src/lib/utils/clipboard.ts
Diff
commit 058bb08261dc8b4eb8c3ee9761cf41fbe1d02eee
Author: rltakashige <rl.takashige@gmail.com>
Date: Wed Apr 15 23:30:12 2026 +0100
Allow copying on dashboard even on HTTP (#1902)
## Motivation
<!-- Why is this change needed? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here -->
## Changes
<!-- Describe what you changed in detail -->
## Why It Works
<!-- Explain why your approach solves the problem -->
## Test Plan
### Manual Testing
<!-- Hardware: (e.g., MacBook Pro M1 Max 32GB, Mac Mini M2 16GB,
connected via Thunderbolt 4) -->
<!-- What you did: -->
<!-- - -->
### Automated Testing
<!-- Describe changes to automated tests, or how existing tests cover
this change -->
<!-- - -->
---
.../src/lib/components/IntegrationCard.svelte | 20 ++++++--
dashboard/src/lib/utils/clipboard.ts | 55 ++++++++++++++++++++++
2 files changed, 70 insertions(+), 5 deletions(-)
diff --git a/dashboard/src/lib/components/IntegrationCard.svelte b/dashboard/src/lib/components/IntegrationCard.svelte
index 83978b4d..2398b819 100644
--- a/dashboard/src/lib/components/IntegrationCard.svelte
+++ b/dashboard/src/lib/components/IntegrationCard.svelte
@@ -1,4 +1,6 @@
<script lang="ts">
+ import { copyText } from "$lib/utils/clipboard";
+
interface Props {
title: string;
subtitle: string;
@@ -16,11 +18,17 @@
}: Props = $props();
let copied = $state(false);
+ let failed = $state(false);
async function copyToClipboard() {
- await navigator.clipboard.writeText(config);
- copied = true;
- setTimeout(() => (copied = false), 2000);
+ const ok = await copyText(config);
+ if (ok) {
+ copied = true;
+ setTimeout(() => (copied = false), 2000);
+ } else {
+ failed = true;
+ setTimeout(() => (failed = false), 2000);
+ }
}
</script>
@@ -37,9 +45,11 @@
class="px-3 py-1.5 text-xs rounded border transition-all duration-200 cursor-pointer
{copied
? 'border-green-500/50 text-green-400 bg-green-500/10'
- : 'border-exo-light-gray/30 text-exo-light-gray hover:border-exo-yellow/50 hover:text-exo-yellow'}"
+ : failed
+ ? 'border-red-500/50 text-red-400 bg-red-500/10'
+ : 'border-exo-light-gray/30 text-exo-light-gray hover:border-exo-yellow/50 hover:text-exo-yellow'}"
>
- {copied ? "Copied!" : "Copy"}
+ {copied ? "Copied!" : failed ? "Copy failed" : "Copy"}
</button>
</div>
{#if description}
diff --git a/dashboard/src/lib/utils/clipboard.ts b/dashboard/src/lib/utils/clipboard.ts
new file mode 100644
index 00000000..02e6f456
--- /dev/null
+++ b/dashboard/src/lib/utils/clipboard.ts
@@ -0,0 +1,55 @@
+export async function copyText(text: string): Promise<boolean> {
+ if (
+ typeof window !== "undefined" &&
+ window.isSecureContext &&
+ navigator.clipboard?.writeText
+ ) {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ // fall through to execCommand fallback
+ }
+ }
+
+ if (typeof document === "undefined") {
+ return false;
+ }
+
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "fixed";
+ textarea.style.top = "0";
+ textarea.style.left = "0";
+ textarea.style.width = "1px";
+ textarea.style.height = "1px";
+ textarea.style.padding = "0";
+ textarea.style.border = "none";
+ textarea.style.outline = "none";
+ textarea.style.boxShadow = "none";
+ textarea.style.background = "transparent";
+ textarea.style.opacity = "0";
+ document.body.appendChild(textarea);
+
+ const previousSelection = document.getSelection();
+ const previousRange =
+ previousSelection && previousSelection.rangeCount > 0
+ ? previousSelection.getRangeAt(0)
+ : null;
+
+ try {
+ textarea.focus();
+ textarea.select();
+ textarea.setSelectionRange(0, text.length);
+ return document.execCommand("copy");
+ } catch {
+ return false;
+ } finally {
+ document.body.removeChild(textarea);
+ if (previousRange && previousSelection) {
+ previousSelection.removeAllRanges();
+ previousSelection.addRange(previousRange);
+ }
+ }
+}
← 3eead802 Better environment variables in MacOS app (#1901)
·
back to Exo
·
Update mlx and mlx lm to latest (#1906) 28c79784 →