[object Object]

← 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

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 →